Merge pull request #20077 from backstage/rugvip/mocker
backend-test-utils: add MockDirectory, refactor tests in backend-common
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-test-utils': patch
|
||||
---
|
||||
|
||||
Added `createMockDirectory()` to help out with file system mocking in tests.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Properly close write stream when writing temporary archive for processing zip-based `.readTree()` responses.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Removed `mock-fs` dev dependency.
|
||||
@@ -9,9 +9,10 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-2019
|
||||
runs-on: windows-2022
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [18.x, 20.x]
|
||||
|
||||
|
||||
@@ -125,7 +125,6 @@
|
||||
"@types/fs-extra": "^9.0.3",
|
||||
"@types/http-errors": "^2.0.0",
|
||||
"@types/minimist": "^1.2.0",
|
||||
"@types/mock-fs": "^4.13.0",
|
||||
"@types/morgan": "^1.9.0",
|
||||
"@types/node-forge": "^1.3.0",
|
||||
"@types/pg": "^8.6.6",
|
||||
@@ -138,7 +137,6 @@
|
||||
"aws-sdk-client-mock": "^2.0.0",
|
||||
"better-sqlite3": "^8.0.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"msw": "^1.0.0",
|
||||
"mysql2": "^2.2.5",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
|
||||
@@ -23,12 +23,13 @@ import {
|
||||
AzureDevOpsCredentialLike,
|
||||
AzureIntegrationConfig,
|
||||
} from '@backstage/integration';
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import {
|
||||
createMockDirectory,
|
||||
setupRequestMockHandlers,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import * as os from 'os';
|
||||
import path from 'path';
|
||||
import { NotModifiedError } from '@backstage/errors';
|
||||
import { getVoidLogger } from '../logging';
|
||||
@@ -43,6 +44,8 @@ type AzureIntegrationConfigLike = Partial<
|
||||
|
||||
const logger = getVoidLogger();
|
||||
|
||||
const mockDir = createMockDirectory({ mockOsTmpDir: true });
|
||||
|
||||
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: new ConfigReader({}),
|
||||
});
|
||||
@@ -69,18 +72,8 @@ const urlReaderFactory = (azureIntegration: AzureIntegrationConfigLike) => {
|
||||
);
|
||||
};
|
||||
|
||||
const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp';
|
||||
|
||||
describe('AzureUrlReader', () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
[tmpDir]: mockFs.directory(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
beforeEach(mockDir.clear);
|
||||
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
@@ -270,7 +263,7 @@ describe('AzureUrlReader', () => {
|
||||
'https://dev.azure.com/organization/project/_git/repository',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: tmpDir });
|
||||
const dir = await response.dir({ targetDir: mockDir.path });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'),
|
||||
|
||||
@@ -19,18 +19,21 @@ import {
|
||||
BitbucketCloudIntegration,
|
||||
readBitbucketCloudIntegrationConfig,
|
||||
} from '@backstage/integration';
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import {
|
||||
createMockDirectory,
|
||||
setupRequestMockHandlers,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { NotModifiedError } from '@backstage/errors';
|
||||
import { BitbucketCloudUrlReader } from './BitbucketCloudUrlReader';
|
||||
import { DefaultReadTreeResponseFactory } from './tree';
|
||||
import getRawBody from 'raw-body';
|
||||
|
||||
const mockDir = createMockDirectory({ mockOsTmpDir: true });
|
||||
|
||||
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: new ConfigReader({}),
|
||||
});
|
||||
@@ -49,18 +52,8 @@ const reader = new BitbucketCloudUrlReader(
|
||||
{ treeResponseFactory },
|
||||
);
|
||||
|
||||
const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp';
|
||||
|
||||
describe('BitbucketCloudUrlReader', () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
[tmpDir]: mockFs.directory(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
beforeEach(mockDir.clear);
|
||||
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
@@ -316,7 +309,7 @@ describe('BitbucketCloudUrlReader', () => {
|
||||
'https://bitbucket.org/backstage/mock',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: tmpDir });
|
||||
const dir = await response.dir({ targetDir: mockDir.path });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'),
|
||||
@@ -346,7 +339,7 @@ describe('BitbucketCloudUrlReader', () => {
|
||||
'https://bitbucket.org/backstage/mock/src/master/docs',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: tmpDir });
|
||||
const dir = await response.dir({ targetDir: mockDir.path });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'index.md'), 'utf8'),
|
||||
|
||||
@@ -19,17 +19,20 @@ import {
|
||||
BitbucketServerIntegration,
|
||||
readBitbucketServerIntegrationConfig,
|
||||
} from '@backstage/integration';
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import {
|
||||
createMockDirectory,
|
||||
setupRequestMockHandlers,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { NotModifiedError } from '@backstage/errors';
|
||||
import { BitbucketServerUrlReader } from './BitbucketServerUrlReader';
|
||||
import { DefaultReadTreeResponseFactory } from './tree';
|
||||
|
||||
createMockDirectory({ mockOsTmpDir: true });
|
||||
|
||||
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: new ConfigReader({}),
|
||||
});
|
||||
@@ -46,19 +49,7 @@ const reader = new BitbucketServerUrlReader(
|
||||
{ treeResponseFactory },
|
||||
);
|
||||
|
||||
const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp';
|
||||
|
||||
describe('BitbucketServerUrlReader', () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
[tmpDir]: mockFs.directory(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
|
||||
@@ -19,12 +19,13 @@ import {
|
||||
BitbucketIntegration,
|
||||
readBitbucketIntegrationConfig,
|
||||
} from '@backstage/integration';
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import {
|
||||
createMockDirectory,
|
||||
setupRequestMockHandlers,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { NotModifiedError } from '@backstage/errors';
|
||||
import { BitbucketUrlReader } from './BitbucketUrlReader';
|
||||
@@ -68,6 +69,10 @@ describe('BitbucketUrlReader.factory', () => {
|
||||
});
|
||||
|
||||
describe('BitbucketUrlReader', () => {
|
||||
const mockDir = createMockDirectory({ mockOsTmpDir: true });
|
||||
|
||||
beforeEach(mockDir.clear);
|
||||
|
||||
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: new ConfigReader({}),
|
||||
});
|
||||
@@ -98,18 +103,6 @@ describe('BitbucketUrlReader', () => {
|
||||
{ treeResponseFactory },
|
||||
);
|
||||
|
||||
const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp';
|
||||
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
[tmpDir]: mockFs.directory(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
@@ -391,7 +384,7 @@ describe('BitbucketUrlReader', () => {
|
||||
'https://bitbucket.org/backstage/mock',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: tmpDir });
|
||||
const dir = await response.dir({ targetDir: mockDir.path });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'),
|
||||
@@ -436,7 +429,7 @@ describe('BitbucketUrlReader', () => {
|
||||
'https://bitbucket.org/backstage/mock/src/master/docs',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: tmpDir });
|
||||
const dir = await response.dir({ targetDir: mockDir.path });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'index.md'), 'utf8'),
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import {
|
||||
createMockDirectory,
|
||||
setupRequestMockHandlers,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { NotModifiedError, NotFoundError } from '@backstage/errors';
|
||||
import {
|
||||
@@ -24,7 +27,6 @@ import {
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import mockFs from 'mock-fs';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { getVoidLogger } from '../logging';
|
||||
@@ -33,6 +35,8 @@ import { DefaultReadTreeResponseFactory } from './tree';
|
||||
import { GerritUrlReader } from './GerritUrlReader';
|
||||
import getRawBody from 'raw-body';
|
||||
|
||||
const mockDir = createMockDirectory({ mockOsTmpDir: true });
|
||||
|
||||
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: new ConfigReader({}),
|
||||
});
|
||||
@@ -89,6 +93,8 @@ describe.skip('GerritUrlReader', () => {
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
beforeEach(mockDir.clear);
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@@ -249,14 +255,13 @@ describe.skip('GerritUrlReader', () => {
|
||||
path.resolve(__dirname, '__fixtures__/gerrit/gerrit-master-docs.tar.gz'),
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
'/tmp/': mockFs.directory(),
|
||||
'/tmp/gerrit-clone-123abc/repo/mkdocs.yml': mkdocsContent,
|
||||
'/tmp/gerrit-clone-123abc/repo/docs/first.md': mdContent,
|
||||
beforeEach(async () => {
|
||||
mockDir.setContent({
|
||||
'repo/mkdocs.yml': mkdocsContent,
|
||||
'repo/docs/first.md': mdContent,
|
||||
});
|
||||
const spy = jest.spyOn(fs, 'mkdtemp');
|
||||
spy.mockImplementation(() => '/tmp/gerrit-clone-123abc');
|
||||
spy.mockImplementation(() => mockDir.path);
|
||||
|
||||
worker.use(
|
||||
rest.get(
|
||||
@@ -289,7 +294,6 @@ describe.skip('GerritUrlReader', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
|
||||
@@ -20,12 +20,13 @@ import {
|
||||
GithubIntegration,
|
||||
readGithubIntegrationConfig,
|
||||
} from '@backstage/integration';
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import {
|
||||
createMockDirectory,
|
||||
setupRequestMockHandlers,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { NotFoundError, NotModifiedError } from '@backstage/errors';
|
||||
import {
|
||||
@@ -37,6 +38,8 @@ import {
|
||||
} from './GithubUrlReader';
|
||||
import { DefaultReadTreeResponseFactory } from './tree';
|
||||
|
||||
const mockDir = createMockDirectory({ mockOsTmpDir: true });
|
||||
|
||||
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: new ConfigReader({}),
|
||||
});
|
||||
@@ -69,21 +72,11 @@ const gheProcessor = new GithubUrlReader(
|
||||
{ treeResponseFactory, credentialsProvider: mockCredentialsProvider },
|
||||
);
|
||||
|
||||
const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp';
|
||||
|
||||
describe('GithubUrlReader', () => {
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
[tmpDir]: mockFs.directory(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
beforeEach(mockDir.clear);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -420,7 +413,7 @@ describe('GithubUrlReader', () => {
|
||||
'https://github.com/backstage/mock',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: tmpDir });
|
||||
const dir = await response.dir({ targetDir: mockDir.path });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'),
|
||||
@@ -501,7 +494,7 @@ describe('GithubUrlReader', () => {
|
||||
'https://github.com/backstage/mock/tree/main/docs',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: tmpDir });
|
||||
const dir = await response.dir({ targetDir: mockDir.path });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'index.md'), 'utf8'),
|
||||
|
||||
@@ -15,12 +15,13 @@
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import {
|
||||
createMockDirectory,
|
||||
setupRequestMockHandlers,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { getVoidLogger } from '../logging';
|
||||
import { GitlabUrlReader } from './GitlabUrlReader';
|
||||
@@ -33,6 +34,8 @@ import {
|
||||
|
||||
const logger = getVoidLogger();
|
||||
|
||||
const mockDir = createMockDirectory({ mockOsTmpDir: true });
|
||||
|
||||
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: new ConfigReader({}),
|
||||
});
|
||||
@@ -65,18 +68,8 @@ const hostedGitlabProcessor = new GitlabUrlReader(
|
||||
{ treeResponseFactory },
|
||||
);
|
||||
|
||||
const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp';
|
||||
|
||||
describe('GitlabUrlReader', () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
[tmpDir]: mockFs.directory(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
beforeEach(mockDir.clear);
|
||||
|
||||
const worker = setupServer();
|
||||
setupRequestMockHandlers(worker);
|
||||
@@ -400,7 +393,7 @@ describe('GitlabUrlReader', () => {
|
||||
'https://gitlab.com/backstage/mock',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: tmpDir });
|
||||
const dir = await response.dir({ targetDir: mockDir.path });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'),
|
||||
@@ -457,7 +450,7 @@ describe('GitlabUrlReader', () => {
|
||||
'https://gitlab.com/backstage/mock/tree/main/docs',
|
||||
);
|
||||
|
||||
const dir = await response.dir({ targetDir: tmpDir });
|
||||
const dir = await response.dir({ targetDir: mockDir.path });
|
||||
|
||||
await expect(
|
||||
fs.readFile(path.join(dir, 'index.md'), 'utf8'),
|
||||
|
||||
@@ -15,41 +15,54 @@
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import path, { resolve as resolvePath } from 'path';
|
||||
import path from 'path';
|
||||
import { FromReadableArrayOptions } from '../types';
|
||||
import { ReadableArrayResponse } from './ReadableArrayResponse';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
const path1 = '/file1.yaml';
|
||||
const name1 = 'file1.yaml';
|
||||
const file1 = fs.readFileSync(
|
||||
path.resolve(__filename, '../../__fixtures__/awsS3/awsS3-mock-object.yaml'),
|
||||
);
|
||||
|
||||
const path2 = '/file2.yaml';
|
||||
const name2 = 'file2.yaml';
|
||||
const file2 = fs.readFileSync(
|
||||
path.resolve(__filename, '../../__fixtures__/awsS3/awsS3-mock-object2.yaml'),
|
||||
);
|
||||
|
||||
describe('ReadableArrayResponse', () => {
|
||||
const sourceDir = createMockDirectory();
|
||||
const targetDir = createMockDirectory();
|
||||
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
[path1]: file1,
|
||||
[path2]: file2,
|
||||
'/tmp': mockFs.directory(),
|
||||
sourceDir.setContent({
|
||||
[name1]: file1,
|
||||
[name2]: file2,
|
||||
});
|
||||
targetDir.clear();
|
||||
});
|
||||
|
||||
const openStreams = new Array<fs.ReadStream>();
|
||||
function createReadStream(filePath: string) {
|
||||
const stream = fs.createReadStream(filePath);
|
||||
openStreams.push(stream);
|
||||
return stream;
|
||||
}
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
openStreams.forEach(stream => stream.destroy());
|
||||
openStreams.length = 0;
|
||||
});
|
||||
|
||||
const path1 = sourceDir.resolve(name1);
|
||||
const path2 = sourceDir.resolve(name2);
|
||||
|
||||
it('should read files', async () => {
|
||||
const arr: FromReadableArrayOptions = [
|
||||
{ data: fs.createReadStream(path1), path: path1 },
|
||||
{ data: fs.createReadStream(path2), path: path2 },
|
||||
{ data: createReadStream(path1), path: path1 },
|
||||
{ data: createReadStream(path2), path: path2 },
|
||||
];
|
||||
|
||||
const res = new ReadableArrayResponse(arr, '/tmp', 'etag');
|
||||
const res = new ReadableArrayResponse(arr, targetDir.path, 'etag');
|
||||
const files = await res.files();
|
||||
|
||||
expect(files).toEqual([
|
||||
@@ -57,26 +70,21 @@ describe('ReadableArrayResponse', () => {
|
||||
{ path: path2, content: expect.any(Function) },
|
||||
]);
|
||||
const contents = await Promise.all(files.map(f => f.content()));
|
||||
expect(contents.map(c => c.toString('utf8').trim())).toEqual([
|
||||
'site_name: Test',
|
||||
'site_name: Test2',
|
||||
]);
|
||||
expect(contents).toEqual([file1, file2]);
|
||||
});
|
||||
|
||||
it('should extract entire archive into directory', async () => {
|
||||
const arr: FromReadableArrayOptions = [
|
||||
{ data: fs.createReadStream(path1), path: path1 },
|
||||
{ data: fs.createReadStream(path2), path: path2 },
|
||||
{ data: createReadStream(path1), path: path1 },
|
||||
{ data: createReadStream(path2), path: path2 },
|
||||
];
|
||||
|
||||
const res = new ReadableArrayResponse(arr, '/tmp', 'etag');
|
||||
const res = new ReadableArrayResponse(arr, targetDir.path, 'etag');
|
||||
const dir = await res.dir();
|
||||
|
||||
await expect(
|
||||
fs.readFile(resolvePath(dir, 'file1.yaml'), 'utf8'),
|
||||
).resolves.toMatch(/site_name: Test/);
|
||||
await expect(
|
||||
fs.readFile(resolvePath(dir, 'file2.yaml'), 'utf8'),
|
||||
).resolves.toMatch(/site_name: Test2/);
|
||||
expect(targetDir.content({ path: dir })).toEqual({
|
||||
[name1]: file1.toString('utf8'),
|
||||
[name2]: file2.toString('utf8'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,30 +15,31 @@
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { TarArchiveResponse } from './TarArchiveResponse';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
const archiveData = fs.readFileSync(
|
||||
resolvePath(__filename, '../../__fixtures__/mock-main.tar.gz'),
|
||||
);
|
||||
|
||||
describe('TarArchiveResponse', () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
'/test-archive.tar.gz': archiveData,
|
||||
'/tmp': mockFs.directory(),
|
||||
});
|
||||
});
|
||||
const sourceDir = createMockDirectory();
|
||||
const targetDir = createMockDirectory();
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
beforeAll(() => {
|
||||
sourceDir.setContent({ 'test-archive.tar.gz': archiveData });
|
||||
});
|
||||
beforeEach(() => {
|
||||
targetDir.clear();
|
||||
});
|
||||
|
||||
it('should read files', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.tar.gz');
|
||||
const stream = fs.createReadStream(
|
||||
sourceDir.resolve('test-archive.tar.gz'),
|
||||
);
|
||||
|
||||
const res = new TarArchiveResponse(stream, '', '/tmp', 'etag');
|
||||
const res = new TarArchiveResponse(stream, '', targetDir.path, 'etag');
|
||||
const files = await res.files();
|
||||
|
||||
expect(files).toEqual([
|
||||
@@ -61,10 +62,16 @@ describe('TarArchiveResponse', () => {
|
||||
});
|
||||
|
||||
it('should read files with filter', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.tar.gz');
|
||||
const stream = fs.createReadStream(
|
||||
sourceDir.resolve('test-archive.tar.gz'),
|
||||
);
|
||||
|
||||
const res = new TarArchiveResponse(stream, '', '/tmp', 'etag', path =>
|
||||
path.endsWith('.yml'),
|
||||
const res = new TarArchiveResponse(
|
||||
stream,
|
||||
'',
|
||||
targetDir.path,
|
||||
'etag',
|
||||
path => path.endsWith('.yml'),
|
||||
);
|
||||
const files = await res.files();
|
||||
|
||||
@@ -80,16 +87,18 @@ describe('TarArchiveResponse', () => {
|
||||
});
|
||||
|
||||
it('should read as archive and files', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.tar.gz');
|
||||
const stream = fs.createReadStream(
|
||||
sourceDir.resolve('test-archive.tar.gz'),
|
||||
);
|
||||
|
||||
const res = new TarArchiveResponse(stream, '', '/tmp', 'etag');
|
||||
const res = new TarArchiveResponse(stream, '', targetDir.path, 'etag');
|
||||
const buffer = await res.archive();
|
||||
|
||||
await expect(res.archive()).rejects.toThrow(
|
||||
'Response has already been read',
|
||||
);
|
||||
|
||||
const res2 = new TarArchiveResponse(buffer, '', '/tmp', 'etag');
|
||||
const res2 = new TarArchiveResponse(buffer, '', targetDir.path, 'etag');
|
||||
const files = await res2.files();
|
||||
|
||||
expect(files).toEqual([
|
||||
@@ -112,9 +121,11 @@ describe('TarArchiveResponse', () => {
|
||||
});
|
||||
|
||||
it('should extract entire archive into directory', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.tar.gz');
|
||||
const stream = fs.createReadStream(
|
||||
sourceDir.resolve('test-archive.tar.gz'),
|
||||
);
|
||||
|
||||
const res = new TarArchiveResponse(stream, '', '/tmp', 'etag');
|
||||
const res = new TarArchiveResponse(stream, '', targetDir.path, 'etag');
|
||||
const dir = await res.dir();
|
||||
await expect(
|
||||
fs.readFile(resolvePath(dir, 'mkdocs.yml'), 'utf8'),
|
||||
@@ -125,67 +136,91 @@ describe('TarArchiveResponse', () => {
|
||||
});
|
||||
|
||||
it('should extract archive into directory with a subpath', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.tar.gz');
|
||||
const stream = fs.createReadStream(
|
||||
sourceDir.resolve('test-archive.tar.gz'),
|
||||
);
|
||||
|
||||
const res = new TarArchiveResponse(stream, 'docs', '/tmp', 'etag');
|
||||
const res = new TarArchiveResponse(stream, 'docs', targetDir.path, 'etag');
|
||||
const dir = await res.dir();
|
||||
|
||||
expect(dir).toMatch(/^[\/\\]tmp[\/\\].*$/);
|
||||
await expect(
|
||||
fs.readFile(resolvePath(dir, 'index.md'), 'utf8'),
|
||||
).resolves.toBe('# Test\n');
|
||||
expect(targetDir.content({ path: dir })).toEqual({
|
||||
'index.md': '# Test\n',
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract archive into directory with a subpath and filter', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.tar.gz');
|
||||
|
||||
const res = new TarArchiveResponse(stream, '', '/tmp', 'etag', path =>
|
||||
path.endsWith('.yml'),
|
||||
const stream = fs.createReadStream(
|
||||
sourceDir.resolve('test-archive.tar.gz'),
|
||||
);
|
||||
const dir = await res.dir({ targetDir: '/tmp' });
|
||||
|
||||
expect(dir).toBe('/tmp');
|
||||
await expect(fs.pathExists(resolvePath(dir, 'mkdocs.yml'))).resolves.toBe(
|
||||
true,
|
||||
const res = new TarArchiveResponse(
|
||||
stream,
|
||||
'',
|
||||
targetDir.path,
|
||||
'etag',
|
||||
path => path.endsWith('.yml'),
|
||||
);
|
||||
await expect(
|
||||
fs.pathExists(resolvePath(dir, 'docs/index.md')),
|
||||
).resolves.toBe(false);
|
||||
|
||||
targetDir.addContent({ sub: {} });
|
||||
const dir = await res.dir({ targetDir: targetDir.resolve('sub') });
|
||||
|
||||
expect(dir).toBe(targetDir.resolve('sub'));
|
||||
expect(targetDir.content()).toEqual({
|
||||
sub: {
|
||||
'mkdocs.yml': 'site_name: Test\n',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should leave temporary directories in place in the case of an error', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.tar.gz');
|
||||
it('should clean up temporary directories in place in the case of an error', async () => {
|
||||
const stream = fs.createReadStream(
|
||||
sourceDir.resolve('test-archive.tar.gz'),
|
||||
);
|
||||
|
||||
const res = new TarArchiveResponse(stream, '', '/tmp', 'etag', () => {
|
||||
throw new Error('NOPE');
|
||||
});
|
||||
const res = new TarArchiveResponse(
|
||||
stream,
|
||||
'',
|
||||
targetDir.path,
|
||||
'etag',
|
||||
() => {
|
||||
throw new Error('NOPE');
|
||||
},
|
||||
);
|
||||
|
||||
const tmpDir = await fs.mkdtemp('/tmp/test');
|
||||
// selects the wrong overload by default
|
||||
const mkdtemp = jest.spyOn(fs, 'mkdtemp') as unknown as jest.SpyInstance<
|
||||
Promise<string>,
|
||||
[]
|
||||
>;
|
||||
mkdtemp.mockResolvedValue(tmpDir);
|
||||
targetDir.addContent({ sub: {} });
|
||||
const sub = targetDir.resolve('sub');
|
||||
|
||||
await expect(fs.pathExists(tmpDir)).resolves.toBe(true);
|
||||
const mkdtemp = jest
|
||||
.spyOn(fs, 'mkdtemp')
|
||||
.mockImplementation(async () => sub);
|
||||
|
||||
await expect(fs.pathExists(sub)).resolves.toBe(true);
|
||||
await expect(res.dir()).rejects.toThrow('NOPE');
|
||||
await expect(fs.pathExists(tmpDir)).resolves.toBe(false);
|
||||
await expect(fs.pathExists(sub)).resolves.toBe(false);
|
||||
|
||||
mkdtemp.mockRestore();
|
||||
});
|
||||
|
||||
it('should leave directory in place if provided in the case of an error', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.tar.gz');
|
||||
const stream = fs.createReadStream(
|
||||
sourceDir.resolve('test-archive.tar.gz'),
|
||||
);
|
||||
|
||||
const res = new TarArchiveResponse(stream, '', '/tmp', 'etag', () => {
|
||||
throw new Error('NOPE');
|
||||
});
|
||||
const res = new TarArchiveResponse(
|
||||
stream,
|
||||
'',
|
||||
targetDir.path,
|
||||
'etag',
|
||||
() => {
|
||||
throw new Error('NOPE');
|
||||
},
|
||||
);
|
||||
|
||||
const tmpDir = await fs.mkdtemp('/tmp/test');
|
||||
targetDir.addContent({ sub: {} });
|
||||
const sub = targetDir.resolve('sub');
|
||||
|
||||
await expect(fs.pathExists(tmpDir)).resolves.toBe(true);
|
||||
await expect(res.dir({ targetDir: tmpDir })).rejects.toThrow('NOPE');
|
||||
await expect(fs.pathExists(tmpDir)).resolves.toBe(true);
|
||||
await expect(fs.pathExists(sub)).resolves.toBe(true);
|
||||
await expect(res.dir({ targetDir: sub })).rejects.toThrow('NOPE');
|
||||
await expect(fs.pathExists(sub)).resolves.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import { Readable } from 'stream';
|
||||
import { create as createArchive } from 'archiver';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { ZipArchiveResponse } from './ZipArchiveResponse';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
const archiveData = fs.readFileSync(
|
||||
resolvePath(__filename, '../../__fixtures__/mock-main.zip'),
|
||||
@@ -35,24 +35,36 @@ const archiveWithMaliciousEntry = fs.readFileSync(
|
||||
);
|
||||
|
||||
describe('ZipArchiveResponse', () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
'/test-archive.zip': archiveData,
|
||||
'/test-archive-with-extra-root-dir.zip': archiveDataWithExtraDir,
|
||||
'/test-archive-corrupted.zip': archiveDataCorrupted,
|
||||
'/test-archive-malicious.zip': archiveWithMaliciousEntry,
|
||||
'/tmp': mockFs.directory(),
|
||||
const sourceDir = createMockDirectory();
|
||||
const targetDir = createMockDirectory();
|
||||
|
||||
beforeAll(() => {
|
||||
sourceDir.setContent({
|
||||
'test-archive.zip': archiveData,
|
||||
'test-archive-with-extra-root-dir.zip': archiveDataWithExtraDir,
|
||||
'test-archive-corrupted.zip': archiveDataCorrupted,
|
||||
'test-archive-malicious.zip': archiveWithMaliciousEntry,
|
||||
});
|
||||
});
|
||||
beforeEach(() => {
|
||||
targetDir.clear();
|
||||
});
|
||||
|
||||
const openStreams = new Array<fs.ReadStream>();
|
||||
function createReadStream(filePath: string) {
|
||||
const stream = fs.createReadStream(filePath);
|
||||
openStreams.push(stream);
|
||||
return stream;
|
||||
}
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
openStreams.forEach(stream => stream.destroy());
|
||||
openStreams.length = 0;
|
||||
});
|
||||
|
||||
it('should read files', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.zip');
|
||||
const stream = createReadStream(sourceDir.resolve('test-archive.zip'));
|
||||
|
||||
const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag');
|
||||
const res = new ZipArchiveResponse(stream, '', targetDir.path, 'etag');
|
||||
const files = await res.files();
|
||||
|
||||
expect(files).toEqual([
|
||||
@@ -76,10 +88,14 @@ describe('ZipArchiveResponse', () => {
|
||||
});
|
||||
|
||||
it('should read files with filter', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.zip');
|
||||
const stream = createReadStream(sourceDir.resolve('test-archive.zip'));
|
||||
|
||||
const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag', path =>
|
||||
path.endsWith('.yml'),
|
||||
const res = new ZipArchiveResponse(
|
||||
stream,
|
||||
'',
|
||||
targetDir.path,
|
||||
'etag',
|
||||
path => path.endsWith('.yml'),
|
||||
);
|
||||
const files = await res.files();
|
||||
|
||||
@@ -95,16 +111,16 @@ describe('ZipArchiveResponse', () => {
|
||||
});
|
||||
|
||||
it('should read as archive and files', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.zip');
|
||||
const stream = createReadStream(sourceDir.resolve('test-archive.zip'));
|
||||
|
||||
const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag');
|
||||
const res = new ZipArchiveResponse(stream, '', targetDir.path, 'etag');
|
||||
const buffer = await res.archive();
|
||||
|
||||
await expect(res.archive()).rejects.toThrow(
|
||||
'Response has already been read',
|
||||
);
|
||||
|
||||
const res2 = new ZipArchiveResponse(buffer, '', '/tmp', 'etag');
|
||||
const res2 = new ZipArchiveResponse(buffer, '', targetDir.path, 'etag');
|
||||
const files = await res2.files();
|
||||
|
||||
expect(files).toEqual([
|
||||
@@ -127,9 +143,9 @@ describe('ZipArchiveResponse', () => {
|
||||
});
|
||||
|
||||
it('should extract entire archive into directory', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.zip');
|
||||
const stream = createReadStream(sourceDir.resolve('test-archive.zip'));
|
||||
|
||||
const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag');
|
||||
const res = new ZipArchiveResponse(stream, '', targetDir.path, 'etag');
|
||||
const dir = await res.dir();
|
||||
|
||||
await expect(
|
||||
@@ -141,38 +157,45 @@ describe('ZipArchiveResponse', () => {
|
||||
});
|
||||
|
||||
it('should extract archive into directory with a subpath', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.zip');
|
||||
const stream = createReadStream(sourceDir.resolve('test-archive.zip'));
|
||||
|
||||
const res = new ZipArchiveResponse(stream, 'docs/', targetDir.path, 'etag');
|
||||
|
||||
const res = new ZipArchiveResponse(stream, 'docs/', '/tmp', 'etag');
|
||||
const dir = await res.dir();
|
||||
expect(dir).toMatch(/^[\/\\]tmp[\/\\].*$/);
|
||||
await expect(
|
||||
fs.readFile(resolvePath(dir, 'index.md'), 'utf8'),
|
||||
).resolves.toBe('# Test\n');
|
||||
expect(targetDir.content({ path: dir })).toEqual({
|
||||
'index.md': '# Test\n',
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract archive into directory with a subpath and filter', async () => {
|
||||
const stream = fs.createReadStream('/test-archive.zip');
|
||||
const stream = createReadStream(sourceDir.resolve('test-archive.zip'));
|
||||
|
||||
const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag', path =>
|
||||
path.endsWith('.yml'),
|
||||
const res = new ZipArchiveResponse(
|
||||
stream,
|
||||
'',
|
||||
targetDir.path,
|
||||
'etag',
|
||||
path => path.endsWith('.yml'),
|
||||
);
|
||||
const dir = await res.dir({ targetDir: '/tmp' });
|
||||
|
||||
expect(dir).toBe('/tmp');
|
||||
await expect(fs.pathExists(resolvePath(dir, 'mkdocs.yml'))).resolves.toBe(
|
||||
true,
|
||||
);
|
||||
await expect(
|
||||
fs.pathExists(resolvePath(dir, 'docs/index.md')),
|
||||
).resolves.toBe(false);
|
||||
targetDir.addContent({ sub: {} });
|
||||
const sub = targetDir.resolve('sub');
|
||||
const dir = await res.dir({ targetDir: sub });
|
||||
|
||||
expect(dir).toBe(sub);
|
||||
|
||||
expect(targetDir.content()).toEqual({
|
||||
sub: {
|
||||
'mkdocs.yml': 'site_name: Test\n',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract a large archive', async () => {
|
||||
const fileCount = 10;
|
||||
const fileSize = 1000 * 1000;
|
||||
const filePath = await new Promise<string>((resolve, reject) => {
|
||||
const outFile = '/large-archive.zip';
|
||||
const outFile = targetDir.resolve('large-archive.zip');
|
||||
const archive = createArchive('zip');
|
||||
|
||||
archive.on('error', reject);
|
||||
@@ -193,14 +216,16 @@ describe('ZipArchiveResponse', () => {
|
||||
archive.finalize();
|
||||
});
|
||||
|
||||
const stream = fs.createReadStream(filePath);
|
||||
const stream = createReadStream(filePath);
|
||||
|
||||
const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag');
|
||||
const res = new ZipArchiveResponse(stream, '', targetDir.path, 'etag');
|
||||
|
||||
targetDir.addContent({ sub: {} });
|
||||
const sub = targetDir.resolve('sub');
|
||||
const dir = await res.dir({
|
||||
targetDir: '/out',
|
||||
targetDir: sub,
|
||||
});
|
||||
|
||||
expect(dir).toBe('/out');
|
||||
const files = await fs.readdir(dir);
|
||||
expect(files).toHaveLength(fileCount);
|
||||
|
||||
@@ -211,9 +236,11 @@ describe('ZipArchiveResponse', () => {
|
||||
});
|
||||
|
||||
it('should throw on invalid archive', async () => {
|
||||
const stream = fs.createReadStream('/test-archive-corrupted.zip');
|
||||
const stream = createReadStream(
|
||||
sourceDir.resolve('test-archive-corrupted.zip'),
|
||||
);
|
||||
|
||||
const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag');
|
||||
const res = new ZipArchiveResponse(stream, '', targetDir.path, 'etag');
|
||||
const filesPromise = res.files();
|
||||
|
||||
await expect(filesPromise).rejects.toThrow(
|
||||
@@ -222,18 +249,22 @@ describe('ZipArchiveResponse', () => {
|
||||
});
|
||||
|
||||
it('should throw on entries with a path outside the destination dir', async () => {
|
||||
const stream = fs.createReadStream('/test-archive-malicious.zip');
|
||||
const stream = createReadStream(
|
||||
sourceDir.resolve('test-archive-malicious.zip'),
|
||||
);
|
||||
|
||||
const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag');
|
||||
const res = new ZipArchiveResponse(stream, '', targetDir.path, 'etag');
|
||||
await expect(res.files()).rejects.toThrow(
|
||||
'invalid relative path: ../side.txt',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw on entries that attempt to write outside destination dir', async () => {
|
||||
const stream = fs.createReadStream('/test-archive-malicious.zip');
|
||||
const stream = createReadStream(
|
||||
sourceDir.resolve('test-archive-malicious.zip'),
|
||||
);
|
||||
|
||||
const res = new ZipArchiveResponse(stream, '', '/tmp', 'etag');
|
||||
const res = new ZipArchiveResponse(stream, '', targetDir.path, 'etag');
|
||||
await expect(res.dir()).rejects.toThrow(
|
||||
'invalid relative path: ../side.txt',
|
||||
);
|
||||
|
||||
@@ -93,9 +93,13 @@ export class ZipArchiveResponse implements ReadTreeResponse {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
writeStream.on('error', reject);
|
||||
writeStream.on('finish', () =>
|
||||
resolve({ fileName: tmpFile, cleanup: () => fs.remove(tmpFile) }),
|
||||
);
|
||||
writeStream.on('finish', () => {
|
||||
writeStream.end();
|
||||
resolve({
|
||||
fileName: tmpFile,
|
||||
cleanup: () => fs.rm(tmpDir, { recursive: true }),
|
||||
});
|
||||
});
|
||||
stream.pipe(writeStream);
|
||||
});
|
||||
}
|
||||
@@ -152,7 +156,7 @@ export class ZipArchiveResponse implements ReadTreeResponse {
|
||||
});
|
||||
});
|
||||
|
||||
temporary.cleanup();
|
||||
await temporary.cleanup();
|
||||
|
||||
return files;
|
||||
}
|
||||
@@ -175,7 +179,7 @@ export class ZipArchiveResponse implements ReadTreeResponse {
|
||||
|
||||
archive.finalize();
|
||||
|
||||
temporary.cleanup();
|
||||
await temporary.cleanup();
|
||||
|
||||
return archive;
|
||||
}
|
||||
@@ -204,7 +208,7 @@ export class ZipArchiveResponse implements ReadTreeResponse {
|
||||
});
|
||||
});
|
||||
|
||||
temporary.cleanup();
|
||||
await temporary.cleanup();
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
@@ -14,27 +14,24 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import Docker from 'dockerode';
|
||||
import mockFs from 'mock-fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import Stream, { PassThrough } from 'stream';
|
||||
import { ContainerRunner } from './ContainerRunner';
|
||||
import { DockerContainerRunner, UserOptions } from './DockerContainerRunner';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
const mockDocker = new Docker() as jest.Mocked<Docker>;
|
||||
const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
|
||||
|
||||
describe('DockerContainerRunner', () => {
|
||||
let containerTaskApi: ContainerRunner;
|
||||
|
||||
const inputDir = createMockDirectory();
|
||||
const outputDir = createMockDirectory();
|
||||
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
[rootDir]: {
|
||||
input: mockFs.directory(),
|
||||
output: mockFs.directory(),
|
||||
},
|
||||
});
|
||||
inputDir.clear();
|
||||
outputDir.clear();
|
||||
|
||||
jest.spyOn(mockDocker, 'pull').mockImplementation((async (
|
||||
_image: string,
|
||||
@@ -59,16 +56,15 @@ describe('DockerContainerRunner', () => {
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
const imageName = 'dockerOrg/image';
|
||||
const args = ['bash', '-c', 'echo test'];
|
||||
const mountDirs = {
|
||||
[path.join(rootDir, 'input')]: '/input',
|
||||
[path.join(rootDir, 'output')]: '/output',
|
||||
[inputDir.path]: '/input',
|
||||
[outputDir.path]: '/output',
|
||||
};
|
||||
const workingDir = path.join(rootDir, 'input');
|
||||
const workingDir = inputDir.path;
|
||||
const envVars = { HOME: '/tmp', LOG_LEVEL: 'debug' };
|
||||
const envVarsArray = ['HOME=/tmp', 'LOG_LEVEL=debug'];
|
||||
|
||||
@@ -117,8 +113,8 @@ describe('DockerContainerRunner', () => {
|
||||
HostConfig: {
|
||||
AutoRemove: true,
|
||||
Binds: expect.arrayContaining([
|
||||
`${path.join(rootDir, 'input')}:/input`,
|
||||
`${path.join(rootDir, 'output')}:/output`,
|
||||
`${await fs.realpath(inputDir.path)}:/input`,
|
||||
`${await fs.realpath(outputDir.path)}:/output`,
|
||||
]),
|
||||
},
|
||||
Volumes: {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
```ts
|
||||
/// <reference types="jest" />
|
||||
/// <reference types="node" />
|
||||
|
||||
import { Backend } from '@backstage/backend-app-api';
|
||||
import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
@@ -30,9 +31,44 @@ import { ServiceRef } from '@backstage/backend-plugin-api';
|
||||
import { TokenManagerService } from '@backstage/backend-plugin-api';
|
||||
import { UrlReaderService } from '@backstage/backend-plugin-api';
|
||||
|
||||
// @public
|
||||
export function createMockDirectory(
|
||||
options?: MockDirectoryOptions,
|
||||
): MockDirectory;
|
||||
|
||||
// @public (undocumented)
|
||||
export function isDockerDisabledForTests(): boolean;
|
||||
|
||||
// @public
|
||||
export interface MockDirectory {
|
||||
addContent(root: MockDirectoryContent): void;
|
||||
clear(): void;
|
||||
content(
|
||||
options?: MockDirectoryContentOptions,
|
||||
): MockDirectoryContent | undefined;
|
||||
readonly path: string;
|
||||
remove(): void;
|
||||
resolve(...paths: string[]): string;
|
||||
setContent(root: MockDirectoryContent): void;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type MockDirectoryContent = {
|
||||
[name in string]: MockDirectoryContent | string | Buffer;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface MockDirectoryContentOptions {
|
||||
path?: string;
|
||||
shouldReadAsText?: boolean | ((path: string, buffer: Buffer) => boolean);
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface MockDirectoryOptions {
|
||||
content?: MockDirectoryContent;
|
||||
mockOsTmpDir?: boolean;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export namespace mockServices {
|
||||
// (undocumented)
|
||||
|
||||
@@ -46,15 +46,18 @@
|
||||
"@backstage/backend-common": "workspace:^",
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/plugin-auth-node": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"better-sqlite3": "^8.0.0",
|
||||
"express": "^4.17.1",
|
||||
"fs-extra": "^10.0.1",
|
||||
"knex": "^2.0.0",
|
||||
"msw": "^1.0.0",
|
||||
"mysql2": "^2.2.5",
|
||||
"pg": "^8.3.0",
|
||||
"testcontainers": "^8.1.2",
|
||||
"textextensions": "^5.16.0",
|
||||
"uuid": "^8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import os from 'os';
|
||||
import { join as joinPath, relative as relativePath } from 'path';
|
||||
import { createMockDirectory, MockDirectory } from './MockDirectory';
|
||||
|
||||
describe('createMockDirectory', () => {
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
beforeEach(mockDir.clear);
|
||||
|
||||
it('should resolve paths', () => {
|
||||
expect(mockDir.path).toEqual(expect.any(String));
|
||||
expect(relativePath(mockDir.path, mockDir.resolve('a'))).toBe('a');
|
||||
expect(relativePath(mockDir.path, mockDir.resolve('a/b/c'))).toBe(
|
||||
joinPath('a', 'b', 'c'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove itself', async () => {
|
||||
await expect(fs.pathExists(mockDir.path)).resolves.toBe(true);
|
||||
mockDir.remove();
|
||||
await expect(fs.pathExists(mockDir.path)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should populate a directory with text files', () => {
|
||||
mockDir.setContent({
|
||||
'a.txt': 'a',
|
||||
'a/b.txt': 'b',
|
||||
'a/b/c.txt': 'c',
|
||||
'a/b/d.txt': 'd',
|
||||
});
|
||||
|
||||
expect(mockDir.content()).toEqual({
|
||||
'a.txt': 'a',
|
||||
a: {
|
||||
'b.txt': 'b',
|
||||
b: {
|
||||
'c.txt': 'c',
|
||||
'd.txt': 'd',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should mix text and binary files', () => {
|
||||
mockDir.setContent({
|
||||
'a.txt': 'a',
|
||||
'a/b.txt': 'b',
|
||||
'a/b/c.bin': Buffer.from([0xc]),
|
||||
'a/b/d.bin': Buffer.from([0xd]),
|
||||
});
|
||||
|
||||
expect(mockDir.content()).toEqual({
|
||||
'a.txt': 'a',
|
||||
a: {
|
||||
'b.txt': 'b',
|
||||
b: {
|
||||
'c.bin': Buffer.from([0xc]),
|
||||
'd.bin': Buffer.from([0xd]),
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to add content', () => {
|
||||
mockDir.setContent({
|
||||
'a.txt': 'a',
|
||||
b: {},
|
||||
});
|
||||
|
||||
expect(mockDir.content()).toEqual({
|
||||
'a.txt': 'a',
|
||||
b: {},
|
||||
});
|
||||
|
||||
mockDir.addContent({
|
||||
'b.txt': 'b',
|
||||
b: {
|
||||
'c.txt': 'c',
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockDir.content()).toEqual({
|
||||
'a.txt': 'a',
|
||||
'b.txt': 'b',
|
||||
b: {
|
||||
'c.txt': 'c',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should replace existing files', () => {
|
||||
mockDir.setContent({
|
||||
'a.txt': 'a',
|
||||
});
|
||||
|
||||
mockDir.addContent({
|
||||
'a.txt': 'a2',
|
||||
});
|
||||
|
||||
expect(mockDir.content()).toEqual({
|
||||
'a.txt': 'a2',
|
||||
});
|
||||
});
|
||||
|
||||
it('should read content from sub dirs', () => {
|
||||
mockDir.setContent({
|
||||
'a.txt': 'a',
|
||||
'b/b.txt': 'b',
|
||||
'b/c/c.txt': 'c',
|
||||
});
|
||||
|
||||
const expected = {
|
||||
'a.txt': 'a',
|
||||
b: {
|
||||
'b.txt': 'b',
|
||||
c: {
|
||||
'c.txt': 'c',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(mockDir.content()).toEqual(expected);
|
||||
expect(mockDir.content({ path: mockDir.path })).toEqual(expected);
|
||||
expect(mockDir.content({ path: mockDir.resolve('.') })).toEqual(expected);
|
||||
expect(mockDir.content({ path: 'b' })).toEqual(expected.b);
|
||||
expect(mockDir.content({ path: './b' })).toEqual(expected.b);
|
||||
expect(mockDir.content({ path: mockDir.resolve('b') })).toEqual(expected.b);
|
||||
expect(mockDir.content({ path: 'b/c' })).toEqual(expected.b.c);
|
||||
expect(mockDir.content({ path: './b/c' })).toEqual(expected.b.c);
|
||||
expect(mockDir.content({ path: mockDir.resolve('b/c') })).toEqual(
|
||||
expected.b.c,
|
||||
);
|
||||
expect(mockDir.content({ path: mockDir.resolve('b', 'c') })).toEqual(
|
||||
expected.b.c,
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow text reading to be configured', () => {
|
||||
const text = 'a';
|
||||
const binary = Buffer.from('a', 'utf8');
|
||||
|
||||
mockDir.setContent({
|
||||
a: binary,
|
||||
'a.txt': text,
|
||||
'a.bin': binary,
|
||||
});
|
||||
|
||||
expect(mockDir.content()).toEqual({
|
||||
a: binary,
|
||||
'a.txt': text,
|
||||
'a.bin': binary,
|
||||
});
|
||||
|
||||
expect(mockDir.content({ shouldReadAsText: false })).toEqual({
|
||||
a: binary,
|
||||
'a.txt': binary,
|
||||
'a.bin': binary,
|
||||
});
|
||||
|
||||
expect(mockDir.content({ shouldReadAsText: true })).toEqual({
|
||||
a: text,
|
||||
'a.txt': text,
|
||||
'a.bin': text,
|
||||
});
|
||||
|
||||
expect(
|
||||
mockDir.content({ shouldReadAsText: path => path.length > 3 }),
|
||||
).toEqual({
|
||||
a: binary,
|
||||
'a.txt': text,
|
||||
'a.bin': text,
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide a posix path to shouldReadAsText', () => {
|
||||
const shouldReadAsText = jest.fn().mockReturnValue(true);
|
||||
|
||||
mockDir.setContent({ 'a/b/c': 'c' });
|
||||
|
||||
expect(mockDir.content({ shouldReadAsText })).toEqual({
|
||||
a: { b: { c: 'c' } },
|
||||
});
|
||||
expect(shouldReadAsText).toHaveBeenCalledWith(
|
||||
'a/b/c',
|
||||
Buffer.from('c', 'utf8'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should not override directories', () => {
|
||||
mockDir.setContent({
|
||||
'a.txt': 'a',
|
||||
b: {},
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
mockDir.addContent({
|
||||
'a.txt': {},
|
||||
}),
|
||||
).toThrow('EEXIST');
|
||||
|
||||
expect(() =>
|
||||
mockDir.addContent({
|
||||
b: 'b',
|
||||
}),
|
||||
).toThrow('EISDIR');
|
||||
});
|
||||
|
||||
it('examples should work', () => {
|
||||
mockDir.setContent({
|
||||
'test.txt': 'content',
|
||||
'sub-dir': {
|
||||
'file.txt': 'content',
|
||||
'nested-dir/file.txt': 'content',
|
||||
},
|
||||
'empty-dir': {},
|
||||
'binary-file': Buffer.from([0, 1, 2]),
|
||||
});
|
||||
|
||||
mockDir.addContent({
|
||||
'test.txt': 'content',
|
||||
'sub-dir': {
|
||||
'file.txt': 'content',
|
||||
'nested-dir/file.txt': 'content',
|
||||
},
|
||||
'empty-dir': {},
|
||||
'binary-file': Buffer.from([0, 1, 2]),
|
||||
});
|
||||
|
||||
expect(mockDir.content()).toEqual({
|
||||
'test.txt': 'content',
|
||||
'sub-dir': {
|
||||
'file.txt': 'content',
|
||||
'nested-dir': {
|
||||
'file.txt': 'content',
|
||||
},
|
||||
},
|
||||
'empty-dir': {},
|
||||
'binary-file': Buffer.from([0, 1, 2]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject non-child paths', () => {
|
||||
const path = mockDir.resolve('/root/a.txt');
|
||||
expect(() => mockDir.setContent({ '/root/a.txt': 'a' })).toThrow(
|
||||
`Provided path must resolve to a child path of the mock directory, got '${path}'`,
|
||||
);
|
||||
expect(() => mockDir.addContent({ '/root/a.txt': 'a' })).toThrow(
|
||||
`Provided path must resolve to a child path of the mock directory, got '${path}'`,
|
||||
);
|
||||
expect(() => mockDir.content({ path: '/root/a.txt' })).toThrow(
|
||||
`Provided path must resolve to a child path of the mock directory, got '${path}'`,
|
||||
);
|
||||
});
|
||||
|
||||
describe('tmpdir mock', () => {
|
||||
let tmpDirMock: MockDirectory;
|
||||
|
||||
describe('inner', () => {
|
||||
tmpDirMock = createMockDirectory({ mockOsTmpDir: true });
|
||||
|
||||
it('should mock os.tmpdir()', () => {
|
||||
expect(os.tmpdir()).toBe(tmpDirMock.path);
|
||||
});
|
||||
|
||||
it('should refuce to mock os.tmpdir() again', () => {
|
||||
expect(() => createMockDirectory({ mockOsTmpDir: true })).toThrow(
|
||||
'Cannot mock os.tmpdir() when it has already been mocked',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should restore os.tmpdir()', () => {
|
||||
expect(os.tmpdir()).not.toBe(tmpDirMock.path);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,391 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import os from 'os';
|
||||
import { isChildPath } from '@backstage/backend-common';
|
||||
import fs from 'fs-extra';
|
||||
import textextensions from 'textextensions';
|
||||
import {
|
||||
dirname,
|
||||
extname,
|
||||
join as joinPath,
|
||||
resolve as resolvePath,
|
||||
relative as relativePath,
|
||||
win32,
|
||||
posix,
|
||||
} from 'path';
|
||||
|
||||
const tmpdirMarker = Symbol('os-tmpdir-mock');
|
||||
|
||||
/**
|
||||
* The content of a mock directory represented by a nested object structure.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* When used as input, the keys may contain forward slashes to indicate nested directories.
|
||||
* Then returned as output, each directory will always be represented as a separate object.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* {
|
||||
* 'test.txt': 'content',
|
||||
* 'sub-dir': {
|
||||
* 'file.txt': 'content',
|
||||
* 'nested-dir/file.txt': 'content',
|
||||
* },
|
||||
* 'empty-dir': {},
|
||||
* 'binary-file': Buffer.from([0, 1, 2]),
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type MockDirectoryContent = {
|
||||
[name in string]: MockDirectoryContent | string | Buffer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for {@link MockDirectory.content}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface MockDirectoryContentOptions {
|
||||
/**
|
||||
* The path to read content from. Defaults to the root of the mock directory.
|
||||
*
|
||||
* An absolute path can also be provided, as long as it is a child path of the mock directory.
|
||||
*/
|
||||
path?: string;
|
||||
|
||||
/**
|
||||
* Whether or not to return files as text rather than buffers.
|
||||
*
|
||||
* Defaults to checking the file extension against a list of known text extensions.
|
||||
*/
|
||||
shouldReadAsText?: boolean | ((path: string, buffer: Buffer) => boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* A utility for creating a mock directory that is automatically cleaned up.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface MockDirectory {
|
||||
/**
|
||||
* The path to the root of the mock directory
|
||||
*/
|
||||
readonly path: string;
|
||||
|
||||
/**
|
||||
* Resolves a path relative to the root of the mock directory.
|
||||
*/
|
||||
resolve(...paths: string[]): string;
|
||||
|
||||
/**
|
||||
* Sets the content of the mock directory. This will remove any existing content.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* mockDir.setContent({
|
||||
* 'test.txt': 'content',
|
||||
* 'sub-dir': {
|
||||
* 'file.txt': 'content',
|
||||
* 'nested-dir/file.txt': 'content',
|
||||
* },
|
||||
* 'empty-dir': {},
|
||||
* 'binary-file': Buffer.from([0, 1, 2]),
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
setContent(root: MockDirectoryContent): void;
|
||||
|
||||
/**
|
||||
* Adds content of the mock directory. This will overwrite existing files.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* mockDir.addContent({
|
||||
* 'test.txt': 'content',
|
||||
* 'sub-dir': {
|
||||
* 'file.txt': 'content',
|
||||
* 'nested-dir/file.txt': 'content',
|
||||
* },
|
||||
* 'empty-dir': {},
|
||||
* 'binary-file': Buffer.from([0, 1, 2]),
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
addContent(root: MockDirectoryContent): void;
|
||||
|
||||
/**
|
||||
* Reads the content of the mock directory.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Text files will be returned as strings, while binary files will be returned as buffers.
|
||||
* By default the file extension is used to determine whether a file should be read as text.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* expect(mockDir.content()).toEqual({
|
||||
* 'test.txt': 'content',
|
||||
* 'sub-dir': {
|
||||
* 'file.txt': 'content',
|
||||
* 'nested-dir': {
|
||||
* 'file.txt': 'content',
|
||||
* },
|
||||
* },
|
||||
* 'empty-dir': {},
|
||||
* 'binary-file': Buffer.from([0, 1, 2]),
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
content(
|
||||
options?: MockDirectoryContentOptions,
|
||||
): MockDirectoryContent | undefined;
|
||||
|
||||
/**
|
||||
* Clears the content of the mock directory, ensuring that the directory itself exists.
|
||||
*/
|
||||
clear(): void;
|
||||
|
||||
/**
|
||||
* Removes the mock directory and all its contents.
|
||||
*/
|
||||
remove(): void;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
type MockEntry =
|
||||
| {
|
||||
type: 'file';
|
||||
path: string;
|
||||
content: Buffer;
|
||||
}
|
||||
| {
|
||||
type: 'dir';
|
||||
path: string;
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
class MockDirectoryImpl {
|
||||
readonly #root: string;
|
||||
|
||||
constructor(root: string) {
|
||||
this.#root = root;
|
||||
}
|
||||
|
||||
get path(): string {
|
||||
return this.#root;
|
||||
}
|
||||
|
||||
resolve(...paths: string[]): string {
|
||||
return resolvePath(this.#root, ...paths);
|
||||
}
|
||||
|
||||
setContent(root: MockDirectoryContent): void {
|
||||
this.remove();
|
||||
|
||||
return this.addContent(root);
|
||||
}
|
||||
|
||||
addContent(root: MockDirectoryContent): void {
|
||||
const entries = this.#transformInput(root);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = resolvePath(this.#root, entry.path);
|
||||
if (!isChildPath(this.#root, fullPath)) {
|
||||
throw new Error(
|
||||
`Provided path must resolve to a child path of the mock directory, got '${fullPath}'`,
|
||||
);
|
||||
}
|
||||
|
||||
if (entry.type === 'dir') {
|
||||
fs.ensureDirSync(fullPath, { mode: 0o777 });
|
||||
} else if (entry.type === 'file') {
|
||||
fs.ensureDirSync(dirname(fullPath), { mode: 0o777 });
|
||||
fs.writeFileSync(fullPath, entry.content, { mode: 0o666 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content(
|
||||
options?: MockDirectoryContentOptions,
|
||||
): MockDirectoryContent | undefined {
|
||||
const shouldReadAsText =
|
||||
(typeof options?.shouldReadAsText === 'boolean'
|
||||
? () => options?.shouldReadAsText
|
||||
: options?.shouldReadAsText) ??
|
||||
((path: string) => textextensions.includes(extname(path).slice(1)));
|
||||
|
||||
const root = resolvePath(this.#root, options?.path ?? '');
|
||||
if (!isChildPath(this.#root, root)) {
|
||||
throw new Error(
|
||||
`Provided path must resolve to a child path of the mock directory, got '${root}'`,
|
||||
);
|
||||
}
|
||||
|
||||
function read(path: string): MockDirectoryContent | undefined {
|
||||
if (!fs.pathExistsSync(path)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(path, { withFileTypes: true });
|
||||
return Object.fromEntries(
|
||||
entries.map(entry => {
|
||||
const fullPath = resolvePath(path, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
return [entry.name, read(fullPath)];
|
||||
}
|
||||
const content = fs.readFileSync(fullPath);
|
||||
const relativePosixPath = relativePath(root, fullPath)
|
||||
.split(win32.sep)
|
||||
.join(posix.sep);
|
||||
|
||||
if (shouldReadAsText(relativePosixPath, content)) {
|
||||
return [entry.name, content.toString('utf8')];
|
||||
}
|
||||
return [entry.name, content];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return read(root);
|
||||
}
|
||||
|
||||
clear = (): void => {
|
||||
this.setContent({});
|
||||
};
|
||||
|
||||
remove = (): void => {
|
||||
fs.removeSync(this.#root);
|
||||
};
|
||||
|
||||
#transformInput(input: MockDirectoryContent[string]): MockEntry[] {
|
||||
const entries: MockEntry[] = [];
|
||||
|
||||
function traverse(node: MockDirectoryContent[string], path: string) {
|
||||
const trimmedPath = path.startsWith('/') ? path.slice(1) : path; // trim leading slash
|
||||
if (typeof node === 'string') {
|
||||
entries.push({
|
||||
type: 'file',
|
||||
path: trimmedPath,
|
||||
content: Buffer.from(node, 'utf8'),
|
||||
});
|
||||
} else if (node instanceof Buffer) {
|
||||
entries.push({ type: 'file', path: trimmedPath, content: node });
|
||||
} else {
|
||||
entries.push({ type: 'dir', path: trimmedPath });
|
||||
for (const [name, child] of Object.entries(node)) {
|
||||
traverse(child, `${trimmedPath}/${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverse(input, '');
|
||||
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for {@link createMockDirectory}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface MockDirectoryOptions {
|
||||
/**
|
||||
* In addition to creating a temporary directory, also mock `os.tmpdir()` to return the
|
||||
* mock directory path until the end of the test suite.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
mockOsTmpDir?: boolean;
|
||||
|
||||
/**
|
||||
* Initializes the directory with the given content, see {@link MockDirectory.setContent}.
|
||||
*/
|
||||
content?: MockDirectoryContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new temporary mock directory that will be removed after the tests have completed.
|
||||
*
|
||||
* @public
|
||||
* @remarks
|
||||
*
|
||||
* This method is intended to be called outside of any test, either at top-level or
|
||||
* within a `describe` block. It will call `afterAll` to make sure that the mock directory
|
||||
* is removed after the tests have run.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* describe('MySubject', () => {
|
||||
* const mockDir = createMockDirectory();
|
||||
*
|
||||
* beforeEach(mockDir.clear);
|
||||
*
|
||||
* it('should work', () => {
|
||||
* // ... use mockDir
|
||||
* })
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function createMockDirectory(
|
||||
options?: MockDirectoryOptions,
|
||||
): MockDirectory {
|
||||
const tmpDir = process.env.RUNNER_TEMP || os.tmpdir(); // GitHub Actions
|
||||
const root = fs.mkdtempSync(joinPath(tmpDir, 'backstage-tmp-test-dir-'));
|
||||
|
||||
const mocker = new MockDirectoryImpl(root);
|
||||
|
||||
const origTmpdir = options?.mockOsTmpDir ? os.tmpdir : undefined;
|
||||
if (origTmpdir) {
|
||||
if (Object.hasOwn(origTmpdir, tmpdirMarker)) {
|
||||
throw new Error(
|
||||
'Cannot mock os.tmpdir() when it has already been mocked',
|
||||
);
|
||||
}
|
||||
const mock = Object.assign(() => mocker.path, { [tmpdirMarker]: true });
|
||||
os.tmpdir = mock;
|
||||
}
|
||||
|
||||
// In CI we expect there to be no need to clean up temporary directories
|
||||
const needsCleanup = !process.env.CI;
|
||||
if (needsCleanup) {
|
||||
process.on('beforeExit', mocker.remove);
|
||||
}
|
||||
|
||||
try {
|
||||
afterAll(() => {
|
||||
if (origTmpdir) {
|
||||
os.tmpdir = origTmpdir;
|
||||
}
|
||||
if (needsCleanup) {
|
||||
mocker.remove();
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
|
||||
if (options?.content) {
|
||||
mocker.setContent(options.content);
|
||||
}
|
||||
|
||||
return mocker;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export {
|
||||
createMockDirectory,
|
||||
type MockDirectory,
|
||||
type MockDirectoryOptions,
|
||||
type MockDirectoryContent,
|
||||
type MockDirectoryContentOptions,
|
||||
} from './MockDirectory';
|
||||
@@ -22,5 +22,6 @@
|
||||
|
||||
export * from './database';
|
||||
export * from './msw';
|
||||
export * from './filesystem';
|
||||
export * from './next';
|
||||
export * from './util';
|
||||
|
||||
@@ -3464,7 +3464,6 @@ __metadata:
|
||||
"@types/http-errors": ^2.0.0
|
||||
"@types/luxon": ^3.0.0
|
||||
"@types/minimist": ^1.2.0
|
||||
"@types/mock-fs": ^4.13.0
|
||||
"@types/morgan": ^1.9.0
|
||||
"@types/node-forge": ^1.3.0
|
||||
"@types/pg": ^8.6.6
|
||||
@@ -3497,7 +3496,6 @@ __metadata:
|
||||
luxon: ^3.0.0
|
||||
minimatch: ^5.0.0
|
||||
minimist: ^1.2.5
|
||||
mock-fs: ^5.2.0
|
||||
morgan: ^1.10.0
|
||||
msw: ^1.0.0
|
||||
mysql2: ^2.2.5
|
||||
@@ -3645,17 +3643,20 @@ __metadata:
|
||||
"@backstage/backend-plugin-api": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/errors": "workspace:^"
|
||||
"@backstage/plugin-auth-node": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
"@types/supertest": ^2.0.8
|
||||
better-sqlite3: ^8.0.0
|
||||
express: ^4.17.1
|
||||
fs-extra: ^10.0.1
|
||||
knex: ^2.0.0
|
||||
msw: ^1.0.0
|
||||
mysql2: ^2.2.5
|
||||
pg: ^8.3.0
|
||||
supertest: ^6.1.3
|
||||
testcontainers: ^8.1.2
|
||||
textextensions: ^5.16.0
|
||||
uuid: ^8.0.0
|
||||
peerDependencies:
|
||||
"@types/jest": "*"
|
||||
@@ -40465,10 +40466,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"textextensions@npm:^5.12.0, textextensions@npm:^5.13.0":
|
||||
version: 5.14.0
|
||||
resolution: "textextensions@npm:5.14.0"
|
||||
checksum: 1f610ccf2a2c1445fb7156c23b5c8defd608c74e8047df18abd4b9ee44c74d29b453ba104d14c53c91f9026670f7c923114cd12200ee5ec458cc518fd0798c74
|
||||
"textextensions@npm:^5.12.0, textextensions@npm:^5.13.0, textextensions@npm:^5.16.0":
|
||||
version: 5.16.0
|
||||
resolution: "textextensions@npm:5.16.0"
|
||||
checksum: d2abd5c962760046aa85d9ca542bd8bdb451370fc0a5e5f807aa80dd2f50175ec10d5ce9d28ae96968aaf6a1b1bea254cf4715f24852d0dcf29c6a60af7f793c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user