diff --git a/.changeset/nasty-needles-hear.md b/.changeset/nasty-needles-hear.md new file mode 100644 index 0000000000..32a9d0dfb7 --- /dev/null +++ b/.changeset/nasty-needles-hear.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Added `/testUtils` entry point, with a utility for mocking resolve package paths as returned by `resolvePackagePath`. diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index a163311128..63158fc30e 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -10,6 +10,7 @@ "exports": { ".": "./src/index.ts", "./alpha": "./src/alpha.ts", + "./testUtils": "./src/testUtils.ts", "./package.json": "./package.json" }, "typesVersions": { @@ -17,6 +18,9 @@ "alpha": [ "src/alpha.ts" ], + "testUtils": [ + "src/testUtils.ts" + ], "package.json": [ "package.json" ] diff --git a/packages/backend-common/src/paths.ts b/packages/backend-common/src/paths.ts index c8a8849d6c..4ec8e01bc4 100644 --- a/packages/backend-common/src/paths.ts +++ b/packages/backend-common/src/paths.ts @@ -18,6 +18,12 @@ import { isChildPath } from '@backstage/cli-common'; import { NotAllowedError } from '@backstage/errors'; import { resolve as resolvePath } from 'path'; +/** @internal */ +export const packagePathMocks = new Map< + string, + (paths: string[]) => string | undefined +>(); + /** * Resolve a path relative to the root of a package directory. * Additional path arguments are resolved relative to the package dir. @@ -29,6 +35,14 @@ import { resolve as resolvePath } from 'path'; * @public */ export function resolvePackagePath(name: string, ...paths: string[]) { + const mockedResolve = packagePathMocks.get(name); + if (mockedResolve) { + const resolved = mockedResolve(paths); + if (resolved) { + return resolved; + } + } + const req = typeof __non_webpack_require__ === 'undefined' ? require diff --git a/packages/backend-common/src/testUtils.ts b/packages/backend-common/src/testUtils.ts new file mode 100644 index 0000000000..9616ab1701 --- /dev/null +++ b/packages/backend-common/src/testUtils.ts @@ -0,0 +1,79 @@ +/* + * 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 { packagePathMocks } from './paths'; +import { posix as posixPath, resolve as resolvePath } from 'path'; + +/** @public */ +export interface PackagePathResolutionOverride { + /** Restores the normal behavior of resolvePackagePath */ + restore(): void; +} + +/** @public */ +export interface OverridePackagePathResolutionOptions { + /** The name of the package to mock the resolved path of */ + packageName: string; + + /** A replacement for the root package path */ + path?: string; + + /** + * Replacements for package sub-paths, each key must be an exact match of the posix-style path + * that is being resolved within the package. + * + * For example, code calling `resolvePackagePath('x', 'foo', 'bar')` would match only the following + * configuration: `overridePackagePathResolution({ packageName: 'x', paths: { 'foo/bar': baz } })` + */ + paths?: { [path in string]: string | (() => string) }; +} + +/** + * This utility helps you override the paths returned by `resolvePackagePath` for a given package. + * + * @public + */ +export function overridePackagePathResolution( + options: OverridePackagePathResolutionOptions, +): PackagePathResolutionOverride { + const name = options.packageName; + + if (packagePathMocks.has(name)) { + throw new Error( + `Tried to override resolution for '${name}' more than once for package '${name}'`, + ); + } + + packagePathMocks.set(name, paths => { + const joinedPath = posixPath.join(...paths); + const localResolver = options.paths?.[joinedPath]; + if (localResolver) { + return typeof localResolver === 'function' + ? localResolver() + : localResolver; + } + if (options.path) { + return resolvePath(options.path, ...paths); + } + return undefined; + }); + + return { + restore() { + packagePathMocks.delete(name); + }, + }; +} diff --git a/packages/backend-common/testUtils-api-report.md b/packages/backend-common/testUtils-api-report.md new file mode 100644 index 0000000000..9dbdade10e --- /dev/null +++ b/packages/backend-common/testUtils-api-report.md @@ -0,0 +1,26 @@ +## API Report File for "@backstage/backend-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// @public +export function overridePackagePathResolution( + options: OverridePackagePathResolutionOptions, +): PackagePathResolutionOverride; + +// @public (undocumented) +export interface OverridePackagePathResolutionOptions { + packageName: string; + path?: string; + paths?: { + [path in string]: string | (() => string); + }; +} + +// @public (undocumented) +export interface PackagePathResolutionOverride { + restore(): void; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts b/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts index 9e3efdaec0..8040d03f44 100644 --- a/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts +++ b/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts @@ -91,7 +91,7 @@ describe('createMockDirectory', () => { mockDir.addContent({ 'b.txt': 'b', - b: { + [mockDir.resolve('b')]: { 'c.txt': 'c', }, }); diff --git a/packages/backend-test-utils/src/filesystem/MockDirectory.ts b/packages/backend-test-utils/src/filesystem/MockDirectory.ts index 20345e4ff9..61eb4fce24 100644 --- a/packages/backend-test-utils/src/filesystem/MockDirectory.ts +++ b/packages/backend-test-utils/src/filesystem/MockDirectory.ts @@ -279,19 +279,18 @@ class MockDirectoryImpl { 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, + path, content: Buffer.from(node, 'utf8'), }); } else if (node instanceof Buffer) { - entries.push({ type: 'file', path: trimmedPath, content: node }); + entries.push({ type: 'file', path, content: node }); } else { - entries.push({ type: 'dir', path: trimmedPath }); + entries.push({ type: 'dir', path }); for (const [name, child] of Object.entries(node)) { - traverse(child, `${trimmedPath}/${name}`); + traverse(child, path ? `${path}/${name}` : name); } } } diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 0c0ecc8f60..0874b66a27 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -69,7 +69,6 @@ "@backstage/cli": "workspace:^", "@backstage/types": "workspace:^", "@types/supertest": "^2.0.8", - "mock-fs": "^5.2.0", "msw": "^1.0.0", "node-fetch": "^2.6.7", "supertest": "^6.1.3" diff --git a/plugins/app-backend/src/lib/assets/findStaticAssets.test.ts b/plugins/app-backend/src/lib/assets/findStaticAssets.test.ts index 6527c28ea4..0d5cd277cf 100644 --- a/plugins/app-backend/src/lib/assets/findStaticAssets.test.ts +++ b/plugins/app-backend/src/lib/assets/findStaticAssets.test.ts @@ -14,40 +14,40 @@ * limitations under the License. */ -import mockFs from 'mock-fs'; +import { createMockDirectory } from '@backstage/backend-test-utils'; import { findStaticAssets } from './findStaticAssets'; describe('findStaticAssets', () => { + const mockDir = createMockDirectory(); + afterEach(() => { - mockFs.restore(); + mockDir.clear(); }); it('should find assets', async () => { - mockFs({ - '/test': { - 'a.js': 'alert("hello")', - 'a.js.map': '', - 'b.js': 'b', - 'b.js.map': '', - js: { - 'd.js': 'd', - 'd.js.map': '', - x: { + mockDir.setContent({ + 'a.js': 'alert("hello")', + 'a.js.map': '', + 'b.js': 'b', + 'b.js.map': '', + js: { + 'd.js': 'd', + 'd.js.map': '', + x: { + 'e.map': '', + y: { 'e.map': '', - y: { + z: { + 'e.js': 'e', 'e.map': '', - z: { - 'e.js': 'e', - 'e.map': '', - }, }, }, }, - styles: { 'c.css': 'body { color: red; }' }, }, + styles: { 'c.css': 'body { color: red; }' }, }); - const assets = await findStaticAssets('/test'); + const assets = await findStaticAssets(mockDir.path); expect(assets.length).toBe(5); expect(assets.map(a => a.path)).toEqual( expect.arrayContaining([ diff --git a/plugins/app-backend/src/service/appPlugin.test.ts b/plugins/app-backend/src/service/appPlugin.test.ts index 09061b287d..647697c204 100644 --- a/plugins/app-backend/src/service/appPlugin.test.ts +++ b/plugins/app-backend/src/service/appPlugin.test.ts @@ -14,33 +14,36 @@ * limitations under the License. */ -import mockFs from 'mock-fs'; -import { resolve as resolvePath } from 'path'; import fetch from 'node-fetch'; -import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { + createMockDirectory, + mockServices, + startTestBackend, +} from '@backstage/backend-test-utils'; import { appPlugin } from './appPlugin'; import { createRootLogger } from '@backstage/backend-common'; +import { overridePackagePathResolution } from '@backstage/backend-common/testUtils'; + +const mockDir = createMockDirectory(); +overridePackagePathResolution({ + packageName: 'app', + path: mockDir.path, +}); // Make sure root logger is initialized ahead of FS mock createRootLogger(); describe('appPlugin', () => { beforeEach(() => { - mockFs({ - [resolvePath(process.cwd(), 'node_modules/app')]: { - 'package.json': '{}', - dist: { - static: {}, - 'index.html': 'winning', - }, + mockDir.setContent({ + 'package.json': '{}', + dist: { + static: {}, + 'index.html': 'winning', }, }); }); - afterEach(() => { - mockFs.restore(); - }); - it('boots', async () => { const { server } = await startTestBackend({ features: [ diff --git a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts index f5e494f514..2ce7975e41 100644 --- a/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts +++ b/plugins/catalog-backend/src/modules/core/PlaceholderProcessor.test.ts @@ -372,7 +372,7 @@ describe('PlaceholderProcessor', () => { () => {}, ), ).rejects.toThrow( - /^Placeholder \$text could not form a URL out of \.\/a\/b\/catalog-info\.yaml and \.\.\/c\/catalog-info\.yaml, TypeError \[ERR_INVALID_URL\]/, + /^Placeholder \$text could not form a URL out of \.\/a\/b\/catalog-info\.yaml and \.\.\/c\/catalog-info\.yaml, TypeError/, ); expect(reader.readUrl).not.toHaveBeenCalled(); diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index e0df3aa94b..bb39a01c6c 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -89,7 +89,6 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/aws4": "^1.5.1", - "mock-fs": "^5.2.0", "msw": "^1.0.0", "supertest": "^6.1.3", "ws": "^8.13.0" diff --git a/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.test.ts b/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.test.ts index b6b9d0efb8..3a98652c37 100644 --- a/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.test.ts +++ b/plugins/kubernetes-backend/src/auth/ServiceAccountStrategy.test.ts @@ -13,8 +13,37 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { createMockDirectory } from '@backstage/backend-test-utils'; import { ServiceAccountStrategy } from './ServiceAccountStrategy'; -import mockFs from 'mock-fs'; + +const mockDir = createMockDirectory({ + content: { + 'token.txt': 'in-cluster-token', + }, +}); + +jest.mock('@kubernetes/client-node', () => ({ + KubeConfig: class { + #loaded = false; + loadFromCluster() { + this.#loaded = true; + } + getCurrentUser() { + if (!this.#loaded) { + throw new Error('loadFromCluster not called'); + } + return { + authProvider: { + config: { + get tokenFile() { + return mockDir.resolve('token.txt'); + }, + }, + }, + }; + } + }, +})); describe('ServiceAccountStrategy', () => { describe('#getCredential', () => { @@ -32,16 +61,9 @@ describe('ServiceAccountStrategy', () => { token: 'from config', }); }); - describe('when serviceAccountToken is absent from config', () => { - afterEach(() => { - mockFs.restore(); - }); + describe('when serviceAccountToken is absent from config', () => { it('reads in-cluster token', async () => { - mockFs({ - '/var/run/secrets/kubernetes.io/serviceaccount/token': - 'in-cluster-token', - }); const strategy = new ServiceAccountStrategy(); const credential = await strategy.getCredential({ diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts index 2cecbbeaec..574a1475fd 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -26,8 +26,17 @@ import { rest, } from 'msw'; import { setupServer } from 'msw/node'; -import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; -import mockFs from 'mock-fs'; +import { + createMockDirectory, + setupRequestMockHandlers, +} from '@backstage/backend-test-utils'; +import { Config } from '@kubernetes/client-node'; + +const mockCertDir = createMockDirectory({ + content: { + 'ca.crt': 'MOCKCA', + }, +}); const OBJECTS_TO_FETCH = new Set([ { @@ -728,13 +737,7 @@ describe('KubernetesFetcher', () => { expect(agent.options.ca).toBeUndefined(); }); describe('with a CA file on disk', () => { - afterEach(() => { - mockFs.restore(); - }); it('should trust contents of specified caFile', async () => { - mockFs({ - '/path/to/ca.crt': 'MOCKCA', - }); worker.use( rest.get('https://localhost:9999/api/v1/pods', (req, res, ctx) => res( @@ -752,7 +755,7 @@ describe('KubernetesFetcher', () => { name: 'cluster1', url: 'https://localhost:9999', authMetadata: {}, - caFile: '/path/to/ca.crt', + caFile: mockCertDir.resolve('ca.crt'), }, credential: { type: 'bearer token', token: 'token' }, objectTypesToFetch: new Set([ @@ -899,17 +902,18 @@ describe('KubernetesFetcher', () => { describe('Backstage running on k8s', () => { const initialHost = process.env.KUBERNETES_SERVICE_HOST; const initialPort = process.env.KUBERNETES_SERVICE_PORT; + const initialCaPath = Config.SERVICEACCOUNT_CA_PATH; + afterEach(() => { process.env.KUBERNETES_SERVICE_HOST = initialHost; process.env.KUBERNETES_SERVICE_PORT = initialPort; - mockFs.restore(); + Config.SERVICEACCOUNT_CA_PATH = initialCaPath; }); + it('makes in-cluster requests when cluster details has no token', async () => { process.env.KUBERNETES_SERVICE_HOST = '10.10.10.10'; process.env.KUBERNETES_SERVICE_PORT = '443'; - mockFs({ - '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt': '', - }); + Config.SERVICEACCOUNT_CA_PATH = mockCertDir.resolve('ca.crt'); worker.use( rest.get('https://10.10.10.10/api/v1/pods', (req, res, ctx) => res( diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json index f49ecf1baa..56bb4f86a4 100644 --- a/plugins/techdocs-node/package.json +++ b/plugins/techdocs-node/package.json @@ -63,17 +63,16 @@ "js-yaml": "^4.0.0", "json5": "^2.1.3", "mime-types": "^2.1.27", - "mock-fs": "^5.2.0", "p-limit": "^3.1.0", "recursive-readdir": "^2.2.2", "winston": "^3.2.1" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/fs-extra": "^9.0.5", "@types/js-yaml": "^4.0.0", "@types/mime-types": "^2.1.0", - "@types/mock-fs": "^4.13.0", "@types/recursive-readdir": "^2.2.0", "@types/supertest": "^2.0.8", "aws-sdk-client-mock": "^2.0.0", diff --git a/plugins/techdocs-node/src/stages/generate/helpers.test.ts b/plugins/techdocs-node/src/stages/generate/helpers.test.ts index 4d422d7125..22940e9ce8 100644 --- a/plugins/techdocs-node/src/stages/generate/helpers.test.ts +++ b/plugins/techdocs-node/src/stages/generate/helpers.test.ts @@ -17,9 +17,8 @@ import { getVoidLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; +import { createMockDirectory } from '@backstage/backend-test-utils'; import fs from 'fs-extra'; -import mockFs from 'mock-fs'; -import os from 'os'; import path, { resolve as resolvePath } from 'path'; import { ParsedLocationAnnotation } from '../../helpers'; import { @@ -88,14 +87,12 @@ const mkdocsYmlWithEnvTag = fs.readFileSync( const mockLogger = getVoidLogger(); const warn = jest.spyOn(mockLogger, 'warn'); -const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; - const scmIntegrations = ScmIntegrations.fromConfig(new ConfigReader({})); describe('helpers', () => { - afterEach(() => { - mockFs.restore(); - }); + const mockDir = createMockDirectory(); + + afterEach(mockDir.clear); describe('getGeneratorKey', () => { it('should return techdocs as the only generator key', () => { @@ -188,13 +185,13 @@ describe('helpers', () => { describe('patchMkdocsYmlPreBuild', () => { beforeEach(() => { - mockFs({ - '/mkdocs.yml': mkdocsYml, - '/mkdocs_default.yml': mkdocsDefaultYml, - '/mkdocs_with_repo_url.yml': mkdocsYmlWithRepoUrl, - '/mkdocs_with_edit_uri.yml': mkdocsYmlWithEditUri, - '/mkdocs_with_extensions.yml': mkdocsYmlWithExtensions, - '/mkdocs_with_comments.yml': mkdocsYmlWithComments, + mockDir.setContent({ + 'mkdocs.yml': mkdocsYml, + 'mkdocs_default.yml': mkdocsDefaultYml, + 'mkdocs_with_repo_url.yml': mkdocsYmlWithRepoUrl, + 'mkdocs_with_edit_uri.yml': mkdocsYmlWithEditUri, + 'mkdocs_with_extensions.yml': mkdocsYmlWithExtensions, + 'mkdocs_with_comments.yml': mkdocsYmlWithComments, }); }); @@ -205,13 +202,13 @@ describe('helpers', () => { }; await patchMkdocsYmlPreBuild( - '/mkdocs.yml', + mockDir.resolve('mkdocs.yml'), mockLogger, parsedLocationAnnotation, scmIntegrations, ); - const updatedMkdocsYml = await fs.readFile('/mkdocs.yml'); + const updatedMkdocsYml = await fs.readFile(mockDir.resolve('mkdocs.yml')); expect(updatedMkdocsYml.toString()).toContain( 'repo_url: https://github.com/backstage/backstage', @@ -225,13 +222,15 @@ describe('helpers', () => { }; await patchMkdocsYmlPreBuild( - '/mkdocs_with_extensions.yml', + mockDir.resolve('mkdocs_with_extensions.yml'), mockLogger, parsedLocationAnnotation, scmIntegrations, ); - const updatedMkdocsYml = await fs.readFile('/mkdocs_with_extensions.yml'); + const updatedMkdocsYml = await fs.readFile( + mockDir.resolve('mkdocs_with_extensions.yml'), + ); expect(updatedMkdocsYml.toString()).toContain( 'repo_url: https://github.com/backstage/backstage', @@ -248,13 +247,15 @@ describe('helpers', () => { }; await patchMkdocsYmlPreBuild( - '/mkdocs_with_repo_url.yml', + mockDir.resolve('mkdocs_with_repo_url.yml'), mockLogger, parsedLocationAnnotation, scmIntegrations, ); - const updatedMkdocsYml = await fs.readFile('/mkdocs_with_repo_url.yml'); + const updatedMkdocsYml = await fs.readFile( + mockDir.resolve('mkdocs_with_repo_url.yml'), + ); expect(updatedMkdocsYml.toString()).toContain( 'repo_url: https://github.com/backstage/backstage', @@ -271,13 +272,15 @@ describe('helpers', () => { }; await patchMkdocsYmlPreBuild( - '/mkdocs_with_edit_uri.yml', + mockDir.resolve('mkdocs_with_edit_uri.yml'), mockLogger, parsedLocationAnnotation, scmIntegrations, ); - const updatedMkdocsYml = await fs.readFile('/mkdocs_with_edit_uri.yml'); + const updatedMkdocsYml = await fs.readFile( + mockDir.resolve('mkdocs_with_edit_uri.yml'), + ); expect(updatedMkdocsYml.toString()).toContain( 'edit_uri: https://github.com/backstage/backstage/edit/main/docs', @@ -294,13 +297,15 @@ describe('helpers', () => { }; await patchMkdocsYmlPreBuild( - '/mkdocs_with_comments.yml', + mockDir.resolve('mkdocs_with_comments.yml'), mockLogger, parsedLocationAnnotation, scmIntegrations, ); - const updatedMkdocsYml = await fs.readFile('/mkdocs_with_comments.yml'); + const updatedMkdocsYml = await fs.readFile( + mockDir.resolve('mkdocs_with_comments.yml'), + ); expect(updatedMkdocsYml.toString()).toContain( '# This is a comment that is removed after editing', @@ -312,20 +317,20 @@ describe('helpers', () => { describe('patchMkdocsYmlWithPlugins', () => { beforeEach(() => { - mockFs({ - '/mkdocs_with_techdocs_plugin.yml': mkdocsYmlWithTechdocsPlugins, - '/mkdocs_without_plugins.yml': mkdocsYmlWithoutPlugins, - '/mkdocs_with_additional_plugins.yml': mkdocsYmlWithAdditionalPlugins, + mockDir.setContent({ + 'mkdocs_with_techdocs_plugin.yml': mkdocsYmlWithTechdocsPlugins, + 'mkdocs_without_plugins.yml': mkdocsYmlWithoutPlugins, + 'mkdocs_with_additional_plugins.yml': mkdocsYmlWithAdditionalPlugins, }); }); it('should not add additional plugins if techdocs exists already in mkdocs file', async () => { await patchMkdocsYmlWithPlugins( - '/mkdocs_with_techdocs_plugin.yml', + mockDir.resolve('mkdocs_with_techdocs_plugin.yml'), mockLogger, ); const updatedMkdocsYml = await fs.readFile( - '/mkdocs_with_techdocs_plugin.yml', + mockDir.resolve('mkdocs_with_techdocs_plugin.yml'), ); const parsedYml = yaml.load(updatedMkdocsYml.toString()) as { plugins: string[]; @@ -335,11 +340,13 @@ describe('helpers', () => { }); it("should add the needed plugin if it doesn't exist in mkdocs file", async () => { await patchMkdocsYmlWithPlugins( - '/mkdocs_without_plugins.yml', + mockDir.resolve('mkdocs_without_plugins.yml'), mockLogger, ); - const updatedMkdocsYml = await fs.readFile('/mkdocs_without_plugins.yml'); + const updatedMkdocsYml = await fs.readFile( + mockDir.resolve('mkdocs_without_plugins.yml'), + ); const parsedYml = yaml.load(updatedMkdocsYml.toString()) as { plugins: string[]; }; @@ -348,11 +355,11 @@ describe('helpers', () => { }); it('should not override existing plugins', async () => { await patchMkdocsYmlWithPlugins( - '/mkdocs_with_additional_plugins.yml', + mockDir.resolve('mkdocs_with_additional_plugins.yml'), mockLogger, ); const updatedMkdocsYml = await fs.readFile( - '/mkdocs_with_additional_plugins.yml', + mockDir.resolve('mkdocs_with_additional_plugins.yml'), ); const parsedYml = yaml.load(updatedMkdocsYml.toString()) as { plugins: string[]; @@ -364,13 +371,13 @@ describe('helpers', () => { }); it('should add all provided default plugins', async () => { await patchMkdocsYmlWithPlugins( - '/mkdocs_with_additional_plugins.yml', + mockDir.resolve('mkdocs_with_additional_plugins.yml'), mockLogger, ['techdocs-core', 'custom-plugin'], ); const updatedMkdocsYml = await fs.readFile( - '/mkdocs_with_additional_plugins.yml', + mockDir.resolve('mkdocs_with_additional_plugins.yml'), ); const parsedYml = yaml.load(updatedMkdocsYml.toString()) as { plugins: string[]; @@ -386,45 +393,45 @@ describe('helpers', () => { warn.mockClear(); }); it('should have no effect if docs/index.md exists', async () => { - mockFs({ - '/docs/index.md': 'index.md content', - '/docs/README.md': 'docs/README.md content', + mockDir.setContent({ + 'docs/index.md': 'index.md content', + 'docs/README.md': 'docs/README.md content', }); - await patchIndexPreBuild({ inputDir: '/', logger: mockLogger }); + await patchIndexPreBuild({ inputDir: mockDir.path, logger: mockLogger }); - await expect(fs.readFile('/docs/index.md', 'utf-8')).resolves.toEqual( - 'index.md content', - ); + await expect( + fs.readFile(mockDir.resolve('docs/index.md'), 'utf-8'), + ).resolves.toEqual('index.md content'); expect(warn).not.toHaveBeenCalledWith(); }); it("should use docs/README.md if docs/index.md doesn't exists", async () => { - mockFs({ - '/docs/README.md': 'docs/README.md content', - '/README.md': 'main README.md content', + mockDir.setContent({ + 'docs/README.md': 'docs/README.md content', + 'README.md': 'main README.md content', }); - await patchIndexPreBuild({ inputDir: '/', logger: mockLogger }); + await patchIndexPreBuild({ inputDir: mockDir.path, logger: mockLogger }); - await expect(fs.readFile('/docs/index.md', 'utf-8')).resolves.toEqual( - 'docs/README.md content', - ); + await expect( + fs.readFile(mockDir.resolve('docs/index.md'), 'utf-8'), + ).resolves.toEqual('docs/README.md content'); expect(warn.mock.calls).toEqual([ [`${path.normalize('docs/index.md')} not found.`], ]); }); it('should use README.md if neither docs/index.md or docs/README.md exist', async () => { - mockFs({ - '/README.md': 'main README.md content', + mockDir.setContent({ + 'README.md': 'main README.md content', }); - await patchIndexPreBuild({ inputDir: '/', logger: mockLogger }); + await patchIndexPreBuild({ inputDir: mockDir.path, logger: mockLogger }); - await expect(fs.readFile('/docs/index.md', 'utf-8')).resolves.toEqual( - 'main README.md content', - ); + await expect( + fs.readFile(mockDir.resolve('docs/index.md'), 'utf-8'), + ).resolves.toEqual('main README.md content'); expect(warn.mock.calls).toEqual([ [`${path.normalize('docs/index.md')} not found.`], [`${path.normalize('docs/README.md')} not found.`], @@ -433,11 +440,13 @@ describe('helpers', () => { }); it('should not use any file as index.md if no one matches the requirements', async () => { - mockFs({}); + mockDir.setContent({}); - await patchIndexPreBuild({ inputDir: '/', logger: mockLogger }); + await patchIndexPreBuild({ inputDir: mockDir.path, logger: mockLogger }); - await expect(fs.readFile('/docs/index.md', 'utf-8')).rejects.toThrow(); + await expect( + fs.readFile(mockDir.resolve('docs/index.md'), 'utf-8'), + ).rejects.toThrow(); const paths = [ path.normalize('docs/index.md'), path.normalize('docs/README.md'), @@ -449,7 +458,7 @@ describe('helpers', () => { ...paths.map(p => [`${p} not found.`]), [ `Could not find any techdocs' index file. Please make sure at least one of ${paths - .map(p => path.sep + p) + .map(p => mockDir.resolve(p)) .join(' ')} exists.`, ], ]); @@ -463,13 +472,11 @@ describe('helpers', () => { }; beforeEach(() => { - mockFs({ - [rootDir]: mockFiles, - }); + mockDir.setContent(mockFiles); }); it('should create the file if it does not exist', async () => { - const filePath = path.join(rootDir, 'wrong_techdocs_metadata.json'); + const filePath = mockDir.resolve('wrong_techdocs_metadata.json'); await createOrUpdateMetadata(filePath, mockLogger); // Check if the file exists @@ -479,7 +486,7 @@ describe('helpers', () => { }); it('should throw error when the JSON is invalid', async () => { - const filePath = path.join(rootDir, 'invalid_techdocs_metadata.json'); + const filePath = mockDir.resolve('invalid_techdocs_metadata.json'); await expect( createOrUpdateMetadata(filePath, mockLogger), @@ -487,7 +494,7 @@ describe('helpers', () => { }); it('should add build timestamp to the metadata json', async () => { - const filePath = path.join(rootDir, 'techdocs_metadata.json'); + const filePath = mockDir.resolve('techdocs_metadata.json'); await createOrUpdateMetadata(filePath, mockLogger); @@ -496,7 +503,7 @@ describe('helpers', () => { }); it('should add list of files to the metadata json', async () => { - const filePath = path.join(rootDir, 'techdocs_metadata.json'); + const filePath = mockDir.resolve('techdocs_metadata.json'); await createOrUpdateMetadata(filePath, mockLogger); @@ -508,16 +515,14 @@ describe('helpers', () => { describe('storeEtagMetadata', () => { beforeEach(() => { - mockFs({ - [rootDir]: { - 'invalid_techdocs_metadata.json': 'dsds', - 'techdocs_metadata.json': '{"site_name": "Tech Docs"}', - }, + mockDir.setContent({ + 'invalid_techdocs_metadata.json': 'dsds', + 'techdocs_metadata.json': '{"site_name": "Tech Docs"}', }); }); it('should throw error when the JSON is invalid', async () => { - const filePath = path.join(rootDir, 'invalid_techdocs_metadata.json'); + const filePath = mockDir.resolve('invalid_techdocs_metadata.json'); await expect(storeEtagMetadata(filePath, 'etag123abc')).rejects.toThrow( 'Unexpected token', @@ -525,7 +530,7 @@ describe('helpers', () => { }); it('should add etag to the metadata json', async () => { - const filePath = path.join(rootDir, 'techdocs_metadata.json'); + const filePath = mockDir.resolve('techdocs_metadata.json'); await storeEtagMetadata(filePath, 'etag123abc'); @@ -535,34 +540,31 @@ describe('helpers', () => { }); describe('getMkdocsYml', () => { - const inputDir = resolvePath(__filename, '../__fixtures__/'); const siteOptions = { name: mockEntity.metadata.title, }; it('returns expected contents when .yml file is present', async () => { - const key = path.join(inputDir, 'mkdocs.yml'); - mockFs({ [key]: mkdocsYml }); + mockDir.setContent({ 'mkdocs.yml': mkdocsYml }); const { path: mkdocsPath, content, configIsTemporary, - } = await getMkdocsYml(inputDir, siteOptions); + } = await getMkdocsYml(mockDir.path, siteOptions); - expect(mkdocsPath).toBe(key); + expect(mkdocsPath).toBe(mockDir.resolve('mkdocs.yml')); expect(content).toBe(mkdocsYml.toString()); expect(configIsTemporary).toBe(false); }); it('returns expected contents when .yaml file is present', async () => { - const key = path.join(inputDir, 'mkdocs.yaml'); - mockFs({ [key]: mkdocsYml }); + mockDir.setContent({ 'mkdocs.yaml': mkdocsYml }); const { path: mkdocsPath, content, configIsTemporary, - } = await getMkdocsYml(inputDir, siteOptions); - expect(mkdocsPath).toBe(key); + } = await getMkdocsYml(mockDir.path, siteOptions); + expect(mkdocsPath).toBe(mockDir.resolve('mkdocs.yaml')); expect(content).toBe(mkdocsYml.toString()); expect(configIsTemporary).toBe(false); }); @@ -571,17 +573,16 @@ describe('helpers', () => { const defaultSiteOptions = { name: 'Default Test site name', }; - const key = path.join(inputDir, 'mkdocs.yml'); const mockPathExists = jest.spyOn(fs, 'pathExists'); mockPathExists.mockImplementation(() => Promise.resolve(false)); - mockFs({ [key]: mkdocsDefaultYml }); + mockDir.setContent({ 'mkdocs.yml': mkdocsDefaultYml }); const { path: mkdocsPath, content, configIsTemporary, - } = await getMkdocsYml(inputDir, defaultSiteOptions); + } = await getMkdocsYml(mockDir.path, defaultSiteOptions); - expect(mkdocsPath).toBe(key); + expect(mkdocsPath).toBe(mockDir.resolve('mkdocs.yml')); expect(content.split(/[\r\n]+/g)).toEqual( mkdocsDefaultYml.toString().split(/[\r\n]+/g), ); diff --git a/plugins/techdocs-node/src/stages/publish/awsS3.test.ts b/plugins/techdocs-node/src/stages/publish/awsS3.test.ts index f3b2162a6b..9a9c9b4542 100644 --- a/plugins/techdocs-node/src/stages/publish/awsS3.test.ts +++ b/plugins/techdocs-node/src/stages/publish/awsS3.test.ts @@ -35,16 +35,17 @@ import { import { mockClient, AwsClientStub } from 'aws-sdk-client-mock'; import express from 'express'; import request from 'supertest'; -import mockFs from 'mock-fs'; import path from 'path'; import fs from 'fs-extra'; import { AwsS3Publish } from './awsS3'; -import { storageRootDir } from '../../testUtils/StorageFilesMock'; import { Readable } from 'stream'; +import { createMockDirectory } from '@backstage/backend-test-utils'; const env = process.env; let s3Mock: AwsClientStub; +const mockDir = createMockDirectory(); + function getMockCredentialProvider(): Promise { return Promise.resolve({ sdkCredentialProvider: async () => { @@ -66,7 +67,7 @@ const getEntityRootDir = (entity: Entity) => { metadata: { namespace, name }, } = entity; - return path.join(storageRootDir, namespace || DEFAULT_NAMESPACE, kind, name); + return mockDir.resolve(namespace || DEFAULT_NAMESPACE, kind, name); }; class ErrorReadable extends Readable { @@ -182,27 +183,23 @@ describe('AwsS3Publish', () => { getMockCredentialProvider(), ); - mockFs({ + mockDir.setContent({ [directory]: files, }); - const { StorageFilesMock } = require('../../testUtils/StorageFilesMock'); - const storage = new StorageFilesMock(); - storage.emptyFiles(); - s3Mock = mockClient(S3Client); s3Mock.on(HeadObjectCommand).callsFake(input => { - if (!storage.fileExists(input.Key)) { + if (!fs.pathExistsSync(mockDir.resolve(input.Key))) { throw new Error('File does not exist'); } return {}; }); s3Mock.on(GetObjectCommand).callsFake(input => { - if (storage.fileExists(input.Key)) { + if (fs.pathExistsSync(mockDir.resolve(input.Key))) { return { - Body: Readable.from(storage.readFile(input.Key)), + Body: Readable.from(fs.readFileSync(mockDir.resolve(input.Key))), }; } @@ -237,12 +234,11 @@ describe('AwsS3Publish', () => { s3Mock.on(UploadPartCommand).rejects(); s3Mock.on(PutObjectCommand).callsFake(input => { - storage.writeFile(input.Key, input.Body); + mockDir.addContent({ [input.Key]: input.Body }); }); }); afterEach(() => { - mockFs.restore(); process.env = env; }); @@ -378,8 +374,7 @@ describe('AwsS3Publish', () => { }); it('should fail to publish a directory', async () => { - const wrongPathToGeneratedDirectory = path.join( - storageRootDir, + const wrongPathToGeneratedDirectory = mockDir.resolve( 'wrong', 'path', 'to', @@ -393,11 +388,9 @@ describe('AwsS3Publish', () => { directory: wrongPathToGeneratedDirectory, }); - await expect(fails).rejects.toMatchObject({ - message: expect.stringContaining( - 'Unable to upload file(s) to AWS S3. Error: Failed to read template directory: ENOENT, no such file or directory', - ), - }); + await expect(fails).rejects.toThrow( + `Unable to upload file(s) to AWS S3. Error: Failed to read template directory: ENOENT: no such file or directory, scandir '${wrongPathToGeneratedDirectory}'`, + ); await expect(fails).rejects.toMatchObject({ message: expect.stringContaining(wrongPathToGeneratedDirectory), diff --git a/plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts b/plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts index 01565aa2df..09b1fcfd31 100644 --- a/plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts +++ b/plugins/techdocs-node/src/stages/publish/azureBlobStorage.test.ts @@ -19,7 +19,6 @@ import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; -import mockFs from 'mock-fs'; import path from 'path'; import fs from 'fs-extra'; import { AzureBlobStoragePublish } from './azureBlobStorage'; @@ -28,10 +27,9 @@ import { BlobUploadCommonResponse, ContainerGetPropertiesResponse, } from '@azure/storage-blob'; -import { - storageRootDir, - StorageFilesMock, -} from '../../testUtils/StorageFilesMock'; +import { createMockDirectory } from '@backstage/backend-test-utils'; + +const mockDir = createMockDirectory(); jest.mock('@azure/identity', () => ({ __esModule: true, @@ -40,13 +38,12 @@ jest.mock('@azure/identity', () => ({ jest.mock('@azure/storage-blob', () => { class BlockBlobClient { - constructor( - private readonly blobName: string, - private readonly storage: StorageFilesMock, - ) {} + constructor(private readonly blobName: string) {} uploadFile(source: string): Promise { - this.storage.writeFile(this.blobName, source); + mockDir.addContent({ + [this.blobName]: fs.readFileSync(source, 'utf8'), + }); return Promise.resolve({ _response: { request: { @@ -59,14 +56,14 @@ jest.mock('@azure/storage-blob', () => { } exists() { - return this.storage.fileExists(this.blobName); + return fs.pathExistsSync(mockDir.resolve(this.blobName)); } download() { const emitter = new EventEmitter(); setTimeout(() => { - if (this.storage.fileExists(this.blobName)) { - emitter.emit('data', this.storage.readFile(this.blobName)); + if (fs.pathExistsSync(mockDir.resolve(this.blobName))) { + emitter.emit('data', fs.readFileSync(mockDir.resolve(this.blobName))); emitter.emit('end'); } else { emitter.emit( @@ -126,10 +123,7 @@ jest.mock('@azure/storage-blob', () => { } class ContainerClient { - constructor( - private readonly containerName: string, - protected readonly storage: StorageFilesMock, - ) {} + constructor(private readonly containerName: string) {} getProperties(): Promise { return Promise.resolve({ @@ -145,7 +139,7 @@ jest.mock('@azure/storage-blob', () => { } getBlockBlobClient(blobName: string) { - return new BlockBlobClient(blobName, this.storage); + return new BlockBlobClient(blobName); } listBlobsFlat() { @@ -180,31 +174,24 @@ jest.mock('@azure/storage-blob', () => { class ContainerClientFailUpload extends ContainerClient { getBlockBlobClient(blobName: string) { - return new BlockBlobClientFailUpload(blobName, this.storage); + return new BlockBlobClientFailUpload(blobName); } } class BlobServiceClient { - storage = new StorageFilesMock(); - constructor( public readonly url: string, private readonly credential?: StorageSharedKeyCredential, - ) { - this.storage.emptyFiles(); - } + ) {} getContainerClient(containerName: string) { if (containerName === 'bad_container') { - return new ContainerClientFailGetProperties( - containerName, - this.storage, - ); + return new ContainerClientFailGetProperties(containerName); } if (this.credential?.accountName === 'bad_account_credentials') { - return new ContainerClientFailUpload(containerName, this.storage); + return new ContainerClientFailUpload(containerName); } - return new ContainerClient(containerName, this.storage); + return new ContainerClient(containerName); } } @@ -231,7 +218,7 @@ const getEntityRootDir = (entity: Entity) => { metadata: { namespace, name }, } = entity; - return path.join(storageRootDir, namespace || DEFAULT_NAMESPACE, kind, name); + return mockDir.resolve(namespace || DEFAULT_NAMESPACE, kind, name); }; const logger = getVoidLogger(); @@ -314,15 +301,11 @@ describe('AzureBlobStoragePublish', () => { }; beforeEach(async () => { - mockFs({ + mockDir.setContent({ [directory]: files, }); }); - afterEach(() => { - mockFs.restore(); - }); - describe('getReadiness', () => { it('should validate correct config', async () => { const publisher = createPublisherFromConfig(); @@ -374,8 +357,7 @@ describe('AzureBlobStoragePublish', () => { }); it('should fail to publish a directory', async () => { - const wrongPathToGeneratedDirectory = path.join( - storageRootDir, + const wrongPathToGeneratedDirectory = mockDir.resolve( 'wrong', 'path', 'to', @@ -391,11 +373,9 @@ describe('AzureBlobStoragePublish', () => { directory: wrongPathToGeneratedDirectory, }); - await expect(fails).rejects.toMatchObject({ - message: expect.stringContaining( - 'Unable to upload file(s) to Azure. Error: Failed to read template directory: ENOENT, no such file or directory', - ), - }); + await expect(fails).rejects.toThrow( + `Unable to upload file(s) to Azure. Error: Failed to read template directory: ENOENT: no such file or directory, scandir '${wrongPathToGeneratedDirectory}'`, + ); await expect(fails).rejects.toMatchObject({ message: expect.stringContaining(wrongPathToGeneratedDirectory), diff --git a/plugins/techdocs-node/src/stages/publish/googleStorage.test.ts b/plugins/techdocs-node/src/stages/publish/googleStorage.test.ts index 4418e00c81..17bfb3e3c4 100644 --- a/plugins/techdocs-node/src/stages/publish/googleStorage.test.ts +++ b/plugins/techdocs-node/src/stages/publish/googleStorage.test.ts @@ -19,26 +19,21 @@ import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; -import mockFs from 'mock-fs'; import path from 'path'; import fs from 'fs-extra'; import { Readable } from 'stream'; import { GoogleGCSPublish } from './googleStorage'; -import { - storageRootDir, - StorageFilesMock, -} from '../../testUtils/StorageFilesMock'; +import { createMockDirectory } from '@backstage/backend-test-utils'; + +const mockDir = createMockDirectory(); jest.mock('@google-cloud/storage', () => { class GCSFile { - constructor( - private readonly filePath: string, - private readonly storage: StorageFilesMock, - ) {} + constructor(private readonly filePath: string) {} exists() { return new Promise(async (resolve, reject) => { - if (this.storage.fileExists(this.filePath)) { + if (fs.pathExistsSync(mockDir.resolve(this.filePath))) { resolve([true]); } else { reject(); @@ -51,11 +46,14 @@ jest.mock('@google-cloud/storage', () => { readable._read = () => {}; process.nextTick(() => { - if (this.storage.fileExists(this.filePath)) { + if (fs.pathExistsSync(mockDir.resolve(this.filePath))) { if (readable.eventNames().includes('pipe')) { readable.emit('pipe'); } - readable.emit('data', this.storage.readFile(this.filePath)); + readable.emit( + 'data', + fs.readFileSync(mockDir.resolve(this.filePath)), + ); readable.emit('end'); } else { readable.emit( @@ -74,10 +72,7 @@ jest.mock('@google-cloud/storage', () => { } class Bucket { - constructor( - private readonly bucketName: string, - private readonly storage: StorageFilesMock, - ) {} + constructor(private readonly bucketName: string) {} async getMetadata() { if (this.bucketName === 'bad_bucket_name') { @@ -88,7 +83,9 @@ jest.mock('@google-cloud/storage', () => { upload(source: string, { destination }: { destination: string }) { return new Promise(async resolve => { - this.storage.writeFile(destination, source); + mockDir.addContent({ + [destination]: fs.readFileSync(source, 'utf8'), + }); resolve(null); }); } @@ -97,7 +94,7 @@ jest.mock('@google-cloud/storage', () => { if (this.bucketName === 'delete_stale_files_error') { throw Error('Message'); } - return new GCSFile(destinationFilePath, this.storage); + return new GCSFile(destinationFilePath); } getFilesStream() { @@ -119,14 +116,8 @@ jest.mock('@google-cloud/storage', () => { } class Storage { - storage = new StorageFilesMock(); - - constructor() { - this.storage.emptyFiles(); - } - bucket(bucketName: string) { - return new Bucket(bucketName, this.storage); + return new Bucket(bucketName); } } @@ -142,7 +133,7 @@ const getEntityRootDir = (entity: Entity) => { metadata: { namespace, name }, } = entity; - return path.join(storageRootDir, namespace || DEFAULT_NAMESPACE, kind, name); + return mockDir.resolve(namespace || DEFAULT_NAMESPACE, kind, name); }; const logger = getVoidLogger(); @@ -220,15 +211,11 @@ describe('GoogleGCSPublish', () => { }; beforeEach(() => { - mockFs({ + mockDir.setContent({ [directory]: files, }); }); - afterEach(() => { - mockFs.restore(); - }); - describe('getReadiness', () => { it('should validate correct config', async () => { const publisher = createPublisherFromConfig(); @@ -300,8 +287,7 @@ describe('GoogleGCSPublish', () => { }); it('should fail to publish a directory', async () => { - const wrongPathToGeneratedDirectory = path.join( - storageRootDir, + const wrongPathToGeneratedDirectory = mockDir.resolve( 'wrong', 'path', 'to', @@ -315,13 +301,9 @@ describe('GoogleGCSPublish', () => { directory: wrongPathToGeneratedDirectory, }); - // Can not do exact error message match due to mockFs adding unexpected characters in the path when throwing the error - // Issue reported https://github.com/tschaub/mock-fs/issues/118 - await expect(fails).rejects.toMatchObject({ - message: expect.stringContaining( - `Unable to upload file(s) to Google Cloud Storage. Error: Failed to read template directory: ENOENT, no such file or directory`, - ), - }); + await expect(fails).rejects.toThrow( + `Unable to upload file(s) to Google Cloud Storage. Error: Failed to read template directory: ENOENT: no such file or directory, scandir '${wrongPathToGeneratedDirectory}'`, + ); await expect(fails).rejects.toMatchObject({ message: expect.stringContaining(wrongPathToGeneratedDirectory), diff --git a/plugins/techdocs-node/src/stages/publish/helpers.test.ts b/plugins/techdocs-node/src/stages/publish/helpers.test.ts index 7b303b3bf5..2a74cbee61 100644 --- a/plugins/techdocs-node/src/stages/publish/helpers.test.ts +++ b/plugins/techdocs-node/src/stages/publish/helpers.test.ts @@ -13,9 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import mockFs from 'mock-fs'; -import * as os from 'os'; -import * as path from 'path'; import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { getStaleFiles, @@ -27,6 +24,7 @@ import { lowerCaseEntityTripletInStoragePath, normalizeExternalStorageRootPath, } from './helpers'; +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('getHeadersForFileExtension', () => { const correctMapOfExtensions = [ @@ -57,30 +55,24 @@ describe('getHeadersForFileExtension', () => { }); describe('getFileTreeRecursively', () => { - const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; + const mockDir = createMockDirectory(); beforeEach(() => { - mockFs({ - [root]: { - file1: '', - subDirA: { - file2: '', - emptyDir1: mockFs.directory(), - }, - emptyDir2: mockFs.directory(), + mockDir.setContent({ + file1: '', + subDirA: { + file2: '', + emptyDir1: {}, }, + emptyDir2: {}, }); }); - afterEach(() => { - mockFs.restore(); - }); - it('returns complete file tree of a path', async () => { - const fileList = await getFileTreeRecursively(root); + const fileList = await getFileTreeRecursively(mockDir.path); expect(fileList.length).toBe(2); - expect(fileList).toContain(path.resolve(root, 'file1')); - expect(fileList).toContain(path.resolve(root, 'subDirA/file2')); + expect(fileList).toContain(mockDir.resolve('file1')); + expect(fileList).toContain(mockDir.resolve('subDirA/file2')); }); }); diff --git a/plugins/techdocs-node/src/stages/publish/local.test.ts b/plugins/techdocs-node/src/stages/publish/local.test.ts index c1a5f02d67..4019f80d11 100644 --- a/plugins/techdocs-node/src/stages/publish/local.test.ts +++ b/plugins/techdocs-node/src/stages/publish/local.test.ts @@ -16,15 +16,15 @@ import { getVoidLogger, PluginEndpointDiscovery, - resolvePackagePath, } from '@backstage/backend-common'; +import { overridePackagePathResolution } from '@backstage/backend-common/testUtils'; import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; -import mockFs from 'mock-fs'; import * as os from 'os'; import { LocalPublish } from './local'; import path from 'path'; +import { createMockDirectory } from '@backstage/backend-test-utils'; const createMockEntity = (annotations = {}, lowerCase = false) => { return { @@ -44,30 +44,28 @@ const testDiscovery: jest.Mocked = { getExternalBaseUrl: jest.fn(), }; +const mockPublishDir = createMockDirectory(); + +overridePackagePathResolution({ + packageName: '@backstage/plugin-techdocs-backend', + paths: { + 'static/docs': mockPublishDir.path, + }, +}); + const logger = getVoidLogger(); -const tmpDir = - os.platform() === 'win32' ? 'C:\\tmp\\generatedDir' : '/tmp/generatedDir'; - -const resolvedDir = resolvePackagePath( - '@backstage/plugin-techdocs-backend', - 'static/docs', -); - describe('local publisher', () => { + const mockDir = createMockDirectory(); + describe('publish', () => { beforeEach(() => { - mockFs({ - [tmpDir]: { - 'index.html': '', - }, + mockPublishDir.clear(); + mockDir.setContent({ + 'index.html': '', }); }); - afterEach(() => { - mockFs.restore(); - }); - it('should publish generated documentation dir', async () => { const mockConfig = new ConfigReader({}); @@ -79,7 +77,7 @@ describe('local publisher', () => { const mockEntity = createMockEntity(); const lowerMockEntity = createMockEntity(undefined, true); - await publisher.publish({ entity: mockEntity, directory: tmpDir }); + await publisher.publish({ entity: mockEntity, directory: mockDir.path }); expect(await publisher.hasDocsBeenGenerated(mockEntity)).toBe(true); @@ -102,12 +100,14 @@ describe('local publisher', () => { const mockEntity = createMockEntity(); const lowerMockEntity = createMockEntity(undefined, true); - await publisher.publish({ entity: mockEntity, directory: tmpDir }); + await publisher.publish({ entity: mockEntity, directory: mockDir.path }); expect(await publisher.hasDocsBeenGenerated(mockEntity)).toBe(true); // Lower/upper should be treated differently. - expect(await publisher.hasDocsBeenGenerated(lowerMockEntity)).toBe(false); + expect(await publisher.hasDocsBeenGenerated(lowerMockEntity)).toBe( + os.platform() === 'darwin', // MacOS is case-insensitive + ); }); it('should throw with unsafe triplet', async () => { @@ -126,7 +126,7 @@ describe('local publisher', () => { }; await expect(() => - publisher.publish({ entity: mockEntity, directory: tmpDir }), + publisher.publish({ entity: mockEntity, directory: mockDir.path }), ).rejects.toThrow('Unable to publish TechDocs site'); }); @@ -149,7 +149,7 @@ describe('local publisher', () => { }; await expect(() => - publisher.publish({ entity: mockEntity, directory: tmpDir }), + publisher.publish({ entity: mockEntity, directory: mockDir.path }), ).rejects.toThrow('Unable to publish TechDocs site'); }); }); @@ -165,25 +165,19 @@ describe('local publisher', () => { beforeEach(() => { app = express().use(publisher.docsRouter()); - mockFs({ - [resolvedDir]: { - 'unsafe.html': '', - 'unsafe.svg': '', - default: { - testkind: { - testname: { - 'index.html': 'found it', - }, + mockPublishDir.setContent({ + 'unsafe.html': '', + 'unsafe.svg': '', + default: { + testkind: { + testname: { + 'index.html': 'found it', }, }, }, }); }); - afterEach(() => { - mockFs.restore(); - }); - it('should pass text/plain content-type for unsafe types', async () => { const htmlResponse = await request(app).get(`/unsafe.html`); expect(htmlResponse.text).toEqual(''); @@ -228,7 +222,7 @@ describe('local publisher', () => { const response = await request(app).get( '/default/TestKind/TestName/index.html', ); - expect(response.status).toBe(404); + expect(response.status).toBe(os.platform() === 'darwin' ? 200 : 404); }); it('should work with a configured directory', async () => { @@ -236,15 +230,13 @@ describe('local publisher', () => { techdocs: { publisher: { local: { - publishDirectory: tmpDir, + publishDirectory: mockDir.path, }, }, }, }); - mockFs({ - [tmpDir]: { - 'index.html': 'found it', - }, + mockDir.setContent({ + 'index.html': 'found it', }); const legacyPublisher = LocalPublish.fromConfig( customConfig, diff --git a/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts b/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts index 49e68080df..93fe54ac27 100644 --- a/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts +++ b/plugins/techdocs-node/src/stages/publish/openStackSwift.test.ts @@ -23,13 +23,14 @@ import { import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; -import mockFs from 'mock-fs'; import fs from 'fs-extra'; import path from 'path'; import { OpenStackSwiftPublish } from './openStackSwift'; import { PublisherBase, TechDocsMetadata } from './types'; -import { storageRootDir } from '../../testUtils/StorageFilesMock'; import { Stream, Readable } from 'stream'; +import { createMockDirectory } from '@backstage/backend-test-utils'; + +const mockDir = createMockDirectory(); jest.mock('@trendyol-js/openstack-swift-sdk', () => { const { @@ -45,7 +46,7 @@ jest.mock('@trendyol-js/openstack-swift-sdk', () => { const checkFileExists = async (Key: string): Promise => { // Key will always have / as file separator irrespective of OS since cloud providers expects /. // Normalize Key to OS specific path before checking if file exists. - const filePath = path.join(storageRootDir, Key); + const filePath = mockDir.resolve(Key); try { await fs.access(filePath, fs.constants.F_OK); @@ -96,7 +97,7 @@ jest.mock('@trendyol-js/openstack-swift-sdk', () => { stream: Readable, ) { try { - const filePath = path.join(storageRootDir, destination); + const filePath = mockDir.resolve(destination); const fileBuffer = await streamToBuffer(stream); await fs.writeFile(filePath, fileBuffer); @@ -114,7 +115,7 @@ jest.mock('@trendyol-js/openstack-swift-sdk', () => { } async download(_containerName: string, file: string) { - const filePath = path.join(storageRootDir, file); + const filePath = mockDir.resolve(file); const fileExists = await checkFileExists(file); if (!fileExists) { return new NotFound(); @@ -151,7 +152,7 @@ const getEntityRootDir = (entity: Entity) => { metadata: { namespace, name }, } = entity; - return path.join(storageRootDir, namespace || DEFAULT_NAMESPACE, kind, name); + return mockDir.resolve(namespace || DEFAULT_NAMESPACE, kind, name); }; const getPosixEntityRootDir = (entity: Entity) => { @@ -193,11 +194,11 @@ beforeEach(() => { publisher = OpenStackSwiftPublish.fromConfig(mockConfig, logger); }); -afterEach(() => { - mockFs.restore(); -}); - describe('OpenStackSwiftPublish', () => { + afterEach(() => { + mockDir.clear(); + }); + describe('getReadiness', () => { it('should validate correct config', async () => { expect(await publisher.getReadiness()).toEqual({ @@ -239,7 +240,7 @@ describe('OpenStackSwiftPublish', () => { const entity = createMockEntity(); const entityRootDir = getEntityRootDir(entity); - mockFs({ + mockDir.setContent({ [entityRootDir]: { 'index.html': '', '404.html': '', @@ -254,12 +255,9 @@ describe('OpenStackSwiftPublish', () => { const entity = createMockEntity(); const entityRootDir = getEntityRootDir(entity); - expect( - await publisher.publish({ - entity, - directory: entityRootDir, - }), - ).toMatchObject({ + await expect( + publisher.publish({ entity, directory: entityRootDir }), + ).resolves.toMatchObject({ objects: expect.arrayContaining([ 'test-namespace/TestKind/test-component-name/404.html', `test-namespace/TestKind/test-component-name/index.html`, @@ -269,8 +267,7 @@ describe('OpenStackSwiftPublish', () => { }); it('should fail to publish a directory', async () => { - const wrongPathToGeneratedDirectory = path.join( - storageRootDir, + const wrongPathToGeneratedDirectory = mockDir.resolve( 'wrong', 'path', 'to', @@ -290,13 +287,9 @@ describe('OpenStackSwiftPublish', () => { directory: wrongPathToGeneratedDirectory, }); - // Can not do exact error message match due to mockFs adding unexpected characters in the path when throwing the error - // Issue reported https://github.com/tschaub/mock-fs/issues/118 - await expect(fails).rejects.toMatchObject({ - message: expect.stringContaining( - `Unable to upload file(s) to OpenStack Swift. Error: Failed to read template directory: ENOENT, no such file or directory`, - ), - }); + await expect(fails).rejects.toThrow( + `Unable to upload file(s) to OpenStack Swift. Error: Failed to read template directory: ENOENT: no such file or directory, scandir '${wrongPathToGeneratedDirectory}'`, + ); await expect(fails).rejects.toMatchObject({ message: expect.stringContaining(wrongPathToGeneratedDirectory), }); @@ -308,7 +301,7 @@ describe('OpenStackSwiftPublish', () => { const entity = createMockEntity(); const entityRootDir = getEntityRootDir(entity); - mockFs({ + mockDir.setContent({ [entityRootDir]: { 'index.html': 'file-content', }, @@ -330,7 +323,7 @@ describe('OpenStackSwiftPublish', () => { const entity = createMockEntity(); const entityRootDir = getEntityRootDir(entity); - mockFs({ + mockDir.setContent({ [entityRootDir]: { 'techdocs_metadata.json': '{"site_name": "backstage", "site_description": "site_content", "etag": "etag", "build_timestamp": 612741599}', @@ -353,7 +346,7 @@ describe('OpenStackSwiftPublish', () => { const entity = createMockEntity(); const entityRootDir = getEntityRootDir(entity); - mockFs({ + mockDir.setContent({ [entityRootDir]: { 'techdocs_metadata.json': `{'site_name': 'backstage', 'site_description': 'site_content', 'etag': 'etag', 'build_timestamp': 612741599}`, }, @@ -393,7 +386,7 @@ describe('OpenStackSwiftPublish', () => { beforeEach(() => { app = express().use(publisher.docsRouter()); - mockFs({ + mockDir.setContent({ [entityRootDir]: { html: { 'unsafe.html': '', diff --git a/plugins/techdocs-node/src/testUtils/StorageFilesMock.ts b/plugins/techdocs-node/src/testUtils/StorageFilesMock.ts deleted file mode 100644 index dd28110d00..0000000000 --- a/plugins/techdocs-node/src/testUtils/StorageFilesMock.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2020 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 path from 'path'; -import fs from 'fs-extra'; -import { IStorageFilesMock } from './types'; - -export const storageRootDir: string = - os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; - -const encoding = 'utf8'; - -export class StorageFilesMock implements IStorageFilesMock { - static rootDir = storageRootDir; - - private files: Record; - - constructor() { - this.files = {}; - } - - public emptyFiles(): void { - this.files = {}; - } - - public fileExists(targetPath: string): boolean { - const filePath = path.join(storageRootDir, targetPath); - const posixPath = filePath.split(path.posix.sep).join(path.sep); - return this.files[posixPath] !== undefined; - } - - public readFile(targetPath: string): Buffer { - const filePath = path.join(storageRootDir, targetPath); - return Buffer.from(this.files[filePath] ?? '', encoding); - } - - public writeFile(targetPath: string, sourcePath: string): void; - public writeFile(targetPath: string, sourceBuffer: Buffer): void; - public writeFile(targetPath: string, source: string | Buffer): void { - const filePath = path.join(storageRootDir, targetPath); - if (typeof source === 'string') { - this.files[filePath] = fs.readFileSync(source).toString(encoding); - } else { - this.files[filePath] = source.toString(encoding); - } - } -} diff --git a/plugins/techdocs-node/src/testUtils/types.ts b/plugins/techdocs-node/src/testUtils/types.ts deleted file mode 100644 index b58e004ab8..0000000000 --- a/plugins/techdocs-node/src/testUtils/types.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2020 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 interface IStorageFilesMock { - emptyFiles(): void; - fileExists(targetPath: string): boolean; - readFile(targetPath: string): Buffer; - writeFile(targetPath: string, sourcePath: string): void; - writeFile(targetPath: string, sourceBuffer: Buffer): void; - writeFile(targetPath: string, source: string | Buffer): void; -} diff --git a/yarn.lock b/yarn.lock index 556534cc2b..3383ae4e02 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4833,7 +4833,6 @@ __metadata: knex: ^2.0.0 lodash: ^4.17.21 luxon: ^3.0.0 - mock-fs: ^5.2.0 msw: ^1.0.0 node-fetch: ^2.6.7 supertest: ^6.1.3 @@ -7600,7 +7599,6 @@ __metadata: http-proxy-middleware: ^2.0.6 lodash: ^4.17.21 luxon: ^3.0.0 - mock-fs: ^5.2.0 morgan: ^1.10.0 msw: ^1.0.0 node-fetch: ^2.6.7 @@ -9688,6 +9686,7 @@ __metadata: "@azure/identity": ^3.2.1 "@azure/storage-blob": ^12.5.0 "@backstage/backend-common": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" @@ -9701,7 +9700,6 @@ __metadata: "@types/fs-extra": ^9.0.5 "@types/js-yaml": ^4.0.0 "@types/mime-types": ^2.1.0 - "@types/mock-fs": ^4.13.0 "@types/recursive-readdir": ^2.2.0 "@types/supertest": ^2.0.8 aws-sdk-client-mock: ^2.0.0 @@ -9712,7 +9710,6 @@ __metadata: js-yaml: ^4.0.0 json5: ^2.1.3 mime-types: ^2.1.27 - mock-fs: ^5.2.0 p-limit: ^3.1.0 recursive-readdir: ^2.2.2 supertest: ^6.1.3