From 81726cd2d45a504e373baff347d8cfe3f7c7ef01 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 2 Oct 2023 16:54:44 +0200 Subject: [PATCH] backend-test-utils: refactor to createMockDirectory + remove root option Signed-off-by: Patrik Oldsberg --- .changeset/flat-pandas-tan.md | 2 +- .../src/reading/AzureUrlReader.test.ts | 4 +- .../reading/BitbucketCloudUrlReader.test.ts | 4 +- .../reading/BitbucketServerUrlReader.test.ts | 4 +- .../src/reading/BitbucketUrlReader.test.ts | 4 +- .../src/reading/GerritUrlReader.test.ts | 4 +- .../src/reading/GithubUrlReader.test.ts | 4 +- .../src/reading/GitlabUrlReader.test.ts | 4 +- .../tree/ReadableArrayResponse.test.ts | 6 +- .../reading/tree/TarArchiveResponse.test.ts | 6 +- .../reading/tree/ZipArchiveResponse.test.ts | 6 +- .../src/util/DockerContainerRunner.test.ts | 6 +- packages/backend-test-utils/api-report.md | 19 +- .../src/filesystem/MockDirectory.test.ts | 28 +- .../src/filesystem/MockDirectory.ts | 273 +++++++++--------- .../src/filesystem/index.ts | 5 +- 16 files changed, 186 insertions(+), 193 deletions(-) diff --git a/.changeset/flat-pandas-tan.md b/.changeset/flat-pandas-tan.md index 8461c9beb8..dfe7965bec 100644 --- a/.changeset/flat-pandas-tan.md +++ b/.changeset/flat-pandas-tan.md @@ -2,4 +2,4 @@ '@backstage/backend-test-utils': patch --- -Added `MockDirectory` to help out with file system mocking in tests. +Added `createMockDirectory()` to help out with file system mocking in tests. diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index 45d64c5b96..2b4bc84be7 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -24,7 +24,7 @@ import { AzureIntegrationConfig, } from '@backstage/integration'; import { - MockDirectory, + createMockDirectory, setupRequestMockHandlers, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; @@ -44,7 +44,7 @@ type AzureIntegrationConfigLike = Partial< const logger = getVoidLogger(); -const mockDir = MockDirectory.mockOsTmpDir(); +const mockDir = createMockDirectory({ mockOsTmpDir: true }); const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), diff --git a/packages/backend-common/src/reading/BitbucketCloudUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketCloudUrlReader.test.ts index 7b1e0f9fb3..dc182af619 100644 --- a/packages/backend-common/src/reading/BitbucketCloudUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketCloudUrlReader.test.ts @@ -20,7 +20,7 @@ import { readBitbucketCloudIntegrationConfig, } from '@backstage/integration'; import { - MockDirectory, + createMockDirectory, setupRequestMockHandlers, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; @@ -32,7 +32,7 @@ import { BitbucketCloudUrlReader } from './BitbucketCloudUrlReader'; import { DefaultReadTreeResponseFactory } from './tree'; import getRawBody from 'raw-body'; -const mockDir = MockDirectory.mockOsTmpDir(); +const mockDir = createMockDirectory({ mockOsTmpDir: true }); const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), diff --git a/packages/backend-common/src/reading/BitbucketServerUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketServerUrlReader.test.ts index 82b467f70f..2870138535 100644 --- a/packages/backend-common/src/reading/BitbucketServerUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketServerUrlReader.test.ts @@ -20,7 +20,7 @@ import { readBitbucketServerIntegrationConfig, } from '@backstage/integration'; import { - MockDirectory, + createMockDirectory, setupRequestMockHandlers, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; @@ -31,7 +31,7 @@ import { NotModifiedError } from '@backstage/errors'; import { BitbucketServerUrlReader } from './BitbucketServerUrlReader'; import { DefaultReadTreeResponseFactory } from './tree'; -MockDirectory.mockOsTmpDir(); +createMockDirectory({ mockOsTmpDir: true }); const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index 5e3884cf8e..4258ac9d5b 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -20,7 +20,7 @@ import { readBitbucketIntegrationConfig, } from '@backstage/integration'; import { - MockDirectory, + createMockDirectory, setupRequestMockHandlers, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; @@ -69,7 +69,7 @@ describe('BitbucketUrlReader.factory', () => { }); describe('BitbucketUrlReader', () => { - const mockDir = MockDirectory.mockOsTmpDir(); + const mockDir = createMockDirectory({ mockOsTmpDir: true }); beforeEach(mockDir.clear); diff --git a/packages/backend-common/src/reading/GerritUrlReader.test.ts b/packages/backend-common/src/reading/GerritUrlReader.test.ts index d91e49e533..4a3b0a5de6 100644 --- a/packages/backend-common/src/reading/GerritUrlReader.test.ts +++ b/packages/backend-common/src/reading/GerritUrlReader.test.ts @@ -15,7 +15,7 @@ */ import { - MockDirectory, + createMockDirectory, setupRequestMockHandlers, } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; @@ -35,7 +35,7 @@ import { DefaultReadTreeResponseFactory } from './tree'; import { GerritUrlReader } from './GerritUrlReader'; import getRawBody from 'raw-body'; -const mockDir = MockDirectory.mockOsTmpDir(); +const mockDir = createMockDirectory({ mockOsTmpDir: true }); const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index 82ce07a3a9..5e6d30f519 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -21,7 +21,7 @@ import { readGithubIntegrationConfig, } from '@backstage/integration'; import { - MockDirectory, + createMockDirectory, setupRequestMockHandlers, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; @@ -38,7 +38,7 @@ import { } from './GithubUrlReader'; import { DefaultReadTreeResponseFactory } from './tree'; -const mockDir = MockDirectory.mockOsTmpDir(); +const mockDir = createMockDirectory({ mockOsTmpDir: true }); const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index 098d48458d..3c065e8b81 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -16,7 +16,7 @@ import { ConfigReader } from '@backstage/config'; import { - MockDirectory, + createMockDirectory, setupRequestMockHandlers, } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; @@ -34,7 +34,7 @@ import { const logger = getVoidLogger(); -const mockDir = MockDirectory.mockOsTmpDir(); +const mockDir = createMockDirectory({ mockOsTmpDir: true }); const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), diff --git a/packages/backend-common/src/reading/tree/ReadableArrayResponse.test.ts b/packages/backend-common/src/reading/tree/ReadableArrayResponse.test.ts index 1c95f653c1..4108697bce 100644 --- a/packages/backend-common/src/reading/tree/ReadableArrayResponse.test.ts +++ b/packages/backend-common/src/reading/tree/ReadableArrayResponse.test.ts @@ -18,7 +18,7 @@ import fs from 'fs-extra'; import path from 'path'; import { FromReadableArrayOptions } from '../types'; import { ReadableArrayResponse } from './ReadableArrayResponse'; -import { MockDirectory } from '@backstage/backend-test-utils'; +import { createMockDirectory } from '@backstage/backend-test-utils'; const name1 = 'file1.yaml'; const file1 = fs.readFileSync( @@ -31,8 +31,8 @@ const file2 = fs.readFileSync( ); describe('ReadableArrayResponse', () => { - const sourceDir = MockDirectory.create(); - const targetDir = MockDirectory.create(); + const sourceDir = createMockDirectory(); + const targetDir = createMockDirectory(); beforeEach(() => { sourceDir.setContent({ diff --git a/packages/backend-common/src/reading/tree/TarArchiveResponse.test.ts b/packages/backend-common/src/reading/tree/TarArchiveResponse.test.ts index 784a4fb9bf..f69fb2e0e1 100644 --- a/packages/backend-common/src/reading/tree/TarArchiveResponse.test.ts +++ b/packages/backend-common/src/reading/tree/TarArchiveResponse.test.ts @@ -17,15 +17,15 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; import { TarArchiveResponse } from './TarArchiveResponse'; -import { MockDirectory } from '@backstage/backend-test-utils'; +import { createMockDirectory } from '@backstage/backend-test-utils'; const archiveData = fs.readFileSync( resolvePath(__filename, '../../__fixtures__/mock-main.tar.gz'), ); describe('TarArchiveResponse', () => { - const sourceDir = MockDirectory.create(); - const targetDir = MockDirectory.create(); + const sourceDir = createMockDirectory(); + const targetDir = createMockDirectory(); beforeAll(() => { sourceDir.setContent({ 'test-archive.tar.gz': archiveData }); diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts index c92ee46aad..0eecad2098 100644 --- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts +++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.test.ts @@ -19,7 +19,7 @@ import { Readable } from 'stream'; import { create as createArchive } from 'archiver'; import { resolve as resolvePath } from 'path'; import { ZipArchiveResponse } from './ZipArchiveResponse'; -import { MockDirectory } from '@backstage/backend-test-utils'; +import { createMockDirectory } from '@backstage/backend-test-utils'; const archiveData = fs.readFileSync( resolvePath(__filename, '../../__fixtures__/mock-main.zip'), @@ -35,8 +35,8 @@ const archiveWithMaliciousEntry = fs.readFileSync( ); describe('ZipArchiveResponse', () => { - const sourceDir = MockDirectory.create(); - const targetDir = MockDirectory.create(); + const sourceDir = createMockDirectory(); + const targetDir = createMockDirectory(); beforeAll(() => { sourceDir.setContent({ diff --git a/packages/backend-common/src/util/DockerContainerRunner.test.ts b/packages/backend-common/src/util/DockerContainerRunner.test.ts index 60f457a670..737bf3d178 100644 --- a/packages/backend-common/src/util/DockerContainerRunner.test.ts +++ b/packages/backend-common/src/util/DockerContainerRunner.test.ts @@ -19,15 +19,15 @@ import Docker from 'dockerode'; import Stream, { PassThrough } from 'stream'; import { ContainerRunner } from './ContainerRunner'; import { DockerContainerRunner, UserOptions } from './DockerContainerRunner'; -import { MockDirectory } from '@backstage/backend-test-utils'; +import { createMockDirectory } from '@backstage/backend-test-utils'; const mockDocker = new Docker() as jest.Mocked; describe('DockerContainerRunner', () => { let containerTaskApi: ContainerRunner; - const inputDir = MockDirectory.create(); - const outputDir = MockDirectory.create(); + const inputDir = createMockDirectory(); + const outputDir = createMockDirectory(); beforeEach(() => { inputDir.clear(); diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index dff218edb1..ac4c5ffe85 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -31,20 +31,23 @@ 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 class MockDirectory { +export interface MockDirectory { addContent(root: MockDirectoryContent): void; - clear: () => void; + clear(): void; content( options?: MockDirectoryContentOptions, ): MockDirectoryContent | undefined; - static create(options?: MockDirectoryCreateOptions): MockDirectory; - static mockOsTmpDir(): MockDirectory; - get path(): string; - remove: () => void; + readonly path: string; + remove(): void; resolve(...paths: string[]): string; setContent(root: MockDirectoryContent): void; } @@ -61,8 +64,8 @@ export interface MockDirectoryContentOptions { } // @public -export interface MockDirectoryCreateOptions { - root?: string; +export interface MockDirectoryOptions { + mockOsTmpDir?: boolean; } // @public (undocumented) diff --git a/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts b/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts index 19c56119f0..8c4ff49e64 100644 --- a/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts +++ b/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts @@ -21,10 +21,10 @@ import { resolve as resolvePath, relative as relativePath, } from 'path'; -import { MockDirectory } from './MockDirectory'; +import { createMockDirectory, MockDirectory } from './MockDirectory'; -describe('MockDirectory', () => { - const mockDir = MockDirectory.create(); +describe('createMockDirectory', () => { + const mockDir = createMockDirectory(); beforeEach(mockDir.clear); @@ -277,7 +277,7 @@ describe('MockDirectory', () => { let cleanupMockDir: MockDirectory; describe('inner', () => { - cleanupMockDir = MockDirectory.create(); + cleanupMockDir = createMockDirectory(); it('should populate a directory', () => { cleanupMockDir.setContent({ @@ -299,7 +299,7 @@ describe('MockDirectory', () => { let tmpDirMock: MockDirectory; describe('inner', () => { - tmpDirMock = MockDirectory.mockOsTmpDir(); + tmpDirMock = createMockDirectory({ mockOsTmpDir: true }); it('should mock os.tmpdir()', () => { expect(os.tmpdir()).toBe(tmpDirMock.path); @@ -310,22 +310,4 @@ describe('MockDirectory', () => { expect(os.tmpdir()).not.toBe(tmpDirMock.path); }); }); - - describe('existing directory', () => { - let existingMockDir: MockDirectory; - - describe('inner', () => { - existingMockDir = MockDirectory.create({ root: __dirname }); // hardcore mode - - it('should read existing directory', () => { - expect(existingMockDir.content()).toMatchObject({ - 'index.ts': expect.any(String), - }); - }); - }); - - it('should remove existing directory', () => { - expect(fs.pathExistsSync(__dirname)).toBe(true); - }); - }); }); diff --git a/packages/backend-test-utils/src/filesystem/MockDirectory.ts b/packages/backend-test-utils/src/filesystem/MockDirectory.ts index cb7ca5e726..43df843946 100644 --- a/packages/backend-test-utils/src/filesystem/MockDirectory.ts +++ b/packages/backend-test-utils/src/filesystem/MockDirectory.ts @@ -56,20 +56,6 @@ export type MockDirectoryContent = { [name in string]: MockDirectoryContent | string | Buffer; }; -/** - * Options for {@link MockDirectory.create}. - * - * @public - */ -export interface MockDirectoryCreateOptions { - /** - * The root path to create the directory in. Defaults to a temporary directory. - * - * If an existing directory is provided, it will not be cleaned up after the test. - */ - root?: string; -} - /** * Options for {@link MockDirectory.content}. * @@ -91,107 +77,21 @@ export interface MockDirectoryContentOptions { shouldReadAsText?: boolean | ((path: string, buffer: Buffer) => boolean); } -/** @internal */ -type MockEntry = - | { - type: 'file'; - path: string; - content: Buffer; - } - | { - type: 'dir'; - path: string; - }; - /** * A utility for creating a mock directory that is automatically cleaned up. * * @public */ -export class MockDirectory { - /** - * Creates a new temporary mock directory that will be removed after the tests have completed. - * - * @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 = MockDirectory.create(); - * - * beforeEach(mockDir.clear); - * - * it('should work', () => { - * // ... use mockDir - * }) - * }) - * ``` - */ - static create(options?: MockDirectoryCreateOptions): MockDirectory { - const root = - options?.root ?? - fs.mkdtempSync(joinPath(getTmpDir(), 'backstage-tmp-test-dir-')); - - const mocker = new MockDirectory(root); - - const shouldCleanup = !options?.root || !fs.pathExistsSync(options.root); - if (shouldCleanup) { - process.on('beforeExit', mocker.remove); - - try { - afterAll(mocker.remove); - } catch { - /* ignore */ - } - } - - return mocker; - } - - /** - * Like {@link MockDirectory.create}, but also mocks `os.tmpdir()` to return the - * mock directory path until the end of the test suite. - * - * @returns - */ - static mockOsTmpDir(): MockDirectory { - const mocker = MockDirectory.create(); - const origTmpdir = os.tmpdir; - os.tmpdir = () => mocker.path; - - try { - afterAll(() => { - os.tmpdir = origTmpdir; - }); - } catch { - /* ignore */ - } - return mocker; - } - - readonly #root: string; - - private constructor(root: string) { - this.#root = root; - } - +export interface MockDirectory { /** * The path to the root of the mock directory */ - get path(): string { - return this.#root; - } + readonly path: string; /** * Resolves a path relative to the root of the mock directory. */ - resolve(...paths: string[]): string { - return resolvePath(this.#root, ...paths); - } + resolve(...paths: string[]): string; /** * Sets the content of the mock directory. This will remove any existing content. @@ -209,11 +109,7 @@ export class MockDirectory { * }); * ``` */ - setContent(root: MockDirectoryContent): void { - this.remove(); - - return this.addContent(root); - } + setContent(root: MockDirectoryContent): void; /** * Adds content of the mock directory. This will overwrite existing files. @@ -231,25 +127,7 @@ export class MockDirectory { * }); * ``` */ - 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 }); - } - } - } + addContent(root: MockDirectoryContent): void; /** * Reads the content of the mock directory. @@ -274,6 +152,75 @@ export class MockDirectory { * }); * ``` */ + 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 { @@ -319,16 +266,10 @@ export class MockDirectory { return read(root); } - /** - * Clears the content of the mock directory, ensuring that the directory itself exists. - */ clear = (): void => { this.setContent({}); }; - /** - * Removes the mock directory and all its contents. - */ remove = (): void => { fs.removeSync(this.#root); }; @@ -359,3 +300,69 @@ export class MockDirectory { 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; +} + +/** + * 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 root = fs.mkdtempSync(joinPath(getTmpDir(), 'backstage-tmp-test-dir-')); + + const mocker = new MockDirectoryImpl(root); + + const origTmpdir = options?.mockOsTmpDir ? os.tmpdir : undefined; + if (origTmpdir) { + os.tmpdir = () => mocker.path; + } + + process.on('beforeExit', mocker.remove); + + try { + afterAll(() => { + if (origTmpdir) { + os.tmpdir = origTmpdir; + } + mocker.remove(); + }); + } catch { + /* ignore */ + } + + return mocker; +} diff --git a/packages/backend-test-utils/src/filesystem/index.ts b/packages/backend-test-utils/src/filesystem/index.ts index d1077db980..0a4d8c7d00 100644 --- a/packages/backend-test-utils/src/filesystem/index.ts +++ b/packages/backend-test-utils/src/filesystem/index.ts @@ -15,8 +15,9 @@ */ export { - MockDirectory, + createMockDirectory, + type MockDirectory, + type MockDirectoryOptions, type MockDirectoryContent, type MockDirectoryContentOptions, - type MockDirectoryCreateOptions, } from './MockDirectory';