From ce50a15506b0ad8a953003cf6bd591765d14c52c Mon Sep 17 00:00:00 2001 From: Stef Louwers Date: Mon, 18 Sep 2023 22:56:26 +0200 Subject: [PATCH 01/95] Fixed sorting and searching in the NewRelic table. Signed-off-by: Stef Louwers --- .changeset/heavy-ladybugs-leave.md | 5 ++ .../NewRelicFetchComponent.tsx | 56 ++++++++++++++++--- 2 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 .changeset/heavy-ladybugs-leave.md diff --git a/.changeset/heavy-ladybugs-leave.md b/.changeset/heavy-ladybugs-leave.md new file mode 100644 index 0000000000..5af2324260 --- /dev/null +++ b/.changeset/heavy-ladybugs-leave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-newrelic': patch +--- + +Fixed sorting and searching in the NewRelic table. diff --git a/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx b/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx index 0cd1c3b14c..85183916bd 100644 --- a/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx +++ b/plugins/newrelic/src/components/NewRelicFetchComponent/NewRelicFetchComponent.tsx @@ -22,16 +22,56 @@ import { newRelicApiRef, NewRelicApplications } from '../../api'; import { Progress, Table, TableColumn } from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; +const sortNumeric = + (field: F) => + (a: { [key in F]: number }, b: { [key in F]: number }) => { + return a[field] - b[field]; + }; + +type NewRelicTableData = { + name: string; + responseTime: number; + throughput: number; + errorRate: number; + instanceCount: number; + apdexScore: number; +}; + export const NewRelicAPMTable = ({ applications }: NewRelicApplications) => { - const columns: TableColumn[] = [ - { title: 'Application', field: 'name' }, - { title: 'Response Time (ms)', field: 'responseTime' }, - { title: 'Throughput (rpm)', field: 'throughput' }, - { title: 'Error Rate (%)', field: 'errorRate' }, - { title: 'Instance Count', field: 'instanceCount' }, - { title: 'Apdex', field: 'apdexScore' }, + const columns: TableColumn[] = [ + { title: 'Application', field: 'name', searchable: true }, + { + title: 'Response Time (ms)', + field: 'responseTime', + customSort: sortNumeric('responseTime'), + searchable: false, + }, + { + title: 'Throughput (rpm)', + field: 'throughput', + customSort: sortNumeric('throughput'), + searchable: false, + }, + { + title: 'Error Rate (%)', + field: 'errorRate', + customSort: sortNumeric('errorRate'), + searchable: false, + }, + { + title: 'Instance Count', + field: 'instanceCount', + customSort: sortNumeric('instanceCount'), + searchable: false, + }, + { + title: 'Apdex', + field: 'apdexScore', + customSort: sortNumeric('apdexScore'), + searchable: false, + }, ]; - const data = applications.map(app => { + const data: Array = applications.map(app => { const { name, application_summary: applicationSummary } = app; const { response_time: responseTime, From e9dd103ce015b3ccdbae56f47d1ba05d8b19a132 Mon Sep 17 00:00:00 2001 From: Stef Louwers Date: Mon, 18 Sep 2023 23:14:05 +0200 Subject: [PATCH 02/95] Add Gynzy to ADOPTERS.md Signed-off-by: Stef Louwers --- ADOPTERS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ADOPTERS.md b/ADOPTERS.md index 0d5b9fba59..0481653a87 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -260,3 +260,4 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [Localiza&Co](https://www.localiza.com/) | [Augusto Amormino](https://github.com/augustoamormino), [Jonas Soares](https://github.com/jonaopower), [Alexandre Amormino](https://github.com/alexandreamormino), [Greg Almeida](https://github.com/sephh) | We're excited to announce our adoption of Backstage as our Internal Developer Portal! Our mission is to elevate the Developer Experience by streaming information access. Backstage will serve as the ultimate hub for developer resources, including documentation, tools, software insights, and metrics. Through Backstage, we're simplifying processes, offering software templates to empowered and efficient development journey, enhancing self-service capabilities with the embedded all best practices. | | [V2 Digital](https://v2.digital) | [Joe Patterson](https://github.com/jrwpatterson)| We will be using it to be a corporate dashboard plus our software catalog. | | [AppsFlyer](https://www.appsflyer.com/) | [Shahar Shmaram](https://github.com/shmaram) | Internal Developer Portal, a catalog of all company resources, custom providers and processors, scaffolder for generating new resources. +| [Gynzy](https://gynzy.com) | [Stef Louwers](https://github.com/fhp) | We are building an internal developer portal to get an overview of all our software components. | From 715432ce71d8900fc6e0c657e35d2f0879890315 Mon Sep 17 00:00:00 2001 From: Kurt King Date: Fri, 22 Sep 2023 07:53:34 -0600 Subject: [PATCH 03/95] chore: remove alpha from import Signed-off-by: Kurt King --- docs/features/techdocs/addons.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/features/techdocs/addons.md b/docs/features/techdocs/addons.md index a41995c260..2e17891917 100644 --- a/docs/features/techdocs/addons.md +++ b/docs/features/techdocs/addons.md @@ -65,7 +65,7 @@ page in your `App.tsx`: // packages/app/src/App.tsx import { TechDocsReaderPage } from '@backstage/plugin-techdocs'; -import { TechDocsAddons } from '@backstage/plugin-techdocs-react/alpha'; +import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; // ... @@ -99,7 +99,7 @@ is very similar; instead of adding the `` registry under a import { EntityLayout } from '@backstage/plugin-catalog'; import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; -import { TechDocsAddons } from '@backstage/plugin-techdocs-react/alpha'; +import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; // ... @@ -146,7 +146,7 @@ an Addon, follow these steps: import { createTechDocsAddonExtension, TechDocsAddonLocations, -} from '@backstage/plugin-techdocs-react/alpha'; +} from '@backstage/plugin-techdocs-react'; import { CatGifComponent, CatGifComponentProps } from './addons'; // ... @@ -179,7 +179,7 @@ provided by the Addon framework. // plugins/your-plugin/src/addons/MakeAllImagesCatGifs.tsx import React, { useEffect } from 'react'; -import { useShadowRootElements } from '@backstage/plugin-techdocs-react/alpha'; +import { useShadowRootElements } from '@backstage/plugin-techdocs-react'; // This is a normal react component; in order to make it an Addon, you would // still create and provide it via your plugin as described above. The only From 3605370af64a3ddb82b5841e0cbc68f89c8c69d7 Mon Sep 17 00:00:00 2001 From: Larry Knott Date: Fri, 22 Sep 2023 16:09:09 -0600 Subject: [PATCH 04/95] Only show pagination controls when necessary Signed-off-by: Larry Knott --- .changeset/fuzzy-pillows-remain.md | 5 +++++ plugins/techdocs/src/home/components/Tables/DocsTable.tsx | 7 +++++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 .changeset/fuzzy-pillows-remain.md diff --git a/.changeset/fuzzy-pillows-remain.md b/.changeset/fuzzy-pillows-remain.md new file mode 100644 index 0000000000..d4fcb30415 --- /dev/null +++ b/.changeset/fuzzy-pillows-remain.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Only show pagination controls when necessary diff --git a/plugins/techdocs/src/home/components/Tables/DocsTable.tsx b/plugins/techdocs/src/home/components/Tables/DocsTable.tsx index 5e5b41ee3e..927eaf7256 100644 --- a/plugins/techdocs/src/home/components/Tables/DocsTable.tsx +++ b/plugins/techdocs/src/home/components/Tables/DocsTable.tsx @@ -94,14 +94,17 @@ export const DocsTable = (props: DocsTableProps) => { actionFactories.createCopyDocsUrlAction(copyToClipboard), ]; + const pageSize = 20; + const paging = documents && documents.length > pageSize; + return ( <> {loading || (documents && documents.length > 0) ? ( isLoading={loading} options={{ - paging: true, - pageSize: 20, + paging, + pageSize, search: true, actionsColumnIndex: -1, ...options, From 01ed4782ac49fbad33bc574b34f0e30ab63f6e87 Mon Sep 17 00:00:00 2001 From: Larry Knott Date: Mon, 2 Oct 2023 12:37:51 -0700 Subject: [PATCH 05/95] Update Changeset to be more specific as suggested by @vinzscam Signed-off-by: Larry Knott --- .changeset/fuzzy-pillows-remain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fuzzy-pillows-remain.md b/.changeset/fuzzy-pillows-remain.md index d4fcb30415..59e3dd7896 100644 --- a/.changeset/fuzzy-pillows-remain.md +++ b/.changeset/fuzzy-pillows-remain.md @@ -2,4 +2,4 @@ '@backstage/plugin-techdocs': patch --- -Only show pagination controls when necessary +Improved `DocsTable` to display pagination controls dynamically, appearing only when needed. From 86a07ebcf99fda9fbe06c4b61e966b4c705331de Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 4 Oct 2023 15:18:09 +0200 Subject: [PATCH 06/95] feat: creating scaffolder project area Signed-off-by: blam --- OWNERS.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/OWNERS.md b/OWNERS.md index 000a3e2444..a69bc0c322 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -116,6 +116,16 @@ Scope: Tooling for frontend and backend schema-first OpenAPI development. | -------------- | ------------ | --------------------------------------- | ------------- | | Aramis Sennyey | Spotify | [sennyeya](https://github.com/sennyeya) | `Aramis#7984` | +### Scaffolder + +Team: @backstage/scaffolder-maintainers + +Scope: The Scaffolder frontend and backend plugins, and related tooling. + +| Name | Organization | GitHub | Discord | +| ---------- | ------------ | ----------------------------------- | -------- | +| Paul Cowan | | [dagda1](https://github.com/dadga1) | `dagda1` | + ## Sponsors | Name | Organization | GitHub | Email | From cdeac1bda4f01e65dabf98228d70b188fe0f1e0d Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 4 Oct 2023 16:39:21 +0200 Subject: [PATCH 07/95] chore: add in bogdan too, to list of project area maintainers Signed-off-by: blam --- OWNERS.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/OWNERS.md b/OWNERS.md index a69bc0c322..f360f7e08e 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -122,9 +122,10 @@ Team: @backstage/scaffolder-maintainers Scope: The Scaffolder frontend and backend plugins, and related tooling. -| Name | Organization | GitHub | Discord | -| ---------- | ------------ | ----------------------------------- | -------- | -| Paul Cowan | | [dagda1](https://github.com/dadga1) | `dagda1` | +| Name | Organization | GitHub | Discord | +| ------------------- | -------------- | ------------------------------------- | ---------------- | +| Bogdan Nechyporenko | Bol.com | [acierto](https://github.com/acierto) | `bogdan_haarlem` | +| Paul Cowan | frontendrescue | [dagda1](https://github.com/dadga1) | `dagda1` | ## Sponsors From 3247b26d63ee47fcf88f8e78d9eeda5312f013e5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 2 Oct 2023 19:16:00 +0200 Subject: [PATCH 08/95] kubernetes-backend: refactor tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- plugins/kubernetes-backend/package.json | 1 - .../src/auth/ServiceAccountStrategy.test.ts | 40 ++++++++++++++----- .../src/service/KubernetesFetcher.test.ts | 30 ++++++++------ yarn.lock | 1 - 4 files changed, 48 insertions(+), 24 deletions(-) diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 54c5a35201..0fc766b6f5 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/yarn.lock b/yarn.lock index 20922a9959..83f578a4c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7626,7 +7626,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 From 5ebf9b47c1894aae6acfbbbabef4991019e874f2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 2 Oct 2023 19:43:39 +0200 Subject: [PATCH 09/95] app-backend: refactor to avoid mock-fs Signed-off-by: Patrik Oldsberg --- plugins/app-backend/package.json | 1 - .../app-backend/src/service/appPlugin.test.ts | 41 ++++++++++++------- yarn.lock | 1 - 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index cdbb36d23c..d225cc0c38 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/service/appPlugin.test.ts b/plugins/app-backend/src/service/appPlugin.test.ts index 09061b287d..7bffdd00f6 100644 --- a/plugins/app-backend/src/service/appPlugin.test.ts +++ b/plugins/app-backend/src/service/appPlugin.test.ts @@ -14,33 +14,46 @@ * 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'; +const mockDir = createMockDirectory(); + +jest.mock('../../../../packages/backend-common/src/paths', () => { + const actual = jest.requireActual( + '../../../../packages/backend-common/src/paths', + ); + return { + ...actual, + resolvePackagePath: (pkg: string, ...args: string[]) => { + if (pkg === 'app') { + return mockDir.resolve(...args); + } + return actual.resolvePackagePath(pkg, ...args); + }, + }; +}); + // 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/yarn.lock b/yarn.lock index 83f578a4c1..e4904f874a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4830,7 +4830,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 From 689a1aaf8f2cdc0672e69a7792dd17bafbc7e7f2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Oct 2023 16:55:19 +0200 Subject: [PATCH 10/95] catalog-backend: update test text match for node 20 Signed-off-by: Patrik Oldsberg --- .../src/modules/core/PlaceholderProcessor.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(); From eace331db02844a41a6e7da5c6cee1478f2da30a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Oct 2023 17:10:06 +0200 Subject: [PATCH 11/95] techdocs-node: refactor generate helpers test to avoid mock-fs Signed-off-by: Patrik Oldsberg --- plugins/techdocs-node/package.json | 1 + .../src/stages/generate/helpers.test.ts | 175 +++++++++--------- yarn.lock | 1 + 3 files changed, 90 insertions(+), 87 deletions(-) diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json index 35feb3b0b4..816dac3a00 100644 --- a/plugins/techdocs-node/package.json +++ b/plugins/techdocs-node/package.json @@ -69,6 +69,7 @@ "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", 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/yarn.lock b/yarn.lock index e4904f874a..3b4b943ee8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9667,6 +9667,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:^" From 8f773cfd02d07e773ab3b250a24f0f1d51e486ce Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Oct 2023 17:12:00 +0200 Subject: [PATCH 12/95] techdocs-node: refactor publish helpers test to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../src/stages/publish/helpers.test.ts | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) 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')); }); }); From 4c39e38f1e0b778834f18f1c7c16fb4638508ee5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Oct 2023 17:48:41 +0200 Subject: [PATCH 13/95] backend-common: initial utility for mocking resolved package paths Signed-off-by: Patrik Oldsberg --- .changeset/nasty-needles-hear.md | 5 ++ packages/backend-common/package.json | 4 ++ packages/backend-common/src/paths.ts | 14 +++++ packages/backend-common/src/testUtils.ts | 71 ++++++++++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 .changeset/nasty-needles-hear.md create mode 100644 packages/backend-common/src/testUtils.ts 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 ee43b74c16..164f388434 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..0b23333434 --- /dev/null +++ b/packages/backend-common/src/testUtils.ts @@ -0,0 +1,71 @@ +/* + * 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 PackagePathMock { + /** Restored the normal behavior of resolvePackagePath */ + restore(): void; +} + +/** @public */ +export interface PackagePathMockOptions { + /** The name of the package to mock the resolved path of */ + name: 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: `createPackagePathMock({name: 'x', paths: {'foo/bar': baz}})` + */ + paths?: { [path in string]: string | (() => string) }; +} + +/** @public */ +export function createPackagePathMock( + options: PackagePathMockOptions, +): PackagePathMock { + if (packagePathMocks.has(options.name)) { + throw new Error( + `Duplicate package path mock for package '${options.name}'`, + ); + } + + packagePathMocks.set(options.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(options.name); + }, + }; +} From b160cb555167319d7f81c40481400e30bd2698aa Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Oct 2023 17:49:24 +0200 Subject: [PATCH 14/95] techdocs-node: refactor publish local test to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../src/stages/publish/local.test.ts | 76 +++++++++---------- 1 file changed, 34 insertions(+), 42 deletions(-) diff --git a/plugins/techdocs-node/src/stages/publish/local.test.ts b/plugins/techdocs-node/src/stages/publish/local.test.ts index c1a5f02d67..7a1832d0b3 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 { createPackagePathMock } 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(); + +createPackagePathMock({ + name: '@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, From 699203d94fe59e4982f0e837ee209c9cb7b6cbf5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Oct 2023 20:57:35 +0200 Subject: [PATCH 15/95] backend-test-utils: make it possible to pass absolute paths to setContent Signed-off-by: Patrik Oldsberg --- .../src/filesystem/MockDirectory.test.ts | 2 +- .../backend-test-utils/src/filesystem/MockDirectory.ts | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) 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); } } } From f7ab98987a5c14cccedbd6900f7dda8a4f323379 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Oct 2023 22:07:24 +0200 Subject: [PATCH 16/95] techdocs-node: refactor openStackSwift tests to avoid mock-fs and storage mock Signed-off-by: Patrik Oldsberg --- .../src/stages/publish/openStackSwift.test.ts | 53 ++++++++----------- 1 file changed, 23 insertions(+), 30 deletions(-) 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': '', From 9f10308ea1f017b4636a5fd981caf3ab8dbcc66e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Oct 2023 22:57:15 +0200 Subject: [PATCH 17/95] techdocs-node: refactor googleStorage tests to avoid mock-fs and storage mock Signed-off-by: Patrik Oldsberg --- .../src/stages/publish/googleStorage.test.ts | 62 +++++++------------ 1 file changed, 22 insertions(+), 40 deletions(-) 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), From ade78cead74245f68e515acfb77a523cced869ba Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Oct 2023 23:16:49 +0200 Subject: [PATCH 18/95] techdocs-node: refactor azureBlobStorage tests to avoid mock-fs and storage mock Signed-off-by: Patrik Oldsberg --- .../stages/publish/azureBlobStorage.test.ts | 66 +++++++------------ 1 file changed, 23 insertions(+), 43 deletions(-) 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), From 21832cfb8e5631cf6ce1aa5a852b93719c02bb63 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Oct 2023 23:24:36 +0200 Subject: [PATCH 19/95] techdocs-node: refactor awsS3 tests to avoid mock-fs and storage mock Signed-off-by: Patrik Oldsberg --- .../src/stages/publish/awsS3.test.ts | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) 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), From b07405337509b74a1657d93516466da8b00da6df Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Oct 2023 23:29:48 +0200 Subject: [PATCH 20/95] techdocs-node: removed unused fs testing utils Signed-off-by: Patrik Oldsberg --- plugins/techdocs-node/package.json | 2 - .../src/testUtils/StorageFilesMock.ts | 61 ------------------- plugins/techdocs-node/src/testUtils/types.ts | 24 -------- yarn.lock | 2 - 4 files changed, 89 deletions(-) delete mode 100644 plugins/techdocs-node/src/testUtils/StorageFilesMock.ts delete mode 100644 plugins/techdocs-node/src/testUtils/types.ts diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json index 816dac3a00..9f8d41e9b2 100644 --- a/plugins/techdocs-node/package.json +++ b/plugins/techdocs-node/package.json @@ -63,7 +63,6 @@ "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" @@ -74,7 +73,6 @@ "@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/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 3b4b943ee8..334f8baae0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9681,7 +9681,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 @@ -9692,7 +9691,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 From 344cfbcfbcc1dbb83408467523f27fab26713b73 Mon Sep 17 00:00:00 2001 From: Heikki Hellgren Date: Thu, 5 Oct 2023 09:37:10 +0300 Subject: [PATCH 21/95] feat: allow prepared directory clean up for custom preparers When using custom preparer for TechDocs, the `preparedDir` might end up taking disk space. This requires all custom preparers to implement a new method `shouldCleanPreparedDirectory` which indicates whether the prepared directory should be cleaned after generation. Signed-off-by: Heikki Hellgren --- .changeset/early-toes-develop.md | 11 +++++++++++ plugins/techdocs-backend/src/DocsBuilder/builder.ts | 5 ++--- plugins/techdocs-node/api-report.md | 3 +++ plugins/techdocs-node/src/stages/prepare/dir.ts | 5 +++++ plugins/techdocs-node/src/stages/prepare/types.ts | 5 +++++ plugins/techdocs-node/src/stages/prepare/url.ts | 5 +++++ 6 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 .changeset/early-toes-develop.md diff --git a/.changeset/early-toes-develop.md b/.changeset/early-toes-develop.md new file mode 100644 index 0000000000..3262ff0b98 --- /dev/null +++ b/.changeset/early-toes-develop.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-techdocs-backend': minor +'@backstage/plugin-techdocs-node': minor +--- + +Allow prepared directory clean up for custom preparers + +When using custom preparer for TechDocs, the `preparedDir` might +end up taking disk space. This requires all custom preparers to +implement a new method `shouldCleanPreparedDirectory` which indicates +whether the prepared directory should be cleaned after generation. diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index b4c2d2ee26..84160ec8df 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -14,8 +14,8 @@ * limitations under the License. */ import { - Entity, DEFAULT_NAMESPACE, + Entity, stringifyEntityRef, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; @@ -28,7 +28,6 @@ import { PreparerBase, PreparerBuilder, PublisherBase, - UrlPreparer, } from '@backstage/plugin-techdocs-node'; import fs from 'fs-extra'; import os from 'os'; @@ -194,7 +193,7 @@ export class DocsBuilder { // Remove Prepared directory since it is no longer needed. // Caveat: Can not remove prepared directory in case of git preparer since the // local git repository is used to get etag on subsequent requests. - if (this.preparer instanceof UrlPreparer) { + if (this.preparer.shouldCleanPreparedDirectory()) { this.logger.debug( `Removing prepared directory ${preparedDir} since the site has been generated`, ); diff --git a/plugins/techdocs-node/api-report.md b/plugins/techdocs-node/api-report.md index b7941ac89a..11a9ecaa84 100644 --- a/plugins/techdocs-node/api-report.md +++ b/plugins/techdocs-node/api-report.md @@ -21,6 +21,7 @@ import { Writable } from 'stream'; export class DirectoryPreparer implements PreparerBase { static fromConfig(config: Config, options: PreparerConfig): DirectoryPreparer; prepare(entity: Entity, options?: PreparerOptions): Promise; + shouldCleanPreparedDirectory(): boolean; } // @public @@ -132,6 +133,7 @@ export const parseReferenceAnnotation: ( // @public export type PreparerBase = { prepare(entity: Entity, options?: PreparerOptions): Promise; + shouldCleanPreparedDirectory(): boolean; }; // @public @@ -274,5 +276,6 @@ export const transformDirLocation: ( export class UrlPreparer implements PreparerBase { static fromConfig(options: PreparerConfig): UrlPreparer; prepare(entity: Entity, options?: PreparerOptions): Promise; + shouldCleanPreparedDirectory(): boolean; } ``` diff --git a/plugins/techdocs-node/src/stages/prepare/dir.ts b/plugins/techdocs-node/src/stages/prepare/dir.ts index a2abf73d43..6be9510a51 100644 --- a/plugins/techdocs-node/src/stages/prepare/dir.ts +++ b/plugins/techdocs-node/src/stages/prepare/dir.ts @@ -60,6 +60,11 @@ export class DirectoryPreparer implements PreparerBase { this.scmIntegrations = ScmIntegrations.fromConfig(config); } + /** {@inheritDoc PreparerBase.shouldCleanPreparedDirectory} */ + shouldCleanPreparedDirectory() { + return false; + } + /** {@inheritDoc PreparerBase.prepare} */ async prepare( entity: Entity, diff --git a/plugins/techdocs-node/src/stages/prepare/types.ts b/plugins/techdocs-node/src/stages/prepare/types.ts index aee9d216ab..79cbf68a00 100644 --- a/plugins/techdocs-node/src/stages/prepare/types.ts +++ b/plugins/techdocs-node/src/stages/prepare/types.ts @@ -78,6 +78,11 @@ export type PreparerBase = { * @throws `NotModifiedError` when the prepared directory has not been changed since the last build. */ prepare(entity: Entity, options?: PreparerOptions): Promise; + + /** + * Indicates whether the prepared directory should be cleaned after generation. + */ + shouldCleanPreparedDirectory(): boolean; }; /** diff --git a/plugins/techdocs-node/src/stages/prepare/url.ts b/plugins/techdocs-node/src/stages/prepare/url.ts index 4bb5701e18..84ea1ccda0 100644 --- a/plugins/techdocs-node/src/stages/prepare/url.ts +++ b/plugins/techdocs-node/src/stages/prepare/url.ts @@ -47,6 +47,11 @@ export class UrlPreparer implements PreparerBase { this.reader = reader; } + /** {@inheritDoc PreparerBase.shouldCleanPreparedDirectory} */ + shouldCleanPreparedDirectory() { + return true; + } + /** {@inheritDoc PreparerBase.prepare} */ async prepare( entity: Entity, From 289c22464038538773d3856bd3742b3114fef69a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 13:19:57 +0200 Subject: [PATCH 22/95] backend-common: tweak package path resolution mock API Signed-off-by: Patrik Oldsberg --- packages/backend-common/src/testUtils.ts | 34 +++++++++++-------- .../backend-common/testUtils-api-report.md | 26 ++++++++++++++ .../app-backend/src/service/appPlugin.test.ts | 18 +++------- .../src/stages/publish/local.test.ts | 6 ++-- 4 files changed, 53 insertions(+), 31 deletions(-) create mode 100644 packages/backend-common/testUtils-api-report.md diff --git a/packages/backend-common/src/testUtils.ts b/packages/backend-common/src/testUtils.ts index 0b23333434..8be6010010 100644 --- a/packages/backend-common/src/testUtils.ts +++ b/packages/backend-common/src/testUtils.ts @@ -18,38 +18,44 @@ import { packagePathMocks } from './paths'; import { posix as posixPath, resolve as resolvePath } from 'path'; /** @public */ -export interface PackagePathMock { +export interface PackagePathResolutionOverride { /** Restored the normal behavior of resolvePackagePath */ restore(): void; } /** @public */ -export interface PackagePathMockOptions { +export interface OverridePackagePathResolutionOptions { /** The name of the package to mock the resolved path of */ - name: string; + 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: `createPackagePathMock({name: 'x', paths: {'foo/bar': baz}})` + * configuration: `overridePackagePathResolution({ packageNAme: 'x', paths: { 'foo/bar': baz } })` */ paths?: { [path in string]: string | (() => string) }; } -/** @public */ -export function createPackagePathMock( - options: PackagePathMockOptions, -): PackagePathMock { - if (packagePathMocks.has(options.name)) { - throw new Error( - `Duplicate package path mock for package '${options.name}'`, - ); +/** + * 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(`Duplicate package path mock for package '${name}'`); } - packagePathMocks.set(options.name, paths => { + packagePathMocks.set(name, paths => { const joinedPath = posixPath.join(...paths); const localResolver = options.paths?.[joinedPath]; if (localResolver) { @@ -65,7 +71,7 @@ export function createPackagePathMock( return { restore() { - packagePathMocks.delete(options.name); + 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/plugins/app-backend/src/service/appPlugin.test.ts b/plugins/app-backend/src/service/appPlugin.test.ts index 7bffdd00f6..647697c204 100644 --- a/plugins/app-backend/src/service/appPlugin.test.ts +++ b/plugins/app-backend/src/service/appPlugin.test.ts @@ -22,22 +22,12 @@ import { } 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(); - -jest.mock('../../../../packages/backend-common/src/paths', () => { - const actual = jest.requireActual( - '../../../../packages/backend-common/src/paths', - ); - return { - ...actual, - resolvePackagePath: (pkg: string, ...args: string[]) => { - if (pkg === 'app') { - return mockDir.resolve(...args); - } - return actual.resolvePackagePath(pkg, ...args); - }, - }; +overridePackagePathResolution({ + packageName: 'app', + path: mockDir.path, }); // Make sure root logger is initialized ahead of FS mock diff --git a/plugins/techdocs-node/src/stages/publish/local.test.ts b/plugins/techdocs-node/src/stages/publish/local.test.ts index 7a1832d0b3..4019f80d11 100644 --- a/plugins/techdocs-node/src/stages/publish/local.test.ts +++ b/plugins/techdocs-node/src/stages/publish/local.test.ts @@ -17,7 +17,7 @@ import { getVoidLogger, PluginEndpointDiscovery, } from '@backstage/backend-common'; -import { createPackagePathMock } from '@backstage/backend-common/testUtils'; +import { overridePackagePathResolution } from '@backstage/backend-common/testUtils'; import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; @@ -46,8 +46,8 @@ const testDiscovery: jest.Mocked = { const mockPublishDir = createMockDirectory(); -createPackagePathMock({ - name: '@backstage/plugin-techdocs-backend', +overridePackagePathResolution({ + packageName: '@backstage/plugin-techdocs-backend', paths: { 'static/docs': mockPublishDir.path, }, From c30ac4aa60b33b7a6ea175e9aef23aa9d27cf0a9 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 5 Oct 2023 14:05:56 +0200 Subject: [PATCH 23/95] feat: adding hunter to the catalog maintainers group Signed-off-by: blam --- OWNERS.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/OWNERS.md b/OWNERS.md index 135d617203..ee6676f8df 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -22,12 +22,13 @@ Team: @backstage/catalog-maintainers Scope: The catalog plugin and catalog model -| Name | Organization | Team | GitHub | Discord | -| -------------- | ------------ | --------- | ---------------------------------------- | ---------------- | -| Rickard Dybeck | Spotify | Chipmunks | [alde](http://github.com/alde) | rdybeck#8083 | -| Mike Blockley | Spotify | Chipmunks | [mikeyhc](http://github.com/mikeyhc) | mikey-spot#5363 | -| Elon Jefferson | Spotify | Chipmunks | [Edje-C](http://github.com/Edje-C) | elon-spotty#6086 | -| Nurit Izrailov | Spotify | Chipmunks | [nuritizra](http://github.com/nuritizra) | - | +| Name | Organization | Team | GitHub | Discord | +| --------------- | ------------ | --------- | ---------------------------------------- | ------------------- | +| Rickard Dybeck | Spotify | Chipmunks | [alde](http://github.com/alde) | `rdybeck#8083` | +| Mike Blockley | Spotify | Chipmunks | [mikeyhc](http://github.com/mikeyhc) | `mikey-spot#5363` | +| Elon Jefferson | Spotify | Chipmunks | [Edje-C](http://github.com/Edje-C) | `elon-spotty#6086 ` | +| Nurit Izrailov | Spotify | Chipmunks | [nuritizra](http://github.com/nuritizra) | - | +| Hunter Dougless | Spotify | Chipmunks | [hntrdglss](http://github.com/hntrdglss) | `hntrdglss#1849` | ### Discoverability From 239676efbf0dcd3d7e841fde043b375744edfb69 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 12:10:16 +0000 Subject: [PATCH 24/95] chore(deps): update dependency @testing-library/user-event to v14.5.1 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7b52a29afc..62316c6cdf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16801,11 +16801,11 @@ __metadata: linkType: hard "@testing-library/user-event@npm:^14.0.0": - version: 14.3.0 - resolution: "@testing-library/user-event@npm:14.3.0" + version: 14.5.1 + resolution: "@testing-library/user-event@npm:14.5.1" peerDependencies: "@testing-library/dom": ">=7.21.4" - checksum: cbd5954460496519cb2ff3fa506ca598d7e4c2e3d2f2e129b21909758f5ec87573aad7d6c79aebffd4bd0ea843315b3064a2a76e545f196bd4c82489cb3afc1d + checksum: 3e6bc9fd53dfe2f3648190193ed2fd4bca2a1bfb47f68810df3b33f05412526e5fd5c4ef9dc5375635e0f4cdf1859916867b597eed22bda1321e04242ea6c519 languageName: node linkType: hard From c849ca28f2243d1b6d7424f33534d1f76ca1ded7 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 5 Oct 2023 14:39:41 +0200 Subject: [PATCH 25/95] chore: add sevek Signed-off-by: blam --- OWNERS.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/OWNERS.md b/OWNERS.md index ee6676f8df..50d43f5640 100644 --- a/OWNERS.md +++ b/OWNERS.md @@ -22,13 +22,14 @@ Team: @backstage/catalog-maintainers Scope: The catalog plugin and catalog model -| Name | Organization | Team | GitHub | Discord | -| --------------- | ------------ | --------- | ---------------------------------------- | ------------------- | -| Rickard Dybeck | Spotify | Chipmunks | [alde](http://github.com/alde) | `rdybeck#8083` | -| Mike Blockley | Spotify | Chipmunks | [mikeyhc](http://github.com/mikeyhc) | `mikey-spot#5363` | -| Elon Jefferson | Spotify | Chipmunks | [Edje-C](http://github.com/Edje-C) | `elon-spotty#6086 ` | -| Nurit Izrailov | Spotify | Chipmunks | [nuritizra](http://github.com/nuritizra) | - | -| Hunter Dougless | Spotify | Chipmunks | [hntrdglss](http://github.com/hntrdglss) | `hntrdglss#1849` | +| Name | Organization | Team | GitHub | Discord | +| --------------- | ------------ | --------- | ----------------------------------------- | ------------------- | +| Rickard Dybeck | Spotify | Chipmunks | [alde](https://github.com/alde) | `rdybeck#8083` | +| Mike Blockley | Spotify | Chipmunks | [mikeyhc](https://github.com/mikeyhc) | `mikey-spot#5363` | +| Elon Jefferson | Spotify | Chipmunks | [Edje-C](https://github.com/Edje-C) | `elon-spotty#6086 ` | +| Nurit Izrailov | Spotify | Chipmunks | [nuritizra](https://github.com/nuritizra) | - | +| Hunter Dougless | Spotify | Chipmunks | [hntrdglss](https://github.com/hntrdglss) | `hntrdglss#1849` | +| Seve Kim | Spotify | Chipmunks | [sevedkim](https://github.com/sevedkim) | `seve#9951` | ### Discoverability From 6f5349d5c8beaf7e26e9837993e7866e466edaac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 14:22:21 +0000 Subject: [PATCH 26/95] chore(deps): update dependency eslint to v8.50.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 101c0a96fc..997ed33a18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11166,10 +11166,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:8.49.0": - version: 8.49.0 - resolution: "@eslint/js@npm:8.49.0" - checksum: a6601807c8aeeefe866926ad92ed98007c034a735af20ff709009e39ad1337474243d47908500a3bde04e37bfba16bcf1d3452417f962e1345bc8756edd6b830 +"@eslint/js@npm:8.50.0": + version: 8.50.0 + resolution: "@eslint/js@npm:8.50.0" + checksum: 302478f2acaaa7228729ec6a04f56641590185e1d8cd1c836a6db8a6b8009f80a57349341be9fbb9aa1721a7a569d1be3ffc598a33300d22816f11832095386c languageName: node linkType: hard @@ -25186,13 +25186,13 @@ __metadata: linkType: hard "eslint@npm:^8.33.0, eslint@npm:^8.6.0": - version: 8.49.0 - resolution: "eslint@npm:8.49.0" + version: 8.50.0 + resolution: "eslint@npm:8.50.0" dependencies: "@eslint-community/eslint-utils": ^4.2.0 "@eslint-community/regexpp": ^4.6.1 "@eslint/eslintrc": ^2.1.2 - "@eslint/js": 8.49.0 + "@eslint/js": 8.50.0 "@humanwhocodes/config-array": ^0.11.11 "@humanwhocodes/module-importer": ^1.0.1 "@nodelib/fs.walk": ^1.2.8 @@ -25228,7 +25228,7 @@ __metadata: text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: 4dfe257e1e42da2f9da872b05aaaf99b0f5aa022c1a91eee8f2af1ab72651b596366320c575ccd4e0469f7b4c97aff5bb85ae3323ebd6a293c3faef4028b0d81 + checksum: 9ebfe5615dc84700000d218e32ddfdcfc227ca600f65f18e5541ec34f8902a00356a9a8804d9468fd6c8637a5ef6a3897291dad91ba6579d5b32ffeae5e31768 languageName: node linkType: hard From 4c0a87b708b4c3fce5fb24ec86f962f64a032a96 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 14:23:04 +0000 Subject: [PATCH 27/95] chore(deps): update dependency typescript to ~4.9.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- storybook/package.json | 2 +- storybook/yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/storybook/package.json b/storybook/package.json index 0f88ae634c..6a86cd90bd 100644 --- a/storybook/package.json +++ b/storybook/package.json @@ -27,7 +27,7 @@ "@storybook/react": "^6.5.9", "@storybook/testing-library": "^0.2.0", "storybook-dark-mode": "^1.1.0", - "typescript": "~4.7.0" + "typescript": "~4.9.0" }, "resolutions": { "webpack": "^5.73.0" diff --git a/storybook/yarn.lock b/storybook/yarn.lock index 91547eed81..9a533c46c0 100644 --- a/storybook/yarn.lock +++ b/storybook/yarn.lock @@ -10687,7 +10687,7 @@ __metadata: react-hot-loader: ^4.13.0 storybook-dark-mode: ^1.1.0 swc-loader: ^0.2.3 - typescript: ~4.7.0 + typescript: ~4.9.0 peerDependencies: "@backstage/core-app-api": "*" "@backstage/core-plugin-api": "*" @@ -11218,23 +11218,23 @@ __metadata: languageName: node linkType: hard -"typescript@npm:~4.7.0": - version: 4.7.4 - resolution: "typescript@npm:4.7.4" +"typescript@npm:~4.9.0": + version: 4.9.5 + resolution: "typescript@npm:4.9.5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 5750181b1cd7e6482c4195825547e70f944114fb47e58e4aa7553e62f11b3f3173766aef9c281783edfd881f7b8299cf35e3ca8caebe73d8464528c907a164df + checksum: ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db languageName: node linkType: hard -"typescript@patch:typescript@~4.7.0#~builtin": - version: 4.7.4 - resolution: "typescript@patch:typescript@npm%3A4.7.4#~builtin::version=4.7.4&hash=a1c5e5" +"typescript@patch:typescript@~4.9.0#~builtin": + version: 4.9.5 + resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=a1c5e5" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 9096d8f6c16cb80ef3bf96fcbbd055bf1c4a43bd14f3b7be45a9fbe7ada46ec977f604d5feed3263b4f2aa7d4c7477ce5f9cd87de0d6feedec69a983f3a4f93e + checksum: 2eee5c37cad4390385db5db5a8e81470e42e8f1401b0358d7390095d6f681b410f2c4a0c496c6ff9ebd775423c7785cdace7bcdad76c7bee283df3d9718c0f20 languageName: node linkType: hard From 5bd46ca10ab330d634e8c7ae2ad295d13b1c4b66 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 15:08:01 +0000 Subject: [PATCH 28/95] fix(deps): update dependency @azure/storage-blob to v12.16.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 159e631e93..15220f6fea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1776,8 +1776,8 @@ __metadata: linkType: hard "@azure/storage-blob@npm:^12.5.0": - version: 12.15.0 - resolution: "@azure/storage-blob@npm:12.15.0" + version: 12.16.0 + resolution: "@azure/storage-blob@npm:12.16.0" dependencies: "@azure/abort-controller": ^1.0.0 "@azure/core-http": ^3.0.0 @@ -1787,7 +1787,7 @@ __metadata: "@azure/logger": ^1.0.0 events: ^3.0.0 tslib: ^2.2.0 - checksum: fe5399e7107685f1e81bd782fbd10e11c6aec01141c63f24f138a985aa709da96e83fc5dd3295408b0609981b2fb71d2304935056692e4cf3833635157799769 + checksum: c69a726afc7fa647e6bc6eb2b2863ed94088246a959f5e8f2b3fdd0f613846cb5eec7e991f2b50dd3ba88cd0b32e60da1e3d87eeeb5dbc692df4f61a3c507491 languageName: node linkType: hard From a70db55b2efbbb91db318f760d9a0d498e7faaf7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 16:41:26 +0200 Subject: [PATCH 29/95] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Patrik Oldsberg --- packages/backend-common/src/testUtils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/backend-common/src/testUtils.ts b/packages/backend-common/src/testUtils.ts index 8be6010010..9616ab1701 100644 --- a/packages/backend-common/src/testUtils.ts +++ b/packages/backend-common/src/testUtils.ts @@ -19,7 +19,7 @@ import { posix as posixPath, resolve as resolvePath } from 'path'; /** @public */ export interface PackagePathResolutionOverride { - /** Restored the normal behavior of resolvePackagePath */ + /** Restores the normal behavior of resolvePackagePath */ restore(): void; } @@ -36,7 +36,7 @@ export interface OverridePackagePathResolutionOptions { * 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 } })` + * configuration: `overridePackagePathResolution({ packageName: 'x', paths: { 'foo/bar': baz } })` */ paths?: { [path in string]: string | (() => string) }; } @@ -52,7 +52,9 @@ export function overridePackagePathResolution( const name = options.packageName; if (packagePathMocks.has(name)) { - throw new Error(`Duplicate package path mock for package '${name}'`); + throw new Error( + `Tried to override resolution for '${name}' more than once for package '${name}'`, + ); } packagePathMocks.set(name, paths => { From 17f5a8fa0c0981a136e8f067d779317b0ba81e84 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 17:30:24 +0200 Subject: [PATCH 30/95] app-backend: refactor findStaticAssets test to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../src/lib/assets/findStaticAssets.test.ts | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) 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([ From 49dbfa5a2f4407a93e7b7ac9da6ee7ced380ab64 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 17:33:50 +0000 Subject: [PATCH 31/95] fix(deps): update dependency @google-cloud/firestore to v6.8.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 15220f6fea..0465043d53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11337,14 +11337,14 @@ __metadata: linkType: hard "@google-cloud/firestore@npm:^6.0.0": - version: 6.7.0 - resolution: "@google-cloud/firestore@npm:6.7.0" + version: 6.8.0 + resolution: "@google-cloud/firestore@npm:6.8.0" dependencies: fast-deep-equal: ^3.1.1 functional-red-black-tree: ^1.0.1 google-gax: ^3.5.7 - protobufjs: ^7.0.0 - checksum: 8464d4d866adcbd80cd528230408f7161a402df7b74d4036b2f81c1f9b83051057364e1c8264477b7a46ce03c2b8fa0d28799f6ccd77d905274a71ca93b2593f + protobufjs: ^7.2.5 + checksum: e8e1fd7cc6fd688e771c3d2f62c2f33d23357e11ee03f6d2f2aeb0ea29378f8e62f2511936011b515bbeedf304b5e831e4f4a46b8905dbc421fe2fa521d2e43f languageName: node linkType: hard @@ -36168,7 +36168,7 @@ __metadata: languageName: node linkType: hard -"protobufjs@npm:7.2.4, protobufjs@npm:^7.0.0": +"protobufjs@npm:7.2.4": version: 7.2.4 resolution: "protobufjs@npm:7.2.4" dependencies: @@ -36188,6 +36188,26 @@ __metadata: languageName: node linkType: hard +"protobufjs@npm:^7.0.0, protobufjs@npm:^7.2.5": + version: 7.2.5 + resolution: "protobufjs@npm:7.2.5" + dependencies: + "@protobufjs/aspromise": ^1.1.2 + "@protobufjs/base64": ^1.1.2 + "@protobufjs/codegen": ^2.0.4 + "@protobufjs/eventemitter": ^1.1.0 + "@protobufjs/fetch": ^1.1.0 + "@protobufjs/float": ^1.0.2 + "@protobufjs/inquire": ^1.1.0 + "@protobufjs/path": ^1.1.2 + "@protobufjs/pool": ^1.1.0 + "@protobufjs/utf8": ^1.1.0 + "@types/node": ">=13.7.0" + long: ^5.0.0 + checksum: 3770a072114061faebbb17cfd135bc4e187b66bc6f40cd8bac624368b0270871ec0cfb43a02b9fb4f029c8335808a840f1afba3c2e7ede7063b98ae6b98a703f + languageName: node + linkType: hard + "protocol-buffers-schema@npm:^3.6.0": version: 3.6.0 resolution: "protocol-buffers-schema@npm:3.6.0" From 2b92328b8b97aa5498e7896a3a781fb99da4a168 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 18:11:53 +0000 Subject: [PATCH 32/95] fix(deps): update dependency @newrelic/browser-agent to v1.243.1 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0465043d53..556534cc2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13614,14 +13614,14 @@ __metadata: linkType: hard "@newrelic/browser-agent@npm:^1.236.0": - version: 1.239.1 - resolution: "@newrelic/browser-agent@npm:1.239.1" + version: 1.243.1 + resolution: "@newrelic/browser-agent@npm:1.243.1" dependencies: core-js: ^3.26.0 fflate: ^0.7.4 - rrweb: ^2.0.0-alpha.8 + rrweb: 2.0.0-alpha.8 web-vitals: ^3.1.0 - checksum: c1b0df2fff4b5bef3718f94f0fa418dc4db087d6b4bd94547d274acd2fc8f771888baea456d1e559a36d11436d11a34615b3d1cce12d604758a689b950eb7597 + checksum: df7e7aa015560a1140151d094309327ee1ec0a37d876514743171043a20a61d2723dbc2a2945adbac8fc4c04059d45ba3fa248cce3dbe27f21bc7d3c735ad046 languageName: node linkType: hard @@ -14957,12 +14957,12 @@ __metadata: languageName: node linkType: hard -"@rrweb/types@npm:^2.0.0-alpha.9": - version: 2.0.0-alpha.9 - resolution: "@rrweb/types@npm:2.0.0-alpha.9" +"@rrweb/types@npm:^2.0.0-alpha.8": + version: 2.0.0-alpha.11 + resolution: "@rrweb/types@npm:2.0.0-alpha.11" dependencies: - rrweb-snapshot: ^2.0.0-alpha.9 - checksum: adc6bc7a6e45294ae7b85a137ae6774a822f103f0a29ecf8d59b72c88b6ddfaedc5a16d71971c08a2ab25e3e3af89f9582d8508bcf6b33d97fc326cdf91a09b2 + rrweb-snapshot: ^2.0.0-alpha.11 + checksum: 63c815597daacb7f6978c973c53e6e8f10301c0224ce675792c2c8a2cdf059845aefef92b2ee259837813081796398e3432195fdd8cb72c437ab77a20c50b2c7 languageName: node linkType: hard @@ -38312,35 +38312,35 @@ __metadata: languageName: unknown linkType: soft -"rrdom@npm:^2.0.0-alpha.9": - version: 2.0.0-alpha.9 - resolution: "rrdom@npm:2.0.0-alpha.9" +"rrdom@npm:^2.0.0-alpha.8": + version: 2.0.0-alpha.11 + resolution: "rrdom@npm:2.0.0-alpha.11" dependencies: - rrweb-snapshot: ^2.0.0-alpha.9 - checksum: d56df9acc0348f4226a2d195692a422a431498c889a40046242f5643437d3388840de90078fb26488378fc250049a9d25ee5394a089f435e2f88668276bf17d4 + rrweb-snapshot: ^2.0.0-alpha.11 + checksum: cfc8f18698902224bd4b666586497b2682d3d11e30946c2b9fe374b8209d977c0d6c4157231a2a4bfed12761a79db1cbc9a139741a9b3d7c943536cd9eb3bb50 languageName: node linkType: hard -"rrweb-snapshot@npm:^2.0.0-alpha.9": - version: 2.0.0-alpha.9 - resolution: "rrweb-snapshot@npm:2.0.0-alpha.9" - checksum: 987f3ce493178dcc6782e959adef3e3f39e711bc8d5ac7dba5dfaf2fd01477aa2d3cd959598fa4e54cf15f2c397e95cf8323180a420857ca2a4d76abaec0a552 +"rrweb-snapshot@npm:^2.0.0-alpha.11, rrweb-snapshot@npm:^2.0.0-alpha.8": + version: 2.0.0-alpha.11 + resolution: "rrweb-snapshot@npm:2.0.0-alpha.11" + checksum: 8b5e40ebe17d61546f9c93b4cc266156be0fc28b7452e4fea6ea4c24a35a857c021e15503d218f10d3fc8c478c793450f1f2ebf9c751c1d7a24e25322b9d1677 languageName: node linkType: hard -"rrweb@npm:^2.0.0-alpha.8": - version: 2.0.0-alpha.9 - resolution: "rrweb@npm:2.0.0-alpha.9" +"rrweb@npm:2.0.0-alpha.8": + version: 2.0.0-alpha.8 + resolution: "rrweb@npm:2.0.0-alpha.8" dependencies: - "@rrweb/types": ^2.0.0-alpha.9 + "@rrweb/types": ^2.0.0-alpha.8 "@types/css-font-loading-module": 0.0.7 "@xstate/fsm": ^1.4.0 base64-arraybuffer: ^1.0.1 fflate: ^0.4.4 mitt: ^3.0.0 - rrdom: ^2.0.0-alpha.9 - rrweb-snapshot: ^2.0.0-alpha.9 - checksum: b87ec509dff99eef90496bc5685c57f6ec7868fb12c6b49a8d824a41e1a812aaa6b4aa792b50cdfdd68ec49287d51bba48ea764dfce149f3d9ce2363327328ff + rrdom: ^2.0.0-alpha.8 + rrweb-snapshot: ^2.0.0-alpha.8 + checksum: a07e4a56fe75e452a7d0f1e3d066c21ef086516c08249ee8c57de55c0937bdd08281881523727e33cae94151c218f278e9ab3fdd477acb111f65a5c18334c31f languageName: node linkType: hard From 499e34656e9f6b88312a52b55ba127994f542086 Mon Sep 17 00:00:00 2001 From: Rutuja Marathe Date: Thu, 5 Oct 2023 14:53:13 -0400 Subject: [PATCH 33/95] fix(adr): align icon in AdrSearchResultListItem Signed-off-by: Rutuja Marathe --- .changeset/many-pianos-bow.md | 5 + .../src/search/AdrSearchResultListItem.tsx | 109 ++++++++++-------- 2 files changed, 63 insertions(+), 51 deletions(-) create mode 100644 .changeset/many-pianos-bow.md diff --git a/.changeset/many-pianos-bow.md b/.changeset/many-pianos-bow.md new file mode 100644 index 0000000000..6c4e3be47f --- /dev/null +++ b/.changeset/many-pianos-bow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-adr': patch +--- + +Fix icon alignment in `AdrSearchResultListItem` diff --git a/plugins/adr/src/search/AdrSearchResultListItem.tsx b/plugins/adr/src/search/AdrSearchResultListItem.tsx index 118333a439..eebf74861d 100644 --- a/plugins/adr/src/search/AdrSearchResultListItem.tsx +++ b/plugins/adr/src/search/AdrSearchResultListItem.tsx @@ -33,6 +33,9 @@ import { ResultHighlight } from '@backstage/plugin-search-common'; import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; const useStyles = makeStyles({ + item: { + display: 'flex', + }, flexContainer: { flexWrap: 'wrap', }, @@ -66,59 +69,63 @@ export function AdrSearchResultListItem(props: AdrSearchResultListItemProps) { return ( <> - + {icon && {icon}} - - {highlight?.fields.title ? ( - - ) : ( - result.title - )} - - } - secondary={ - - {highlight?.fields.text ? ( - - ) : ( - result.text - )} - - } - /> - - + + {highlight?.fields.title ? ( + + ) : ( + result.title + )} + + } + secondary={ + + {highlight?.fields.text ? ( + + ) : ( + result.text + )} + + } /> - {result.status && ( - - )} - {result.date && } - + + + {result.status && ( + + )} + {result.date && ( + + )} + + From 7aa073e481e49b19a81307838944a6de8e2ce36e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 19:09:52 +0000 Subject: [PATCH 34/95] fix(deps): update dependency @opensearch-project/opensearch to v2.4.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3383ae4e02..105682eaac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14312,15 +14312,15 @@ __metadata: linkType: hard "@opensearch-project/opensearch@npm:^2.2.1": - version: 2.3.1 - resolution: "@opensearch-project/opensearch@npm:2.3.1" + version: 2.4.0 + resolution: "@opensearch-project/opensearch@npm:2.4.0" dependencies: aws4: ^1.11.0 debug: ^4.3.1 hpagent: ^1.2.0 ms: ^2.1.3 secure-json-parse: ^2.4.0 - checksum: 70324153eb9d74c005d5517a997f7027b829387af482956593a23b80c24ba8c9f9cdff40fec3d72e7066f3f16070b543c2d6f59f578e1be898784b49f9f6720f + checksum: 961ba055276c2cea9733247e57c1a1df76671a777b46ba4caa64a15c8b2aeb12d205c02026a6181382d3b575af49c4bf72b7bcb8fe82bc8dc2c519e1792746f3 languageName: node linkType: hard From 72c995e0f011efbfeac547f09ac00f3a186c34aa Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 19:10:49 +0000 Subject: [PATCH 35/95] fix(deps): update dependency @stoplight/spectral-formatters to v1.3.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3383ae4e02..b6ab463fb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15810,8 +15810,8 @@ __metadata: linkType: hard "@stoplight/spectral-formatters@npm:^1.1.0": - version: 1.2.0 - resolution: "@stoplight/spectral-formatters@npm:1.2.0" + version: 1.3.0 + resolution: "@stoplight/spectral-formatters@npm:1.3.0" dependencies: "@stoplight/path": ^1.3.2 "@stoplight/spectral-core": ^1.15.1 @@ -15820,10 +15820,11 @@ __metadata: chalk: 4.1.2 cliui: 7.0.4 lodash: ^4.17.21 + node-sarif-builder: ^2.0.3 strip-ansi: 6.0 text-table: ^0.2.0 tslib: ^2.5.0 - checksum: e84cc06ed33348f532f513b64d7179e2e261d6b87e45be66169088bf8d4c0fbc8fcda92adcde0d3f88f03eb5f31fbf9b2c853cdd49d4d1ae809d9a76920adb16 + checksum: d56757f5204571c5d86551bb8ea56183236c9dab69d95104abcf639a4ff3a465efa5e393f68fd9032c852e0078c514b343a9eaa3aea3ecb8e465f4eeb92bd29f languageName: node linkType: hard @@ -18431,6 +18432,13 @@ __metadata: languageName: node linkType: hard +"@types/sarif@npm:^2.1.4": + version: 2.1.5 + resolution: "@types/sarif@npm:2.1.5" + checksum: 6fb813f6988b3416dfd38388dd6af07eedf7d62b288b8cd576f0eb9bd2b26523d7905cee78f4f6316eb3181472bb028caac282e4eb1b2d0042dc7ee2cbbf37c5 + languageName: node + linkType: hard + "@types/scheduler@npm:*": version: 0.16.1 resolution: "@types/scheduler@npm:0.16.1" @@ -33663,6 +33671,16 @@ __metadata: languageName: node linkType: hard +"node-sarif-builder@npm:^2.0.3": + version: 2.0.3 + resolution: "node-sarif-builder@npm:2.0.3" + dependencies: + "@types/sarif": ^2.1.4 + fs-extra: ^10.0.0 + checksum: 397dd9bfb0780c6753fb47d1fd0465f3c8a935082cb1bbd7ad6232d18b6343d9d499c6bc572ad0415db282efd6058fe8b7a6657020434adef4fbf93a8b95306e + languageName: node + linkType: hard + "nodemon@npm:^3.0.1": version: 3.0.1 resolution: "nodemon@npm:3.0.1" From a8aaed09048df76c84db6505e627977ff3c2d291 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 20:06:53 +0000 Subject: [PATCH 36/95] fix(deps): update dependency @apollo/client to v3.8.5 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 56e8fd22bb..e6c95ef857 100644 --- a/yarn.lock +++ b/yarn.lock @@ -103,8 +103,8 @@ __metadata: linkType: hard "@apollo/client@npm:^3.0.0": - version: 3.8.4 - resolution: "@apollo/client@npm:3.8.4" + version: 3.8.5 + resolution: "@apollo/client@npm:3.8.5" dependencies: "@graphql-typed-document-node/core": ^3.1.1 "@wry/context": ^0.7.3 @@ -134,7 +134,7 @@ __metadata: optional: true subscriptions-transport-ws: optional: true - checksum: 509e37cdce7462cacda0a86c413ce471cd8f618625fb8ac3a60d6347d12f37a4fc60e12fc3fc1a375799caa21e56ff58d709e13ef5e13ab15e4dfc828a527848 + checksum: 242db8340d5f04ff8f2c9ad6a0dc6ab6d596fb5c763865b0d392f6e5771d55de28faa981dcadaea80ee8b21d4ff40bfa51152a10bcae4158ecf035e47b0d17c4 languageName: node linkType: hard From 54375e70e6fc54ed778db63e1f22a8dad06adcde Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 20:07:31 +0000 Subject: [PATCH 37/95] fix(deps): update dependency @stoplight/spectral-rulesets to v1.18.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 56e8fd22bb..24846fb8b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15873,8 +15873,8 @@ __metadata: linkType: hard "@stoplight/spectral-rulesets@npm:^1.16.0": - version: 1.17.0 - resolution: "@stoplight/spectral-rulesets@npm:1.17.0" + version: 1.18.0 + resolution: "@stoplight/spectral-rulesets@npm:1.18.0" dependencies: "@asyncapi/specs": ^4.1.0 "@stoplight/better-ajv-errors": 1.0.3 @@ -15890,7 +15890,7 @@ __metadata: json-schema-traverse: ^1.0.0 lodash: ~4.17.21 tslib: ^2.3.0 - checksum: 3f79636fde7e2ae26f6af5f5e50e2faa385cbd7b22ea658ea15cf0f1c04a3827e168340f877310f4ab7d6bacc82910e21b2aa43f60b95a923dc430ee0978569b + checksum: 7abdc837acf64f1408bd71bf98169af6dad9b9adcdb7758ab705599989bf7f5c3e5739bd409f959f21e2d9e06919c6cbfff2500fd3180f45b2f4e1dbf2c62129 languageName: node linkType: hard From 7087b46fa4eb396d8c9a66953786adbfb309e84c Mon Sep 17 00:00:00 2001 From: Engin Diri Date: Thu, 5 Oct 2023 22:49:12 +0200 Subject: [PATCH 38/95] Update pulumi.yaml Add scaffolder actions to description of the Pulumi Backstage plugin Signed-off-by: Engin Diri --- microsite/data/plugins/pulumi.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/data/plugins/pulumi.yaml b/microsite/data/plugins/pulumi.yaml index ea97bcdef3..0177b7df0d 100644 --- a/microsite/data/plugins/pulumi.yaml +++ b/microsite/data/plugins/pulumi.yaml @@ -3,7 +3,7 @@ title: Pulumi author: Pulumi authorUrl: https://www.pulumi.com category: Infrastructure -description: View Pulumi stack information in Backstage. +description: Use Pulumi scaffolder actions and view Pulumi stack information in Backstage. documentation: https://github.com/pulumi/pulumi-backstage-plugin iconUrl: https://www.pulumi.com/logos/brand/avatar-on-white.png npmPackageName: '@pulumi/backstage-plugin-pulumi' From 06a1af52262c46712ab4d7de3a07986e075ef844 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 6 Oct 2023 00:15:30 +0200 Subject: [PATCH 39/95] e2e-test: fix hanging backend process on Windows Signed-off-by: Patrik Oldsberg --- packages/e2e-test/src/commands/run.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 5d373b24cf..a9256695ec 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -427,6 +427,8 @@ async function dropClientDatabases(client: string) { async function testBackendStart(appDir: string, ...args: string[]) { const child = spawnPiped(['yarn', 'workspace', 'backend', 'start', ...args], { cwd: appDir, + // Windows does not like piping stdin here, the child process will hang when requiring the 'process' module + stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, GITHUB_TOKEN: 'abc', From 819d75619ddaa7efcf7dfabe6bf420ed8590ba33 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 22:17:02 +0000 Subject: [PATCH 40/95] fix(deps): update dependency @tanstack/react-query to v4.35.7 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index c8bf7b84ab..743923e8f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16669,18 +16669,18 @@ __metadata: languageName: node linkType: hard -"@tanstack/query-core@npm:4.33.0": - version: 4.33.0 - resolution: "@tanstack/query-core@npm:4.33.0" - checksum: fae325f1d79b936435787797c32367331d5b8e9c5ced84852bf2085115e3aafef57a7ae530a6b0af46da4abafb4b0afaef885926b71715a0e6f166d74da61c7f +"@tanstack/query-core@npm:4.35.7": + version: 4.35.7 + resolution: "@tanstack/query-core@npm:4.35.7" + checksum: b82600ee0b2cea085eb6065d36bf944f4fff72b3fa575b5aa55590498fa431f951c5557a4e7ee36ae5b5de5fd5856cb841b974ff6f78cf773199f0f8c860b6c8 languageName: node linkType: hard "@tanstack/react-query@npm:^4.1.3": - version: 4.33.0 - resolution: "@tanstack/react-query@npm:4.33.0" + version: 4.35.7 + resolution: "@tanstack/react-query@npm:4.35.7" dependencies: - "@tanstack/query-core": 4.33.0 + "@tanstack/query-core": 4.35.7 use-sync-external-store: ^1.2.0 peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -16691,7 +16691,7 @@ __metadata: optional: true react-native: optional: true - checksum: b3cf4afa427435e464e077b3f23c891e38e5f78873518f15c1d061ad55f1464d6241ecd92d796a5dbc9412b4fd7eb30b01f2a9cfc285ee9f30dfdd2ca0ecaf4b + checksum: ba4e1aa883a42424085c81d44816fb7ee8efdc8c8d1c190320e78818ce8a45cfcafd2a9c0e217d19ff25b0ec58592c98f5286b5a9b738a799c3ee016a02f9447 languageName: node linkType: hard From 929b80851f3340aaf27e583abcf24d28c6821503 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 22:17:35 +0000 Subject: [PATCH 41/95] fix(deps): update dependency bfj to v7.1.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 60 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/yarn.lock b/yarn.lock index c8bf7b84ab..726bd52d49 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20651,14 +20651,15 @@ __metadata: linkType: hard "bfj@npm:^7.0.2": - version: 7.0.2 - resolution: "bfj@npm:7.0.2" + version: 7.1.0 + resolution: "bfj@npm:7.1.0" dependencies: - bluebird: ^3.5.5 - check-types: ^11.1.1 + bluebird: ^3.7.2 + check-types: ^11.2.3 hoopy: ^0.1.4 + jsonpath: ^1.1.1 tryer: ^1.0.1 - checksum: 0ca673234170eb3dcf00fb1d867ba274729ab05779dd19b35628c49da7adc32472b5f0bca0554ffdca15b094f9b36f16f2a8992ba8884ebd1d351d7f27abee7b + checksum: 36da9ed36c60f377a3f43bb0433092af7dc40442914b8155a1330ae86b1905640baf57e9c195ab83b36d6518b27cf8ed880adff663aa444c193be149e027d722 languageName: node linkType: hard @@ -20745,7 +20746,7 @@ __metadata: languageName: node linkType: hard -"bluebird@npm:^3.5.5, bluebird@npm:^3.7.2": +"bluebird@npm:^3.7.2": version: 3.7.2 resolution: "bluebird@npm:3.7.2" checksum: 869417503c722e7dc54ca46715f70e15f4d9c602a423a02c825570862d12935be59ed9c7ba34a9b31f186c017c23cac6b54e35446f8353059c101da73eac22ef @@ -21521,10 +21522,10 @@ __metadata: languageName: node linkType: hard -"check-types@npm:^11.1.1": - version: 11.1.2 - resolution: "check-types@npm:11.1.2" - checksum: 6c339a5dfe326e34a5275016c7f9464665405cd79007c057852acd677d265ddfe36236ad5567bd1e601ea88fa78bf1f882b6bc3dc7c5616c26f6b54b2c0ef4fc +"check-types@npm:^11.2.3": + version: 11.2.3 + resolution: "check-types@npm:11.2.3" + checksum: f99ff09ae65e63cfcfa40a1275c0a70d8c43ffbf9ac35095f3bf030cc70361c92e075a9975a1144329e50b4fe4620be6bedb4568c18abc96071a3e23aed3ed8e languageName: node linkType: hard @@ -24900,7 +24901,7 @@ __metadata: languageName: node linkType: hard -"escodegen@npm:^1.13.0": +"escodegen@npm:^1.13.0, escodegen@npm:^1.8.1": version: 1.14.3 resolution: "escodegen@npm:1.14.3" dependencies: @@ -25255,6 +25256,16 @@ __metadata: languageName: node linkType: hard +"esprima@npm:1.2.2": + version: 1.2.2 + resolution: "esprima@npm:1.2.2" + bin: + esparse: ./bin/esparse.js + esvalidate: ./bin/esvalidate.js + checksum: 4f10006f0e315f2f7d8cf6630e465f183512f1ab2e862b11785a133ce37ed1696573deefb5256e510eaa4368342b13b393334477f6ccdcdb8f10e782b0f5e6dc + languageName: node + linkType: hard + "esprima@npm:^4.0.0, esprima@npm:^4.0.1, esprima@npm:~4.0.0": version: 4.0.1 resolution: "esprima@npm:4.0.1" @@ -30645,6 +30656,17 @@ __metadata: languageName: node linkType: hard +"jsonpath@npm:^1.1.1": + version: 1.1.1 + resolution: "jsonpath@npm:1.1.1" + dependencies: + esprima: 1.2.2 + static-eval: 2.0.2 + underscore: 1.12.1 + checksum: 5480d8e9e424fe2ed4ade6860b6e2cefddb21adb3a99abe0254cd9428e8ef9b0c9fb5729d6a5a514e90df50d645ccea9f3be48d627570e6222dd5dadc28eba7b + languageName: node + linkType: hard + "jsonpointer@npm:^5.0.0, jsonpointer@npm:^5.0.1": version: 5.0.1 resolution: "jsonpointer@npm:5.0.1" @@ -39582,6 +39604,15 @@ __metadata: languageName: node linkType: hard +"static-eval@npm:2.0.2": + version: 2.0.2 + resolution: "static-eval@npm:2.0.2" + dependencies: + escodegen: ^1.8.1 + checksum: 335a923c5ccb29add404ac23d0a55c0da6cee3071f6f67a7053aeac0dedc6dbfc53ac9269e9c25f403f5b7603a291ef47d7114f99bde241184f7aa3f9286dc32 + languageName: node + linkType: hard + "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" @@ -41351,6 +41382,13 @@ __metadata: languageName: node linkType: hard +"underscore@npm:1.12.1": + version: 1.12.1 + resolution: "underscore@npm:1.12.1" + checksum: ec327603aa112b99fe9d74cd9bf3b3b7451465a9d2610ceab269a532e3f191650ab017903be34dc86fe406a11d04d8905a3b04dd4c129493e51bee09a3f3074c + languageName: node + linkType: hard + "underscore@npm:^1.12.1, underscore@npm:^1.13.6, underscore@npm:~1.13.2": version: 1.13.6 resolution: "underscore@npm:1.13.6" From 4d19cee8b8026d453ddb82b463d9ace399845399 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 23:15:22 +0000 Subject: [PATCH 42/95] fix(deps): update dependency core-js to v3.33.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 814f443c89..7ba1e33d7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22536,9 +22536,9 @@ __metadata: linkType: hard "core-js@npm:^3.26.0, core-js@npm:^3.6.5": - version: 3.32.2 - resolution: "core-js@npm:3.32.2" - checksum: d6fac7e8eb054eefc211c76cd0a0ff07447a917122757d085f469f046ec888d122409c7db1a9601c3eb5fa767608ed380bcd219eace02bdf973da155680edeec + version: 3.33.0 + resolution: "core-js@npm:3.33.0" + checksum: dd62217935ac281faf6f833bb306fb891162919fcf9c1f0c975b1b91e82ac09a940f5deb5950bbb582739ceef716e8bd7e4f9eab8328932fb029d3bc2ecb2881 languageName: node linkType: hard From 55ea5030e5d149536997a2d54e14c81de2e72ab0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Oct 2023 23:15:59 +0000 Subject: [PATCH 43/95] fix(deps): update dependency elastic-builder to v2.22.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 814f443c89..3285195be0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24120,8 +24120,8 @@ __metadata: linkType: hard "elastic-builder@npm:^2.16.0": - version: 2.21.0 - resolution: "elastic-builder@npm:2.21.0" + version: 2.22.0 + resolution: "elastic-builder@npm:2.22.0" dependencies: lodash.has: ^4.5.2 lodash.hasin: ^4.5.2 @@ -24131,7 +24131,7 @@ __metadata: lodash.isobject: ^3.0.2 lodash.isstring: ^4.0.1 lodash.omit: ^4.5.0 - checksum: c612efc567c48835c97b1156ac8e6bcf826d83b8606e42bfde108410d3b1279bf7ead1eac58ff45808c2780668db78d5801aebc75f02b588d4f6de1fa0e0cded + checksum: 484ba84df363e76c993ecbe7e91297f6a9ee32b7062fc62554cd32405f0dea9d94618be9abceaa98b54169091208a52350ab77243dd5027df30061319168d84b languageName: node linkType: hard From 3aa8c54abe95bf04ac8c302d1b50cd1ce23d0f48 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 04:27:27 +0000 Subject: [PATCH 44/95] fix(deps): update dependency eslint-plugin-jest to v27.4.2 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3285195be0..7b1062d709 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25028,8 +25028,8 @@ __metadata: linkType: hard "eslint-plugin-jest@npm:^27.0.0": - version: 27.2.3 - resolution: "eslint-plugin-jest@npm:27.2.3" + version: 27.4.2 + resolution: "eslint-plugin-jest@npm:27.4.2" dependencies: "@typescript-eslint/utils": ^5.10.0 peerDependencies: @@ -25041,7 +25041,7 @@ __metadata: optional: true jest: optional: true - checksum: 4c7e07f52f17749ac6fd0ff5fcd5ce30b88983ba31eeee322e4d48859f55eaa112f06172e586ad2031c00ff28bb2dfdc3d35c83895251b9c0e860fa47dfc5ff4 + checksum: 99a8301ae00c37da97866b8b13c89a077716d2c653b26bc417d242e7300a43237c0017fd488c43966fa38585f19050facdbbc71d03ca36a1ce6f2ba930a9143e languageName: node linkType: hard From b3ca8f552b2fd3183257703eda59466ae13d201f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 05:21:14 +0000 Subject: [PATCH 45/95] fix(deps): update dependency humanize-duration to v3.30.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 473fe405a9..2329ce64b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -28109,9 +28109,9 @@ __metadata: linkType: hard "humanize-duration@npm:^3.25.1, humanize-duration@npm:^3.26.0, humanize-duration@npm:^3.27.0, humanize-duration@npm:^3.27.1": - version: 3.29.0 - resolution: "humanize-duration@npm:3.29.0" - checksum: 205e959586e774a36561072cd0f2994d727b9e2156a19ff68ee20c9c29544d9eeb3e853cdb7011a32498859043442069ef82c0bd18f1175ed27a733303ab480f + version: 3.30.0 + resolution: "humanize-duration@npm:3.30.0" + checksum: 6eaf888219801d47d42cfb03523e736367c260e3f32cddb4e30c30f49500e08e7cdfd413f02a5ed24943ef382b3e89c8a11e6eda3a432846385903a9f49e576a languageName: node linkType: hard From e351627bf8eee495fe6fdb05da6215cc5f312755 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 06:13:27 +0000 Subject: [PATCH 46/95] fix(deps): update dependency jose to v4.15.2 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2329ce64b1..cd8657fca4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30100,9 +30100,9 @@ __metadata: linkType: hard "jose@npm:^4.14.4, jose@npm:^4.6.0": - version: 4.14.6 - resolution: "jose@npm:4.14.6" - checksum: eae81a234e7bf1446b1bd80722b3462b014e3835b155c3a7799c1c5043163a53a0dc28d347004151b031e6b7b863403aabf8814d9cc217ce21f8c2f3ebd4b335 + version: 4.15.2 + resolution: "jose@npm:4.15.2" + checksum: 8f0cab1eef31243abe14a935b2b330cd95f10f9b69808fd642088ae5000e50e566664934537d2c6413ab2f6b54acd8265a5033da05157aa1260c5f1d7e57fab0 languageName: node linkType: hard From 54635729f2643c79f700343918c5747f3223155e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 07:10:10 +0000 Subject: [PATCH 47/95] fix(deps): update dependency openid-client to v5.6.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index cd8657fca4..e604b3b2d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -30099,7 +30099,7 @@ __metadata: languageName: node linkType: hard -"jose@npm:^4.14.4, jose@npm:^4.6.0": +"jose@npm:^4.15.1, jose@npm:^4.6.0": version: 4.15.2 resolution: "jose@npm:4.15.2" checksum: 8f0cab1eef31243abe14a935b2b330cd95f10f9b69808fd642088ae5000e50e566664934537d2c6413ab2f6b54acd8265a5033da05157aa1260c5f1d7e57fab0 @@ -34271,14 +34271,14 @@ __metadata: linkType: hard "openid-client@npm:^5.2.1, openid-client@npm:^5.3.0": - version: 5.4.3 - resolution: "openid-client@npm:5.4.3" + version: 5.6.0 + resolution: "openid-client@npm:5.6.0" dependencies: - jose: ^4.14.4 + jose: ^4.15.1 lru-cache: ^6.0.0 object-hash: ^2.2.0 oidc-token-hash: ^5.0.3 - checksum: 0e5a126b77dad0320e8f7023ac7ad7f5f1f82ad5f985f7ab0b42a7cf36700dfb78f0bef9b59c1fae915dce0148ef191b49921cd0a01443b64c04f862d9dc03e0 + checksum: 414fe43c5aac07d8b8f5ea52ef1812374ff28140910977a1360781284b09d4a03c916d910017bf15003d602d859b3dcf9857d1dfe6f61508c8f5c52de334e6ea languageName: node linkType: hard From 1c0b43cb296d808d1f946851f36a5395e01f8793 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 07:10:40 +0000 Subject: [PATCH 48/95] fix(deps): update dependency photoswipe to v5.4.2 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index cd8657fca4..02f24a08e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35227,9 +35227,9 @@ __metadata: linkType: hard "photoswipe@npm:^5.3.7": - version: 5.3.9 - resolution: "photoswipe@npm:5.3.9" - checksum: 2d9a6168d28f7d6386ff01800c4bb5189595eed0b7e4b04c24b35c8feec479885cb69a0e782225562acd0c9492e9465429dd75877a70028945b2ff1b9eae4ae6 + version: 5.4.2 + resolution: "photoswipe@npm:5.4.2" + checksum: 4d74b189ede377d17868cc3ebf066fb549642387b387dd922d7714d8cc0ede7e74f8313ded15def42c6ad3b15c6800e2ce92b4034cfc815655fa2bb6433037b4 languageName: node linkType: hard From fe59c70aee5493db980e8776d98842fad17b680f Mon Sep 17 00:00:00 2001 From: namkyu1999 Date: Fri, 6 Oct 2023 16:10:42 +0900 Subject: [PATCH 49/95] feat: add litmus plugin to the backstage marketplace Signed-off-by: namkyu1999 --- microsite/data/plugins/litmus.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 microsite/data/plugins/litmus.yaml diff --git a/microsite/data/plugins/litmus.yaml b/microsite/data/plugins/litmus.yaml new file mode 100644 index 0000000000..110463c6be --- /dev/null +++ b/microsite/data/plugins/litmus.yaml @@ -0,0 +1,10 @@ +--- +title: Litmus +author: litmuschaos.io +authorUrl: https://github.com/litmuschaos/backstage-plugin +category: Chaos Engineering +description: This plugin lets you view the status of Litmus resources and launch Chaos Experiments directly inside Backstage. +documentation: https://github.com/litmuschaos/backstage-plugin/blob/master/README.md +iconUrl: https://raw.githubusercontent.com/cncf/artwork/master/projects/litmus/icon/color/litmus-icon-color.svg +npmPackageName: 'backstage-plugin-litmus' +addedDate: '2023-10-06' \ No newline at end of file From a1f9bf80726c5a3c569ebdbe21210138766a40fc Mon Sep 17 00:00:00 2001 From: namkyu1999 Date: Fri, 6 Oct 2023 16:26:17 +0900 Subject: [PATCH 50/95] chore Signed-off-by: namkyu1999 --- microsite/data/plugins/litmus.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsite/data/plugins/litmus.yaml b/microsite/data/plugins/litmus.yaml index 110463c6be..d27bc4c7d7 100644 --- a/microsite/data/plugins/litmus.yaml +++ b/microsite/data/plugins/litmus.yaml @@ -7,4 +7,4 @@ description: This plugin lets you view the status of Litmus resources and launch documentation: https://github.com/litmuschaos/backstage-plugin/blob/master/README.md iconUrl: https://raw.githubusercontent.com/cncf/artwork/master/projects/litmus/icon/color/litmus-icon-color.svg npmPackageName: 'backstage-plugin-litmus' -addedDate: '2023-10-06' \ No newline at end of file +addedDate: '2023-10-06' From ff205d4a57613e536b18e60d0ecc9d5e88ebff19 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 14:07:52 +0200 Subject: [PATCH 51/95] backend-plugin-manager: refactor test to remove mock-fs Signed-off-by: Patrik Oldsberg --- packages/backend-plugin-manager/package.json | 1 - .../src/manager/plugin-manager.test.ts | 53 ++++++++++--------- yarn.lock | 1 - 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/packages/backend-plugin-manager/package.json b/packages/backend-plugin-manager/package.json index 67697981a1..33675dda1f 100644 --- a/packages/backend-plugin-manager/package.json +++ b/packages/backend-plugin-manager/package.json @@ -55,7 +55,6 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/config-loader": "workspace:^", - "mock-fs": "^5.2.0", "wait-for-expect": "^3.0.2" }, "files": [ diff --git a/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts b/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts index 093776e5b0..f356163dea 100644 --- a/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts +++ b/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts @@ -20,10 +20,9 @@ import { coreServices, createServiceFactory, } from '@backstage/backend-plugin-api'; -import mockFs, { directory, symlink } from 'mock-fs'; import * as path from 'path'; import * as url from 'url'; - +import fs from 'fs'; import { BackendDynamicPlugin, BaseDynamicPlugin, @@ -43,11 +42,13 @@ import { ConfigSources } from '@backstage/config-loader'; import { Logs, MockedLogger, LogContent } from '../__testUtils__/testUtils'; import { PluginScanner } from '../scanner/plugin-scanner'; import { findPaths } from '@backstage/cli-common'; +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('backend-plugin-manager', () => { + const mockDir = createMockDirectory(); + describe('loadPlugins', () => { afterEach(() => { - mockFs.restore(); jest.resetModules(); }); @@ -56,7 +57,7 @@ describe('backend-plugin-manager', () => { packageManifest: ScannedPluginManifest; indexFile?: { retativePath: string[]; - content?: string; + content: string; }; expectedLogs?(location: URL): { errors?: LogContent[]; @@ -354,17 +355,13 @@ describe('backend-plugin-manager', () => { }, ])('$name', async (tc: TestCase): Promise => { const plugin: ScannedPluginPackage = { - location: url.pathToFileURL( - path.resolve(`/node_modules/jest-tests/${randomUUID()}`), - ), + location: url.pathToFileURL(mockDir.resolve(randomUUID())), manifest: tc.packageManifest, }; const mockedFiles = { [path.join(url.fileURLToPath(plugin.location), 'package.json')]: - mockFs.file({ - content: JSON.stringify(plugin), - }), + JSON.stringify(plugin), }; if (tc.indexFile) { mockedFiles[ @@ -372,11 +369,9 @@ describe('backend-plugin-manager', () => { url.fileURLToPath(plugin.location), ...tc.indexFile.retativePath, ) - ] = mockFs.file({ - content: tc.indexFile.content, - }); + ] = tc.indexFile.content; } - mockFs(mockedFiles); + mockDir.setContent(mockedFiles); const logger = new MockedLogger(); const pluginManager = new (PluginManager as any)(logger, [plugin], { @@ -440,8 +435,11 @@ describe('backend-plugin-manager', () => { }); describe('dynamicPluginsServiceFactory', () => { + const otherMockDir = createMockDirectory(); + afterEach(() => { - mockFs.restore(); + mockDir.clear(); + otherMockDir.clear(); jest.resetModules(); }); @@ -449,15 +447,20 @@ describe('backend-plugin-manager', () => { const logger = new MockedLogger(); const rootLogger = new MockedLogger(); - mockFs({ - [findPaths(__dirname).resolveTargetRoot('package.json')]: mockFs.load( + mockDir.setContent({ + 'package.json': fs.readFileSync( findPaths(__dirname).resolveTargetRoot('package.json'), ), - '/somewhere/dynamic-plugins-root/a-dynamic-plugin': symlink({ - path: '/somewhere-else/a-dynamic-plugin', - }), - '/somewhere-else/a-dynamic-plugin': directory({}), + 'dynamic-plugins-root': {}, }); + otherMockDir.setContent({ + 'a-dynamic-plugin': {}, + }); + + fs.symlinkSync( + otherMockDir.resolve('a-dynamic-plugin'), + mockDir.resolve('dynamic-plugins-root/a-dynamic-plugin'), + ); const fromConfigSpier = jest.spyOn(PluginManager, 'fromConfig'); const applyConfigSpier = jest @@ -468,7 +471,7 @@ describe('backend-plugin-manager', () => { .mockImplementation(async () => [ { location: url.pathToFileURL( - path.resolve('/somewhere/dynamic-plugins-root/a-dynamic-plugin'), + mockDir.resolve('dynamic-plugins-root/a-dynamic-plugin'), ), manifest: { name: 'test', @@ -533,11 +536,11 @@ describe('backend-plugin-manager', () => { expect(scanRootSpier).toHaveBeenCalled(); expect(mockedModuleLoader.bootstrap).toHaveBeenCalledWith( findPaths(__dirname).targetRoot, - [path.resolve('/somewhere-else/a-dynamic-plugin')], + [fs.realpathSync(otherMockDir.resolve('a-dynamic-plugin'))], ); expect(mockedModuleLoader.load).toHaveBeenCalledWith( - path.resolve( - '/somewhere/dynamic-plugins-root/a-dynamic-plugin/dist/index.cjs.js', + mockDir.resolve( + 'dynamic-plugins-root/a-dynamic-plugin/dist/index.cjs.js', ), ); }); diff --git a/yarn.lock b/yarn.lock index 56e8fd22bb..b58f118a2e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3604,7 +3604,6 @@ __metadata: chokidar: ^3.5.3 express: ^4.17.1 lodash: ^4.17.21 - mock-fs: ^5.2.0 wait-for-expect: ^3.0.2 winston: ^3.2.1 languageName: unknown From bbe138fa44ea1ec41b6e41f45e8269afd41411dc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 15:06:42 +0200 Subject: [PATCH 52/95] backend-plugin-manager: migrate plugin-scanner tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../src/scanner/plugin-scanner.test.ts | 493 ++++++++---------- 1 file changed, 204 insertions(+), 289 deletions(-) diff --git a/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts b/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts index 6a36a377f5..04ce69ec98 100644 --- a/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts +++ b/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts @@ -15,13 +15,16 @@ */ import { PluginScanner } from './plugin-scanner'; -import mockFs from 'mock-fs'; import { JsonObject } from '@backstage/types'; import { Logs, MockedLogger } from '../__testUtils__/testUtils'; import { ConfigReader } from '@backstage/config'; import path from 'path'; +import fs from 'fs'; import * as url from 'url'; import { ScannedPluginPackage } from './types'; +import { createMockDirectory } from '@backstage/backend-test-utils'; + +const mockDir = createMockDirectory(); describe('plugin-scanner', () => { const env = process.env; @@ -30,7 +33,7 @@ describe('plugin-scanner', () => { }); afterEach(() => { - mockFs.restore(); + mockDir.clear(); process.env = env; }); @@ -61,85 +64,77 @@ describe('plugin-scanner', () => { }, { name: 'valid config with relative root directory path', - backstageRoot: '/backstageRoot', + backstageRoot: mockDir.resolve('backstageRoot'), fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory(), - }, - }), + backstageRoot: { + 'dist-dynamic': {}, + }, }, config: { dynamicPlugins: { rootDirectory: 'dist-dynamic', }, }, - expectedRootDirectory: path.resolve('/backstageRoot/dist-dynamic'), + expectedRootDirectory: mockDir.resolve('backstageRoot/dist-dynamic'), }, { name: 'valid config with absolute root directory path inside the backstage root', - backstageRoot: '/backstageRoot', + backstageRoot: mockDir.resolve('backstageRoot'), fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory(), - }, - }), + backstageRoot: { + 'dist-dynamic': {}, + }, }, config: { dynamicPlugins: { - rootDirectory: '/backstageRoot/dist-dynamic', + rootDirectory: mockDir.resolve('backstageRoot/dist-dynamic'), }, }, - expectedRootDirectory: path.resolve('/backstageRoot/dist-dynamic'), + expectedRootDirectory: mockDir.resolve('backstageRoot/dist-dynamic'), }, { name: 'valid config with absolute root directory path outside the backstage root', - backstageRoot: '/backstageRoot', + backstageRoot: mockDir.resolve('backstageRoot'), fileSystem: { - '/somewhere': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory(), - }, - }), + somewhere: { + 'dist-dynamic': {}, + }, }, config: { dynamicPlugins: { - rootDirectory: '/somewhere/dist-dynamic', + rootDirectory: mockDir.resolve('somewhere/dist-dynamic'), }, }, - expectedError: `Dynamic plugins under '${path.resolve( - '/somewhere/dist-dynamic', - )}' cannot access backstage modules in '${path.resolve( - '/backstageRoot/node_modules', + expectedError: `Dynamic plugins under '${mockDir.resolve( + 'somewhere/dist-dynamic', + )}' cannot access backstage modules in '${mockDir.resolve( + 'backstageRoot/node_modules', )}'. -Please add '${path.resolve( - '/backstageRoot/node_modules', +Please add '${mockDir.resolve( + 'backstageRoot/node_modules', )}' to the 'NODE_PATH' when running the backstage backend.`, }, { name: 'valid config with absolute root directory path outside the backstage root but with backstage root included in NODE_PATH', - backstageRoot: '/backstageRoot', + backstageRoot: mockDir.resolve('backstageRoot'), fileSystem: { - '/somewhere': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory(), - }, - }), + somewhere: { + 'dist-dynamic': {}, + }, }, config: { dynamicPlugins: { - rootDirectory: '/somewhere/dist-dynamic', + rootDirectory: mockDir.resolve('somewhere/dist-dynamic'), }, }, environment: { - NODE_PATH: `${path.resolve('/somewhere-else')}${ + NODE_PATH: `${mockDir.resolve('somewhere-else')}${ path.delimiter - }${path.resolve('/backstageRoot', 'node_modules')}${ + }${mockDir.resolve('backstageRoot', 'node_modules')}${ path.delimiter - }${path.resolve('anywhere-else')}`, + }${mockDir.resolve('anywhere-else')}`, }, - expectedRootDirectory: path.resolve('/somewhere/dist-dynamic'), + expectedRootDirectory: mockDir.resolve('somewhere/dist-dynamic'), }, { name: 'invalid config: dynamicPlugins not an object', @@ -186,13 +181,11 @@ Please add '${path.resolve( }, { name: 'valid config pointing to a file instead of a directory', - backstageRoot: '/backstageRoot', + backstageRoot: mockDir.resolve('backstageRoot'), fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.file(), - }, - }), + backstageRoot: { + 'dist-dynamic': '', + }, }, config: { dynamicPlugins: { @@ -218,7 +211,7 @@ Please add '${path.resolve( ); } if (tc.fileSystem) { - mockFs(tc.fileSystem); + mockDir.setContent(tc.fileSystem); } if (tc.expectedError) { /* eslint-disable-next-line jest/no-conditional-expect */ @@ -241,6 +234,7 @@ Please add '${path.resolve( name: string; preferAlpha?: boolean; fileSystem?: any; + symlinks?: { source: string; target: string }[]; expectedLogs?: Logs; expectedPluginPackages?: ScannedPluginPackage[]; expectedError?: string; @@ -261,31 +255,23 @@ Please add '${path.resolve( { name: 'manifest found in directory', fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - main: 'dist/index.cjs.js', - backstage: { role: 'backend-plugin' }, - }), - }), - }, - }), - }, - }), + backstageRoot: { + 'dist-dynamic': { + 'test-backend-plugin': { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }), + }, }, - }), + }, }, expectedPluginPackages: [ { location: url.pathToFileURL( - path.resolve('/backstageRoot/dist-dynamic/test-backend-plugin'), + mockDir.resolve('backstageRoot/dist-dynamic/test-backend-plugin'), ), manifest: { name: 'test-backend-plugin-dynamic', @@ -299,38 +285,34 @@ Please add '${path.resolve( { name: 'backend plugin found in symlink', fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.symlink({ - path: '/somewhere-else/test-backend-plugin-target', - }), - }, + backstageRoot: { + 'dist-dynamic': {}, + }, + 'somewhere-else': { + 'test-backend-plugin-target': { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, }), }, - }), - '/somewhere-else': mockFs.directory({ - items: { - 'test-backend-plugin-target': mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - main: 'dist/index.cjs.js', - backstage: { role: 'backend-plugin' }, - }), - }), - }, - }), - }, - }), + }, }, + symlinks: [ + { + source: mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin', + ), + target: mockDir.resolve( + 'somewhere-else/test-backend-plugin-target', + ), + }, + ], expectedPluginPackages: [ { location: url.pathToFileURL( - path.resolve('/backstageRoot/dist-dynamic/test-backend-plugin'), + mockDir.resolve('backstageRoot/dist-dynamic/test-backend-plugin'), ), manifest: { name: 'test-backend-plugin-dynamic', @@ -344,22 +326,18 @@ Please add '${path.resolve( { name: 'ignored folder child: not a directory', fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.file({}), - }, - }), + backstageRoot: { + 'dist-dynamic': { + 'test-backend-plugin': '', }, - }), + }, }, expectedPluginPackages: [], expectedLogs: { infos: [ { - message: `skipping '${path.resolve( - '/backstageRoot/dist-dynamic/test-backend-plugin', + message: `skipping '${mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin', )}' since it is not a directory`, }, ], @@ -368,29 +346,29 @@ Please add '${path.resolve( { name: 'ignored folder child symlink: target is not a directory', fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.symlink({ - path: '/somewhere-else/test-backend-plugin-target', - }), - }, - }), - }, - }), - '/somewhere-else': mockFs.directory({ - items: { - 'test-backend-plugin-target': mockFs.file({}), - }, - }), + backstageRoot: { + 'dist-dynamic': {}, + }, + 'somewhere-else': { + 'test-backend-plugin-target': '', + }, }, + symlinks: [ + { + source: mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin', + ), + target: mockDir.resolve( + 'somewhere-else/test-backend-plugin-target', + ), + }, + ], expectedPluginPackages: [], expectedLogs: { infos: [ { - message: `skipping '${path.resolve( - '/backstageRoot/dist-dynamic/test-backend-plugin', + message: `skipping '${mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin', )}' since it is not a directory`, }, ], @@ -400,42 +378,30 @@ Please add '${path.resolve( name: 'alpha manifest available but not preferred', preferAlpha: false, fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - main: 'dist/index.cjs.js', - backstage: { role: 'backend-plugin' }, - }), - }), - alpha: mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - main: '../dist/alpha.cjs.js', - }), - }), - }, - }), - }, + backstageRoot: { + 'dist-dynamic': { + 'test-backend-plugin': { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }), + alpha: { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: '../dist/alpha.cjs.js', }), }, - }), + }, }, - }), + }, }, expectedPluginPackages: [ { location: url.pathToFileURL( - path.resolve('/backstageRoot/dist-dynamic/test-backend-plugin'), + mockDir.resolve('backstageRoot/dist-dynamic/test-backend-plugin'), ), manifest: { name: 'test-backend-plugin-dynamic', @@ -450,43 +416,31 @@ Please add '${path.resolve( name: 'alpha manifest preferred and found in directory', preferAlpha: true, fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - main: 'dist/index.cjs.js', - backstage: { role: 'backend-plugin' }, - }), - }), - alpha: mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - main: '../dist/alpha.cjs.js', - }), - }), - }, - }), - }, + backstageRoot: { + 'dist-dynamic': { + 'test-backend-plugin': { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }), + alpha: { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: '../dist/alpha.cjs.js', }), }, - }), + }, }, - }), + }, }, expectedPluginPackages: [ { location: url.pathToFileURL( - path.resolve( - '/backstageRoot/dist-dynamic/test-backend-plugin/alpha', + mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin/alpha', ), ), manifest: { @@ -502,32 +456,24 @@ Please add '${path.resolve( name: 'alpha manifest preferred but skipped because not a directory', preferAlpha: true, fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - main: 'dist/index.cjs.js', - backstage: { role: 'backend-plugin' }, - }), - }), - alpha: mockFs.file({}), - }, - }), - }, - }), + backstageRoot: { + 'dist-dynamic': { + 'test-backend-plugin': { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }), + alpha: '', + }, }, - }), + }, }, expectedPluginPackages: [ { location: url.pathToFileURL( - path.resolve('/backstageRoot/dist-dynamic/test-backend-plugin'), + mockDir.resolve('backstageRoot/dist-dynamic/test-backend-plugin'), ), manifest: { name: 'test-backend-plugin-dynamic', @@ -540,8 +486,8 @@ Please add '${path.resolve( expectedLogs: { warns: [ { - message: `skipping '${path.resolve( - '/backstageRoot/dist-dynamic/test-backend-plugin/alpha', + message: `skipping '${mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin/alpha', )}' since it is not a directory`, }, ], @@ -551,40 +497,28 @@ Please add '${path.resolve( name: 'invalid alpha package.json', preferAlpha: true, fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - main: 'dist/index.cjs.js', - backstage: { role: 'backend-plugin' }, - }), - }), - alpha: mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: "invalid json content, 1, '", - }), - }, - }), - }, - }), + backstageRoot: { + 'dist-dynamic': { + 'test-backend-plugin': { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }), + alpha: { + 'package.json': "invalid json content, 1, '", }, - }), + }, }, - }), + }, }, expectedPluginPackages: [], expectedLogs: { errors: [ { - message: `failed to load dynamic plugin manifest from '${path.resolve( - '/backstageRoot/dist-dynamic/test-backend-plugin/alpha', + message: `failed to load dynamic plugin manifest from '${mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin/alpha', )}'`, meta: { name: 'SyntaxError', @@ -597,28 +531,20 @@ Please add '${path.resolve( { name: 'invalid package.json', fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: "invalid json content, 1, '", - }), - }, - }), - }, - }), + backstageRoot: { + 'dist-dynamic': { + 'test-backend-plugin': { + 'package.json': "invalid json content, 1, '", + }, }, - }), + }, }, expectedPluginPackages: [], expectedLogs: { errors: [ { - message: `failed to load dynamic plugin manifest from '${path.resolve( - '/backstageRoot/dist-dynamic/test-backend-plugin', + message: `failed to load dynamic plugin manifest from '${mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin', )}'`, meta: { name: 'SyntaxError', @@ -631,32 +557,24 @@ Please add '${path.resolve( { name: 'missing backstage role in package.json', fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - main: 'dist/index.cjs.js', - }), - }), - }, - }), - }, - }), + backstageRoot: { + 'dist-dynamic': { + 'test-backend-plugin': { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + }), + }, }, - }), + }, }, expectedPluginPackages: [], expectedLogs: { errors: [ { - message: `failed to load dynamic plugin manifest from '${path.resolve( - '/backstageRoot/dist-dynamic/test-backend-plugin', + message: `failed to load dynamic plugin manifest from '${mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin', )}'`, meta: { name: 'Error', @@ -669,32 +587,24 @@ Please add '${path.resolve( { name: 'missing main field in package.json', fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - backstage: { role: 'backend-plugin' }, - }), - }), - }, - }), - }, - }), + backstageRoot: { + 'dist-dynamic': { + 'test-backend-plugin': { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + backstage: { role: 'backend-plugin' }, + }), + }, }, - }), + }, }, expectedPluginPackages: [], expectedLogs: { errors: [ { - message: `failed to load dynamic plugin manifest from '${path.resolve( - '/backstageRoot/dist-dynamic/test-backend-plugin', + message: `failed to load dynamic plugin manifest from '${mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin', )}'`, meta: { name: 'Error', @@ -706,7 +616,7 @@ Please add '${path.resolve( }, ])('$name', async (tc: TestCase): Promise => { const logger = new MockedLogger(); - const backstageRoot = '/backstageRoot'; + const backstageRoot = mockDir.resolve('backstageRoot'); async function toTest(): Promise { const pluginScanner = new PluginScanner( new ConfigReader( @@ -725,7 +635,12 @@ Please add '${path.resolve( return await pluginScanner.scanRoot(); } if (tc.fileSystem) { - mockFs(tc.fileSystem); + mockDir.setContent(tc.fileSystem); + } + if (tc.symlinks) { + for (const { source, target } of tc.symlinks) { + fs.symlinkSync(target, source); + } } if (tc.expectedError) { /* eslint-disable-next-line jest/no-conditional-expect */ From 28cb8fd4fe48d780ac4ee114e961e3bc8b367015 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 13:36:48 +0200 Subject: [PATCH 53/95] backend-app-api: refactor test to remove mock-fs Signed-off-by: Patrik Oldsberg --- packages/backend-app-api/package.json | 1 - .../featureDiscoveryServiceFactory.test.ts | 55 +++++++++---------- yarn.lock | 1 - 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 24959ff290..db04e7626c 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -87,7 +87,6 @@ "@types/node-forge": "^1.3.0", "@types/stoppable": "^1.1.0", "http-errors": "^2.0.0", - "mock-fs": "^5.2.0", "supertest": "^6.1.3" }, "configSchema": "config.d.ts", diff --git a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts index 448f1a8ff7..92f34bb94f 100644 --- a/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts +++ b/packages/backend-app-api/src/alpha/featureDiscoveryServiceFactory.test.ts @@ -14,28 +14,31 @@ * limitations under the License. */ -import mockFs from 'mock-fs'; -import { resolve as resolvePath, dirname } from 'path'; -import { startTestBackend, mockServices } from '@backstage/backend-test-utils'; +import { + startTestBackend, + mockServices, + createMockDirectory, +} from '@backstage/backend-test-utils'; import { featureDiscoveryServiceFactory } from './featureDiscoveryServiceFactory'; -const rootDir = dirname(process.argv[1]); +const mockDir = createMockDirectory(); +process.argv[1] = mockDir.path; + +const pluginApiPath = require.resolve('@backstage/backend-plugin-api'); describe('featureDiscoveryServiceFactory', () => { beforeEach(() => { - mockFs({ - [rootDir]: { - 'package.json': JSON.stringify({ - name: 'example-app', - dependencies: { - 'detected-plugin': '0.0.0', - 'detected-module': '0.0.0', - 'detected-plugin-with-alpha': '0.0.0', - 'detected-library': '0.0.0', - }, - }), - }, - [resolvePath(rootDir, 'node_modules/detected-plugin')]: { + mockDir.setContent({ + 'package.json': JSON.stringify({ + name: 'example-app', + dependencies: { + 'detected-plugin': '0.0.0', + 'detected-module': '0.0.0', + 'detected-plugin-with-alpha': '0.0.0', + 'detected-library': '0.0.0', + }, + }), + 'node_modules/detected-plugin': { 'package.json': JSON.stringify({ name: 'detected-plugin', main: 'index.js', @@ -44,7 +47,7 @@ describe('featureDiscoveryServiceFactory', () => { }, }), 'index.js': ` - const { createBackendPlugin, coreServices } = require('@backstage/backend-plugin-api'); + const { createBackendPlugin, coreServices } = require('${pluginApiPath}'); exports.default = createBackendPlugin({ pluginId: 'detected', register(env) { @@ -58,7 +61,7 @@ describe('featureDiscoveryServiceFactory', () => { }); `, }, - [resolvePath(rootDir, 'node_modules/detected-module')]: { + 'node_modules/detected-module': { 'package.json': JSON.stringify({ name: 'detected-module', main: 'index.js', @@ -67,7 +70,7 @@ describe('featureDiscoveryServiceFactory', () => { }, }), 'index.js': ` - const { createBackendModule, coreServices } = require('@backstage/backend-plugin-api'); + const { createBackendModule, coreServices } = require('${pluginApiPath}'); exports.default = createBackendModule({ pluginId: 'detected', moduleId: 'derp', @@ -82,7 +85,7 @@ describe('featureDiscoveryServiceFactory', () => { }); `, }, - [resolvePath(rootDir, 'node_modules/detected-plugin-with-alpha')]: { + 'node_modules/detected-plugin-with-alpha': { 'package.json': JSON.stringify({ name: 'detected-plugin-with-alpha', main: 'index.js', @@ -101,7 +104,7 @@ describe('featureDiscoveryServiceFactory', () => { }), 'index.js': `exports.default = undefined;`, 'alpha.js': ` - const { createBackendPlugin, coreServices } = require('@backstage/backend-plugin-api'); + const { createBackendPlugin, coreServices } = require('${pluginApiPath}'); exports.default = createBackendPlugin({ pluginId: 'detected-alpha', register(env) { @@ -115,7 +118,7 @@ describe('featureDiscoveryServiceFactory', () => { }); `, }, - [resolvePath(rootDir, 'node_modules/detected-library')]: { + 'node_modules/detected-library': { 'package.json': JSON.stringify({ name: 'detected-library', main: 'index.js', @@ -124,7 +127,7 @@ describe('featureDiscoveryServiceFactory', () => { }, }), 'index.js': ` - const { createServiceFactory, createServiceRef, coreServices } = require('@backstage/backend-plugin-api'); + const { createServiceFactory, createServiceRef, coreServices } = require('${pluginApiPath}'); exports.default = createServiceFactory({ service: createServiceRef({ id: 'test', scope: 'root' }), deps: { logger: coreServices.rootLogger }, @@ -138,10 +141,6 @@ describe('featureDiscoveryServiceFactory', () => { }); }); - afterEach(() => { - mockFs.restore(); - }); - it('should detect plugin and module packages when "all" is specified', async () => { const mock = mockServices.rootLogger.mock({ child: () => mock }); diff --git a/yarn.lock b/yarn.lock index 56e8fd22bb..4e68a1161b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3415,7 +3415,6 @@ __metadata: logform: ^2.3.2 minimatch: ^5.0.0 minimist: ^1.2.5 - mock-fs: ^5.2.0 morgan: ^1.10.0 node-forge: ^1.3.1 selfsigned: ^2.0.0 From 6f9514ac1fcbaaff36252481284152609d95657b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 15:49:30 +0200 Subject: [PATCH 54/95] config-loader: refactor schema collection tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../config-loader/src/schema/collect.test.ts | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/packages/config-loader/src/schema/collect.test.ts b/packages/config-loader/src/schema/collect.test.ts index 2754f788dd..f70220011a 100644 --- a/packages/config-loader/src/schema/collect.test.ts +++ b/packages/config-loader/src/schema/collect.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import mockFs from 'mock-fs'; +import { createMockDirectory } from '@backstage/backend-test-utils'; import { collectConfigSchemas } from './collect'; import path from 'path'; @@ -28,25 +28,15 @@ const mockSchema = { }, }; -// Gotta make sure this is in the compiler cache before we start mocking the filesystem -require('typescript-json-schema'); - -// We need to load in actual TS libraries when using mock-fs. -// This lookup is to allow the `typescript` dependency to exist either -// at top level or inside node_modules of typescript-json-schema -const typescriptModuleDir = path.dirname( - require.resolve('typescript/package.json', { - paths: [require.resolve('typescript-json-schema')], - }), -); - describe('collectConfigSchemas', () => { + const mockDir = createMockDirectory(); + afterEach(() => { - mockFs.restore(); + mockDir.clear(); }); it('should not find any schemas without packages', async () => { - mockFs({ + mockDir.setContent({ 'lerna.json': JSON.stringify({ packages: ['packages/*'], }), @@ -56,7 +46,7 @@ describe('collectConfigSchemas', () => { }); it('should find schema in a local package', async () => { - mockFs({ + mockDir.setContent({ node_modules: { a: { 'package.json': JSON.stringify({ @@ -66,6 +56,7 @@ describe('collectConfigSchemas', () => { }, }, }); + process.chdir(mockDir.path); await expect(collectConfigSchemas(['a'], [])).resolves.toEqual([ { @@ -76,7 +67,7 @@ describe('collectConfigSchemas', () => { }); it('should find schema at explicit package path', async () => { - mockFs({ + mockDir.setContent({ root: { 'package.json': JSON.stringify({ name: 'root', @@ -84,6 +75,7 @@ describe('collectConfigSchemas', () => { }), }, }); + process.chdir(mockDir.path); await expect( collectConfigSchemas([], [path.join('root', 'package.json')]), @@ -96,7 +88,7 @@ describe('collectConfigSchemas', () => { }); it('should find schema in transitive dependencies and explicit path', async () => { - mockFs({ + mockDir.setContent({ root: { 'package.json': JSON.stringify({ name: 'root', @@ -152,6 +144,7 @@ describe('collectConfigSchemas', () => { }, }, }); + process.chdir(mockDir.path); await expect( collectConfigSchemas(['a'], [path.join('root', 'package.json')]), @@ -178,7 +171,7 @@ describe('collectConfigSchemas', () => { }); it('should schema of different types', async () => { - mockFs({ + mockDir.setContent({ node_modules: { a: { 'package.json': JSON.stringify({ @@ -198,15 +191,16 @@ describe('collectConfigSchemas', () => { name: 'c', configSchema: 'schema.d.ts', }), - 'schema.d.ts': `export interface Config { + 'schema.d.ts': ` + export interface Config { /** @visibility secret */ tsKey: string - }`, + } + `, }, }, - // TypeScript compilation needs to load some real files inside the typescript dir - [typescriptModuleDir]: (mockFs as any).load(typescriptModuleDir), }); + process.chdir(mockDir.path); await expect(collectConfigSchemas(['a', 'b', 'c'], [])).resolves.toEqual([ { @@ -235,7 +229,7 @@ describe('collectConfigSchemas', () => { }); it('should load schema from different package versions', async () => { - mockFs({ + mockDir.setContent({ node_modules: { a: { 'package.json': JSON.stringify({ @@ -275,6 +269,7 @@ describe('collectConfigSchemas', () => { }, }, }); + process.chdir(mockDir.path); await expect(collectConfigSchemas(['a'], [])).resolves.toEqual([ { @@ -303,7 +298,7 @@ describe('collectConfigSchemas', () => { }); it('should not allow unknown schema file types', async () => { - mockFs({ + mockDir.setContent({ node_modules: { a: { 'package.json': JSON.stringify({ @@ -314,6 +309,7 @@ describe('collectConfigSchemas', () => { }, }, }); + process.chdir(mockDir.path); await expect(collectConfigSchemas(['a'], [])).rejects.toThrow( 'Config schema files must be .json or .d.ts, got schema.yaml', @@ -321,7 +317,7 @@ describe('collectConfigSchemas', () => { }); it('should reject typescript config declaration without a Config type', async () => { - mockFs({ + mockDir.setContent({ node_modules: { a: { 'package.json': JSON.stringify({ @@ -331,9 +327,8 @@ describe('collectConfigSchemas', () => { 'schema.d.ts': `export interface NotConfig {}`, }, }, - // TypeScript compilation needs to load some real files inside the typescript dir - [typescriptModuleDir]: (mockFs as any).load(typescriptModuleDir), }); + process.chdir(mockDir.path); await expect(collectConfigSchemas(['a'], [])).rejects.toThrow( `Invalid schema in ${path.join( From afd0290285de64f3d13b09eba71f93e04161dec5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 15:54:04 +0200 Subject: [PATCH 55/95] config-loader: refactor remaining tests and remove mock-fs dep Signed-off-by: Patrik Oldsberg --- packages/config-loader/package.json | 1 - packages/config-loader/src/loader.test.ts | 174 +++++++++--------- .../config-loader/src/schema/load.test.ts | 18 +- yarn.lock | 1 - 4 files changed, 100 insertions(+), 94 deletions(-) diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index fb17d00378..2478bc7036 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -57,7 +57,6 @@ "@types/json-schema-merge-allof": "^0.6.0", "@types/mock-fs": "^4.10.0", "@types/yup": "^0.29.13", - "mock-fs": "^5.2.0", "msw": "^1.0.0", "zen-observable": "^0.10.0" }, diff --git a/packages/config-loader/src/loader.test.ts b/packages/config-loader/src/loader.test.ts index 9554e8767e..a8299962b7 100644 --- a/packages/config-loader/src/loader.test.ts +++ b/packages/config-loader/src/loader.test.ts @@ -16,15 +16,62 @@ import { AppConfig } from '@backstage/config'; import { loadConfig } from './loader'; -import mockFs from 'mock-fs'; import fs from 'fs-extra'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { resolve as resolvePath, sep } from 'path'; - -const root = resolvePath('/'); +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('loadConfig', () => { + const mockDir = createMockDirectory({ + content: { + 'app-config.yaml': ` + app: + title: Example App + sessionKey: + $file: secrets/session-key.txt + escaped: \$\${Escaped} + `, + 'app-config2.yaml': ` + app: + title: Example App 2 + sessionKey: + $file: secrets/session-key.txt + escaped: \$\${Escaped} + `, + 'app-config.development.yaml': ` + app: + sessionKey: development-key + backend: + $include: ./included.yaml + other: + $include: secrets/included.yaml + `, + 'secrets/session-key.txt': 'abc123', + 'secrets/included.yaml': ` + secret: + $file: session-key.txt + `, + 'included.yaml': ` + foo: + bar: token \${MY_SECRET} + `, + 'app-config.substitute.yaml': ` + app: + someConfig: + $include: \${SUBSTITUTE_ME}.yaml + noSubstitute: + $file: \$\${ESCAPE_ME}.txt + `, + 'substituted.yaml': ` + secret: + $file: secrets/\${SUBSTITUTE_ME}.txt + `, + 'secrets/substituted.txt': '123abc', + '${ESCAPE_ME}.txt': 'notSubstituted', + 'empty.yaml': '# just a comment', + }, + }); + const server = setupServer(); const initialLoaderHandler = rest.get( `https://some.domain.io/app-config.yaml`, @@ -61,58 +108,9 @@ describe('loadConfig', () => { beforeEach(() => { process.env.MY_SECRET = 'is-secret'; process.env.SUBSTITUTE_ME = 'substituted'; - - mockFs({ - '/root/app-config.yaml': ` - app: - title: Example App - sessionKey: - $file: secrets/session-key.txt - escaped: \$\${Escaped} - `, - '/root/app-config2.yaml': ` - app: - title: Example App 2 - sessionKey: - $file: secrets/session-key.txt - escaped: \$\${Escaped} - `, - '/root/app-config.development.yaml': ` - app: - sessionKey: development-key - backend: - $include: ./included.yaml - other: - $include: secrets/included.yaml - `, - '/root/secrets/session-key.txt': 'abc123', - '/root/secrets/included.yaml': ` - secret: - $file: session-key.txt - `, - '/root/included.yaml': ` - foo: - bar: token \${MY_SECRET} - `, - '/root/app-config.substitute.yaml': ` - app: - someConfig: - $include: \${SUBSTITUTE_ME}.yaml - noSubstitute: - $file: \$\${ESCAPE_ME}.txt - `, - '/root/substituted.yaml': ` - secret: - $file: secrets/\${SUBSTITUTE_ME}.txt - `, - '/root/secrets/substituted.txt': '123abc', - '/root/${ESCAPE_ME}.txt': 'notSubstituted', - '/root/empty.yaml': '# just a comment', - }); }); afterEach(() => { - mockFs.restore(); server.resetHandlers(); }); @@ -121,7 +119,7 @@ describe('loadConfig', () => { it('load config from default path', async () => { await expect( loadConfig({ - configRoot: '/root', + configRoot: mockDir.path, configTargets: [], }), ).resolves.toEqual({ @@ -135,7 +133,7 @@ describe('loadConfig', () => { escaped: '${Escaped}', }, }, - path: `${root}root${sep}app-config.yaml`, + path: mockDir.resolve('app-config.yaml'), }, ], }); @@ -148,7 +146,7 @@ describe('loadConfig', () => { await expect( loadConfig({ - configRoot: '/root', + configRoot: mockDir.path, configTargets: [{ url: configUrl }], remote: { reloadIntervalSeconds: 30, @@ -173,10 +171,10 @@ describe('loadConfig', () => { it('loads config with secrets from two different files', async () => { await expect( loadConfig({ - configRoot: '/root', + configRoot: mockDir.path, configTargets: [ - { path: '/root/app-config.yaml' }, - { path: '/root/app-config2.yaml' }, + { path: mockDir.resolve('app-config.yaml') }, + { path: mockDir.resolve('app-config2.yaml') }, ], }), ).resolves.toEqual({ @@ -190,7 +188,7 @@ describe('loadConfig', () => { escaped: '${Escaped}', }, }, - path: '/root/app-config.yaml', + path: mockDir.resolve('app-config.yaml'), }, { context: 'app-config2.yaml', @@ -201,7 +199,7 @@ describe('loadConfig', () => { escaped: '${Escaped}', }, }, - path: '/root/app-config2.yaml', + path: mockDir.resolve('app-config2.yaml'), }, ], }); @@ -210,8 +208,8 @@ describe('loadConfig', () => { it('loads config with secrets from single file', async () => { await expect( loadConfig({ - configRoot: '/root', - configTargets: [{ path: '/root/app-config.yaml' }], + configRoot: mockDir.path, + configTargets: [{ path: mockDir.resolve('app-config.yaml') }], }), ).resolves.toEqual({ appConfigs: [ @@ -224,7 +222,7 @@ describe('loadConfig', () => { escaped: '${Escaped}', }, }, - path: '/root/app-config.yaml', + path: mockDir.resolve('app-config.yaml'), }, ], }); @@ -233,10 +231,10 @@ describe('loadConfig', () => { it('loads development config with secrets', async () => { await expect( loadConfig({ - configRoot: '/root', + configRoot: mockDir.path, configTargets: [ - { path: '/root/app-config.yaml' }, - { path: '/root/app-config.development.yaml' }, + { path: mockDir.resolve('app-config.yaml') }, + { path: mockDir.resolve('app-config.development.yaml') }, ], }), ).resolves.toEqual({ @@ -250,7 +248,7 @@ describe('loadConfig', () => { escaped: '${Escaped}', }, }, - path: '/root/app-config.yaml', + path: mockDir.resolve('app-config.yaml'), }, { context: 'app-config.development.yaml', @@ -267,7 +265,7 @@ describe('loadConfig', () => { secret: 'abc123', }, }, - path: '/root/app-config.development.yaml', + path: mockDir.resolve('app-config.development.yaml'), }, ], }); @@ -276,8 +274,10 @@ describe('loadConfig', () => { it('loads deep substituted config', async () => { await expect( loadConfig({ - configRoot: '/root', - configTargets: [{ path: '/root/app-config.substitute.yaml' }], + configRoot: mockDir.path, + configTargets: [ + { path: mockDir.resolve('app-config.substitute.yaml') }, + ], }), ).resolves.toEqual({ appConfigs: [ @@ -291,7 +291,7 @@ describe('loadConfig', () => { noSubstitute: 'notSubstituted', }, }, - path: '/root/app-config.substitute.yaml', + path: mockDir.resolve('app-config.substitute.yaml'), }, ], }); @@ -303,7 +303,7 @@ describe('loadConfig', () => { await expect( loadConfig({ - configRoot: '/root', + configRoot: mockDir.path, configTargets: [], watch: { onChange: onChange.resolve, @@ -321,12 +321,12 @@ describe('loadConfig', () => { escaped: '${Escaped}', }, }, - path: `${root}root${sep}app-config.yaml`, + path: mockDir.resolve('app-config.yaml'), }, ], }); - await fs.writeJson('/root/app-config.yaml', { + await fs.writeJson(mockDir.resolve('app-config.yaml'), { app: { title: 'New Title', }, @@ -339,7 +339,7 @@ describe('loadConfig', () => { title: 'New Title', }, }, - path: `${root}root${sep}app-config.yaml`, + path: mockDir.resolve('app-config.yaml'), }, ]); @@ -352,8 +352,10 @@ describe('loadConfig', () => { await expect( loadConfig({ - configRoot: '/root', - configTargets: [{ path: '/root/app-config.development.yaml' }], + configRoot: mockDir.path, + configTargets: [ + { path: mockDir.resolve('app-config.development.yaml') }, + ], watch: { onChange: onChange.resolve, stopSignal: stopSignal.promise, @@ -376,14 +378,14 @@ describe('loadConfig', () => { secret: 'abc123', }, }, - path: '/root/app-config.development.yaml', + path: mockDir.resolve('app-config.development.yaml'), }, ], }); // session-key is indirectly included in app-config.development.yaml // via included.yaml - await fs.writeFile('/root/secrets/session-key.txt', 'abc234'); + await fs.writeFile(mockDir.resolve('secrets/session-key.txt'), 'abc234'); await expect(onChange.promise).resolves.toEqual([ { @@ -401,7 +403,7 @@ describe('loadConfig', () => { secret: 'abc234', }, }, - path: '/root/app-config.development.yaml', + path: mockDir.resolve('app-config.development.yaml'), }, ]); @@ -417,7 +419,7 @@ describe('loadConfig', () => { const configUrl = 'https://some.domain.io/app-config.yaml'; await expect( loadConfig({ - configRoot: '/root', + configRoot: mockDir.path, configTargets: [{ url: configUrl }], watch: { onChange: onChange.resolve, @@ -464,7 +466,7 @@ describe('loadConfig', () => { const stopSignal = defer(); await loadConfig({ - configRoot: '/root', + configRoot: mockDir.path, configTargets: [], watch: { onChange: () => { @@ -476,7 +478,7 @@ describe('loadConfig', () => { stopSignal.resolve(); - await fs.writeJson('/root/app-config.yaml', { + await fs.writeJson(mockDir.resolve('app-config.yaml'), { app: { title: 'New Title', }, @@ -487,8 +489,8 @@ describe('loadConfig', () => { it('handles empty files gracefully', async () => { await expect( loadConfig({ - configRoot: '/root', - configTargets: [{ path: '/root/empty.yaml' }], + configRoot: mockDir.path, + configTargets: [{ path: mockDir.resolve('empty.yaml') }], }), ).resolves.toEqual({ appConfigs: [], diff --git a/packages/config-loader/src/schema/load.test.ts b/packages/config-loader/src/schema/load.test.ts index d84bb3871b..85cc9fde5f 100644 --- a/packages/config-loader/src/schema/load.test.ts +++ b/packages/config-loader/src/schema/load.test.ts @@ -14,16 +14,18 @@ * limitations under the License. */ -import mockFs from 'mock-fs'; +import { createMockDirectory } from '@backstage/backend-test-utils'; import { loadConfigSchema } from './load'; describe('loadConfigSchema', () => { + const mockDir = createMockDirectory(); + afterEach(() => { - mockFs.restore(); + mockDir.clear(); }); it('should load schema from packages or data', async () => { - mockFs({ + mockDir.setContent({ node_modules: { a: { 'package.json': JSON.stringify({ @@ -53,6 +55,7 @@ describe('loadConfigSchema', () => { }, }, }); + process.chdir(mockDir.path); const schema = await loadConfigSchema({ dependencies: ['a'], @@ -119,7 +122,7 @@ describe('loadConfigSchema', () => { describe('should consider schema', () => { it('when filtering simple config', async () => { - mockFs({ + mockDir.setContent({ 'package.json': JSON.stringify({ name: 'a', configSchema: { @@ -131,6 +134,7 @@ describe('loadConfigSchema', () => { }, }), }); + process.chdir(mockDir.path); const schema = await loadConfigSchema({ packagePaths: ['package.json'], @@ -156,7 +160,7 @@ describe('loadConfigSchema', () => { }); it('when filtering nested config', async () => { - mockFs({ + mockDir.setContent({ 'package.json': JSON.stringify({ name: 'a', configSchema: { @@ -185,6 +189,7 @@ describe('loadConfigSchema', () => { }, }), }); + process.chdir(mockDir.path); const schema = await loadConfigSchema({ packagePaths: ['package.json'], @@ -244,7 +249,7 @@ describe('loadConfigSchema', () => { }); it('when filtering config with required values', async () => { - mockFs({ + mockDir.setContent({ 'package.json': JSON.stringify({ name: 'a', configSchema: { @@ -261,6 +266,7 @@ describe('loadConfigSchema', () => { }, }), }); + process.chdir(mockDir.path); const schema = await loadConfigSchema({ packagePaths: ['package.json'], diff --git a/yarn.lock b/yarn.lock index 4e68a1161b..522969c7e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3913,7 +3913,6 @@ __metadata: json-schema-traverse: ^1.0.0 lodash: ^4.17.21 minimist: ^1.2.5 - mock-fs: ^5.2.0 msw: ^1.0.0 node-fetch: ^2.6.7 typescript-json-schema: ^0.55.0 From fef854a7b53f0752868fb1594e1b5ea73a5dfd86 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 16:21:44 +0200 Subject: [PATCH 56/95] create-app: refactor tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- packages/create-app/package.json | 1 + packages/create-app/src/createApp.test.ts | 16 ++------- packages/create-app/src/lib/tasks.test.ts | 40 ++++++++++++++--------- yarn.lock | 1 + 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 6ae88a8e4c..56764b6be9 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -42,6 +42,7 @@ "recursive-readdir": "^2.2.2" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", diff --git a/packages/create-app/src/createApp.test.ts b/packages/create-app/src/createApp.test.ts index 7cb9733567..844d2aee1b 100644 --- a/packages/create-app/src/createApp.test.ts +++ b/packages/create-app/src/createApp.test.ts @@ -15,13 +15,13 @@ */ import inquirer from 'inquirer'; -import mockFs from 'mock-fs'; import path from 'path'; import { Command } from 'commander'; import * as tasks from './lib/tasks'; import createApp from './createApp'; import { findPaths } from '@backstage/cli-common'; import { tmpdir } from 'os'; +import { createMockDirectory } from '@backstage/backend-test-utils'; jest.mock('./lib/tasks'); @@ -40,16 +40,7 @@ const moveAppMock = jest.spyOn(tasks, 'moveAppTask'); const buildAppMock = jest.spyOn(tasks, 'buildAppTask'); describe('command entrypoint', () => { - beforeEach(() => { - mockFs({ - [`${__dirname}/package.json`]: '', // required by `findPaths(__dirname)` - 'templates/': mockFs.load(path.resolve(__dirname, '../templates/')), - }); - }); - - afterEach(() => { - mockFs.restore(); - }); + const mockDir = createMockDirectory({ mockOsTmpDir: true }); beforeEach(() => { promptMock.mockResolvedValueOnce({ @@ -62,6 +53,7 @@ describe('command entrypoint', () => { }); afterEach(() => { + mockDir.clear(); jest.resetAllMocks(); }); @@ -75,7 +67,6 @@ describe('command entrypoint', () => { findPaths(__dirname).resolveTarget( 'packages', 'create-app', - 'src', 'templates', 'default-app', ), @@ -97,7 +88,6 @@ describe('command entrypoint', () => { findPaths(__dirname).resolveTarget( 'packages', 'create-app', - 'src', 'templates', 'default-app', ), diff --git a/packages/create-app/src/lib/tasks.test.ts b/packages/create-app/src/lib/tasks.test.ts index 68afdc9222..e122374004 100644 --- a/packages/create-app/src/lib/tasks.test.ts +++ b/packages/create-app/src/lib/tasks.test.ts @@ -15,9 +15,8 @@ */ import fs from 'fs-extra'; -import mockFs from 'mock-fs'; import child_process from 'child_process'; -import path, { resolve as resolvePath } from 'path'; +import { resolve as resolvePath } from 'path'; import os from 'os'; import { Task, @@ -29,6 +28,7 @@ import { tryInitGitRepository, readGitConfig, } from './tasks'; +import { createMockDirectory } from '@backstage/backend-test-utils'; jest.spyOn(Task, 'log').mockReturnValue(undefined); jest.spyOn(Task, 'error').mockReturnValue(undefined); @@ -101,21 +101,33 @@ describe('tasks', () => { ) => void >; + const mockDir = createMockDirectory(); + + const realChdir = process.chdir; + // If anyone calls chdir then make it resolve within the tmpdir + const mockChdir = jest.spyOn(process, 'chdir'); + beforeEach(() => { - mockFs({ - 'projects/my-module.ts': '', - 'projects/dir/my-file.txt': '', - 'tmp/mockApp/.gitignore': '', - 'tmp/mockApp/package.json': '', - 'tmp/mockApp/packages/app/package.json': '', - // load templates into mock filesystem - 'templates/': mockFs.load(path.resolve(__dirname, '../../templates/')), + mockDir.setContent({ + projects: { + 'my-module.ts': '', + 'dir/my-file.txt': '', + }, + 'tmp/mockApp': { + '.gitignore': '', + 'package.json': '', + 'packages/app/package.json': '', + }, }); + realChdir(mockDir.path); + mockChdir.mockImplementation((dir: string) => + realChdir(mockDir.resolve(dir)), + ); }); afterEach(() => { mockExec.mockRestore(); - mockFs.restore(); + mockChdir.mockReset(); }); describe('checkAppExistsTask', () => { @@ -164,8 +176,6 @@ describe('tasks', () => { describe('buildAppTask', () => { it('should change to `appDir` and run `yarn install` and `yarn tsc`', async () => { - const mockChdir = jest.spyOn(process, 'chdir'); - // requires callback implementation to support `promisify` wrapper // https://stackoverflow.com/a/60579617/10044859 mockExec.mockImplementation((_command, callback) => { @@ -199,8 +209,6 @@ describe('tasks', () => { }); it('should error out on incorrect yarn version', async () => { - const mockChdir = jest.spyOn(process, 'chdir'); - // requires callback implementation to support `promisify` wrapper // https://stackoverflow.com/a/60579617/10044859 mockExec.mockImplementation((_command, callback) => { @@ -265,7 +273,7 @@ describe('tasks', () => { describe('templatingTask', () => { it('should generate a project populating context parameters', async () => { - const templateDir = 'templates/default-app'; + const templateDir = resolvePath(__dirname, '../../templates/default-app'); const destinationDir = 'templatedApp'; const context = { name: 'SuperCoolBackstageInstance', diff --git a/yarn.lock b/yarn.lock index 522969c7e4..8846ec0813 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4206,6 +4206,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/create-app@workspace:packages/create-app" dependencies: + "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/cli-common": "workspace:^" "@types/command-exists": ^1.2.0 From c251bfd142c0596d964e00f0e802e392d4d279e5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 08:09:09 +0000 Subject: [PATCH 57/95] fix(deps): update dependency @roadiehq/backstage-plugin-github-pull-requests to v2.5.18 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2ac71e1e21..61885a0698 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14796,8 +14796,8 @@ __metadata: linkType: hard "@roadiehq/backstage-plugin-github-pull-requests@npm:^2.2.7": - version: 2.5.17 - resolution: "@roadiehq/backstage-plugin-github-pull-requests@npm:2.5.17" + version: 2.5.18 + resolution: "@roadiehq/backstage-plugin-github-pull-requests@npm:2.5.18" dependencies: "@backstage/catalog-model": ^1.4.2 "@backstage/core-components": ^0.13.5 @@ -14820,7 +14820,7 @@ __metadata: react: ^16.13.1 || ^17.0.0 react-dom: ^16.13.1 || ^17.0.0 react-router: 6.0.0-beta.0 || ^6.3.0 - checksum: 4a2203aad37ae8b268f3ff11967545bbcfc18741f3e3ea0e7106dcbb3f8af47a7701ebdcdf17e80023590394fb2f923e0f3691b7d0334586892f99335756a47d + checksum: 674644685ab61057d11ed4b372045ad0287fdf17de5c325d007f98726f6839678e47cb0e5b23cf344e62ab0a2c1a037df05ee9d0cad1dba85564e0df79481fc0 languageName: node linkType: hard From 3cad6c2e8ad279126d6ebf0f7d17e47faf428d9c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 08:09:54 +0000 Subject: [PATCH 58/95] fix(deps): update dependency react-grid-layout to v1.4.2 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2ac71e1e21..100a8bc57d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18219,11 +18219,11 @@ __metadata: linkType: hard "@types/react-grid-layout@npm:^1.3.2": - version: 1.3.2 - resolution: "@types/react-grid-layout@npm:1.3.2" + version: 1.3.3 + resolution: "@types/react-grid-layout@npm:1.3.3" dependencies: "@types/react": "*" - checksum: 190492acb69186c651bb99f19028dcd4c65129eae7de6efd41f51b6a6711af490e7b99ad7156b8731b11ecf2ec7c22bcf13c782bfe65be4b4e5c3362b97095bf + checksum: 918173791c2b121f2780ccfcd36b6fe79c8b43d014ab8bbc37d6e70ee1849bfe81281450d121a2361ad15b4ddfbdfc95c9790ac600f732a20897844d735c455b languageName: node linkType: hard @@ -31541,7 +31541,7 @@ __metadata: languageName: node linkType: hard -"lodash.isequal@npm:^4.0.0, lodash.isequal@npm:^4.5.0": +"lodash.isequal@npm:^4.5.0": version: 4.5.0 resolution: "lodash.isequal@npm:4.5.0" checksum: da27515dc5230eb1140ba65ff8de3613649620e8656b19a6270afe4866b7bd461d9ba2ac8a48dcc57f7adac4ee80e1de9f965d89d4d81a0ad52bb3eec2609644 @@ -36743,16 +36743,16 @@ __metadata: languageName: node linkType: hard -"react-draggable@npm:^4.0.0, react-draggable@npm:^4.0.3": - version: 4.4.5 - resolution: "react-draggable@npm:4.4.5" +"react-draggable@npm:^4.0.3, react-draggable@npm:^4.4.5": + version: 4.4.6 + resolution: "react-draggable@npm:4.4.6" dependencies: clsx: ^1.1.1 prop-types: ^15.8.1 peerDependencies: react: ">= 16.3.0" react-dom: ">= 16.3.0" - checksum: 21c3775db086e13020967627c20acd41d1ddbc7c7d7fca51491a5bbb54a0aa7e1730a4bc9af17141eb50a4954e547a5e25b2368f5f54b70db6f2686a897bacf2 + checksum: 9b15aac59244873ac4561c5a2bead43a56e18d406e0a5f242bd4f9d151c074530c02b99387983104bf43417292f9cf8d063e554ed08d88792235e3fbc965f1b8 languageName: node linkType: hard @@ -36812,18 +36812,19 @@ __metadata: linkType: hard "react-grid-layout@npm:^1.3.4": - version: 1.3.4 - resolution: "react-grid-layout@npm:1.3.4" + version: 1.4.2 + resolution: "react-grid-layout@npm:1.4.2" dependencies: - clsx: ^1.1.1 - lodash.isequal: ^4.0.0 + clsx: ^2.0.0 + fast-equals: ^4.0.3 prop-types: ^15.8.1 - react-draggable: ^4.0.0 - react-resizable: ^3.0.4 + react-draggable: ^4.4.5 + react-resizable: ^3.0.5 + resize-observer-polyfill: ^1.5.1 peerDependencies: react: ">= 16.3.0" react-dom: ">= 16.3.0" - checksum: f56c8c452acd9588edf1dc6996a4ea14d9f669d77f6b2ebd50146eaeeb9325c83f5ca44b66bac2b8c24f9cb2ec7ed49396350435991255c3b31e21b8a2e3d243 + checksum: a052d38c290b18e1c513a3b939757c239887009b7f175814b472d007708962870960d3b20d4a15dbdeb955ac55434ce6c5e5e5f8c850cfbdcc90f75c318f1d58 languageName: node linkType: hard @@ -37020,7 +37021,7 @@ __metadata: languageName: node linkType: hard -"react-resizable@npm:^3.0.4": +"react-resizable@npm:^3.0.4, react-resizable@npm:^3.0.5": version: 3.0.5 resolution: "react-resizable@npm:3.0.5" dependencies: From 96b25bbce70a25cc116e27209ab15f17c68d87ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 5 Oct 2023 16:52:56 +0200 Subject: [PATCH 59/95] disable the graph test that breaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../CatalogGraphPage.test.tsx | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx index 2e215e75f9..f2360b678d 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx @@ -34,7 +34,44 @@ jest.mock('react-router-dom', () => ({ useNavigate: () => navigate, })); -describe('', () => { +/* + The tests in this file have been disabled for the following error: + + TypeError: Cannot read properties of null (reading 'document') + + at document (../../../node_modules/d3-drag/src/nodrag.js:5:19) + at SVGSVGElement.mousedowned (../../../node_modules/d3-zoom/src/zoom.js:279:16) + at SVGSVGElement.call (../../../node_modules/d3-selection/src/selection/on.js:3:14) + at SVGSVGElement.callTheUserObjectsOperation (../../../node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30) + at innerInvokeEventListeners (../../../node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:350:25) + at invokeEventListeners (../../../node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:286:3) + at SVGElementImpl._dispatch (../../../node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:233:9) + at SVGElementImpl.dispatchEvent (../../../node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:104:17) + at SVGElement.dispatchEvent (../../../node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:241:34) + at ../../../node_modules/@testing-library/user-event/dist/cjs/event/dispatchEvent.js:47:43 + at cb (../../../node_modules/@testing-library/react/dist/pure.js:66:16) + at batchedUpdates$1 (../../../node_modules/react-dom/cjs/react-dom.development.js:22380:12) + at act (../../../node_modules/react-dom/cjs/react-dom-test-utils.development.js:1042:14) + at Object.eventWrapper (../../../node_modules/@testing-library/react/dist/pure.js:65:26) + at Object.wrapEvent (../../../node_modules/@testing-library/user-event/dist/cjs/event/wrapEvent.js:29:24) + at Object.dispatchEvent (../../../node_modules/@testing-library/user-event/dist/cjs/event/dispatchEvent.js:47:22) + at Object.dispatchUIEvent (../../../node_modules/@testing-library/user-event/dist/cjs/event/dispatchEvent.js:24:26) + at Mouse.down (../../../node_modules/@testing-library/user-event/dist/cjs/system/pointer/mouse.js:83:34) + at PointerHost.press (../../../node_modules/@testing-library/user-event/dist/cjs/system/pointer/index.js:39:24) + at pointerAction (../../../node_modules/@testing-library/user-event/dist/cjs/pointer/index.js:59:43) + at Object.pointer (../../../node_modules/@testing-library/user-event/dist/cjs/pointer/index.js:35:15) + at ../../../node_modules/@testing-library/react/dist/pure.js:59:16 + + This has started happening after upgrading to the later version of @testing-library/user-event, and the d3-drag library + where it happens seems to be unmaintained. Skipping for now. + + https://github.com/d3/d3-drag/issues/79#issuecomment-1631409544 + + https://github.com/d3/d3-drag/issues/89 +*/ + +// eslint-disable-next-line jest/no-disabled-tests +describe.skip('', () => { let wrapper: JSX.Element; const entityC = { apiVersion: 'a', From 3a6a6cb5a941f7aef40129cb8e8bd81290c7a0ab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 09:10:05 +0000 Subject: [PATCH 60/95] fix(deps): update dependency react-hook-form to v7.47.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 42353604ef..d48381e365 100644 --- a/yarn.lock +++ b/yarn.lock @@ -36842,11 +36842,11 @@ __metadata: linkType: hard "react-hook-form@npm:^7.12.2, react-hook-form@npm:^7.13.0": - version: 7.46.1 - resolution: "react-hook-form@npm:7.46.1" + version: 7.47.0 + resolution: "react-hook-form@npm:7.47.0" peerDependencies: react: ^16.8.0 || ^17 || ^18 - checksum: 9c11ba454ce5b2a16e7499f2ca710e0c0cff227f39689b8e7985902f27f99bfc7a2c49f145ef228528764cf8286bf6101fc8a0f5094ce652eb31cab1684518d1 + checksum: dec192fec9c54e436f9e47008635dd7849b6b119ed477a9b0cd491367a0b2ced3427cd937febfb245e1cb7578c863917181d903eff4519c2787bf713ec7d3426 languageName: node linkType: hard From b58a5fcbd08f95644c97624c9b6a87aba4405bdc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Oct 2023 10:15:54 +0000 Subject: [PATCH 61/95] fix(deps): update dependency sass to v1.69.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- microsite/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 81d3ccfb42..80b87c72ce 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -10317,15 +10317,15 @@ __metadata: linkType: hard "sass@npm:^1.57.1": - version: 1.66.1 - resolution: "sass@npm:1.66.1" + version: 1.69.0 + resolution: "sass@npm:1.69.0" dependencies: chokidar: ">=3.0.0 <4.0.0" immutable: ^4.0.0 source-map-js: ">=0.6.2 <2.0.0" bin: sass: sass.js - checksum: 74fc11d0fcd5e16c5331b57dd59865705a299c64e89f2b99646869caeb011dc8d0b6144a6c74a90c264e9ef70654207dbf44fc9b7e3393f8bd14809b904c8a52 + checksum: eabea31ea3b1dd529c7eff345c8b6468afe6ab8011bd4f95caa2cffb8fb115cc055ea21de425be6197f7ed22516f5652eccb98d315d592ea152ada553f964b8a languageName: node linkType: hard From 2929f856cf3e75713ddc19fa8844aed089f84da2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 6 Oct 2023 12:46:55 +0200 Subject: [PATCH 62/95] config-loader: restore cwd after tests Signed-off-by: Patrik Oldsberg --- packages/config-loader/src/schema/collect.test.ts | 6 ++++++ packages/config-loader/src/schema/load.test.ts | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/packages/config-loader/src/schema/collect.test.ts b/packages/config-loader/src/schema/collect.test.ts index f70220011a..f513253619 100644 --- a/packages/config-loader/src/schema/collect.test.ts +++ b/packages/config-loader/src/schema/collect.test.ts @@ -18,6 +18,12 @@ import { createMockDirectory } from '@backstage/backend-test-utils'; import { collectConfigSchemas } from './collect'; import path from 'path'; +// cwd must be restored +const origDir = process.cwd(); +afterAll(() => { + process.chdir(origDir); +}); + const mockSchema = { type: 'object', properties: { diff --git a/packages/config-loader/src/schema/load.test.ts b/packages/config-loader/src/schema/load.test.ts index 85cc9fde5f..525565f3d5 100644 --- a/packages/config-loader/src/schema/load.test.ts +++ b/packages/config-loader/src/schema/load.test.ts @@ -17,6 +17,12 @@ import { createMockDirectory } from '@backstage/backend-test-utils'; import { loadConfigSchema } from './load'; +// cwd must be restored +const origDir = process.cwd(); +afterAll(() => { + process.chdir(origDir); +}); + describe('loadConfigSchema', () => { const mockDir = createMockDirectory(); From d234b41853fc646a07051736a99d0096b75aa98e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 13:36:48 +0200 Subject: [PATCH 63/95] backend-app-api: refactor test to remove mock-fs Signed-off-by: Patrik Oldsberg --- yarn.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 8846ec0813..70690186aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3603,7 +3603,6 @@ __metadata: chokidar: ^3.5.3 express: ^4.17.1 lodash: ^4.17.21 - mock-fs: ^5.2.0 wait-for-expect: ^3.0.2 winston: ^3.2.1 languageName: unknown From c45f587b1abf0d15f4eb21838c9870c41d7023bd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 14:07:52 +0200 Subject: [PATCH 64/95] backend-plugin-manager: refactor test to remove mock-fs Signed-off-by: Patrik Oldsberg --- packages/backend-plugin-manager/package.json | 1 - .../src/manager/plugin-manager.test.ts | 53 ++++++++++--------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/packages/backend-plugin-manager/package.json b/packages/backend-plugin-manager/package.json index 67697981a1..33675dda1f 100644 --- a/packages/backend-plugin-manager/package.json +++ b/packages/backend-plugin-manager/package.json @@ -55,7 +55,6 @@ "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/config-loader": "workspace:^", - "mock-fs": "^5.2.0", "wait-for-expect": "^3.0.2" }, "files": [ diff --git a/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts b/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts index 093776e5b0..f356163dea 100644 --- a/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts +++ b/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts @@ -20,10 +20,9 @@ import { coreServices, createServiceFactory, } from '@backstage/backend-plugin-api'; -import mockFs, { directory, symlink } from 'mock-fs'; import * as path from 'path'; import * as url from 'url'; - +import fs from 'fs'; import { BackendDynamicPlugin, BaseDynamicPlugin, @@ -43,11 +42,13 @@ import { ConfigSources } from '@backstage/config-loader'; import { Logs, MockedLogger, LogContent } from '../__testUtils__/testUtils'; import { PluginScanner } from '../scanner/plugin-scanner'; import { findPaths } from '@backstage/cli-common'; +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('backend-plugin-manager', () => { + const mockDir = createMockDirectory(); + describe('loadPlugins', () => { afterEach(() => { - mockFs.restore(); jest.resetModules(); }); @@ -56,7 +57,7 @@ describe('backend-plugin-manager', () => { packageManifest: ScannedPluginManifest; indexFile?: { retativePath: string[]; - content?: string; + content: string; }; expectedLogs?(location: URL): { errors?: LogContent[]; @@ -354,17 +355,13 @@ describe('backend-plugin-manager', () => { }, ])('$name', async (tc: TestCase): Promise => { const plugin: ScannedPluginPackage = { - location: url.pathToFileURL( - path.resolve(`/node_modules/jest-tests/${randomUUID()}`), - ), + location: url.pathToFileURL(mockDir.resolve(randomUUID())), manifest: tc.packageManifest, }; const mockedFiles = { [path.join(url.fileURLToPath(plugin.location), 'package.json')]: - mockFs.file({ - content: JSON.stringify(plugin), - }), + JSON.stringify(plugin), }; if (tc.indexFile) { mockedFiles[ @@ -372,11 +369,9 @@ describe('backend-plugin-manager', () => { url.fileURLToPath(plugin.location), ...tc.indexFile.retativePath, ) - ] = mockFs.file({ - content: tc.indexFile.content, - }); + ] = tc.indexFile.content; } - mockFs(mockedFiles); + mockDir.setContent(mockedFiles); const logger = new MockedLogger(); const pluginManager = new (PluginManager as any)(logger, [plugin], { @@ -440,8 +435,11 @@ describe('backend-plugin-manager', () => { }); describe('dynamicPluginsServiceFactory', () => { + const otherMockDir = createMockDirectory(); + afterEach(() => { - mockFs.restore(); + mockDir.clear(); + otherMockDir.clear(); jest.resetModules(); }); @@ -449,15 +447,20 @@ describe('backend-plugin-manager', () => { const logger = new MockedLogger(); const rootLogger = new MockedLogger(); - mockFs({ - [findPaths(__dirname).resolveTargetRoot('package.json')]: mockFs.load( + mockDir.setContent({ + 'package.json': fs.readFileSync( findPaths(__dirname).resolveTargetRoot('package.json'), ), - '/somewhere/dynamic-plugins-root/a-dynamic-plugin': symlink({ - path: '/somewhere-else/a-dynamic-plugin', - }), - '/somewhere-else/a-dynamic-plugin': directory({}), + 'dynamic-plugins-root': {}, }); + otherMockDir.setContent({ + 'a-dynamic-plugin': {}, + }); + + fs.symlinkSync( + otherMockDir.resolve('a-dynamic-plugin'), + mockDir.resolve('dynamic-plugins-root/a-dynamic-plugin'), + ); const fromConfigSpier = jest.spyOn(PluginManager, 'fromConfig'); const applyConfigSpier = jest @@ -468,7 +471,7 @@ describe('backend-plugin-manager', () => { .mockImplementation(async () => [ { location: url.pathToFileURL( - path.resolve('/somewhere/dynamic-plugins-root/a-dynamic-plugin'), + mockDir.resolve('dynamic-plugins-root/a-dynamic-plugin'), ), manifest: { name: 'test', @@ -533,11 +536,11 @@ describe('backend-plugin-manager', () => { expect(scanRootSpier).toHaveBeenCalled(); expect(mockedModuleLoader.bootstrap).toHaveBeenCalledWith( findPaths(__dirname).targetRoot, - [path.resolve('/somewhere-else/a-dynamic-plugin')], + [fs.realpathSync(otherMockDir.resolve('a-dynamic-plugin'))], ); expect(mockedModuleLoader.load).toHaveBeenCalledWith( - path.resolve( - '/somewhere/dynamic-plugins-root/a-dynamic-plugin/dist/index.cjs.js', + mockDir.resolve( + 'dynamic-plugins-root/a-dynamic-plugin/dist/index.cjs.js', ), ); }); From d45375bccf67b59b908d17d590e5e1b18673e1be Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 15:06:42 +0200 Subject: [PATCH 65/95] backend-plugin-manager: migrate plugin-scanner tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../src/scanner/plugin-scanner.test.ts | 493 ++++++++---------- 1 file changed, 204 insertions(+), 289 deletions(-) diff --git a/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts b/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts index 6a36a377f5..04ce69ec98 100644 --- a/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts +++ b/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts @@ -15,13 +15,16 @@ */ import { PluginScanner } from './plugin-scanner'; -import mockFs from 'mock-fs'; import { JsonObject } from '@backstage/types'; import { Logs, MockedLogger } from '../__testUtils__/testUtils'; import { ConfigReader } from '@backstage/config'; import path from 'path'; +import fs from 'fs'; import * as url from 'url'; import { ScannedPluginPackage } from './types'; +import { createMockDirectory } from '@backstage/backend-test-utils'; + +const mockDir = createMockDirectory(); describe('plugin-scanner', () => { const env = process.env; @@ -30,7 +33,7 @@ describe('plugin-scanner', () => { }); afterEach(() => { - mockFs.restore(); + mockDir.clear(); process.env = env; }); @@ -61,85 +64,77 @@ describe('plugin-scanner', () => { }, { name: 'valid config with relative root directory path', - backstageRoot: '/backstageRoot', + backstageRoot: mockDir.resolve('backstageRoot'), fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory(), - }, - }), + backstageRoot: { + 'dist-dynamic': {}, + }, }, config: { dynamicPlugins: { rootDirectory: 'dist-dynamic', }, }, - expectedRootDirectory: path.resolve('/backstageRoot/dist-dynamic'), + expectedRootDirectory: mockDir.resolve('backstageRoot/dist-dynamic'), }, { name: 'valid config with absolute root directory path inside the backstage root', - backstageRoot: '/backstageRoot', + backstageRoot: mockDir.resolve('backstageRoot'), fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory(), - }, - }), + backstageRoot: { + 'dist-dynamic': {}, + }, }, config: { dynamicPlugins: { - rootDirectory: '/backstageRoot/dist-dynamic', + rootDirectory: mockDir.resolve('backstageRoot/dist-dynamic'), }, }, - expectedRootDirectory: path.resolve('/backstageRoot/dist-dynamic'), + expectedRootDirectory: mockDir.resolve('backstageRoot/dist-dynamic'), }, { name: 'valid config with absolute root directory path outside the backstage root', - backstageRoot: '/backstageRoot', + backstageRoot: mockDir.resolve('backstageRoot'), fileSystem: { - '/somewhere': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory(), - }, - }), + somewhere: { + 'dist-dynamic': {}, + }, }, config: { dynamicPlugins: { - rootDirectory: '/somewhere/dist-dynamic', + rootDirectory: mockDir.resolve('somewhere/dist-dynamic'), }, }, - expectedError: `Dynamic plugins under '${path.resolve( - '/somewhere/dist-dynamic', - )}' cannot access backstage modules in '${path.resolve( - '/backstageRoot/node_modules', + expectedError: `Dynamic plugins under '${mockDir.resolve( + 'somewhere/dist-dynamic', + )}' cannot access backstage modules in '${mockDir.resolve( + 'backstageRoot/node_modules', )}'. -Please add '${path.resolve( - '/backstageRoot/node_modules', +Please add '${mockDir.resolve( + 'backstageRoot/node_modules', )}' to the 'NODE_PATH' when running the backstage backend.`, }, { name: 'valid config with absolute root directory path outside the backstage root but with backstage root included in NODE_PATH', - backstageRoot: '/backstageRoot', + backstageRoot: mockDir.resolve('backstageRoot'), fileSystem: { - '/somewhere': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory(), - }, - }), + somewhere: { + 'dist-dynamic': {}, + }, }, config: { dynamicPlugins: { - rootDirectory: '/somewhere/dist-dynamic', + rootDirectory: mockDir.resolve('somewhere/dist-dynamic'), }, }, environment: { - NODE_PATH: `${path.resolve('/somewhere-else')}${ + NODE_PATH: `${mockDir.resolve('somewhere-else')}${ path.delimiter - }${path.resolve('/backstageRoot', 'node_modules')}${ + }${mockDir.resolve('backstageRoot', 'node_modules')}${ path.delimiter - }${path.resolve('anywhere-else')}`, + }${mockDir.resolve('anywhere-else')}`, }, - expectedRootDirectory: path.resolve('/somewhere/dist-dynamic'), + expectedRootDirectory: mockDir.resolve('somewhere/dist-dynamic'), }, { name: 'invalid config: dynamicPlugins not an object', @@ -186,13 +181,11 @@ Please add '${path.resolve( }, { name: 'valid config pointing to a file instead of a directory', - backstageRoot: '/backstageRoot', + backstageRoot: mockDir.resolve('backstageRoot'), fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.file(), - }, - }), + backstageRoot: { + 'dist-dynamic': '', + }, }, config: { dynamicPlugins: { @@ -218,7 +211,7 @@ Please add '${path.resolve( ); } if (tc.fileSystem) { - mockFs(tc.fileSystem); + mockDir.setContent(tc.fileSystem); } if (tc.expectedError) { /* eslint-disable-next-line jest/no-conditional-expect */ @@ -241,6 +234,7 @@ Please add '${path.resolve( name: string; preferAlpha?: boolean; fileSystem?: any; + symlinks?: { source: string; target: string }[]; expectedLogs?: Logs; expectedPluginPackages?: ScannedPluginPackage[]; expectedError?: string; @@ -261,31 +255,23 @@ Please add '${path.resolve( { name: 'manifest found in directory', fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - main: 'dist/index.cjs.js', - backstage: { role: 'backend-plugin' }, - }), - }), - }, - }), - }, - }), + backstageRoot: { + 'dist-dynamic': { + 'test-backend-plugin': { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }), + }, }, - }), + }, }, expectedPluginPackages: [ { location: url.pathToFileURL( - path.resolve('/backstageRoot/dist-dynamic/test-backend-plugin'), + mockDir.resolve('backstageRoot/dist-dynamic/test-backend-plugin'), ), manifest: { name: 'test-backend-plugin-dynamic', @@ -299,38 +285,34 @@ Please add '${path.resolve( { name: 'backend plugin found in symlink', fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.symlink({ - path: '/somewhere-else/test-backend-plugin-target', - }), - }, + backstageRoot: { + 'dist-dynamic': {}, + }, + 'somewhere-else': { + 'test-backend-plugin-target': { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, }), }, - }), - '/somewhere-else': mockFs.directory({ - items: { - 'test-backend-plugin-target': mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - main: 'dist/index.cjs.js', - backstage: { role: 'backend-plugin' }, - }), - }), - }, - }), - }, - }), + }, }, + symlinks: [ + { + source: mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin', + ), + target: mockDir.resolve( + 'somewhere-else/test-backend-plugin-target', + ), + }, + ], expectedPluginPackages: [ { location: url.pathToFileURL( - path.resolve('/backstageRoot/dist-dynamic/test-backend-plugin'), + mockDir.resolve('backstageRoot/dist-dynamic/test-backend-plugin'), ), manifest: { name: 'test-backend-plugin-dynamic', @@ -344,22 +326,18 @@ Please add '${path.resolve( { name: 'ignored folder child: not a directory', fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.file({}), - }, - }), + backstageRoot: { + 'dist-dynamic': { + 'test-backend-plugin': '', }, - }), + }, }, expectedPluginPackages: [], expectedLogs: { infos: [ { - message: `skipping '${path.resolve( - '/backstageRoot/dist-dynamic/test-backend-plugin', + message: `skipping '${mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin', )}' since it is not a directory`, }, ], @@ -368,29 +346,29 @@ Please add '${path.resolve( { name: 'ignored folder child symlink: target is not a directory', fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.symlink({ - path: '/somewhere-else/test-backend-plugin-target', - }), - }, - }), - }, - }), - '/somewhere-else': mockFs.directory({ - items: { - 'test-backend-plugin-target': mockFs.file({}), - }, - }), + backstageRoot: { + 'dist-dynamic': {}, + }, + 'somewhere-else': { + 'test-backend-plugin-target': '', + }, }, + symlinks: [ + { + source: mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin', + ), + target: mockDir.resolve( + 'somewhere-else/test-backend-plugin-target', + ), + }, + ], expectedPluginPackages: [], expectedLogs: { infos: [ { - message: `skipping '${path.resolve( - '/backstageRoot/dist-dynamic/test-backend-plugin', + message: `skipping '${mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin', )}' since it is not a directory`, }, ], @@ -400,42 +378,30 @@ Please add '${path.resolve( name: 'alpha manifest available but not preferred', preferAlpha: false, fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - main: 'dist/index.cjs.js', - backstage: { role: 'backend-plugin' }, - }), - }), - alpha: mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - main: '../dist/alpha.cjs.js', - }), - }), - }, - }), - }, + backstageRoot: { + 'dist-dynamic': { + 'test-backend-plugin': { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }), + alpha: { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: '../dist/alpha.cjs.js', }), }, - }), + }, }, - }), + }, }, expectedPluginPackages: [ { location: url.pathToFileURL( - path.resolve('/backstageRoot/dist-dynamic/test-backend-plugin'), + mockDir.resolve('backstageRoot/dist-dynamic/test-backend-plugin'), ), manifest: { name: 'test-backend-plugin-dynamic', @@ -450,43 +416,31 @@ Please add '${path.resolve( name: 'alpha manifest preferred and found in directory', preferAlpha: true, fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - main: 'dist/index.cjs.js', - backstage: { role: 'backend-plugin' }, - }), - }), - alpha: mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - main: '../dist/alpha.cjs.js', - }), - }), - }, - }), - }, + backstageRoot: { + 'dist-dynamic': { + 'test-backend-plugin': { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }), + alpha: { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: '../dist/alpha.cjs.js', }), }, - }), + }, }, - }), + }, }, expectedPluginPackages: [ { location: url.pathToFileURL( - path.resolve( - '/backstageRoot/dist-dynamic/test-backend-plugin/alpha', + mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin/alpha', ), ), manifest: { @@ -502,32 +456,24 @@ Please add '${path.resolve( name: 'alpha manifest preferred but skipped because not a directory', preferAlpha: true, fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - main: 'dist/index.cjs.js', - backstage: { role: 'backend-plugin' }, - }), - }), - alpha: mockFs.file({}), - }, - }), - }, - }), + backstageRoot: { + 'dist-dynamic': { + 'test-backend-plugin': { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }), + alpha: '', + }, }, - }), + }, }, expectedPluginPackages: [ { location: url.pathToFileURL( - path.resolve('/backstageRoot/dist-dynamic/test-backend-plugin'), + mockDir.resolve('backstageRoot/dist-dynamic/test-backend-plugin'), ), manifest: { name: 'test-backend-plugin-dynamic', @@ -540,8 +486,8 @@ Please add '${path.resolve( expectedLogs: { warns: [ { - message: `skipping '${path.resolve( - '/backstageRoot/dist-dynamic/test-backend-plugin/alpha', + message: `skipping '${mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin/alpha', )}' since it is not a directory`, }, ], @@ -551,40 +497,28 @@ Please add '${path.resolve( name: 'invalid alpha package.json', preferAlpha: true, fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - main: 'dist/index.cjs.js', - backstage: { role: 'backend-plugin' }, - }), - }), - alpha: mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: "invalid json content, 1, '", - }), - }, - }), - }, - }), + backstageRoot: { + 'dist-dynamic': { + 'test-backend-plugin': { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + backstage: { role: 'backend-plugin' }, + }), + alpha: { + 'package.json': "invalid json content, 1, '", }, - }), + }, }, - }), + }, }, expectedPluginPackages: [], expectedLogs: { errors: [ { - message: `failed to load dynamic plugin manifest from '${path.resolve( - '/backstageRoot/dist-dynamic/test-backend-plugin/alpha', + message: `failed to load dynamic plugin manifest from '${mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin/alpha', )}'`, meta: { name: 'SyntaxError', @@ -597,28 +531,20 @@ Please add '${path.resolve( { name: 'invalid package.json', fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: "invalid json content, 1, '", - }), - }, - }), - }, - }), + backstageRoot: { + 'dist-dynamic': { + 'test-backend-plugin': { + 'package.json': "invalid json content, 1, '", + }, }, - }), + }, }, expectedPluginPackages: [], expectedLogs: { errors: [ { - message: `failed to load dynamic plugin manifest from '${path.resolve( - '/backstageRoot/dist-dynamic/test-backend-plugin', + message: `failed to load dynamic plugin manifest from '${mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin', )}'`, meta: { name: 'SyntaxError', @@ -631,32 +557,24 @@ Please add '${path.resolve( { name: 'missing backstage role in package.json', fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - main: 'dist/index.cjs.js', - }), - }), - }, - }), - }, - }), + backstageRoot: { + 'dist-dynamic': { + 'test-backend-plugin': { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + main: 'dist/index.cjs.js', + }), + }, }, - }), + }, }, expectedPluginPackages: [], expectedLogs: { errors: [ { - message: `failed to load dynamic plugin manifest from '${path.resolve( - '/backstageRoot/dist-dynamic/test-backend-plugin', + message: `failed to load dynamic plugin manifest from '${mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin', )}'`, meta: { name: 'Error', @@ -669,32 +587,24 @@ Please add '${path.resolve( { name: 'missing main field in package.json', fileSystem: { - '/backstageRoot': mockFs.directory({ - items: { - 'dist-dynamic': mockFs.directory({ - items: { - 'test-backend-plugin': mockFs.directory({ - items: { - 'package.json': mockFs.file({ - content: JSON.stringify({ - name: 'test-backend-plugin-dynamic', - version: '0.0.0', - backstage: { role: 'backend-plugin' }, - }), - }), - }, - }), - }, - }), + backstageRoot: { + 'dist-dynamic': { + 'test-backend-plugin': { + 'package.json': JSON.stringify({ + name: 'test-backend-plugin-dynamic', + version: '0.0.0', + backstage: { role: 'backend-plugin' }, + }), + }, }, - }), + }, }, expectedPluginPackages: [], expectedLogs: { errors: [ { - message: `failed to load dynamic plugin manifest from '${path.resolve( - '/backstageRoot/dist-dynamic/test-backend-plugin', + message: `failed to load dynamic plugin manifest from '${mockDir.resolve( + 'backstageRoot/dist-dynamic/test-backend-plugin', )}'`, meta: { name: 'Error', @@ -706,7 +616,7 @@ Please add '${path.resolve( }, ])('$name', async (tc: TestCase): Promise => { const logger = new MockedLogger(); - const backstageRoot = '/backstageRoot'; + const backstageRoot = mockDir.resolve('backstageRoot'); async function toTest(): Promise { const pluginScanner = new PluginScanner( new ConfigReader( @@ -725,7 +635,12 @@ Please add '${path.resolve( return await pluginScanner.scanRoot(); } if (tc.fileSystem) { - mockFs(tc.fileSystem); + mockDir.setContent(tc.fileSystem); + } + if (tc.symlinks) { + for (const { source, target } of tc.symlinks) { + fs.symlinkSync(target, source); + } } if (tc.expectedError) { /* eslint-disable-next-line jest/no-conditional-expect */ From 9358e965eccee64d0e80b7e06991824ccf697378 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 17:25:01 +0200 Subject: [PATCH 66/95] scaffolder-backend: refactor template action tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../actions/builtin/fetch/template.test.ts | 103 +++++++----------- 1 file changed, 41 insertions(+), 62 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts index 95f8c7b954..727eb5ce0d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts @@ -19,10 +19,8 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { return { ...actual, fetchContents: jest.fn() }; }); -import os from 'os'; -import { join as joinPath, sep as pathSep } from 'path'; +import { join as joinPath, resolve as resolvePath, sep as pathSep } from 'path'; import fs from 'fs-extra'; -import mockFs from 'mock-fs'; import { getVoidLogger, resolvePackagePath, @@ -36,6 +34,7 @@ import { ActionContext, TemplateAction, } from '@backstage/plugin-scaffolder-node'; +import { createMockDirectory } from '@backstage/backend-test-utils'; type FetchTemplateInput = ReturnType< typeof createFetchTemplateAction @@ -43,16 +42,6 @@ type FetchTemplateInput = ReturnType< ? U : never; -const realFiles = Object.fromEntries( - [ - resolvePackagePath( - '@backstage/plugin-scaffolder-backend', - 'assets', - 'nunjucks.js.txt', - ), - ].map(k => [k, mockFs.load(k)]), -); - const aBinaryFile = fs.readFileSync( resolvePackagePath( '@backstage/plugin-scaffolder-backend', @@ -67,14 +56,8 @@ const mockFetchContents = fetchContents as jest.MockedFunction< describe('fetch:template', () => { let action: TemplateAction; - const workspacePath = os.tmpdir(); - const createTemporaryDirectory: jest.MockedFunction< - ActionContext['createTemporaryDirectory'] - > = jest.fn(() => - Promise.resolve( - joinPath(workspacePath, `${createTemporaryDirectory.mock.calls.length}`), - ), - ); + const mockDir = createMockDirectory(); + const workspacePath = mockDir.resolve('workspace'); const logger = getVoidLogger(); @@ -95,24 +78,21 @@ describe('fetch:template', () => { logStream: new PassThrough(), logger, workspacePath, - createTemporaryDirectory, + async createTemporaryDirectory() { + return fs.mkdtemp(mockDir.resolve('tmp-')); + }, }); beforeEach(() => { - mockFs({ - ...realFiles, + mockDir.setContent({ + workspace: {}, }); - action = createFetchTemplateAction({ reader: Symbol('UrlReader') as unknown as UrlReader, integrations: Symbol('Integrations') as unknown as ScmIntegrations, }); }); - afterEach(() => { - mockFs.restore(); - }); - it(`returns a TemplateAction with the id 'fetch:template'`, () => { expect(action.id).toEqual('fetch:template'); }); @@ -190,8 +170,7 @@ describe('fetch:template', () => { }); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [outputPath]: { '{% if values.showDummyFile %}dummy-file.txt{% else %}{% endif %}': 'dummy file', @@ -282,13 +261,8 @@ describe('fetch:template', () => { }); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [outputPath]: { - 'an-executable.sh': mockFs.file({ - content: '#!/usr/bin/env bash', - mode: parseInt('100755', 8), - }), 'empty-dir-${{ values.count }}': {}, 'static.txt': 'static content', '${{ values.name }}.txt': 'static content', @@ -298,15 +272,27 @@ describe('fetch:template', () => { }, '.${{ values.name }}': '${{ values.itemList | dump }}', 'a-binary-file.png': aBinaryFile, - symlink: mockFs.symlink({ - path: 'a-binary-file.png', - }), - brokenSymlink: mockFs.symlink({ - path: './not-a-real-file.txt', - }), }, }); + fs.writeFileSync( + resolvePath(outputPath, 'an-executable.sh'), + '#!/usr/bin/env bash', + { + encoding: 'utf-8', + mode: parseInt('100755', 8), + }, + ); + + fs.symlinkSync( + 'a-binary-file.png', + resolvePath(outputPath, 'symlink'), + ); + fs.symlinkSync( + './not-a-real-file.txt', + resolvePath(outputPath, 'brokenSymlink'), + ); + return Promise.resolve(); }); @@ -378,7 +364,11 @@ describe('fetch:template', () => { await expect( fs.realpath(`${workspacePath}/target/symlink`), - ).resolves.toBe(joinPath(workspacePath, 'target', 'a-binary-file.png')); + ).resolves.toBe( + fs.realpathSync( + joinPath(workspacePath, 'target', 'a-binary-file.png'), + ), + ); }); it('copies broken symlinks as-is without processing them', async () => { @@ -408,8 +398,7 @@ describe('fetch:template', () => { }); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [outputPath]: { processed: { 'templated-content-${{ values.name }}.txt': '${{ values.count }}', @@ -458,8 +447,7 @@ describe('fetch:template', () => { }); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [outputPath]: { processed: { 'templated-content-${{ values.name }}.txt': '${{ values.count }}', @@ -509,8 +497,7 @@ describe('fetch:template', () => { }); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [outputPath]: { '{{ cookiecutter.name }}.txt': 'static content', subdir: { @@ -564,8 +551,7 @@ describe('fetch:template', () => { }); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [outputPath]: { 'empty-dir-${{ values.count }}': {}, 'static.txt': 'static content', @@ -646,8 +632,7 @@ describe('fetch:template', () => { }); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [outputPath]: { '${{ values.name }}.njk': '${{ values.name }}: ${{ values.count }}', '${{ values.name }}.txt.jinja2': @@ -687,8 +672,7 @@ describe('fetch:template', () => { }); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [joinPath(workspacePath, 'target')]: { 'static-content.txt': 'static-content', }, @@ -703,10 +687,6 @@ describe('fetch:template', () => { await action.handler(context); }); - afterEach(() => { - mockFs.restore(); - }); - it('overwrites existing file', async () => { await expect( fs.readFile(`${workspacePath}/target/static-content.txt`, 'utf-8'), @@ -728,8 +708,7 @@ describe('fetch:template', () => { }); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [joinPath(workspacePath, 'target')]: { 'static-content.txt': 'static-content', }, From 688a69ad614b864d51106287ecad8df5506aae05 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 17:26:51 +0200 Subject: [PATCH 67/95] scaffolder-backend: refactor rename action tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../actions/builtin/filesystem/rename.test.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts index 6804c9f5a9..d37e967f43 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts @@ -14,20 +14,19 @@ * limitations under the License. */ -import * as os from 'os'; -import mockFs from 'mock-fs'; import { resolve as resolvePath } from 'path'; import { createFilesystemRenameAction } from './rename'; import { getVoidLogger } from '@backstage/backend-common'; import { PassThrough } from 'stream'; import fs from 'fs-extra'; - -const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; -const workspacePath = resolvePath(root, 'my-workspace'); +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('fs:rename', () => { const action = createFilesystemRenameAction(); + const mockDir = createMockDirectory(); + const workspacePath = resolvePath(mockDir.path, 'workspace'); + const mockInputFiles = [ { from: 'unit-test-a.js', @@ -56,7 +55,7 @@ describe('fs:rename', () => { beforeEach(() => { jest.restoreAllMocks(); - mockFs({ + mockDir.setContent({ [workspacePath]: { 'unit-test-a.js': 'hello', 'unit-test-b.js': 'world', @@ -68,10 +67,6 @@ describe('fs:rename', () => { }); }); - afterEach(() => { - mockFs.restore(); - }); - it('should throw an error when files is not an array', async () => { await expect( action.handler({ From 2d847c6ce93e2f2257d174f567e29add33d75d60 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 17:27:45 +0200 Subject: [PATCH 68/95] scaffolder-backend: refactor rename action example tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../builtin/filesystem/rename.examples.test.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts index 5139469959..cbb0fa0d0b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import * as os from 'os'; -import mockFs from 'mock-fs'; import { resolve as resolvePath } from 'path'; import { createFilesystemRenameAction } from './rename'; import { getVoidLogger } from '@backstage/backend-common'; @@ -23,15 +21,16 @@ import { PassThrough } from 'stream'; import fs from 'fs-extra'; import yaml from 'yaml'; import { examples } from './rename.examples'; - -const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; -const workspacePath = resolvePath(root, 'my-workspace'); +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('fs:rename examples', () => { const action = createFilesystemRenameAction(); const files: { from: string; to: string }[] = yaml.parse(examples[0].example) .steps[0].input.files; + const mockDir = createMockDirectory(); + const workspacePath = resolvePath(mockDir.path, 'workspace'); + const mockContext = { input: { files: files, @@ -46,7 +45,7 @@ describe('fs:rename examples', () => { beforeEach(() => { jest.restoreAllMocks(); - mockFs({ + mockDir.setContent({ [workspacePath]: { [files[0].from]: 'hello', [files[1].from]: 'world', @@ -59,10 +58,6 @@ describe('fs:rename examples', () => { }); }); - afterEach(() => { - mockFs.restore(); - }); - it('should call fs.move with the correct values', async () => { mockContext.input.files.forEach(file => { const filePath = resolvePath(workspacePath, file.from); From d95f8092fadf4317f3120259296e7ba2519e4f99 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:06:53 +0200 Subject: [PATCH 69/95] scaffolder-backend: refactor delete action tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../builtin/filesystem/delete.examples.test.ts | 15 +++++---------- .../actions/builtin/filesystem/delete.test.ts | 15 +++++---------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts index 10994e3824..023f049ebc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts @@ -18,18 +18,17 @@ import { createFilesystemDeleteAction } from './delete'; import { getVoidLogger } from '@backstage/backend-common'; import { PassThrough } from 'stream'; import { resolve as resolvePath } from 'path'; -import * as os from 'os'; -import mockFs from 'mock-fs'; import fs from 'fs-extra'; import yaml from 'yaml'; import { examples } from './delete.examples'; - -const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; -const workspacePath = resolvePath(root, 'my-workspace'); +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('fs:delete examples', () => { const action = createFilesystemDeleteAction(); + const mockDir = createMockDirectory(); + const workspacePath = resolvePath(mockDir.path, 'workspace'); + const files: string[] = yaml.parse(examples[0].example).steps[0].input.files; const mockContext = { @@ -46,7 +45,7 @@ describe('fs:delete examples', () => { beforeEach(() => { jest.restoreAllMocks(); - mockFs({ + mockDir.setContent({ [workspacePath]: { [files[0]]: 'hello', [files[1]]: 'world', @@ -57,10 +56,6 @@ describe('fs:delete examples', () => { }); }); - afterEach(() => { - mockFs.restore(); - }); - it('should call fs.rm with the correct values', async () => { files.forEach(file => { const filePath = resolvePath(workspacePath, file); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts index 713de5b0ad..2a4f45b862 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts @@ -14,20 +14,19 @@ * limitations under the License. */ -import * as os from 'os'; -import mockFs from 'mock-fs'; import { resolve as resolvePath } from 'path'; import { createFilesystemDeleteAction } from './delete'; import { getVoidLogger } from '@backstage/backend-common'; import { PassThrough } from 'stream'; import fs from 'fs-extra'; - -const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; -const workspacePath = resolvePath(root, 'my-workspace'); +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('fs:delete', () => { const action = createFilesystemDeleteAction(); + const mockDir = createMockDirectory(); + const workspacePath = resolvePath(mockDir.path, 'workspace'); + const mockContext = { input: { files: ['unit-test-a.js', 'unit-test-b.js'], @@ -42,7 +41,7 @@ describe('fs:delete', () => { beforeEach(() => { jest.restoreAllMocks(); - mockFs({ + mockDir.setContent({ [workspacePath]: { 'unit-test-a.js': 'hello', 'unit-test-b.js': 'world', @@ -53,10 +52,6 @@ describe('fs:delete', () => { }); }); - afterEach(() => { - mockFs.restore(); - }); - it('should throw an error when files is not an array', async () => { await expect( action.handler({ From d49bd45d69efc7b825a7d9bb0ac4a2be34315ba1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:29:15 +0200 Subject: [PATCH 70/95] backend-test-utils: add MockDirectory content callback + default file modes Signed-off-by: Patrik Oldsberg --- packages/backend-test-utils/api-report.md | 17 ++++++- .../src/filesystem/MockDirectory.test.ts | 14 ++++++ .../src/filesystem/MockDirectory.ts | 49 +++++++++++++++++-- .../src/filesystem/index.ts | 2 + 4 files changed, 77 insertions(+), 5 deletions(-) diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index a477f70298..ac012e2f69 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -54,9 +54,24 @@ export interface MockDirectory { // @public export type MockDirectoryContent = { - [name in string]: MockDirectoryContent | string | Buffer; + [name in string]: + | MockDirectoryContent + | string + | Buffer + | MockDirectoryContentCallback; }; +// @public +export type MockDirectoryContentCallback = ( + ctx: MockDirectoryContentCallbackContext, +) => void; + +// @public +export interface MockDirectoryContentCallbackContext { + path: string; + symlink(target: string): void; +} + // @public export interface MockDirectoryContentOptions { path?: string; diff --git a/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts b/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts index 8040d03f44..fd22f09432 100644 --- a/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts +++ b/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts @@ -119,6 +119,20 @@ describe('createMockDirectory', () => { }); }); + it('should be able to use callback for more detailed file system operations', () => { + mockDir.setContent({ + 'a.txt': 'a', + 'b.txt': ctx => ctx.symlink('./a.txt'), + 'c.txt': ctx => fs.copyFileSync(mockDir.resolve('a.txt'), ctx.path), + }); + + expect(mockDir.content()).toEqual({ + 'a.txt': 'a', + 'b.txt': 'a', + 'c.txt': 'a', + }); + }); + it('should read content from sub dirs', () => { mockDir.setContent({ 'a.txt': 'a', diff --git a/packages/backend-test-utils/src/filesystem/MockDirectory.ts b/packages/backend-test-utils/src/filesystem/MockDirectory.ts index 61eb4fce24..b066b9acd5 100644 --- a/packages/backend-test-utils/src/filesystem/MockDirectory.ts +++ b/packages/backend-test-utils/src/filesystem/MockDirectory.ts @@ -30,6 +30,28 @@ import { const tmpdirMarker = Symbol('os-tmpdir-mock'); +/** + * A context that allows for more advanced file system operations when writing mock directory content. + * + * @public + */ +export interface MockDirectoryContentCallbackContext { + /** Absolute path to the location of this piece of content on the filesystem */ + path: string; + + /** Creates a symbolic link at the current location */ + symlink(target: string): void; +} + +/** + * A callback that allows for more advanced file system operations when writing mock directory content. + * + * @public + */ +export type MockDirectoryContentCallback = ( + ctx: MockDirectoryContentCallbackContext, +) => void; + /** * The content of a mock directory represented by a nested object structure. * @@ -54,7 +76,11 @@ const tmpdirMarker = Symbol('os-tmpdir-mock'); * @public */ export type MockDirectoryContent = { - [name in string]: MockDirectoryContent | string | Buffer; + [name in string]: + | MockDirectoryContent + | string + | Buffer + | MockDirectoryContentCallback; }; /** @@ -178,6 +204,11 @@ type MockEntry = | { type: 'dir'; path: string; + } + | { + type: 'callback'; + path: string; + callback: MockDirectoryContentCallback; }; /** @internal */ @@ -214,10 +245,18 @@ class MockDirectoryImpl { } if (entry.type === 'dir') { - fs.ensureDirSync(fullPath, { mode: 0o777 }); + fs.ensureDirSync(fullPath); } else if (entry.type === 'file') { - fs.ensureDirSync(dirname(fullPath), { mode: 0o777 }); - fs.writeFileSync(fullPath, entry.content, { mode: 0o666 }); + fs.ensureDirSync(dirname(fullPath)); + fs.writeFileSync(fullPath, entry.content); + } else if (entry.type === 'callback') { + fs.ensureDirSync(dirname(fullPath)); + entry.callback({ + path: fullPath, + symlink(target: string) { + fs.symlinkSync(target, fullPath); + }, + }); } } } @@ -287,6 +326,8 @@ class MockDirectoryImpl { }); } else if (node instanceof Buffer) { entries.push({ type: 'file', path, content: node }); + } else if (typeof node === 'function') { + entries.push({ type: 'callback', path, callback: node }); } else { entries.push({ type: 'dir', path }); for (const [name, child] of Object.entries(node)) { diff --git a/packages/backend-test-utils/src/filesystem/index.ts b/packages/backend-test-utils/src/filesystem/index.ts index 0a4d8c7d00..e18b0b55b8 100644 --- a/packages/backend-test-utils/src/filesystem/index.ts +++ b/packages/backend-test-utils/src/filesystem/index.ts @@ -20,4 +20,6 @@ export { type MockDirectoryOptions, type MockDirectoryContent, type MockDirectoryContentOptions, + type MockDirectoryContentCallback, + type MockDirectoryContentCallbackContext, } from './MockDirectory'; From 3bc42d968aa7f06b01ab41757ce11996efbef5ec Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:32:28 +0200 Subject: [PATCH 71/95] scaffolder-backend: refactor file serialization tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../deserializeDirectoryContents.test.ts | 20 ++- .../files/serializeDirectoryContents.test.ts | 114 ++++++++---------- 2 files changed, 55 insertions(+), 79 deletions(-) diff --git a/plugins/scaffolder-backend/src/lib/files/deserializeDirectoryContents.test.ts b/plugins/scaffolder-backend/src/lib/files/deserializeDirectoryContents.test.ts index c272e04205..e374e0d713 100644 --- a/plugins/scaffolder-backend/src/lib/files/deserializeDirectoryContents.test.ts +++ b/plugins/scaffolder-backend/src/lib/files/deserializeDirectoryContents.test.ts @@ -14,29 +14,25 @@ * limitations under the License. */ -import mockFs from 'mock-fs'; +import { createMockDirectory } from '@backstage/backend-test-utils'; import { deserializeDirectoryContents } from './deserializeDirectoryContents'; import { serializeDirectoryContents } from './serializeDirectoryContents'; describe('deserializeDirectoryContents', () => { - beforeEach(() => { - mockFs({ - root: {}, - }); - }); + const mockDir = createMockDirectory(); - afterEach(() => { - mockFs.restore(); + beforeEach(() => { + mockDir.clear(); }); it('deserializes contents into a directory', async () => { - await deserializeDirectoryContents('root', [ + await deserializeDirectoryContents(mockDir.path, [ { path: 'a.txt', content: Buffer.from('a', 'utf8'), }, ]); - await expect(serializeDirectoryContents('root')).resolves.toEqual([ + await expect(serializeDirectoryContents(mockDir.path)).resolves.toEqual([ { path: 'a.txt', content: Buffer.from('a', 'utf8'), @@ -47,7 +43,7 @@ describe('deserializeDirectoryContents', () => { }); it('deserializes contents into a deep directory structure', async () => { - await deserializeDirectoryContents('root', [ + await deserializeDirectoryContents(mockDir.path, [ { path: 'a.txt', content: Buffer.from('a', 'utf8'), @@ -61,7 +57,7 @@ describe('deserializeDirectoryContents', () => { content: Buffer.from('c', 'utf8'), }, ]); - await expect(serializeDirectoryContents('root')).resolves.toEqual([ + await expect(serializeDirectoryContents(mockDir.path)).resolves.toEqual([ { path: 'a.txt', content: Buffer.from('a', 'utf8'), diff --git a/plugins/scaffolder-backend/src/lib/files/serializeDirectoryContents.test.ts b/plugins/scaffolder-backend/src/lib/files/serializeDirectoryContents.test.ts index a22ea85301..afc1b755ff 100644 --- a/plugins/scaffolder-backend/src/lib/files/serializeDirectoryContents.test.ts +++ b/plugins/scaffolder-backend/src/lib/files/serializeDirectoryContents.test.ts @@ -14,13 +14,11 @@ * limitations under the License. */ +import { createMockDirectory } from '@backstage/backend-test-utils'; import { serializeDirectoryContents } from './serializeDirectoryContents'; -import mockFs from 'mock-fs'; describe('serializeDirectoryContents', () => { - afterEach(() => { - mockFs.restore(); - }); + const mockDir = createMockDirectory(); it('should list files in this directory', async () => { await expect(serializeDirectoryContents(__dirname)).resolves.toEqual( @@ -54,25 +52,23 @@ describe('serializeDirectoryContents', () => { }); it('should list files in a mock directory', async () => { - mockFs({ - root: { - 'a.txt': 'a', - b: { - 'b1.txt': 'b1', - 'b2.txt': 'b2', - }, - c: { - c1: { - 'c11.txt': 'c11', - c11: { - 'c111.txt': 'c111', - }, + mockDir.setContent({ + 'a.txt': 'a', + b: { + 'b1.txt': 'b1', + 'b2.txt': 'b2', + }, + c: { + c1: { + 'c11.txt': 'c11', + c11: { + 'c111.txt': 'c111', }, }, }, }); - await expect(serializeDirectoryContents('root')).resolves.toEqual([ + await expect(serializeDirectoryContents(mockDir.path)).resolves.toEqual([ { path: 'a.txt', executable: false, @@ -107,16 +103,12 @@ describe('serializeDirectoryContents', () => { }); it('should ignore symlinked files', async () => { - mockFs({ - root: { - 'a.txt': 'some text', - sym: mockFs.symlink({ - path: './a.txt', - }), - }, + mockDir.setContent({ + 'a.txt': 'some text', + sym: ctx => ctx.symlink('./a.txt'), }); - await expect(serializeDirectoryContents('root')).resolves.toEqual([ + await expect(serializeDirectoryContents(mockDir.path)).resolves.toEqual([ { path: 'a.txt', executable: false, @@ -127,18 +119,14 @@ describe('serializeDirectoryContents', () => { }); it('should pick up broken symlinks', async () => { - mockFs({ - root: { - 'b.txt': mockFs.symlink({ - path: './a.txt', - }), - }, + mockDir.setContent({ + 'b.txt': ctx => ctx.symlink('./a.txt'), }); - await expect(serializeDirectoryContents('root')).resolves.toEqual([ + await expect(serializeDirectoryContents(mockDir.path)).resolves.toEqual([ { path: 'b.txt', - executable: false, + executable: true, symlink: true, content: Buffer.from('./a.txt', 'utf8'), }, @@ -146,19 +134,15 @@ describe('serializeDirectoryContents', () => { }); it('should ignore symlinked folder files', async () => { - mockFs({ - root: { - 'a.txt': 'some text', - linkme: { - 'b.txt': 'lols', - }, - sym: mockFs.symlink({ - path: './linkme', - }), + mockDir.setContent({ + 'a.txt': 'some text', + linkme: { + 'b.txt': 'lols', }, + sym: ctx => ctx.symlink('./linkme'), }); - await expect(serializeDirectoryContents('root')).resolves.toEqual([ + await expect(serializeDirectoryContents(mockDir.path)).resolves.toEqual([ { path: 'a.txt', executable: false, @@ -175,16 +159,14 @@ describe('serializeDirectoryContents', () => { }); it('should ignore gitignored files', async () => { - mockFs({ - root: { - '.gitignore': '*.txt', - 'a.txt': 'a', - 'a.log': 'a', - }, + mockDir.setContent({ + '.gitignore': '*.txt', + 'a.txt': 'a', + 'a.log': 'a', }); await expect( - serializeDirectoryContents('root', { + serializeDirectoryContents(mockDir.path, { gitignore: true, }), ).resolves.toEqual([ @@ -204,26 +186,24 @@ describe('serializeDirectoryContents', () => { }); it('should use custom glob patterns', async () => { - mockFs({ - root: { - '.a': 'a', - 'a.log': 'a', - 'a.txt': 'a', - b: { - '.b': 'b', - 'b.log': 'b', - 'b.txt': 'b', - }, - c: { - '.c': 'c', - 'c.log': 'c', - 'c.txt': 'c', - }, + mockDir.setContent({ + '.a': 'a', + 'a.log': 'a', + 'a.txt': 'a', + b: { + '.b': 'b', + 'b.log': 'b', + 'b.txt': 'b', + }, + c: { + '.c': 'c', + 'c.log': 'c', + 'c.txt': 'c', }, }); await expect( - serializeDirectoryContents('root', { + serializeDirectoryContents(mockDir.path, { gitignore: true, globPatterns: ['**/*.txt', '*/.?', '*/*.log', '!c/**/.*', '!b/*.log'], }).then(files => files.sort((a, b) => a.path.localeCompare(b.path))), From 4c6d8cd1f367247e197dc67cef7618507a905bdf Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:40:31 +0200 Subject: [PATCH 72/95] use new MockDirectory symlink helper Signed-off-by: Patrik Oldsberg --- .../src/manager/plugin-manager.test.ts | 7 +-- .../src/scanner/plugin-scanner.test.ts | 48 +++++++------------ .../actions/builtin/fetch/template.test.ts | 27 ++++------- 3 files changed, 27 insertions(+), 55 deletions(-) diff --git a/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts b/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts index f356163dea..4aabd2c072 100644 --- a/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts +++ b/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts @@ -452,16 +452,13 @@ describe('backend-plugin-manager', () => { findPaths(__dirname).resolveTargetRoot('package.json'), ), 'dynamic-plugins-root': {}, + 'dynamic-plugins-root/a-dynamic-plugin': ctx => + ctx.symlink(otherMockDir.resolve('a-dynamic-plugin')), }); otherMockDir.setContent({ 'a-dynamic-plugin': {}, }); - fs.symlinkSync( - otherMockDir.resolve('a-dynamic-plugin'), - mockDir.resolve('dynamic-plugins-root/a-dynamic-plugin'), - ); - const fromConfigSpier = jest.spyOn(PluginManager, 'fromConfig'); const applyConfigSpier = jest .spyOn(PluginScanner.prototype as any, 'applyConfig') diff --git a/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts b/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts index 04ce69ec98..d4ab8940c9 100644 --- a/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts +++ b/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts @@ -19,10 +19,12 @@ import { JsonObject } from '@backstage/types'; import { Logs, MockedLogger } from '../__testUtils__/testUtils'; import { ConfigReader } from '@backstage/config'; import path from 'path'; -import fs from 'fs'; import * as url from 'url'; import { ScannedPluginPackage } from './types'; -import { createMockDirectory } from '@backstage/backend-test-utils'; +import { + MockDirectoryContent, + createMockDirectory, +} from '@backstage/backend-test-utils'; const mockDir = createMockDirectory(); @@ -233,8 +235,7 @@ Please add '${mockDir.resolve( type TestCase = { name: string; preferAlpha?: boolean; - fileSystem?: any; - symlinks?: { source: string; target: string }[]; + fileSystem?: MockDirectoryContent; expectedLogs?: Logs; expectedPluginPackages?: ScannedPluginPackage[]; expectedError?: string; @@ -286,7 +287,12 @@ Please add '${mockDir.resolve( name: 'backend plugin found in symlink', fileSystem: { backstageRoot: { - 'dist-dynamic': {}, + 'dist-dynamic': { + 'test-backend-plugin': ctx => + ctx.symlink( + mockDir.resolve('somewhere-else/test-backend-plugin-target'), + ), + }, }, 'somewhere-else': { 'test-backend-plugin-target': { @@ -299,16 +305,6 @@ Please add '${mockDir.resolve( }, }, }, - symlinks: [ - { - source: mockDir.resolve( - 'backstageRoot/dist-dynamic/test-backend-plugin', - ), - target: mockDir.resolve( - 'somewhere-else/test-backend-plugin-target', - ), - }, - ], expectedPluginPackages: [ { location: url.pathToFileURL( @@ -347,22 +343,17 @@ Please add '${mockDir.resolve( name: 'ignored folder child symlink: target is not a directory', fileSystem: { backstageRoot: { - 'dist-dynamic': {}, + 'dist-dynamic': { + 'test-backend-plugin': ctx => + ctx.symlink( + mockDir.resolve('somewhere-else/test-backend-plugin-target'), + ), + }, }, 'somewhere-else': { 'test-backend-plugin-target': '', }, }, - symlinks: [ - { - source: mockDir.resolve( - 'backstageRoot/dist-dynamic/test-backend-plugin', - ), - target: mockDir.resolve( - 'somewhere-else/test-backend-plugin-target', - ), - }, - ], expectedPluginPackages: [], expectedLogs: { infos: [ @@ -637,11 +628,6 @@ Please add '${mockDir.resolve( if (tc.fileSystem) { mockDir.setContent(tc.fileSystem); } - if (tc.symlinks) { - for (const { source, target } of tc.symlinks) { - fs.symlinkSync(target, source); - } - } if (tc.expectedError) { /* eslint-disable-next-line jest/no-conditional-expect */ expect(toTest).toThrow(tc.expectedError); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts index 727eb5ce0d..d396a00c26 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts @@ -19,7 +19,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { return { ...actual, fetchContents: jest.fn() }; }); -import { join as joinPath, resolve as resolvePath, sep as pathSep } from 'path'; +import { join as joinPath, sep as pathSep } from 'path'; import fs from 'fs-extra'; import { getVoidLogger, @@ -272,27 +272,16 @@ describe('fetch:template', () => { }, '.${{ values.name }}': '${{ values.itemList | dump }}', 'a-binary-file.png': aBinaryFile, + 'an-executable.sh': ctx => + fs.writeFileSync(ctx.path, '#!/usr/bin/env bash', { + encoding: 'utf-8', + mode: parseInt('100755', 8), + }), + symlink: ctx => ctx.symlink('a-binary-file.png'), + brokenSymlink: ctx => ctx.symlink('./not-a-real-file.txt'), }, }); - fs.writeFileSync( - resolvePath(outputPath, 'an-executable.sh'), - '#!/usr/bin/env bash', - { - encoding: 'utf-8', - mode: parseInt('100755', 8), - }, - ); - - fs.symlinkSync( - 'a-binary-file.png', - resolvePath(outputPath, 'symlink'), - ); - fs.symlinkSync( - './not-a-real-file.txt', - resolvePath(outputPath, 'brokenSymlink'), - ); - return Promise.resolve(); }); From aefca1e6d0a50f38fb9dfb909b225f6099c8eafd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:45:36 +0200 Subject: [PATCH 73/95] scaffolder-backend: refactor debug actions tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../builtin/debug/log.examples.test.ts | 21 ++++++++----------- .../actions/builtin/debug/log.test.ts | 17 +++++++-------- .../builtin/debug/wait.examples.test.ts | 15 ++++++------- .../actions/builtin/debug/wait.test.ts | 15 ++++++------- 4 files changed, 28 insertions(+), 40 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts index a7f5a1804a..901f3e5011 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts @@ -15,44 +15,41 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import mockFs from 'mock-fs'; -import os from 'os'; import { Writable } from 'stream'; import { createDebugLogAction } from './log'; import { join } from 'path'; import yaml from 'yaml'; import { examples } from './log.examples'; +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('debug:log examples', () => { const logStream = { write: jest.fn(), } as jest.Mocked> as jest.Mocked; - const mockTmpDir = os.tmpdir(); + const mockDir = createMockDirectory(); + const workspacePath = mockDir.resolve('workspace'); + const mockContext = { input: {}, baseUrl: 'somebase', - workspacePath: mockTmpDir, + workspacePath, logger: getVoidLogger(), logStream, output: jest.fn(), - createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), + createTemporaryDirectory: jest.fn(), }; const action = createDebugLogAction(); beforeEach(() => { - mockFs({ - [`${mockContext.workspacePath}/README.md`]: '', - [`${mockContext.workspacePath}/a-directory/index.md`]: '', + mockDir.setContent({ + [`${workspacePath}/README.md`]: '', + [`${workspacePath}/a-directory/index.md`]: '', }); jest.resetAllMocks(); }); - afterEach(() => { - mockFs.restore(); - }); - it('should log message', async () => { const context = { ...mockContext, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts index 6b7ad8816f..d6fc5174b3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts @@ -15,43 +15,40 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import mockFs from 'mock-fs'; -import os from 'os'; import { Writable } from 'stream'; import { createDebugLogAction } from './log'; import { join } from 'path'; import yaml from 'yaml'; +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('debug:log', () => { const logStream = { write: jest.fn(), } as jest.Mocked> as jest.Mocked; - const mockTmpDir = os.tmpdir(); + const mockDir = createMockDirectory(); + const workspacePath = mockDir.resolve('workspace'); + const mockContext = { input: {}, baseUrl: 'somebase', - workspacePath: mockTmpDir, + workspacePath, logger: getVoidLogger(), logStream, output: jest.fn(), - createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), + createTemporaryDirectory: jest.fn(), }; const action = createDebugLogAction(); beforeEach(() => { - mockFs({ + mockDir.setContent({ [`${mockContext.workspacePath}/README.md`]: '', [`${mockContext.workspacePath}/a-directory/index.md`]: '', }); jest.resetAllMocks(); }); - afterEach(() => { - mockFs.restore(); - }); - it('should do nothing', async () => { await action.handler(mockContext); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts index 595ae7c3b6..11cc83c696 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts @@ -15,12 +15,11 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import mockFs from 'mock-fs'; import { createWaitAction } from './wait'; import { Writable } from 'stream'; -import os from 'os'; import { examples } from './wait.examples'; import yaml from 'yaml'; +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('debug:wait examples', () => { const action = createWaitAction(); @@ -29,25 +28,23 @@ describe('debug:wait examples', () => { write: jest.fn(), } as jest.Mocked> as jest.Mocked; - const mockTmpDir = os.tmpdir(); + const mockDir = createMockDirectory(); + const workspacePath = mockDir.resolve('workspace'); + const mockContext = { input: {}, baseUrl: 'somebase', - workspacePath: mockTmpDir, + workspacePath, logger: getVoidLogger(), logStream, output: jest.fn(), - createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), + createTemporaryDirectory: jest.fn(), }; beforeEach(() => { jest.resetAllMocks(); }); - afterEach(() => { - mockFs.restore(); - }); - it('should wait for specified period of seconds', async () => { const context = { ...mockContext, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts index 0d4cdaba4b..6f80604a6d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts @@ -15,10 +15,9 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import mockFs from 'mock-fs'; import { createWaitAction } from './wait'; import { Writable } from 'stream'; -import os from 'os'; +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('debug:wait', () => { const action = createWaitAction(); @@ -27,25 +26,23 @@ describe('debug:wait', () => { write: jest.fn(), } as jest.Mocked> as jest.Mocked; - const mockTmpDir = os.tmpdir(); + const mockDir = createMockDirectory(); + const workspacePath = mockDir.resolve('workspace'); + const mockContext = { input: {}, baseUrl: 'somebase', - workspacePath: mockTmpDir, + workspacePath, logger: getVoidLogger(), logStream, output: jest.fn(), - createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), + createTemporaryDirectory: jest.fn(), }; beforeEach(() => { jest.resetAllMocks(); }); - afterEach(() => { - mockFs.restore(); - }); - it('should wait for specified period of time', async () => { const context = { ...mockContext, From 82458b16ccc8e1bd173db051b15ac8ccc56852c3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:48:12 +0200 Subject: [PATCH 74/95] scaffolder-backend: refactor template action example tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../builtin/fetch/template.examples.test.ts | 63 +++++++------------ 1 file changed, 21 insertions(+), 42 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts index 43bed5a048..a1c8381178 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts @@ -14,10 +14,8 @@ * limitations under the License. */ -import os from 'os'; import { join as joinPath, sep as pathSep } from 'path'; import fs from 'fs-extra'; -import mockFs from 'mock-fs'; import { getVoidLogger, resolvePackagePath, @@ -33,6 +31,7 @@ import { } from '@backstage/plugin-scaffolder-node'; import { examples } from './template.examples'; import yaml from 'yaml'; +import { createMockDirectory } from '@backstage/backend-test-utils'; jest.mock('@backstage/plugin-scaffolder-node', () => ({ ...jest.requireActual('@backstage/plugin-scaffolder-node'), @@ -45,16 +44,6 @@ type FetchTemplateInput = ReturnType< ? U : never; -const realFiles = Object.fromEntries( - [ - resolvePackagePath( - '@backstage/plugin-scaffolder-backend', - 'assets', - 'nunjucks.js.txt', - ), - ].map(k => [k, mockFs.load(k)]), -); - const aBinaryFile = fs.readFileSync( resolvePackagePath( '@backstage/plugin-scaffolder-backend', @@ -69,14 +58,8 @@ const mockFetchContents = fetchContents as jest.MockedFunction< describe('fetch:template examples', () => { let action: TemplateAction; - const workspacePath = os.tmpdir(); - const createTemporaryDirectory: jest.MockedFunction< - ActionContext['createTemporaryDirectory'] - > = jest.fn(() => - Promise.resolve( - joinPath(workspacePath, `${createTemporaryDirectory.mock.calls.length}`), - ), - ); + const mockDir = createMockDirectory(); + const workspacePath = mockDir.resolve('workspace'); const logger = getVoidLogger(); @@ -90,24 +73,20 @@ describe('fetch:template examples', () => { logStream: new PassThrough(), logger, workspacePath, - createTemporaryDirectory, + + async createTemporaryDirectory() { + return fs.mkdtemp(mockDir.resolve('tmp-')); + }, }); beforeEach(() => { - mockFs({ - ...realFiles, - }); - + mockDir.clear(); action = createFetchTemplateAction({ reader: Symbol('UrlReader') as unknown as UrlReader, integrations: Symbol('Integrations') as unknown as ScmIntegrations, }); }); - afterEach(() => { - mockFs.restore(); - }); - describe('handler', () => { describe('with valid input', () => { let context: ActionContext; @@ -116,13 +95,13 @@ describe('fetch:template examples', () => { context = mockContext(yaml.parse(examples[0].example).steps[0].input); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [outputPath]: { - 'an-executable.sh': mockFs.file({ - content: '#!/usr/bin/env bash', - mode: parseInt('100755', 8), - }), + 'an-executable.sh': ctx => + fs.writeFileSync(ctx.path, '#!/usr/bin/env bash', { + encoding: 'utf8', + mode: parseInt('100755', 8), + }), 'empty-dir-${{ values.count }}': {}, 'static.txt': 'static content', '${{ values.name }}.txt': 'static content', @@ -132,12 +111,8 @@ describe('fetch:template examples', () => { }, '.${{ values.name }}': '${{ values.itemList | dump }}', 'a-binary-file.png': aBinaryFile, - symlink: mockFs.symlink({ - path: 'a-binary-file.png', - }), - brokenSymlink: mockFs.symlink({ - path: './not-a-real-file.txt', - }), + symlink: ctx => ctx.symlink('a-binary-file.png'), + brokenSymlink: ctx => ctx.symlink('./not-a-real-file.txt'), }, }); @@ -212,7 +187,11 @@ describe('fetch:template examples', () => { await expect( fs.realpath(`${workspacePath}/target/symlink`), - ).resolves.toBe(joinPath(workspacePath, 'target', 'a-binary-file.png')); + ).resolves.toBe( + fs.realpathSync( + joinPath(workspacePath, 'target', 'a-binary-file.png'), + ), + ); }); it('copies broken symlinks as-is without processing them', async () => { From 53d110c055af1cfab2c004a35c1396da92032708 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:51:09 +0200 Subject: [PATCH 75/95] scaffolder-backend: refactor github action tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../builtin/publish/githubPullRequest.test.ts | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts index b6987335f9..a1f3712a9d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts @@ -24,21 +24,17 @@ import { ActionContext, TemplateAction, } from '@backstage/plugin-scaffolder-node'; -import mockFs from 'mock-fs'; -import os from 'os'; -import { resolve as resolvePath } from 'path'; +import fs from 'fs-extra'; import { Writable } from 'stream'; import { createPublishGithubPullRequestAction, OctokitWithPullRequestPluginClient, } from './githubPullRequest'; +import { createMockDirectory } from '@backstage/backend-test-utils'; // Make sure root logger is initialized ahead of FS mock createRootLogger(); -const root = os.platform() === 'win32' ? 'C:\\root' : '/root'; -const workspacePath = resolvePath(root, 'my-workspace'); - type GithubPullRequestActionInput = ReturnType< typeof createPublishGithubPullRequestAction > extends TemplateAction @@ -54,7 +50,12 @@ describe('createPublishGithubPullRequestAction', () => { }; }; + const mockDir = createMockDirectory(); + const workspacePath = mockDir.resolve('workspace'); + beforeEach(() => { + mockDir.clear(); + const integrations = ScmIntegrations.fromConfig(new ConfigReader({})); fakeClient = { createPullRequest: jest.fn(async (_: any) => { @@ -92,7 +93,6 @@ describe('createPublishGithubPullRequestAction', () => { }); afterEach(() => { - mockFs.restore(); jest.resetAllMocks(); }); @@ -132,7 +132,7 @@ describe('createPublishGithubPullRequestAction', () => { draft: true, }; - mockFs({ + mockDir.setContent({ [workspacePath]: { 'file.txt': 'Hello there!' }, }); @@ -197,7 +197,7 @@ describe('createPublishGithubPullRequestAction', () => { draft: true, }; - mockFs({ + mockDir.setContent({ [workspacePath]: { 'file.txt': 'Hello there!' }, }); @@ -261,7 +261,7 @@ describe('createPublishGithubPullRequestAction', () => { sourcePath: 'source', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -323,7 +323,7 @@ describe('createPublishGithubPullRequestAction', () => { description: 'This PR is really good', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { 'file.txt': 'Hello there!' }, }); @@ -385,7 +385,7 @@ describe('createPublishGithubPullRequestAction', () => { teamReviewers: ['team-foo'], }; - mockFs({ [workspacePath]: {} }); + mockDir.setContent({ [workspacePath]: {} }); ctx = { createTemporaryDirectory: jest.fn(), @@ -437,7 +437,7 @@ describe('createPublishGithubPullRequestAction', () => { description: 'This PR is really good', }; - mockFs({ [workspacePath]: {} }); + mockDir.setContent({ [workspacePath]: {} }); ctx = { createTemporaryDirectory: jest.fn(), @@ -469,11 +469,9 @@ describe('createPublishGithubPullRequestAction', () => { description: 'This PR is really good', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { - Makefile: mockFs.symlink({ - path: '../../nothing/yet', - }), + Makefile: c => c.symlink('../../nothing/yet'), }, }); @@ -523,12 +521,13 @@ describe('createPublishGithubPullRequestAction', () => { description: 'This PR is really good', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { - 'hello.sh': mockFs.file({ - content: 'echo Hello there!', - mode: 0o100755, - }), + 'hello.sh': c => + fs.writeFileSync(c.path, 'echo Hello there!', { + encoding: 'utf8', + mode: 0o100755, + }), }, }); @@ -588,12 +587,13 @@ describe('createPublishGithubPullRequestAction', () => { description: 'This PR is really good', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { - 'hello.sh': mockFs.file({ - content: 'echo Hello there!', - mode: 0o100775, - }), + 'hello.sh': c => + fs.writeFileSync(c.path, 'echo Hello there!', { + encoding: 'utf8', + mode: 0o100775, + }), }, }); @@ -654,7 +654,7 @@ describe('createPublishGithubPullRequestAction', () => { commitMessage: 'Create my new app, but in the commit message', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { 'file.txt': 'Hello there!' }, }); From 7486c28163202955c740669dd00992b415e8a24a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:52:07 +0200 Subject: [PATCH 76/95] scaffolder-backend: refactor gitlab action tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../publish/gitlabMergeRequest.test.ts | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts index 6445d243b5..71a2273aed 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts @@ -17,18 +17,13 @@ import { createRootLogger, getRootLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import mockFs from 'mock-fs'; -import os from 'os'; -import { resolve as resolvePath } from 'path'; import { Writable } from 'stream'; import { createPublishGitlabMergeRequestAction } from './gitlabMergeRequest'; +import { createMockDirectory } from '@backstage/backend-test-utils'; // Make sure root logger is initialized ahead of FS mock createRootLogger(); -const root = os.platform() === 'win32' ? 'C:\\root' : '/root'; -const workspacePath = resolvePath(root, 'my-workspace'); - const mockGitlabClient = { Namespaces: { show: jest.fn(), @@ -79,7 +74,12 @@ jest.mock('@gitbeaker/node', () => ({ describe('createGitLabMergeRequest', () => { let instance: TemplateAction; + const mockDir = createMockDirectory(); + const workspacePath = mockDir.resolve('workspace'); + beforeEach(() => { + mockDir.clear(); + const config = new ConfigReader({ integrations: { gitlab: [ @@ -100,10 +100,6 @@ describe('createGitLabMergeRequest', () => { instance = createPublishGitlabMergeRequestAction({ integrations }); }); - afterEach(() => { - mockFs.restore(); - }); - describe('createGitLabMergeRequestWithSpecifiedTargetBranch', () => { it('removeSourceBranch is false by default when not passed in options', async () => { const input = { @@ -114,7 +110,7 @@ describe('createGitLabMergeRequest', () => { description: 'This MR is really good', targetPath: 'Subdirectory', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -156,7 +152,7 @@ describe('createGitLabMergeRequest', () => { description: 'This MR is really good', targetPath: 'Subdirectory', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -200,7 +196,7 @@ describe('createGitLabMergeRequest', () => { removeSourceBranch: true, targetPath: 'Subdirectory', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -235,7 +231,7 @@ describe('createGitLabMergeRequest', () => { removeSourceBranch: false, targetPath: 'Subdirectory', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -276,7 +272,7 @@ describe('createGitLabMergeRequest', () => { targetPath: 'Subdirectory', assignee: 'John Smith', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -316,7 +312,7 @@ describe('createGitLabMergeRequest', () => { targetPath: 'Subdirectory', assingnee: 'John Doe', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -356,7 +352,7 @@ describe('createGitLabMergeRequest', () => { removeSourceBranch: false, targetPath: 'Subdirectory', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -395,7 +391,7 @@ describe('createGitLabMergeRequest', () => { targetPath: 'Subdirectory', assignee: 'Unknown', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -431,7 +427,7 @@ describe('createGitLabMergeRequest', () => { branchName: 'new-mr', description: 'This MR is really good', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -480,7 +476,7 @@ describe('createGitLabMergeRequest', () => { description: 'This MR is really good', targetPath: 'source', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -523,7 +519,7 @@ describe('createGitLabMergeRequest', () => { commitAction: 'create', targetPath: 'source', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -565,7 +561,7 @@ describe('createGitLabMergeRequest', () => { commitAction: 'update', targetPath: 'source', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -607,7 +603,7 @@ describe('createGitLabMergeRequest', () => { commitAction: 'delete', targetPath: 'source', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -652,7 +648,7 @@ describe('createGitLabMergeRequest', () => { commitAction: 'create', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -697,7 +693,7 @@ describe('createGitLabMergeRequest', () => { commitAction: 'create', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, From d5d950a916df09c44fd81d2ca5d92589c0ef0327 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:54:27 +0200 Subject: [PATCH 77/95] scaffolder-backend: refactor NunjucksWorkflowRunner tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../tasks/NunjucksWorkflowRunner.test.ts | 33 ++++--------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index d94a8eac69..6907f8cc2c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import mockFs from 'mock-fs'; -import * as winston from 'winston'; - -import { getVoidLogger, resolvePackagePath } from '@backstage/backend-common'; +import { getVoidLogger } from '@backstage/backend-common'; import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; import { TemplateActionRegistry } from '../actions'; import { ScmIntegrations } from '@backstage/integration'; @@ -36,19 +33,7 @@ import { PermissionEvaluator, } from '@backstage/plugin-permission-common'; import { RESOURCE_TYPE_SCAFFOLDER_ACTION } from '@backstage/plugin-scaffolder-common/alpha'; - -// The Stream module is lazy loaded, so make sure it's in the module cache before mocking fs -void winston.transports.Stream; - -const realFiles = Object.fromEntries( - [ - resolvePackagePath( - '@backstage/plugin-scaffolder-backend', - 'assets', - 'nunjucks.js.txt', - ), - ].map(k => [k, mockFs.load(k)]), -); +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('DefaultWorkflowRunner', () => { const logger = getVoidLogger(); @@ -56,6 +41,8 @@ describe('DefaultWorkflowRunner', () => { let runner: NunjucksWorkflowRunner; let fakeActionHandler: jest.Mock; + const mockDir = createMockDirectory(); + const mockedPermissionApi: jest.Mocked = { authorizeConditional: jest.fn(), } as unknown as jest.Mocked; @@ -84,11 +71,7 @@ describe('DefaultWorkflowRunner', () => { }); beforeEach(() => { - winston.format.simple(); // put logform in the require.cache before mocking fs - mockFs({ - '/tmp': mockFs.directory(), - ...realFiles, - }); + mockDir.clear(); jest.resetAllMocks(); actionRegistry = new TemplateActionRegistry(); @@ -148,16 +131,12 @@ describe('DefaultWorkflowRunner', () => { runner = new NunjucksWorkflowRunner({ actionRegistry, integrations, - workingDirectory: '/tmp', + workingDirectory: mockDir.path, logger, permissions: mockedPermissionApi, }); }); - afterEach(() => { - mockFs.restore(); - }); - it('should throw an error if the action does not exist', async () => { const task = createMockTaskWithSpec({ apiVersion: 'scaffolder.backstage.io/v1beta3', From 7d077ae835c72bb485f5bc192295dee9fc841f5f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:56:23 +0200 Subject: [PATCH 78/95] scaffolder-backend: remove mock-fs dependency Signed-off-by: Patrik Oldsberg --- plugins/scaffolder-backend/package.json | 2 -- yarn.lock | 2 -- 2 files changed, 4 deletions(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index cab9abae96..4ff894cd61 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -107,13 +107,11 @@ "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", "@types/libsodium-wrappers": "^0.7.10", - "@types/mock-fs": "^4.13.0", "@types/nunjucks": "^3.1.4", "@types/supertest": "^2.0.8", "@types/zen-observable": "^0.8.0", "esbuild": "^0.19.0", "jest-when": "^3.1.0", - "mock-fs": "^5.2.0", "msw": "^1.0.0", "supertest": "^6.1.3", "wait-for-expect": "^3.0.2", diff --git a/yarn.lock b/yarn.lock index 70690186aa..96df4629a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8722,7 +8722,6 @@ __metadata: "@types/git-url-parse": ^9.0.0 "@types/libsodium-wrappers": ^0.7.10 "@types/luxon": ^3.0.0 - "@types/mock-fs": ^4.13.0 "@types/nunjucks": ^3.1.4 "@types/supertest": ^2.0.8 "@types/zen-observable": ^0.8.0 @@ -8745,7 +8744,6 @@ __metadata: libsodium-wrappers: ^0.7.11 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 From 4515c10ca288de65ece1cb6625385087599bf389 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 6 Oct 2023 13:08:37 +0200 Subject: [PATCH 79/95] Revert ".github/workflows: disable Node 20 tests" This reverts commit 79c3febee739705daa78cfd0c359a25a48bb60da. Signed-off-by: Patrik Oldsberg --- .github/workflows/ci.yml | 6 +++--- .github/workflows/deploy_packages.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbf2dc6705..12c19075a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [18.x] + node-version: [18.x, 20.x] env: CI: true @@ -49,7 +49,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [18.x] + node-version: [18.x, 20.x] env: CI: true @@ -139,7 +139,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [18.x] + node-version: [18.x, 20.x] name: Test ${{ matrix.node-version }} services: diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index 91b25fb323..2329101eb0 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [18.x] + node-version: [18.x, 20.x] services: postgres13: From eb42e005cb1521a11add47c996cd7b0841eed2c7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 17:25:01 +0200 Subject: [PATCH 80/95] scaffolder-backend: refactor template action tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../actions/builtin/fetch/template.test.ts | 103 +++++++----------- 1 file changed, 41 insertions(+), 62 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts index 95f8c7b954..727eb5ce0d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts @@ -19,10 +19,8 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { return { ...actual, fetchContents: jest.fn() }; }); -import os from 'os'; -import { join as joinPath, sep as pathSep } from 'path'; +import { join as joinPath, resolve as resolvePath, sep as pathSep } from 'path'; import fs from 'fs-extra'; -import mockFs from 'mock-fs'; import { getVoidLogger, resolvePackagePath, @@ -36,6 +34,7 @@ import { ActionContext, TemplateAction, } from '@backstage/plugin-scaffolder-node'; +import { createMockDirectory } from '@backstage/backend-test-utils'; type FetchTemplateInput = ReturnType< typeof createFetchTemplateAction @@ -43,16 +42,6 @@ type FetchTemplateInput = ReturnType< ? U : never; -const realFiles = Object.fromEntries( - [ - resolvePackagePath( - '@backstage/plugin-scaffolder-backend', - 'assets', - 'nunjucks.js.txt', - ), - ].map(k => [k, mockFs.load(k)]), -); - const aBinaryFile = fs.readFileSync( resolvePackagePath( '@backstage/plugin-scaffolder-backend', @@ -67,14 +56,8 @@ const mockFetchContents = fetchContents as jest.MockedFunction< describe('fetch:template', () => { let action: TemplateAction; - const workspacePath = os.tmpdir(); - const createTemporaryDirectory: jest.MockedFunction< - ActionContext['createTemporaryDirectory'] - > = jest.fn(() => - Promise.resolve( - joinPath(workspacePath, `${createTemporaryDirectory.mock.calls.length}`), - ), - ); + const mockDir = createMockDirectory(); + const workspacePath = mockDir.resolve('workspace'); const logger = getVoidLogger(); @@ -95,24 +78,21 @@ describe('fetch:template', () => { logStream: new PassThrough(), logger, workspacePath, - createTemporaryDirectory, + async createTemporaryDirectory() { + return fs.mkdtemp(mockDir.resolve('tmp-')); + }, }); beforeEach(() => { - mockFs({ - ...realFiles, + mockDir.setContent({ + workspace: {}, }); - action = createFetchTemplateAction({ reader: Symbol('UrlReader') as unknown as UrlReader, integrations: Symbol('Integrations') as unknown as ScmIntegrations, }); }); - afterEach(() => { - mockFs.restore(); - }); - it(`returns a TemplateAction with the id 'fetch:template'`, () => { expect(action.id).toEqual('fetch:template'); }); @@ -190,8 +170,7 @@ describe('fetch:template', () => { }); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [outputPath]: { '{% if values.showDummyFile %}dummy-file.txt{% else %}{% endif %}': 'dummy file', @@ -282,13 +261,8 @@ describe('fetch:template', () => { }); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [outputPath]: { - 'an-executable.sh': mockFs.file({ - content: '#!/usr/bin/env bash', - mode: parseInt('100755', 8), - }), 'empty-dir-${{ values.count }}': {}, 'static.txt': 'static content', '${{ values.name }}.txt': 'static content', @@ -298,15 +272,27 @@ describe('fetch:template', () => { }, '.${{ values.name }}': '${{ values.itemList | dump }}', 'a-binary-file.png': aBinaryFile, - symlink: mockFs.symlink({ - path: 'a-binary-file.png', - }), - brokenSymlink: mockFs.symlink({ - path: './not-a-real-file.txt', - }), }, }); + fs.writeFileSync( + resolvePath(outputPath, 'an-executable.sh'), + '#!/usr/bin/env bash', + { + encoding: 'utf-8', + mode: parseInt('100755', 8), + }, + ); + + fs.symlinkSync( + 'a-binary-file.png', + resolvePath(outputPath, 'symlink'), + ); + fs.symlinkSync( + './not-a-real-file.txt', + resolvePath(outputPath, 'brokenSymlink'), + ); + return Promise.resolve(); }); @@ -378,7 +364,11 @@ describe('fetch:template', () => { await expect( fs.realpath(`${workspacePath}/target/symlink`), - ).resolves.toBe(joinPath(workspacePath, 'target', 'a-binary-file.png')); + ).resolves.toBe( + fs.realpathSync( + joinPath(workspacePath, 'target', 'a-binary-file.png'), + ), + ); }); it('copies broken symlinks as-is without processing them', async () => { @@ -408,8 +398,7 @@ describe('fetch:template', () => { }); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [outputPath]: { processed: { 'templated-content-${{ values.name }}.txt': '${{ values.count }}', @@ -458,8 +447,7 @@ describe('fetch:template', () => { }); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [outputPath]: { processed: { 'templated-content-${{ values.name }}.txt': '${{ values.count }}', @@ -509,8 +497,7 @@ describe('fetch:template', () => { }); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [outputPath]: { '{{ cookiecutter.name }}.txt': 'static content', subdir: { @@ -564,8 +551,7 @@ describe('fetch:template', () => { }); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [outputPath]: { 'empty-dir-${{ values.count }}': {}, 'static.txt': 'static content', @@ -646,8 +632,7 @@ describe('fetch:template', () => { }); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [outputPath]: { '${{ values.name }}.njk': '${{ values.name }}: ${{ values.count }}', '${{ values.name }}.txt.jinja2': @@ -687,8 +672,7 @@ describe('fetch:template', () => { }); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [joinPath(workspacePath, 'target')]: { 'static-content.txt': 'static-content', }, @@ -703,10 +687,6 @@ describe('fetch:template', () => { await action.handler(context); }); - afterEach(() => { - mockFs.restore(); - }); - it('overwrites existing file', async () => { await expect( fs.readFile(`${workspacePath}/target/static-content.txt`, 'utf-8'), @@ -728,8 +708,7 @@ describe('fetch:template', () => { }); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [joinPath(workspacePath, 'target')]: { 'static-content.txt': 'static-content', }, From 0fca639626c22425ad3becc074a3cfff216e1a5a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 17:26:51 +0200 Subject: [PATCH 81/95] scaffolder-backend: refactor rename action tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../actions/builtin/filesystem/rename.test.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts index 6804c9f5a9..d37e967f43 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.test.ts @@ -14,20 +14,19 @@ * limitations under the License. */ -import * as os from 'os'; -import mockFs from 'mock-fs'; import { resolve as resolvePath } from 'path'; import { createFilesystemRenameAction } from './rename'; import { getVoidLogger } from '@backstage/backend-common'; import { PassThrough } from 'stream'; import fs from 'fs-extra'; - -const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; -const workspacePath = resolvePath(root, 'my-workspace'); +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('fs:rename', () => { const action = createFilesystemRenameAction(); + const mockDir = createMockDirectory(); + const workspacePath = resolvePath(mockDir.path, 'workspace'); + const mockInputFiles = [ { from: 'unit-test-a.js', @@ -56,7 +55,7 @@ describe('fs:rename', () => { beforeEach(() => { jest.restoreAllMocks(); - mockFs({ + mockDir.setContent({ [workspacePath]: { 'unit-test-a.js': 'hello', 'unit-test-b.js': 'world', @@ -68,10 +67,6 @@ describe('fs:rename', () => { }); }); - afterEach(() => { - mockFs.restore(); - }); - it('should throw an error when files is not an array', async () => { await expect( action.handler({ From b95b05baeb3c3fcc496a1cad6ce8f2de9f80a6a7 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 17:27:45 +0200 Subject: [PATCH 82/95] scaffolder-backend: refactor rename action example tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../builtin/filesystem/rename.examples.test.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts index 5139469959..cbb0fa0d0b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/rename.examples.test.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import * as os from 'os'; -import mockFs from 'mock-fs'; import { resolve as resolvePath } from 'path'; import { createFilesystemRenameAction } from './rename'; import { getVoidLogger } from '@backstage/backend-common'; @@ -23,15 +21,16 @@ import { PassThrough } from 'stream'; import fs from 'fs-extra'; import yaml from 'yaml'; import { examples } from './rename.examples'; - -const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; -const workspacePath = resolvePath(root, 'my-workspace'); +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('fs:rename examples', () => { const action = createFilesystemRenameAction(); const files: { from: string; to: string }[] = yaml.parse(examples[0].example) .steps[0].input.files; + const mockDir = createMockDirectory(); + const workspacePath = resolvePath(mockDir.path, 'workspace'); + const mockContext = { input: { files: files, @@ -46,7 +45,7 @@ describe('fs:rename examples', () => { beforeEach(() => { jest.restoreAllMocks(); - mockFs({ + mockDir.setContent({ [workspacePath]: { [files[0].from]: 'hello', [files[1].from]: 'world', @@ -59,10 +58,6 @@ describe('fs:rename examples', () => { }); }); - afterEach(() => { - mockFs.restore(); - }); - it('should call fs.move with the correct values', async () => { mockContext.input.files.forEach(file => { const filePath = resolvePath(workspacePath, file.from); From 12da2feccb9b5d678a919255d997849c711ee310 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:06:53 +0200 Subject: [PATCH 83/95] scaffolder-backend: refactor delete action tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../builtin/filesystem/delete.examples.test.ts | 15 +++++---------- .../actions/builtin/filesystem/delete.test.ts | 15 +++++---------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts index 10994e3824..023f049ebc 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.examples.test.ts @@ -18,18 +18,17 @@ import { createFilesystemDeleteAction } from './delete'; import { getVoidLogger } from '@backstage/backend-common'; import { PassThrough } from 'stream'; import { resolve as resolvePath } from 'path'; -import * as os from 'os'; -import mockFs from 'mock-fs'; import fs from 'fs-extra'; import yaml from 'yaml'; import { examples } from './delete.examples'; - -const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; -const workspacePath = resolvePath(root, 'my-workspace'); +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('fs:delete examples', () => { const action = createFilesystemDeleteAction(); + const mockDir = createMockDirectory(); + const workspacePath = resolvePath(mockDir.path, 'workspace'); + const files: string[] = yaml.parse(examples[0].example).steps[0].input.files; const mockContext = { @@ -46,7 +45,7 @@ describe('fs:delete examples', () => { beforeEach(() => { jest.restoreAllMocks(); - mockFs({ + mockDir.setContent({ [workspacePath]: { [files[0]]: 'hello', [files[1]]: 'world', @@ -57,10 +56,6 @@ describe('fs:delete examples', () => { }); }); - afterEach(() => { - mockFs.restore(); - }); - it('should call fs.rm with the correct values', async () => { files.forEach(file => { const filePath = resolvePath(workspacePath, file); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts index 713de5b0ad..2a4f45b862 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/filesystem/delete.test.ts @@ -14,20 +14,19 @@ * limitations under the License. */ -import * as os from 'os'; -import mockFs from 'mock-fs'; import { resolve as resolvePath } from 'path'; import { createFilesystemDeleteAction } from './delete'; import { getVoidLogger } from '@backstage/backend-common'; import { PassThrough } from 'stream'; import fs from 'fs-extra'; - -const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; -const workspacePath = resolvePath(root, 'my-workspace'); +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('fs:delete', () => { const action = createFilesystemDeleteAction(); + const mockDir = createMockDirectory(); + const workspacePath = resolvePath(mockDir.path, 'workspace'); + const mockContext = { input: { files: ['unit-test-a.js', 'unit-test-b.js'], @@ -42,7 +41,7 @@ describe('fs:delete', () => { beforeEach(() => { jest.restoreAllMocks(); - mockFs({ + mockDir.setContent({ [workspacePath]: { 'unit-test-a.js': 'hello', 'unit-test-b.js': 'world', @@ -53,10 +52,6 @@ describe('fs:delete', () => { }); }); - afterEach(() => { - mockFs.restore(); - }); - it('should throw an error when files is not an array', async () => { await expect( action.handler({ From 45f9748e436ef19bbc5121edfb5bfcd795eb3fef Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:29:15 +0200 Subject: [PATCH 84/95] backend-test-utils: add MockDirectory content callback + default file modes Signed-off-by: Patrik Oldsberg --- packages/backend-test-utils/api-report.md | 17 ++++++- .../src/filesystem/MockDirectory.test.ts | 14 ++++++ .../src/filesystem/MockDirectory.ts | 49 +++++++++++++++++-- .../src/filesystem/index.ts | 2 + 4 files changed, 77 insertions(+), 5 deletions(-) diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index a477f70298..ac012e2f69 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -54,9 +54,24 @@ export interface MockDirectory { // @public export type MockDirectoryContent = { - [name in string]: MockDirectoryContent | string | Buffer; + [name in string]: + | MockDirectoryContent + | string + | Buffer + | MockDirectoryContentCallback; }; +// @public +export type MockDirectoryContentCallback = ( + ctx: MockDirectoryContentCallbackContext, +) => void; + +// @public +export interface MockDirectoryContentCallbackContext { + path: string; + symlink(target: string): void; +} + // @public export interface MockDirectoryContentOptions { path?: string; diff --git a/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts b/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts index 8040d03f44..fd22f09432 100644 --- a/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts +++ b/packages/backend-test-utils/src/filesystem/MockDirectory.test.ts @@ -119,6 +119,20 @@ describe('createMockDirectory', () => { }); }); + it('should be able to use callback for more detailed file system operations', () => { + mockDir.setContent({ + 'a.txt': 'a', + 'b.txt': ctx => ctx.symlink('./a.txt'), + 'c.txt': ctx => fs.copyFileSync(mockDir.resolve('a.txt'), ctx.path), + }); + + expect(mockDir.content()).toEqual({ + 'a.txt': 'a', + 'b.txt': 'a', + 'c.txt': 'a', + }); + }); + it('should read content from sub dirs', () => { mockDir.setContent({ 'a.txt': 'a', diff --git a/packages/backend-test-utils/src/filesystem/MockDirectory.ts b/packages/backend-test-utils/src/filesystem/MockDirectory.ts index 61eb4fce24..b066b9acd5 100644 --- a/packages/backend-test-utils/src/filesystem/MockDirectory.ts +++ b/packages/backend-test-utils/src/filesystem/MockDirectory.ts @@ -30,6 +30,28 @@ import { const tmpdirMarker = Symbol('os-tmpdir-mock'); +/** + * A context that allows for more advanced file system operations when writing mock directory content. + * + * @public + */ +export interface MockDirectoryContentCallbackContext { + /** Absolute path to the location of this piece of content on the filesystem */ + path: string; + + /** Creates a symbolic link at the current location */ + symlink(target: string): void; +} + +/** + * A callback that allows for more advanced file system operations when writing mock directory content. + * + * @public + */ +export type MockDirectoryContentCallback = ( + ctx: MockDirectoryContentCallbackContext, +) => void; + /** * The content of a mock directory represented by a nested object structure. * @@ -54,7 +76,11 @@ const tmpdirMarker = Symbol('os-tmpdir-mock'); * @public */ export type MockDirectoryContent = { - [name in string]: MockDirectoryContent | string | Buffer; + [name in string]: + | MockDirectoryContent + | string + | Buffer + | MockDirectoryContentCallback; }; /** @@ -178,6 +204,11 @@ type MockEntry = | { type: 'dir'; path: string; + } + | { + type: 'callback'; + path: string; + callback: MockDirectoryContentCallback; }; /** @internal */ @@ -214,10 +245,18 @@ class MockDirectoryImpl { } if (entry.type === 'dir') { - fs.ensureDirSync(fullPath, { mode: 0o777 }); + fs.ensureDirSync(fullPath); } else if (entry.type === 'file') { - fs.ensureDirSync(dirname(fullPath), { mode: 0o777 }); - fs.writeFileSync(fullPath, entry.content, { mode: 0o666 }); + fs.ensureDirSync(dirname(fullPath)); + fs.writeFileSync(fullPath, entry.content); + } else if (entry.type === 'callback') { + fs.ensureDirSync(dirname(fullPath)); + entry.callback({ + path: fullPath, + symlink(target: string) { + fs.symlinkSync(target, fullPath); + }, + }); } } } @@ -287,6 +326,8 @@ class MockDirectoryImpl { }); } else if (node instanceof Buffer) { entries.push({ type: 'file', path, content: node }); + } else if (typeof node === 'function') { + entries.push({ type: 'callback', path, callback: node }); } else { entries.push({ type: 'dir', path }); for (const [name, child] of Object.entries(node)) { diff --git a/packages/backend-test-utils/src/filesystem/index.ts b/packages/backend-test-utils/src/filesystem/index.ts index 0a4d8c7d00..e18b0b55b8 100644 --- a/packages/backend-test-utils/src/filesystem/index.ts +++ b/packages/backend-test-utils/src/filesystem/index.ts @@ -20,4 +20,6 @@ export { type MockDirectoryOptions, type MockDirectoryContent, type MockDirectoryContentOptions, + type MockDirectoryContentCallback, + type MockDirectoryContentCallbackContext, } from './MockDirectory'; From 5fd3d627233b8281ed3256eda36aee88ca4b19c0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:32:28 +0200 Subject: [PATCH 85/95] scaffolder-backend: refactor file serialization tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../deserializeDirectoryContents.test.ts | 20 ++- .../files/serializeDirectoryContents.test.ts | 114 ++++++++---------- 2 files changed, 55 insertions(+), 79 deletions(-) diff --git a/plugins/scaffolder-backend/src/lib/files/deserializeDirectoryContents.test.ts b/plugins/scaffolder-backend/src/lib/files/deserializeDirectoryContents.test.ts index c272e04205..e374e0d713 100644 --- a/plugins/scaffolder-backend/src/lib/files/deserializeDirectoryContents.test.ts +++ b/plugins/scaffolder-backend/src/lib/files/deserializeDirectoryContents.test.ts @@ -14,29 +14,25 @@ * limitations under the License. */ -import mockFs from 'mock-fs'; +import { createMockDirectory } from '@backstage/backend-test-utils'; import { deserializeDirectoryContents } from './deserializeDirectoryContents'; import { serializeDirectoryContents } from './serializeDirectoryContents'; describe('deserializeDirectoryContents', () => { - beforeEach(() => { - mockFs({ - root: {}, - }); - }); + const mockDir = createMockDirectory(); - afterEach(() => { - mockFs.restore(); + beforeEach(() => { + mockDir.clear(); }); it('deserializes contents into a directory', async () => { - await deserializeDirectoryContents('root', [ + await deserializeDirectoryContents(mockDir.path, [ { path: 'a.txt', content: Buffer.from('a', 'utf8'), }, ]); - await expect(serializeDirectoryContents('root')).resolves.toEqual([ + await expect(serializeDirectoryContents(mockDir.path)).resolves.toEqual([ { path: 'a.txt', content: Buffer.from('a', 'utf8'), @@ -47,7 +43,7 @@ describe('deserializeDirectoryContents', () => { }); it('deserializes contents into a deep directory structure', async () => { - await deserializeDirectoryContents('root', [ + await deserializeDirectoryContents(mockDir.path, [ { path: 'a.txt', content: Buffer.from('a', 'utf8'), @@ -61,7 +57,7 @@ describe('deserializeDirectoryContents', () => { content: Buffer.from('c', 'utf8'), }, ]); - await expect(serializeDirectoryContents('root')).resolves.toEqual([ + await expect(serializeDirectoryContents(mockDir.path)).resolves.toEqual([ { path: 'a.txt', content: Buffer.from('a', 'utf8'), diff --git a/plugins/scaffolder-backend/src/lib/files/serializeDirectoryContents.test.ts b/plugins/scaffolder-backend/src/lib/files/serializeDirectoryContents.test.ts index a22ea85301..afc1b755ff 100644 --- a/plugins/scaffolder-backend/src/lib/files/serializeDirectoryContents.test.ts +++ b/plugins/scaffolder-backend/src/lib/files/serializeDirectoryContents.test.ts @@ -14,13 +14,11 @@ * limitations under the License. */ +import { createMockDirectory } from '@backstage/backend-test-utils'; import { serializeDirectoryContents } from './serializeDirectoryContents'; -import mockFs from 'mock-fs'; describe('serializeDirectoryContents', () => { - afterEach(() => { - mockFs.restore(); - }); + const mockDir = createMockDirectory(); it('should list files in this directory', async () => { await expect(serializeDirectoryContents(__dirname)).resolves.toEqual( @@ -54,25 +52,23 @@ describe('serializeDirectoryContents', () => { }); it('should list files in a mock directory', async () => { - mockFs({ - root: { - 'a.txt': 'a', - b: { - 'b1.txt': 'b1', - 'b2.txt': 'b2', - }, - c: { - c1: { - 'c11.txt': 'c11', - c11: { - 'c111.txt': 'c111', - }, + mockDir.setContent({ + 'a.txt': 'a', + b: { + 'b1.txt': 'b1', + 'b2.txt': 'b2', + }, + c: { + c1: { + 'c11.txt': 'c11', + c11: { + 'c111.txt': 'c111', }, }, }, }); - await expect(serializeDirectoryContents('root')).resolves.toEqual([ + await expect(serializeDirectoryContents(mockDir.path)).resolves.toEqual([ { path: 'a.txt', executable: false, @@ -107,16 +103,12 @@ describe('serializeDirectoryContents', () => { }); it('should ignore symlinked files', async () => { - mockFs({ - root: { - 'a.txt': 'some text', - sym: mockFs.symlink({ - path: './a.txt', - }), - }, + mockDir.setContent({ + 'a.txt': 'some text', + sym: ctx => ctx.symlink('./a.txt'), }); - await expect(serializeDirectoryContents('root')).resolves.toEqual([ + await expect(serializeDirectoryContents(mockDir.path)).resolves.toEqual([ { path: 'a.txt', executable: false, @@ -127,18 +119,14 @@ describe('serializeDirectoryContents', () => { }); it('should pick up broken symlinks', async () => { - mockFs({ - root: { - 'b.txt': mockFs.symlink({ - path: './a.txt', - }), - }, + mockDir.setContent({ + 'b.txt': ctx => ctx.symlink('./a.txt'), }); - await expect(serializeDirectoryContents('root')).resolves.toEqual([ + await expect(serializeDirectoryContents(mockDir.path)).resolves.toEqual([ { path: 'b.txt', - executable: false, + executable: true, symlink: true, content: Buffer.from('./a.txt', 'utf8'), }, @@ -146,19 +134,15 @@ describe('serializeDirectoryContents', () => { }); it('should ignore symlinked folder files', async () => { - mockFs({ - root: { - 'a.txt': 'some text', - linkme: { - 'b.txt': 'lols', - }, - sym: mockFs.symlink({ - path: './linkme', - }), + mockDir.setContent({ + 'a.txt': 'some text', + linkme: { + 'b.txt': 'lols', }, + sym: ctx => ctx.symlink('./linkme'), }); - await expect(serializeDirectoryContents('root')).resolves.toEqual([ + await expect(serializeDirectoryContents(mockDir.path)).resolves.toEqual([ { path: 'a.txt', executable: false, @@ -175,16 +159,14 @@ describe('serializeDirectoryContents', () => { }); it('should ignore gitignored files', async () => { - mockFs({ - root: { - '.gitignore': '*.txt', - 'a.txt': 'a', - 'a.log': 'a', - }, + mockDir.setContent({ + '.gitignore': '*.txt', + 'a.txt': 'a', + 'a.log': 'a', }); await expect( - serializeDirectoryContents('root', { + serializeDirectoryContents(mockDir.path, { gitignore: true, }), ).resolves.toEqual([ @@ -204,26 +186,24 @@ describe('serializeDirectoryContents', () => { }); it('should use custom glob patterns', async () => { - mockFs({ - root: { - '.a': 'a', - 'a.log': 'a', - 'a.txt': 'a', - b: { - '.b': 'b', - 'b.log': 'b', - 'b.txt': 'b', - }, - c: { - '.c': 'c', - 'c.log': 'c', - 'c.txt': 'c', - }, + mockDir.setContent({ + '.a': 'a', + 'a.log': 'a', + 'a.txt': 'a', + b: { + '.b': 'b', + 'b.log': 'b', + 'b.txt': 'b', + }, + c: { + '.c': 'c', + 'c.log': 'c', + 'c.txt': 'c', }, }); await expect( - serializeDirectoryContents('root', { + serializeDirectoryContents(mockDir.path, { gitignore: true, globPatterns: ['**/*.txt', '*/.?', '*/*.log', '!c/**/.*', '!b/*.log'], }).then(files => files.sort((a, b) => a.path.localeCompare(b.path))), From e68b9bf85062538111caf4bdda864b655922595d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:40:31 +0200 Subject: [PATCH 86/95] use new MockDirectory symlink helper Signed-off-by: Patrik Oldsberg --- .../src/manager/plugin-manager.test.ts | 7 +-- .../src/scanner/plugin-scanner.test.ts | 48 +++++++------------ .../actions/builtin/fetch/template.test.ts | 27 ++++------- 3 files changed, 27 insertions(+), 55 deletions(-) diff --git a/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts b/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts index f356163dea..4aabd2c072 100644 --- a/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts +++ b/packages/backend-plugin-manager/src/manager/plugin-manager.test.ts @@ -452,16 +452,13 @@ describe('backend-plugin-manager', () => { findPaths(__dirname).resolveTargetRoot('package.json'), ), 'dynamic-plugins-root': {}, + 'dynamic-plugins-root/a-dynamic-plugin': ctx => + ctx.symlink(otherMockDir.resolve('a-dynamic-plugin')), }); otherMockDir.setContent({ 'a-dynamic-plugin': {}, }); - fs.symlinkSync( - otherMockDir.resolve('a-dynamic-plugin'), - mockDir.resolve('dynamic-plugins-root/a-dynamic-plugin'), - ); - const fromConfigSpier = jest.spyOn(PluginManager, 'fromConfig'); const applyConfigSpier = jest .spyOn(PluginScanner.prototype as any, 'applyConfig') diff --git a/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts b/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts index 04ce69ec98..d4ab8940c9 100644 --- a/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts +++ b/packages/backend-plugin-manager/src/scanner/plugin-scanner.test.ts @@ -19,10 +19,12 @@ import { JsonObject } from '@backstage/types'; import { Logs, MockedLogger } from '../__testUtils__/testUtils'; import { ConfigReader } from '@backstage/config'; import path from 'path'; -import fs from 'fs'; import * as url from 'url'; import { ScannedPluginPackage } from './types'; -import { createMockDirectory } from '@backstage/backend-test-utils'; +import { + MockDirectoryContent, + createMockDirectory, +} from '@backstage/backend-test-utils'; const mockDir = createMockDirectory(); @@ -233,8 +235,7 @@ Please add '${mockDir.resolve( type TestCase = { name: string; preferAlpha?: boolean; - fileSystem?: any; - symlinks?: { source: string; target: string }[]; + fileSystem?: MockDirectoryContent; expectedLogs?: Logs; expectedPluginPackages?: ScannedPluginPackage[]; expectedError?: string; @@ -286,7 +287,12 @@ Please add '${mockDir.resolve( name: 'backend plugin found in symlink', fileSystem: { backstageRoot: { - 'dist-dynamic': {}, + 'dist-dynamic': { + 'test-backend-plugin': ctx => + ctx.symlink( + mockDir.resolve('somewhere-else/test-backend-plugin-target'), + ), + }, }, 'somewhere-else': { 'test-backend-plugin-target': { @@ -299,16 +305,6 @@ Please add '${mockDir.resolve( }, }, }, - symlinks: [ - { - source: mockDir.resolve( - 'backstageRoot/dist-dynamic/test-backend-plugin', - ), - target: mockDir.resolve( - 'somewhere-else/test-backend-plugin-target', - ), - }, - ], expectedPluginPackages: [ { location: url.pathToFileURL( @@ -347,22 +343,17 @@ Please add '${mockDir.resolve( name: 'ignored folder child symlink: target is not a directory', fileSystem: { backstageRoot: { - 'dist-dynamic': {}, + 'dist-dynamic': { + 'test-backend-plugin': ctx => + ctx.symlink( + mockDir.resolve('somewhere-else/test-backend-plugin-target'), + ), + }, }, 'somewhere-else': { 'test-backend-plugin-target': '', }, }, - symlinks: [ - { - source: mockDir.resolve( - 'backstageRoot/dist-dynamic/test-backend-plugin', - ), - target: mockDir.resolve( - 'somewhere-else/test-backend-plugin-target', - ), - }, - ], expectedPluginPackages: [], expectedLogs: { infos: [ @@ -637,11 +628,6 @@ Please add '${mockDir.resolve( if (tc.fileSystem) { mockDir.setContent(tc.fileSystem); } - if (tc.symlinks) { - for (const { source, target } of tc.symlinks) { - fs.symlinkSync(target, source); - } - } if (tc.expectedError) { /* eslint-disable-next-line jest/no-conditional-expect */ expect(toTest).toThrow(tc.expectedError); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts index 727eb5ce0d..d396a00c26 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.test.ts @@ -19,7 +19,7 @@ jest.mock('@backstage/plugin-scaffolder-node', () => { return { ...actual, fetchContents: jest.fn() }; }); -import { join as joinPath, resolve as resolvePath, sep as pathSep } from 'path'; +import { join as joinPath, sep as pathSep } from 'path'; import fs from 'fs-extra'; import { getVoidLogger, @@ -272,27 +272,16 @@ describe('fetch:template', () => { }, '.${{ values.name }}': '${{ values.itemList | dump }}', 'a-binary-file.png': aBinaryFile, + 'an-executable.sh': ctx => + fs.writeFileSync(ctx.path, '#!/usr/bin/env bash', { + encoding: 'utf-8', + mode: parseInt('100755', 8), + }), + symlink: ctx => ctx.symlink('a-binary-file.png'), + brokenSymlink: ctx => ctx.symlink('./not-a-real-file.txt'), }, }); - fs.writeFileSync( - resolvePath(outputPath, 'an-executable.sh'), - '#!/usr/bin/env bash', - { - encoding: 'utf-8', - mode: parseInt('100755', 8), - }, - ); - - fs.symlinkSync( - 'a-binary-file.png', - resolvePath(outputPath, 'symlink'), - ); - fs.symlinkSync( - './not-a-real-file.txt', - resolvePath(outputPath, 'brokenSymlink'), - ); - return Promise.resolve(); }); From ea14da07779c8852ed20522433ccedb80c3fc9d6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:45:36 +0200 Subject: [PATCH 87/95] scaffolder-backend: refactor debug actions tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../builtin/debug/log.examples.test.ts | 21 ++++++++----------- .../actions/builtin/debug/log.test.ts | 17 +++++++-------- .../builtin/debug/wait.examples.test.ts | 15 ++++++------- .../actions/builtin/debug/wait.test.ts | 15 ++++++------- 4 files changed, 28 insertions(+), 40 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts index a7f5a1804a..901f3e5011 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts @@ -15,44 +15,41 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import mockFs from 'mock-fs'; -import os from 'os'; import { Writable } from 'stream'; import { createDebugLogAction } from './log'; import { join } from 'path'; import yaml from 'yaml'; import { examples } from './log.examples'; +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('debug:log examples', () => { const logStream = { write: jest.fn(), } as jest.Mocked> as jest.Mocked; - const mockTmpDir = os.tmpdir(); + const mockDir = createMockDirectory(); + const workspacePath = mockDir.resolve('workspace'); + const mockContext = { input: {}, baseUrl: 'somebase', - workspacePath: mockTmpDir, + workspacePath, logger: getVoidLogger(), logStream, output: jest.fn(), - createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), + createTemporaryDirectory: jest.fn(), }; const action = createDebugLogAction(); beforeEach(() => { - mockFs({ - [`${mockContext.workspacePath}/README.md`]: '', - [`${mockContext.workspacePath}/a-directory/index.md`]: '', + mockDir.setContent({ + [`${workspacePath}/README.md`]: '', + [`${workspacePath}/a-directory/index.md`]: '', }); jest.resetAllMocks(); }); - afterEach(() => { - mockFs.restore(); - }); - it('should log message', async () => { const context = { ...mockContext, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts index 6b7ad8816f..d6fc5174b3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts @@ -15,43 +15,40 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import mockFs from 'mock-fs'; -import os from 'os'; import { Writable } from 'stream'; import { createDebugLogAction } from './log'; import { join } from 'path'; import yaml from 'yaml'; +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('debug:log', () => { const logStream = { write: jest.fn(), } as jest.Mocked> as jest.Mocked; - const mockTmpDir = os.tmpdir(); + const mockDir = createMockDirectory(); + const workspacePath = mockDir.resolve('workspace'); + const mockContext = { input: {}, baseUrl: 'somebase', - workspacePath: mockTmpDir, + workspacePath, logger: getVoidLogger(), logStream, output: jest.fn(), - createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), + createTemporaryDirectory: jest.fn(), }; const action = createDebugLogAction(); beforeEach(() => { - mockFs({ + mockDir.setContent({ [`${mockContext.workspacePath}/README.md`]: '', [`${mockContext.workspacePath}/a-directory/index.md`]: '', }); jest.resetAllMocks(); }); - afterEach(() => { - mockFs.restore(); - }); - it('should do nothing', async () => { await action.handler(mockContext); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts index 595ae7c3b6..11cc83c696 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts @@ -15,12 +15,11 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import mockFs from 'mock-fs'; import { createWaitAction } from './wait'; import { Writable } from 'stream'; -import os from 'os'; import { examples } from './wait.examples'; import yaml from 'yaml'; +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('debug:wait examples', () => { const action = createWaitAction(); @@ -29,25 +28,23 @@ describe('debug:wait examples', () => { write: jest.fn(), } as jest.Mocked> as jest.Mocked; - const mockTmpDir = os.tmpdir(); + const mockDir = createMockDirectory(); + const workspacePath = mockDir.resolve('workspace'); + const mockContext = { input: {}, baseUrl: 'somebase', - workspacePath: mockTmpDir, + workspacePath, logger: getVoidLogger(), logStream, output: jest.fn(), - createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), + createTemporaryDirectory: jest.fn(), }; beforeEach(() => { jest.resetAllMocks(); }); - afterEach(() => { - mockFs.restore(); - }); - it('should wait for specified period of seconds', async () => { const context = { ...mockContext, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts index 0d4cdaba4b..6f80604a6d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts @@ -15,10 +15,9 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import mockFs from 'mock-fs'; import { createWaitAction } from './wait'; import { Writable } from 'stream'; -import os from 'os'; +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('debug:wait', () => { const action = createWaitAction(); @@ -27,25 +26,23 @@ describe('debug:wait', () => { write: jest.fn(), } as jest.Mocked> as jest.Mocked; - const mockTmpDir = os.tmpdir(); + const mockDir = createMockDirectory(); + const workspacePath = mockDir.resolve('workspace'); + const mockContext = { input: {}, baseUrl: 'somebase', - workspacePath: mockTmpDir, + workspacePath, logger: getVoidLogger(), logStream, output: jest.fn(), - createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), + createTemporaryDirectory: jest.fn(), }; beforeEach(() => { jest.resetAllMocks(); }); - afterEach(() => { - mockFs.restore(); - }); - it('should wait for specified period of time', async () => { const context = { ...mockContext, From 22f18a91e2651dc1129082d670ae213a03ffec90 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:48:12 +0200 Subject: [PATCH 88/95] scaffolder-backend: refactor template action example tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../builtin/fetch/template.examples.test.ts | 63 +++++++------------ 1 file changed, 21 insertions(+), 42 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts index 43bed5a048..a1c8381178 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts @@ -14,10 +14,8 @@ * limitations under the License. */ -import os from 'os'; import { join as joinPath, sep as pathSep } from 'path'; import fs from 'fs-extra'; -import mockFs from 'mock-fs'; import { getVoidLogger, resolvePackagePath, @@ -33,6 +31,7 @@ import { } from '@backstage/plugin-scaffolder-node'; import { examples } from './template.examples'; import yaml from 'yaml'; +import { createMockDirectory } from '@backstage/backend-test-utils'; jest.mock('@backstage/plugin-scaffolder-node', () => ({ ...jest.requireActual('@backstage/plugin-scaffolder-node'), @@ -45,16 +44,6 @@ type FetchTemplateInput = ReturnType< ? U : never; -const realFiles = Object.fromEntries( - [ - resolvePackagePath( - '@backstage/plugin-scaffolder-backend', - 'assets', - 'nunjucks.js.txt', - ), - ].map(k => [k, mockFs.load(k)]), -); - const aBinaryFile = fs.readFileSync( resolvePackagePath( '@backstage/plugin-scaffolder-backend', @@ -69,14 +58,8 @@ const mockFetchContents = fetchContents as jest.MockedFunction< describe('fetch:template examples', () => { let action: TemplateAction; - const workspacePath = os.tmpdir(); - const createTemporaryDirectory: jest.MockedFunction< - ActionContext['createTemporaryDirectory'] - > = jest.fn(() => - Promise.resolve( - joinPath(workspacePath, `${createTemporaryDirectory.mock.calls.length}`), - ), - ); + const mockDir = createMockDirectory(); + const workspacePath = mockDir.resolve('workspace'); const logger = getVoidLogger(); @@ -90,24 +73,20 @@ describe('fetch:template examples', () => { logStream: new PassThrough(), logger, workspacePath, - createTemporaryDirectory, + + async createTemporaryDirectory() { + return fs.mkdtemp(mockDir.resolve('tmp-')); + }, }); beforeEach(() => { - mockFs({ - ...realFiles, - }); - + mockDir.clear(); action = createFetchTemplateAction({ reader: Symbol('UrlReader') as unknown as UrlReader, integrations: Symbol('Integrations') as unknown as ScmIntegrations, }); }); - afterEach(() => { - mockFs.restore(); - }); - describe('handler', () => { describe('with valid input', () => { let context: ActionContext; @@ -116,13 +95,13 @@ describe('fetch:template examples', () => { context = mockContext(yaml.parse(examples[0].example).steps[0].input); mockFetchContents.mockImplementation(({ outputPath }) => { - mockFs({ - ...realFiles, + mockDir.setContent({ [outputPath]: { - 'an-executable.sh': mockFs.file({ - content: '#!/usr/bin/env bash', - mode: parseInt('100755', 8), - }), + 'an-executable.sh': ctx => + fs.writeFileSync(ctx.path, '#!/usr/bin/env bash', { + encoding: 'utf8', + mode: parseInt('100755', 8), + }), 'empty-dir-${{ values.count }}': {}, 'static.txt': 'static content', '${{ values.name }}.txt': 'static content', @@ -132,12 +111,8 @@ describe('fetch:template examples', () => { }, '.${{ values.name }}': '${{ values.itemList | dump }}', 'a-binary-file.png': aBinaryFile, - symlink: mockFs.symlink({ - path: 'a-binary-file.png', - }), - brokenSymlink: mockFs.symlink({ - path: './not-a-real-file.txt', - }), + symlink: ctx => ctx.symlink('a-binary-file.png'), + brokenSymlink: ctx => ctx.symlink('./not-a-real-file.txt'), }, }); @@ -212,7 +187,11 @@ describe('fetch:template examples', () => { await expect( fs.realpath(`${workspacePath}/target/symlink`), - ).resolves.toBe(joinPath(workspacePath, 'target', 'a-binary-file.png')); + ).resolves.toBe( + fs.realpathSync( + joinPath(workspacePath, 'target', 'a-binary-file.png'), + ), + ); }); it('copies broken symlinks as-is without processing them', async () => { From 9b41a43089ae86f293b07d6156e5f36337fe4cd6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:51:09 +0200 Subject: [PATCH 89/95] scaffolder-backend: refactor github action tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../builtin/publish/githubPullRequest.test.ts | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts index b6987335f9..a1f3712a9d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/githubPullRequest.test.ts @@ -24,21 +24,17 @@ import { ActionContext, TemplateAction, } from '@backstage/plugin-scaffolder-node'; -import mockFs from 'mock-fs'; -import os from 'os'; -import { resolve as resolvePath } from 'path'; +import fs from 'fs-extra'; import { Writable } from 'stream'; import { createPublishGithubPullRequestAction, OctokitWithPullRequestPluginClient, } from './githubPullRequest'; +import { createMockDirectory } from '@backstage/backend-test-utils'; // Make sure root logger is initialized ahead of FS mock createRootLogger(); -const root = os.platform() === 'win32' ? 'C:\\root' : '/root'; -const workspacePath = resolvePath(root, 'my-workspace'); - type GithubPullRequestActionInput = ReturnType< typeof createPublishGithubPullRequestAction > extends TemplateAction @@ -54,7 +50,12 @@ describe('createPublishGithubPullRequestAction', () => { }; }; + const mockDir = createMockDirectory(); + const workspacePath = mockDir.resolve('workspace'); + beforeEach(() => { + mockDir.clear(); + const integrations = ScmIntegrations.fromConfig(new ConfigReader({})); fakeClient = { createPullRequest: jest.fn(async (_: any) => { @@ -92,7 +93,6 @@ describe('createPublishGithubPullRequestAction', () => { }); afterEach(() => { - mockFs.restore(); jest.resetAllMocks(); }); @@ -132,7 +132,7 @@ describe('createPublishGithubPullRequestAction', () => { draft: true, }; - mockFs({ + mockDir.setContent({ [workspacePath]: { 'file.txt': 'Hello there!' }, }); @@ -197,7 +197,7 @@ describe('createPublishGithubPullRequestAction', () => { draft: true, }; - mockFs({ + mockDir.setContent({ [workspacePath]: { 'file.txt': 'Hello there!' }, }); @@ -261,7 +261,7 @@ describe('createPublishGithubPullRequestAction', () => { sourcePath: 'source', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -323,7 +323,7 @@ describe('createPublishGithubPullRequestAction', () => { description: 'This PR is really good', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { 'file.txt': 'Hello there!' }, }); @@ -385,7 +385,7 @@ describe('createPublishGithubPullRequestAction', () => { teamReviewers: ['team-foo'], }; - mockFs({ [workspacePath]: {} }); + mockDir.setContent({ [workspacePath]: {} }); ctx = { createTemporaryDirectory: jest.fn(), @@ -437,7 +437,7 @@ describe('createPublishGithubPullRequestAction', () => { description: 'This PR is really good', }; - mockFs({ [workspacePath]: {} }); + mockDir.setContent({ [workspacePath]: {} }); ctx = { createTemporaryDirectory: jest.fn(), @@ -469,11 +469,9 @@ describe('createPublishGithubPullRequestAction', () => { description: 'This PR is really good', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { - Makefile: mockFs.symlink({ - path: '../../nothing/yet', - }), + Makefile: c => c.symlink('../../nothing/yet'), }, }); @@ -523,12 +521,13 @@ describe('createPublishGithubPullRequestAction', () => { description: 'This PR is really good', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { - 'hello.sh': mockFs.file({ - content: 'echo Hello there!', - mode: 0o100755, - }), + 'hello.sh': c => + fs.writeFileSync(c.path, 'echo Hello there!', { + encoding: 'utf8', + mode: 0o100755, + }), }, }); @@ -588,12 +587,13 @@ describe('createPublishGithubPullRequestAction', () => { description: 'This PR is really good', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { - 'hello.sh': mockFs.file({ - content: 'echo Hello there!', - mode: 0o100775, - }), + 'hello.sh': c => + fs.writeFileSync(c.path, 'echo Hello there!', { + encoding: 'utf8', + mode: 0o100775, + }), }, }); @@ -654,7 +654,7 @@ describe('createPublishGithubPullRequestAction', () => { commitMessage: 'Create my new app, but in the commit message', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { 'file.txt': 'Hello there!' }, }); From 98407789cb1cdb1084f32159bf29020d0ba88611 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:52:07 +0200 Subject: [PATCH 90/95] scaffolder-backend: refactor gitlab action tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../publish/gitlabMergeRequest.test.ts | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts index 6445d243b5..71a2273aed 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.test.ts @@ -17,18 +17,13 @@ import { createRootLogger, getRootLogger } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; -import mockFs from 'mock-fs'; -import os from 'os'; -import { resolve as resolvePath } from 'path'; import { Writable } from 'stream'; import { createPublishGitlabMergeRequestAction } from './gitlabMergeRequest'; +import { createMockDirectory } from '@backstage/backend-test-utils'; // Make sure root logger is initialized ahead of FS mock createRootLogger(); -const root = os.platform() === 'win32' ? 'C:\\root' : '/root'; -const workspacePath = resolvePath(root, 'my-workspace'); - const mockGitlabClient = { Namespaces: { show: jest.fn(), @@ -79,7 +74,12 @@ jest.mock('@gitbeaker/node', () => ({ describe('createGitLabMergeRequest', () => { let instance: TemplateAction; + const mockDir = createMockDirectory(); + const workspacePath = mockDir.resolve('workspace'); + beforeEach(() => { + mockDir.clear(); + const config = new ConfigReader({ integrations: { gitlab: [ @@ -100,10 +100,6 @@ describe('createGitLabMergeRequest', () => { instance = createPublishGitlabMergeRequestAction({ integrations }); }); - afterEach(() => { - mockFs.restore(); - }); - describe('createGitLabMergeRequestWithSpecifiedTargetBranch', () => { it('removeSourceBranch is false by default when not passed in options', async () => { const input = { @@ -114,7 +110,7 @@ describe('createGitLabMergeRequest', () => { description: 'This MR is really good', targetPath: 'Subdirectory', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -156,7 +152,7 @@ describe('createGitLabMergeRequest', () => { description: 'This MR is really good', targetPath: 'Subdirectory', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -200,7 +196,7 @@ describe('createGitLabMergeRequest', () => { removeSourceBranch: true, targetPath: 'Subdirectory', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -235,7 +231,7 @@ describe('createGitLabMergeRequest', () => { removeSourceBranch: false, targetPath: 'Subdirectory', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -276,7 +272,7 @@ describe('createGitLabMergeRequest', () => { targetPath: 'Subdirectory', assignee: 'John Smith', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -316,7 +312,7 @@ describe('createGitLabMergeRequest', () => { targetPath: 'Subdirectory', assingnee: 'John Doe', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -356,7 +352,7 @@ describe('createGitLabMergeRequest', () => { removeSourceBranch: false, targetPath: 'Subdirectory', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -395,7 +391,7 @@ describe('createGitLabMergeRequest', () => { targetPath: 'Subdirectory', assignee: 'Unknown', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -431,7 +427,7 @@ describe('createGitLabMergeRequest', () => { branchName: 'new-mr', description: 'This MR is really good', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -480,7 +476,7 @@ describe('createGitLabMergeRequest', () => { description: 'This MR is really good', targetPath: 'source', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -523,7 +519,7 @@ describe('createGitLabMergeRequest', () => { commitAction: 'create', targetPath: 'source', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -565,7 +561,7 @@ describe('createGitLabMergeRequest', () => { commitAction: 'update', targetPath: 'source', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -607,7 +603,7 @@ describe('createGitLabMergeRequest', () => { commitAction: 'delete', targetPath: 'source', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -652,7 +648,7 @@ describe('createGitLabMergeRequest', () => { commitAction: 'create', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, @@ -697,7 +693,7 @@ describe('createGitLabMergeRequest', () => { commitAction: 'create', }; - mockFs({ + mockDir.setContent({ [workspacePath]: { source: { 'foo.txt': 'Hello there!' }, irrelevant: { 'bar.txt': 'Nothing to see here' }, From 1159058cb164aefca95c12e9cd9ad7d4354ad339 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:54:27 +0200 Subject: [PATCH 91/95] scaffolder-backend: refactor NunjucksWorkflowRunner tests to avoid mock-fs Signed-off-by: Patrik Oldsberg --- .../tasks/NunjucksWorkflowRunner.test.ts | 33 ++++--------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index d94a8eac69..6907f8cc2c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import mockFs from 'mock-fs'; -import * as winston from 'winston'; - -import { getVoidLogger, resolvePackagePath } from '@backstage/backend-common'; +import { getVoidLogger } from '@backstage/backend-common'; import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner'; import { TemplateActionRegistry } from '../actions'; import { ScmIntegrations } from '@backstage/integration'; @@ -36,19 +33,7 @@ import { PermissionEvaluator, } from '@backstage/plugin-permission-common'; import { RESOURCE_TYPE_SCAFFOLDER_ACTION } from '@backstage/plugin-scaffolder-common/alpha'; - -// The Stream module is lazy loaded, so make sure it's in the module cache before mocking fs -void winston.transports.Stream; - -const realFiles = Object.fromEntries( - [ - resolvePackagePath( - '@backstage/plugin-scaffolder-backend', - 'assets', - 'nunjucks.js.txt', - ), - ].map(k => [k, mockFs.load(k)]), -); +import { createMockDirectory } from '@backstage/backend-test-utils'; describe('DefaultWorkflowRunner', () => { const logger = getVoidLogger(); @@ -56,6 +41,8 @@ describe('DefaultWorkflowRunner', () => { let runner: NunjucksWorkflowRunner; let fakeActionHandler: jest.Mock; + const mockDir = createMockDirectory(); + const mockedPermissionApi: jest.Mocked = { authorizeConditional: jest.fn(), } as unknown as jest.Mocked; @@ -84,11 +71,7 @@ describe('DefaultWorkflowRunner', () => { }); beforeEach(() => { - winston.format.simple(); // put logform in the require.cache before mocking fs - mockFs({ - '/tmp': mockFs.directory(), - ...realFiles, - }); + mockDir.clear(); jest.resetAllMocks(); actionRegistry = new TemplateActionRegistry(); @@ -148,16 +131,12 @@ describe('DefaultWorkflowRunner', () => { runner = new NunjucksWorkflowRunner({ actionRegistry, integrations, - workingDirectory: '/tmp', + workingDirectory: mockDir.path, logger, permissions: mockedPermissionApi, }); }); - afterEach(() => { - mockFs.restore(); - }); - it('should throw an error if the action does not exist', async () => { const task = createMockTaskWithSpec({ apiVersion: 'scaffolder.backstage.io/v1beta3', From 94e3cb2d6d901875158782bf5440ab96ab7cccdc Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 5 Oct 2023 21:56:23 +0200 Subject: [PATCH 92/95] scaffolder-backend: remove mock-fs dependency Signed-off-by: Patrik Oldsberg --- plugins/scaffolder-backend/package.json | 2 -- yarn.lock | 2 -- 2 files changed, 4 deletions(-) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index cab9abae96..4ff894cd61 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -107,13 +107,11 @@ "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", "@types/libsodium-wrappers": "^0.7.10", - "@types/mock-fs": "^4.13.0", "@types/nunjucks": "^3.1.4", "@types/supertest": "^2.0.8", "@types/zen-observable": "^0.8.0", "esbuild": "^0.19.0", "jest-when": "^3.1.0", - "mock-fs": "^5.2.0", "msw": "^1.0.0", "supertest": "^6.1.3", "wait-for-expect": "^3.0.2", diff --git a/yarn.lock b/yarn.lock index 3bd1e9d44e..013950f540 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8722,7 +8722,6 @@ __metadata: "@types/git-url-parse": ^9.0.0 "@types/libsodium-wrappers": ^0.7.10 "@types/luxon": ^3.0.0 - "@types/mock-fs": ^4.13.0 "@types/nunjucks": ^3.1.4 "@types/supertest": ^2.0.8 "@types/zen-observable": ^0.8.0 @@ -8745,7 +8744,6 @@ __metadata: libsodium-wrappers: ^0.7.11 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 From 06432f900c846935f8ed5d23fff2f170c4c578ea Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 6 Oct 2023 15:25:10 +0200 Subject: [PATCH 93/95] frontend-plugin-api: refactor extension "at" option to "attachTo" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Camila Belo Co-authored-by: Fredrik Adelöw Co-authored-by: Vincenzo Scamporlino Signed-off-by: Patrik Oldsberg --- .changeset/eight-suns-tan.md | 5 ++ .changeset/plenty-toys-cheer.md | 6 ++ .changeset/small-books-deliver.md | 5 ++ .../frontend-app-api/src/extensions/Core.tsx | 2 +- .../src/extensions/CoreLayout.tsx | 2 +- .../src/extensions/CoreNav.tsx | 2 +- .../src/extensions/CoreRoutes.tsx | 2 +- .../extractRouteInfoFromInstanceTree.test.ts | 56 ++++++++++--------- .../src/wiring/createApp.test.tsx | 6 +- .../frontend-app-api/src/wiring/createApp.tsx | 4 +- .../wiring/createExtensionInstance.test.ts | 18 +++--- .../src/wiring/parameters.test.ts | 50 +++++++++++------ .../frontend-app-api/src/wiring/parameters.ts | 46 ++++++++++----- packages/frontend-plugin-api/api-report.md | 15 ++++- .../src/extensions/createApiExtension.test.ts | 4 +- .../src/extensions/createApiExtension.ts | 2 +- .../src/extensions/createNavItemExtension.tsx | 2 +- .../extensions/createPageExtension.test.tsx | 8 +-- .../src/extensions/createPageExtension.tsx | 4 +- .../src/extensions/createThemeExtension.ts | 2 +- .../src/wiring/createExtension.test.ts | 6 +- .../src/wiring/createExtension.ts | 4 +- .../src/wiring/createPlugin.test.ts | 10 ++-- plugins/graphiql/src/alpha.tsx | 2 +- plugins/search-react/alpha-api-report.md | 5 +- plugins/search-react/src/alpha.test.tsx | 4 +- plugins/search-react/src/alpha.tsx | 4 +- 27 files changed, 170 insertions(+), 106 deletions(-) create mode 100644 .changeset/eight-suns-tan.md create mode 100644 .changeset/plenty-toys-cheer.md create mode 100644 .changeset/small-books-deliver.md diff --git a/.changeset/eight-suns-tan.md b/.changeset/eight-suns-tan.md new file mode 100644 index 0000000000..4b8634a9e1 --- /dev/null +++ b/.changeset/eight-suns-tan.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': minor +--- + +Extension attachment point is now configured via `attachTo: { id, input }` instead of `at: 'id/input'`. diff --git a/.changeset/plenty-toys-cheer.md b/.changeset/plenty-toys-cheer.md new file mode 100644 index 0000000000..3c16002632 --- /dev/null +++ b/.changeset/plenty-toys-cheer.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-search-react': patch +'@backstage/plugin-graphiql': patch +--- + +Updated `/alpha` exports to use new `attachTo` option. diff --git a/.changeset/small-books-deliver.md b/.changeset/small-books-deliver.md new file mode 100644 index 0000000000..0d09f7ae1c --- /dev/null +++ b/.changeset/small-books-deliver.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Updates for `at` -> `attachTo` refactor. diff --git a/packages/frontend-app-api/src/extensions/Core.tsx b/packages/frontend-app-api/src/extensions/Core.tsx index 4cb21a8702..4a66afe38d 100644 --- a/packages/frontend-app-api/src/extensions/Core.tsx +++ b/packages/frontend-app-api/src/extensions/Core.tsx @@ -22,7 +22,7 @@ import { export const Core = createExtension({ id: 'core', - at: 'root', + attachTo: { id: 'root', input: 'default' }, inputs: { apis: createExtensionInput({ api: coreExtensionData.apiFactory, diff --git a/packages/frontend-app-api/src/extensions/CoreLayout.tsx b/packages/frontend-app-api/src/extensions/CoreLayout.tsx index 0c5c0446fc..9b0b8b25d1 100644 --- a/packages/frontend-app-api/src/extensions/CoreLayout.tsx +++ b/packages/frontend-app-api/src/extensions/CoreLayout.tsx @@ -24,7 +24,7 @@ import { SidebarPage } from '@backstage/core-components'; export const CoreLayout = createExtension({ id: 'core.layout', - at: 'root', + attachTo: { id: 'root', input: 'default' }, inputs: { nav: createExtensionInput( { diff --git a/packages/frontend-app-api/src/extensions/CoreNav.tsx b/packages/frontend-app-api/src/extensions/CoreNav.tsx index 71ab71a6aa..8e8aa638c0 100644 --- a/packages/frontend-app-api/src/extensions/CoreNav.tsx +++ b/packages/frontend-app-api/src/extensions/CoreNav.tsx @@ -73,7 +73,7 @@ const SidebarNavItem = (props: NavTarget) => { export const CoreNav = createExtension({ id: 'core.nav', - at: 'core.layout/nav', + attachTo: { id: 'core.layout', input: 'nav' }, inputs: { items: createExtensionInput({ target: coreExtensionData.navTarget, diff --git a/packages/frontend-app-api/src/extensions/CoreRoutes.tsx b/packages/frontend-app-api/src/extensions/CoreRoutes.tsx index 5c79b56f43..5006a87cac 100644 --- a/packages/frontend-app-api/src/extensions/CoreRoutes.tsx +++ b/packages/frontend-app-api/src/extensions/CoreRoutes.tsx @@ -24,7 +24,7 @@ import { useRoutes } from 'react-router-dom'; export const CoreRoutes = createExtension({ id: 'core.routes', - at: 'core.layout/content', + attachTo: { id: 'core.layout', input: 'content' }, inputs: { routes: createExtensionInput({ path: coreExtensionData.routePath, diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.test.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.test.ts index c64dc7a3b9..a510adf1b7 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.test.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromInstanceTree.test.ts @@ -40,13 +40,15 @@ const refOrder = [ref1, ref2, ref3, ref4, ref5]; function createTestExtension(options: { id: string; - at?: string; + parent?: string; path?: string; routeRef?: RouteRef; }) { return createExtension({ id: options.id, - at: options.at ?? 'core.routes/children', + attachTo: options.parent + ? { id: options.parent, input: 'children' } + : { id: 'core.routes', input: 'children' }, output: { element: coreExtensionData.reactElement, path: coreExtensionData.routePath.optional(), @@ -126,13 +128,13 @@ describe('discovery', () => { }), createTestExtension({ id: 'page2', - at: 'page1/children', + parent: 'page1', path: 'bar/:id', routeRef: ref2, }), createTestExtension({ id: 'page3', - at: 'page2/children', + parent: 'page2', path: 'baz', routeRef: ref3, }), @@ -143,7 +145,7 @@ describe('discovery', () => { }), createTestExtension({ id: 'page5', - at: 'page1/children', + parent: 'page1', path: 'blop', routeRef: ref5, }), @@ -194,7 +196,7 @@ describe('discovery', () => { }), createTestExtension({ id: 'page2', - at: 'page1/children', + parent: 'page1', path: 'bar/:id', routeRef: ref2, }), @@ -205,13 +207,13 @@ describe('discovery', () => { }), createTestExtension({ id: 'page4', - at: 'page3/children', + parent: 'page3', path: 'divsoup', routeRef: ref4, }), createTestExtension({ id: 'page5', - at: 'page3/children', + parent: 'page3', path: 'blop', routeRef: ref5, }), @@ -242,7 +244,7 @@ describe('discovery', () => { }), createTestExtension({ id: 'page2', - at: 'page1/children', + parent: 'page1', path: '/bar/:id', routeRef: ref2, }), @@ -253,13 +255,13 @@ describe('discovery', () => { }), createTestExtension({ id: 'page4', - at: 'page3/children', + parent: 'page3', path: '/divsoup', routeRef: ref4, }), createTestExtension({ id: 'page5', - at: 'page3/children', + parent: 'page3', path: '/blop', routeRef: ref5, }), @@ -289,16 +291,16 @@ describe('discovery', () => { }), createTestExtension({ id: 'page1', - at: 'foo/children', + parent: 'foo', routeRef: ref1, }), createTestExtension({ id: 'fooChild', - at: 'foo/children', + parent: 'foo', }), createTestExtension({ id: 'page2', - at: 'fooChild/children', + parent: 'fooChild', routeRef: ref2, }), createTestExtension({ @@ -311,17 +313,17 @@ describe('discovery', () => { }), createTestExtension({ id: 'page3Child', - at: 'page3/children', + parent: 'page3', path: '', }), createTestExtension({ id: 'page4', - at: 'page3Child/children', + parent: 'page3Child', routeRef: ref4, }), createTestExtension({ id: 'page5', - at: 'page4/children', + parent: 'page4', routeRef: ref5, }), ]); @@ -361,29 +363,29 @@ describe('discovery', () => { }), createTestExtension({ id: 'page1Child', - at: 'page1/children', + parent: 'page1', path: 'bar', }), createTestExtension({ id: 'page2', - at: 'page1Child/children', + parent: 'page1Child', routeRef: ref2, }), createTestExtension({ id: 'page3', - at: 'page2/children', + parent: 'page2', path: 'baz', routeRef: ref3, }), createTestExtension({ id: 'page4', - at: 'page3/children', + parent: 'page3', path: '/blop', routeRef: ref4, }), createTestExtension({ id: 'page5', - at: 'page2/children', + parent: 'page2', routeRef: ref5, }), ]); @@ -445,30 +447,30 @@ describe('discovery', () => { }), createTestExtension({ id: 'page1', - at: 'r/children', + parent: 'r', path: 'x', routeRef: ref1, }), createTestExtension({ id: 'y', path: 'y', - at: 'r/children', + parent: 'r', }), createTestExtension({ id: 'page2', - at: 'y/children', + parent: 'y', path: '1', routeRef: ref2, }), createTestExtension({ id: 'page3', - at: 'page2/children', + parent: 'page2', path: 'a', routeRef: ref3, }), createTestExtension({ id: 'page4', - at: 'page2/children', + parent: 'page2', path: 'b', routeRef: ref4, }), diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx index f6172f5a4b..14e52af469 100644 --- a/packages/frontend-app-api/src/wiring/createApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx @@ -32,9 +32,7 @@ describe('createInstances', () => { app: { extensions: [ { - root: { - at: '', - }, + root: {}, }, ], }, @@ -58,7 +56,7 @@ describe('createInstances', () => { extensions: [ createExtension({ id: 'root', - at: 'core.routes/route', + attachTo: { id: 'core.routes', input: 'route' }, inputs: {}, output: {}, factory() {}, diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index 540b47849f..e40c943e23 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -202,8 +202,8 @@ export function createInstances(options: { Map >(); for (const instanceParams of extensionParams) { - const [extensionId, pointId = 'default'] = instanceParams.at.split('/'); - + const extensionId = instanceParams.attachTo.id; + const pointId = instanceParams.attachTo.input; let pointMap = attachmentMap.get(extensionId); if (!pointMap) { pointMap = new Map(); diff --git a/packages/frontend-app-api/src/wiring/createExtensionInstance.test.ts b/packages/frontend-app-api/src/wiring/createExtensionInstance.test.ts index 12e109874a..6550d76608 100644 --- a/packages/frontend-app-api/src/wiring/createExtensionInstance.test.ts +++ b/packages/frontend-app-api/src/wiring/createExtensionInstance.test.ts @@ -28,7 +28,7 @@ const inputMirrorDataRef = createExtensionDataRef('mirror'); const simpleExtension = createExtension({ id: 'core.test', - at: 'ignored', + attachTo: { id: 'ignored', input: 'ignored' }, output: { test: testDataRef, other: otherDataRef.optional(), @@ -101,7 +101,7 @@ describe('createExtensionInstance', () => { config: undefined, extension: createExtension({ id: 'core.test', - at: 'ignored', + attachTo: { id: 'ignored', input: 'ignored' }, inputs: { optionalSingletonPresent: createExtensionInput( { @@ -166,7 +166,7 @@ describe('createExtensionInstance', () => { config: { other: 'not-a-number' }, extension: createExtension({ id: 'core.test', - at: 'ignored', + attachTo: { id: 'ignored', input: 'ignored' }, output: {}, factory() { const error = new Error('NOPE'); @@ -188,7 +188,7 @@ describe('createExtensionInstance', () => { config: undefined, extension: createExtension({ id: 'core.test', - at: 'ignored', + attachTo: { id: 'ignored', input: 'ignored' }, output: { test1: testDataRef, test2: testDataRef, @@ -211,7 +211,7 @@ describe('createExtensionInstance', () => { config: undefined, extension: createExtension({ id: 'core.test', - at: 'ignored', + attachTo: { id: 'ignored', input: 'ignored' }, output: { test: testDataRef, }, @@ -232,7 +232,7 @@ describe('createExtensionInstance', () => { config: undefined, extension: createExtension({ id: 'core.test', - at: 'ignored', + attachTo: { id: 'ignored', input: 'ignored' }, inputs: { singleton: createExtensionInput( { @@ -273,7 +273,7 @@ describe('createExtensionInstance', () => { config: undefined, extension: createExtension({ id: 'core.test', - at: 'ignored', + attachTo: { id: 'ignored', input: 'ignored' }, inputs: { singleton: createExtensionInput( { @@ -314,7 +314,7 @@ describe('createExtensionInstance', () => { config: undefined, extension: createExtension({ id: 'core.test', - at: 'ignored', + attachTo: { id: 'ignored', input: 'ignored' }, inputs: { singleton: createExtensionInput( { @@ -350,7 +350,7 @@ describe('createExtensionInstance', () => { config: undefined, extension: createExtension({ id: 'core.test', - at: 'ignored', + attachTo: { id: 'ignored', input: 'ignored' }, inputs: { singleton: createExtensionInput( { diff --git a/packages/frontend-app-api/src/wiring/parameters.test.ts b/packages/frontend-app-api/src/wiring/parameters.test.ts index 10b0e2fc6f..97ae941744 100644 --- a/packages/frontend-app-api/src/wiring/parameters.test.ts +++ b/packages/frontend-app-api/src/wiring/parameters.test.ts @@ -26,7 +26,7 @@ import { function makeExt(id: string, status: 'disabled' | 'enabled' = 'enabled') { return { id, - at: 'root', + attachTo: { id: 'root', input: 'default' }, disabled: status === 'disabled', } as Extension; } @@ -52,8 +52,8 @@ describe('mergeExtensionParameters', () => { parameters: [], }), ).toEqual([ - { extension: a, at: 'root' }, - { extension: b, at: 'root' }, + { extension: a, attachTo: { id: 'root', input: 'default' } }, + { extension: b, attachTo: { id: 'root', input: 'default' } }, ]); }); @@ -68,13 +68,17 @@ describe('mergeExtensionParameters', () => { parameters: [ { id: 'b', - at: 'derp', + attachTo: { id: 'derp', input: 'default' }, }, ], }), ).toEqual([ - { extension: a, at: 'root', source: pluginA }, - { extension: b, at: 'derp' }, + { + extension: a, + attachTo: { id: 'root', input: 'default' }, + source: pluginA, + }, + { extension: b, attachTo: { id: 'derp', input: 'default' } }, ]); }); @@ -102,8 +106,18 @@ describe('mergeExtensionParameters', () => { ], }), ).toEqual([ - { extension: a, at: 'root', source: plugin, config: { foo: { bar: 1 } } }, - { extension: b, at: 'root', source: plugin, config: { foo: { qux: 3 } } }, + { + extension: a, + attachTo: { id: 'root', input: 'default' }, + source: plugin, + config: { foo: { bar: 1 } }, + }, + { + extension: b, + attachTo: { id: 'root', input: 'default' }, + source: plugin, + config: { foo: { qux: 3 } }, + }, ]); }); @@ -126,8 +140,8 @@ describe('mergeExtensionParameters', () => { ], }), ).toEqual([ - { extension: b, at: 'root' }, - { extension: a, at: 'root' }, + { extension: b, attachTo: { id: 'root', input: 'default' } }, + { extension: a, attachTo: { id: 'root', input: 'default' } }, ]); }); }); @@ -315,14 +329,18 @@ describe('expandShorthandExtensionParameters', () => { expect(() => run({ 'core.router': { id: 'some.id' } }), ).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'at', 'disabled', 'config'"`, + `"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`, ); }); - it('supports object at', () => { - expect(run({ 'core.router': { at: 'other.root/inputs' } })).toEqual({ + it('supports object attachTo', () => { + expect( + run({ + 'core.router': { attachTo: { id: 'other.root', input: 'inputs' } }, + }), + ).toEqual({ id: 'core.router', - at: 'other.root/inputs', + attachTo: { id: 'other.root', input: 'inputs' }, }); expect(() => run({ @@ -331,7 +349,7 @@ describe('expandShorthandExtensionParameters', () => { }, }), ).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'at', 'disabled', 'config'"`, + `"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`, ); }); @@ -369,7 +387,7 @@ describe('expandShorthandExtensionParameters', () => { expect(() => run({ 'core.router': { foo: { settings: true } } }), ).toThrowErrorMatchingInlineSnapshot( - `"Invalid extension configuration at app.extensions[1][core.router].foo, unknown parameter; expected one of 'at', 'disabled', 'config'"`, + `"Invalid extension configuration at app.extensions[1][core.router].foo, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`, ); }); }); diff --git a/packages/frontend-app-api/src/wiring/parameters.ts b/packages/frontend-app-api/src/wiring/parameters.ts index efe0fbf678..9474a01d79 100644 --- a/packages/frontend-app-api/src/wiring/parameters.ts +++ b/packages/frontend-app-api/src/wiring/parameters.ts @@ -20,12 +20,12 @@ import { JsonValue } from '@backstage/types'; export interface ExtensionParameters { id: string; - at?: string; + attachTo?: { id: string; input: string }; disabled?: boolean; config?: unknown; } -const knownExtensionParameters = ['at', 'disabled', 'config']; +const knownExtensionParameters = ['attachTo', 'disabled', 'config']; // Since we'll never merge arrays in config the config reader context // isn't too much of a help. Fall back to manual config reading logic @@ -143,15 +143,33 @@ export function expandShorthandExtensionParameters( throw new Error(errorMsg('value must be a boolean or object', id)); } - const at = value.at; + const attachTo = value.attachTo as { id: string; input: string } | undefined; const disabled = value.disabled; const config = value.config; - if (at !== undefined && typeof at !== 'string') { - throw new Error(errorMsg('must be a string', id, 'at')); - } else if (disabled !== undefined && typeof disabled !== 'boolean') { + if (attachTo !== undefined) { + if ( + attachTo === null || + typeof attachTo !== 'object' || + Array.isArray(attachTo) + ) { + throw new Error(errorMsg('must be an object', id, 'attachTo')); + } + if (typeof attachTo.id !== 'string' || attachTo.id === '') { + throw new Error( + errorMsg('must be a non-empty string', id, 'attachTo.id'), + ); + } + if (typeof attachTo.input !== 'string' || attachTo.input === '') { + throw new Error( + errorMsg('must be a non-empty string', id, 'attachTo.input'), + ); + } + } + if (disabled !== undefined && typeof disabled !== 'boolean') { throw new Error(errorMsg('must be a boolean', id, 'disabled')); - } else if ( + } + if ( config !== undefined && (typeof config !== 'object' || config === null || Array.isArray(config)) ) { @@ -175,7 +193,7 @@ export function expandShorthandExtensionParameters( return { id, - at, + attachTo, disabled, config, }; @@ -184,7 +202,7 @@ export function expandShorthandExtensionParameters( export interface ExtensionInstanceParameters { extension: Extension; source?: BackstagePlugin; - at: string; + attachTo: { id: string; input: string }; config?: unknown; } @@ -217,7 +235,7 @@ export function mergeExtensionParameters(options: { extension, params: { source, - at: extension.at, + attachTo: extension.attachTo, disabled: extension.disabled, config: undefined as unknown, }, @@ -226,7 +244,7 @@ export function mergeExtensionParameters(options: { extension, params: { source: undefined, - at: extension.at, + attachTo: extension.attachTo, disabled: extension.disabled, config: undefined as unknown, }, @@ -283,8 +301,8 @@ export function mergeExtensionParameters(options: { ); if (existingIndex !== -1) { const existing = overrides[existingIndex]; - if (overrideParam.at) { - existing.params.at = overrideParam.at; + if (overrideParam.attachTo) { + existing.params.attachTo = overrideParam.attachTo; } if (overrideParam.config) { // TODO: merge config? @@ -309,7 +327,7 @@ export function mergeExtensionParameters(options: { .filter(override => !override.params.disabled) .map(param => ({ extension: param.extension, - at: param.params.at, + attachTo: param.params.attachTo, source: param.params.source, config: param.params.config, })); diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index e28d7d5993..f6781be290 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -136,7 +136,10 @@ export interface CreateExtensionOptions< TConfig, > { // (undocumented) - at: string; + attachTo: { + id: string; + input: string; + }; // (undocumented) configSchema?: PortableSchema; // (undocumented) @@ -182,7 +185,10 @@ export function createPageExtension< } ) & { id: string; - at?: string; + attachTo?: { + id: string; + input: string; + }; disabled?: boolean; inputs?: TInputs; routeRef?: RouteRef; @@ -209,7 +215,10 @@ export interface Extension { // (undocumented) $$type: '@backstage/Extension'; // (undocumented) - at: string; + attachTo: { + id: string; + input: string; + }; // (undocumented) configSchema?: PortableSchema; // (undocumented) diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts index 7426edb766..e90c0b6b85 100644 --- a/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts +++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.test.ts @@ -33,7 +33,7 @@ describe('createApiExtension', () => { expect(extension).toEqual({ $$type: '@backstage/Extension', id: 'apis.test', - at: 'core/apis', + attachTo: { id: 'core', input: 'apis' }, disabled: false, configSchema: undefined, inputs: {}, @@ -67,7 +67,7 @@ describe('createApiExtension', () => { expect(extension).toEqual({ $$type: '@backstage/Extension', id: 'apis.test', - at: 'core/apis', + attachTo: { id: 'core', input: 'apis' }, disabled: false, configSchema: undefined, inputs: {}, diff --git a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts index a142a5af61..8e5a9e9f33 100644 --- a/packages/frontend-plugin-api/src/extensions/createApiExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createApiExtension.ts @@ -51,7 +51,7 @@ export function createApiExtension< return createExtension({ id: `apis.${apiRef.id}`, - at: 'core/apis', + attachTo: { id: 'core', input: 'apis' }, inputs: extensionInputs, configSchema, output: { diff --git a/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx b/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx index 8c55531866..f208c975b7 100644 --- a/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createNavItemExtension.tsx @@ -31,7 +31,7 @@ export function createNavItemExtension(options: { const { id, routeRef, title, icon } = options; return createExtension({ id, - at: 'core.nav/items', + attachTo: { id: 'core.nav', input: 'items' }, configSchema: createSchemaFromZod(z => z.object({ title: z.string().default(title), diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx index 7de6ee3253..125bf3495d 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.test.tsx @@ -35,7 +35,7 @@ describe('createPageExtension', () => { ).toEqual({ $$type: '@backstage/Extension', id: 'test', - at: 'core.routes/routes', + attachTo: { id: 'core.routes', input: 'routes' }, configSchema: expect.anything(), disabled: false, inputs: {}, @@ -50,7 +50,7 @@ describe('createPageExtension', () => { expect( createPageExtension({ id: 'test', - at: 'other/place', + attachTo: { id: 'other', input: 'place' }, disabled: true, configSchema, inputs: { @@ -63,7 +63,7 @@ describe('createPageExtension', () => { ).toEqual({ $$type: '@backstage/Extension', id: 'test', - at: 'other/place', + attachTo: { id: 'other', input: 'place' }, configSchema: expect.anything(), disabled: true, inputs: { @@ -88,7 +88,7 @@ describe('createPageExtension', () => { ).toEqual({ $$type: '@backstage/Extension', id: 'test', - at: 'core.routes/routes', + attachTo: { id: 'core.routes', input: 'routes' }, configSchema: expect.anything(), disabled: false, inputs: {}, diff --git a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx index 5460fb518d..712c55f896 100644 --- a/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx +++ b/packages/frontend-plugin-api/src/extensions/createPageExtension.tsx @@ -44,7 +44,7 @@ export function createPageExtension< } ) & { id: string; - at?: string; + attachTo?: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; routeRef?: RouteRef; @@ -63,7 +63,7 @@ export function createPageExtension< return createExtension({ id: options.id, - at: options.at ?? 'core.routes/routes', + attachTo: options.attachTo ?? { id: 'core.routes', input: 'routes' }, disabled: options.disabled, output: { element: coreExtensionData.reactElement, diff --git a/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts index 5c13dd199e..38763688a4 100644 --- a/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts +++ b/packages/frontend-plugin-api/src/extensions/createThemeExtension.ts @@ -21,7 +21,7 @@ import { AppTheme } from '@backstage/core-plugin-api'; export function createThemeExtension(theme: AppTheme) { return createExtension({ id: `themes.${theme.id}`, - at: 'core/themes', + attachTo: { id: 'core', input: 'themes' }, output: { theme: coreExtensionData.theme, }, diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index 6f4a21399d..757bcd7e3d 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -26,7 +26,7 @@ describe('createExtension', () => { it('should create an extension with a simple output', () => { const extension = createExtension({ id: 'test', - at: 'root', + attachTo: { id: 'root', input: 'default' }, output: { foo: stringData, }, @@ -56,7 +56,7 @@ describe('createExtension', () => { it('should create an extension with a some optional output', () => { const extension = createExtension({ id: 'test', - at: 'root', + attachTo: { id: 'root', input: 'default' }, output: { foo: stringData, bar: stringData.optional(), @@ -94,7 +94,7 @@ describe('createExtension', () => { it('should create an extension with input', () => { const extension = createExtension({ id: 'test', - at: 'root', + attachTo: { id: 'root', input: 'default' }, inputs: { mixed: createExtensionInput({ required: stringData, diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.ts b/packages/frontend-plugin-api/src/wiring/createExtension.ts index dd365a87d8..01d4962965 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.ts @@ -80,7 +80,7 @@ export interface CreateExtensionOptions< TConfig, > { id: string; - at: string; + attachTo: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; output: TOutput; @@ -97,7 +97,7 @@ export interface CreateExtensionOptions< export interface Extension { $$type: '@backstage/Extension'; id: string; - at: string; + attachTo: { id: string; input: string }; disabled: boolean; inputs: AnyExtensionInputMap; output: AnyExtensionDataMap; diff --git a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts index b14838c17a..589ac59d75 100644 --- a/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createPlugin.test.ts @@ -30,7 +30,7 @@ const nameExtensionDataRef = createExtensionDataRef('name'); const TechRadarPage = createExtension({ id: 'plugin.techradar.page', - at: 'test.output/names', + attachTo: { id: 'test.output', input: 'names' }, output: { name: nameExtensionDataRef, }, @@ -41,7 +41,7 @@ const TechRadarPage = createExtension({ const CatalogPage = createExtension({ id: 'plugin.catalog.page', - at: 'test.output/names', + attachTo: { id: 'test.output', input: 'names' }, output: { name: nameExtensionDataRef, }, @@ -55,7 +55,7 @@ const CatalogPage = createExtension({ const TechDocsAddon = createExtension({ id: 'plugin.techdocs.addon.example', - at: 'plugin.techdocs.page/addons', + attachTo: { id: 'plugin.techdocs.page', input: 'addons' }, output: { name: nameExtensionDataRef, }, @@ -69,7 +69,7 @@ const TechDocsAddon = createExtension({ const TechDocsPage = createExtension({ id: 'plugin.techdocs.page', - at: 'test.output/names', + attachTo: { id: 'test.output', input: 'names' }, inputs: { addons: createExtensionInput({ name: nameExtensionDataRef, @@ -85,7 +85,7 @@ const TechDocsPage = createExtension({ const outputExtension = createExtension({ id: 'test.output', - at: 'root', + attachTo: { id: 'root', input: 'default' }, inputs: { names: createExtensionInput({ name: nameExtensionDataRef, diff --git a/plugins/graphiql/src/alpha.tsx b/plugins/graphiql/src/alpha.tsx index 9cd956fc1a..08950b9072 100644 --- a/plugins/graphiql/src/alpha.tsx +++ b/plugins/graphiql/src/alpha.tsx @@ -86,7 +86,7 @@ export function createEndpointExtension(options: { }) { return createExtension({ id: `apis.plugin.graphiql.browse.${options.id}`, - at: 'apis.plugin.graphiql.browse/endpoints', + attachTo: { id: 'apis.plugin.graphiql.browse', input: 'endpoints' }, configSchema: options.configSchema, disabled: options.disabled ?? false, output: { diff --git a/plugins/search-react/alpha-api-report.md b/plugins/search-react/alpha-api-report.md index d62a0f1f0e..9c1248b536 100644 --- a/plugins/search-react/alpha-api-report.md +++ b/plugins/search-react/alpha-api-report.md @@ -48,7 +48,10 @@ export type SearchResultItemExtensionOptions< }, > = { id: string; - at?: string; + attachTo?: { + id: string; + input: string; + }; configSchema?: PortableSchema; component: (options: { config: TConfig; diff --git a/plugins/search-react/src/alpha.test.tsx b/plugins/search-react/src/alpha.test.tsx index b61a400832..8a88c9cc5d 100644 --- a/plugins/search-react/src/alpha.test.tsx +++ b/plugins/search-react/src/alpha.test.tsx @@ -58,7 +58,7 @@ describe('createSearchResultListItemExtension', () => { const TechDocsSearchResultItemExtension = createSearchResultListItemExtension({ id: 'techdocs', - at: 'plugin.search.page/items', + attachTo: { id: 'plugin.search.page', input: 'items' }, configSchema: createSchemaFromZod(z => z.object({ noTrack: z.boolean().default(true), @@ -79,7 +79,7 @@ describe('createSearchResultListItemExtension', () => { const ExploreSearchResultItemExtension = createSearchResultListItemExtension({ id: 'explore', - at: 'plugin.search.page/items', + attachTo: { id: 'plugin.search.page', input: 'items' }, predicate: result => result.type === 'explore', component: async () => ExploreSearchResultItemComponent, }); diff --git a/plugins/search-react/src/alpha.tsx b/plugins/search-react/src/alpha.tsx index 5b44a7bbb5..366112c95e 100644 --- a/plugins/search-react/src/alpha.tsx +++ b/plugins/search-react/src/alpha.tsx @@ -65,7 +65,7 @@ export type SearchResultItemExtensionOptions< /** * The extension attachment point (e.g., search modal or page). */ - at?: string; + attachTo?: { id: string; input: string }; /** * Optional extension config schema. */ @@ -97,7 +97,7 @@ export function createSearchResultListItemExtension< ) as PortableSchema); return createExtension({ id: `plugin.search.result.item.${options.id}`, - at: options.at ?? 'plugin.search.page/items', + attachTo: options.attachTo ?? { id: 'plugin.search.page', input: 'items' }, configSchema, output: { item: searchResultItemExtensionData, From 4461d87d5a4859d12387cc794e4ea1dfa91721d4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 6 Oct 2023 16:43:24 +0200 Subject: [PATCH 94/95] frontend-plugin-api: removed new useRouteRef Signed-off-by: Patrik Oldsberg --- .changeset/empty-schools-check.md | 6 +++ .../app-next/src/examples/pagesPlugin.tsx | 3 +- .../src/extensions/CoreNav.tsx | 4 +- .../src/routing/RoutingContext.tsx | 52 ------------------- .../frontend-app-api/src/wiring/createApp.tsx | 16 ++---- packages/frontend-plugin-api/api-report.md | 3 -- packages/frontend-plugin-api/src/index.ts | 1 - .../frontend-plugin-api/src/routing/index.ts | 17 ------ .../src/routing/useRouteRef.ts | 38 -------------- 9 files changed, 14 insertions(+), 126 deletions(-) create mode 100644 .changeset/empty-schools-check.md delete mode 100644 packages/frontend-app-api/src/routing/RoutingContext.tsx delete mode 100644 packages/frontend-plugin-api/src/routing/index.ts delete mode 100644 packages/frontend-plugin-api/src/routing/useRouteRef.ts diff --git a/.changeset/empty-schools-check.md b/.changeset/empty-schools-check.md new file mode 100644 index 0000000000..c69bbaabb3 --- /dev/null +++ b/.changeset/empty-schools-check.md @@ -0,0 +1,6 @@ +--- +'@backstage/frontend-plugin-api': minor +'@backstage/frontend-app-api': minor +--- + +Removed support for the new `useRouteRef`. diff --git a/packages/app-next/src/examples/pagesPlugin.tsx b/packages/app-next/src/examples/pagesPlugin.tsx index 290c6b118e..1ec1cfa5a1 100644 --- a/packages/app-next/src/examples/pagesPlugin.tsx +++ b/packages/app-next/src/examples/pagesPlugin.tsx @@ -19,9 +19,8 @@ import { Link } from '@backstage/core-components'; import { createPageExtension, createPlugin, - useRouteRef, } from '@backstage/frontend-plugin-api'; -import { createRouteRef } from '@backstage/core-plugin-api'; +import { useRouteRef, createRouteRef } from '@backstage/core-plugin-api'; import { Route, Routes } from 'react-router-dom'; const indexRouteRef = createRouteRef({ id: 'index' }); diff --git a/packages/frontend-app-api/src/extensions/CoreNav.tsx b/packages/frontend-app-api/src/extensions/CoreNav.tsx index 8e8aa638c0..3406c1d637 100644 --- a/packages/frontend-app-api/src/extensions/CoreNav.tsx +++ b/packages/frontend-app-api/src/extensions/CoreNav.tsx @@ -19,9 +19,9 @@ import { createExtension, coreExtensionData, createExtensionInput, - useRouteRef, NavTarget, } from '@backstage/frontend-plugin-api'; +import { useRouteRef } from '@backstage/core-plugin-api'; import { makeStyles } from '@material-ui/core'; import { Sidebar, @@ -66,7 +66,7 @@ const SidebarLogo = () => { const SidebarNavItem = (props: NavTarget) => { const { icon: Icon, title, routeRef } = props; - const to = useRouteRef(routeRef)(); + const to = useRouteRef(routeRef)({}); // TODO: Support opening modal, for example, the search one return ; }; diff --git a/packages/frontend-app-api/src/routing/RoutingContext.tsx b/packages/frontend-app-api/src/routing/RoutingContext.tsx deleted file mode 100644 index 237c8aa5df..0000000000 --- a/packages/frontend-app-api/src/routing/RoutingContext.tsx +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 { RouteRef } from '@backstage/core-plugin-api'; -import React, { createContext, ReactNode } from 'react'; - -export interface RoutingContextType { - resolve( - routeRef: RouteRef, - options: { pathname: string }, - ): (() => string) | undefined; -} - -export const RoutingContext = createContext({ - resolve: () => () => '', -}); - -export class RouteResolver { - constructor(private readonly routePaths: Map) {} - - resolve(anyRouteRef: RouteRef<{}>): (() => string) | undefined { - const basePath = this.routePaths.get(anyRouteRef); - if (!basePath) { - return undefined; - } - return () => basePath; - } -} - -export function RoutingProvider(props: { - routePaths: Map; - children?: ReactNode; -}) { - return ( - - {props.children} - - ); -} diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index e40c943e23..02f3d17d7c 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -34,7 +34,6 @@ import { mergeExtensionParameters, readAppExtensionParameters, } from './parameters'; -import { RoutingProvider } from '../routing/RoutingContext'; import { AnyApiFactory, ApiHolder, @@ -74,7 +73,7 @@ import { defaultConfigLoaderSync } from '../../../core-app-api/src/app/defaultCo // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { overrideBaseUrlConfigs } from '../../../core-app-api/src/app/overrideBaseUrlConfigs'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { RoutingProvider as LegacyRoutingProvider } from '../../../core-app-api/src/routing/RoutingProvider'; +import { RoutingProvider } from '../../../core-app-api/src/routing/RoutingProvider'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { apis as defaultApis, @@ -308,15 +307,10 @@ export function createApp(options: { - - - {/* TODO: set base path using the logic from AppRouter */} - {rootElements} - - + + {/* TODO: set base path using the logic from AppRouter */} + {rootElements} + diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index f6781be290..18baf88e5d 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -333,7 +333,4 @@ export type PortableSchema = { parse: (input: unknown) => TOutput; schema: JsonObject; }; - -// @public (undocumented) -export function useRouteRef(routeRef: RouteRef): () => string; ``` diff --git a/packages/frontend-plugin-api/src/index.ts b/packages/frontend-plugin-api/src/index.ts index 4e1a383899..512723b5d4 100644 --- a/packages/frontend-plugin-api/src/index.ts +++ b/packages/frontend-plugin-api/src/index.ts @@ -24,4 +24,3 @@ export * from './components'; export * from './extensions'; export * from './schema'; export * from './wiring'; -export * from './routing'; diff --git a/packages/frontend-plugin-api/src/routing/index.ts b/packages/frontend-plugin-api/src/routing/index.ts deleted file mode 100644 index 4506538fa0..0000000000 --- a/packages/frontend-plugin-api/src/routing/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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 { useRouteRef } from './useRouteRef'; diff --git a/packages/frontend-plugin-api/src/routing/useRouteRef.ts b/packages/frontend-plugin-api/src/routing/useRouteRef.ts deleted file mode 100644 index ad1c16ea8e..0000000000 --- a/packages/frontend-plugin-api/src/routing/useRouteRef.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 { RouteRef } from '@backstage/core-plugin-api'; -// eslint-disable-next-line @backstage/no-forbidden-package-imports -import { RoutingContext } from '@backstage/frontend-app-api/src/routing/RoutingContext'; -import { useContext, useMemo } from 'react'; -import { useLocation } from 'react-router-dom'; - -/** @public */ -export function useRouteRef(routeRef: RouteRef): () => string { - const { pathname } = useLocation(); - const resolver = useContext(RoutingContext); - - const routeFunc = useMemo( - () => resolver && resolver.resolve(routeRef, { pathname }), - [resolver, routeRef, pathname], - ); - - if (!routeFunc) { - throw new Error(`Failed to resolve routeRef ${routeRef}`); - } - - return routeFunc; -} From 50728248179edec07a053b504c45fe8f6fff3a2b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 4 Oct 2023 13:29:12 +0200 Subject: [PATCH 95/95] frontend-app-api: extension instance string and JSON serialization Signed-off-by: Patrik Oldsberg --- .changeset/healthy-laws-divide.md | 5 + .../src/wiring/createApp.test.tsx | 109 ++++++++++++++++++ .../src/wiring/createExtensionInstance.ts | 75 ++++++++++-- 3 files changed, 181 insertions(+), 8 deletions(-) create mode 100644 .changeset/healthy-laws-divide.md diff --git a/.changeset/healthy-laws-divide.md b/.changeset/healthy-laws-divide.md new file mode 100644 index 0000000000..4ab29e6426 --- /dev/null +++ b/.changeset/healthy-laws-divide.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Implement `toString()` and `toJSON()` for extension instances. diff --git a/packages/frontend-app-api/src/wiring/createApp.test.tsx b/packages/frontend-app-api/src/wiring/createApp.test.tsx index 14e52af469..abd0fe9b00 100644 --- a/packages/frontend-app-api/src/wiring/createApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.test.tsx @@ -25,6 +25,7 @@ import { screen } from '@testing-library/react'; import { MockConfigApi, renderWithEffects } from '@backstage/test-utils'; import React from 'react'; import { createRouteRef } from '@backstage/core-plugin-api'; +import { createExtensionInstance } from './createExtensionInstance'; describe('createInstances', () => { it('throws an error when a root extension is parametrized', () => { @@ -132,4 +133,112 @@ describe('createApp', () => { await expect(screen.findByText('Derp')).resolves.toBeInTheDocument(); }); + + it('should log an app', () => { + const { rootInstances } = createInstances({ + config: new MockConfigApi({}), + plugins: [], + }); + const root = createExtensionInstance({ + extension: createExtension({ + id: 'root', + attachTo: { id: '', input: '' }, + output: {}, + factory() {}, + }), + config: undefined, + attachments: new Map([['children', rootInstances]]), + }); + + expect(String(root)).toMatchInlineSnapshot(` + " + children [ + + themes [ + + + ] + + + content [ + + ] + nav [ + + ] + + ] + " + `); + }); + + it('should serialize an app as JSON', () => { + const { rootInstances } = createInstances({ + config: new MockConfigApi({}), + plugins: [], + }); + const root = createExtensionInstance({ + extension: createExtension({ + id: 'root', + attachTo: { id: '', input: '' }, + output: {}, + factory() {}, + }), + config: undefined, + attachments: new Map([['children', rootInstances]]), + }); + + expect(JSON.parse(JSON.stringify(root))).toMatchInlineSnapshot(` + { + "attachments": { + "children": [ + { + "attachments": { + "themes": [ + { + "id": "themes.light", + "output": [ + "core.theme", + ], + }, + { + "id": "themes.dark", + "output": [ + "core.theme", + ], + }, + ], + }, + "id": "core", + }, + { + "attachments": { + "content": [ + { + "id": "core.routes", + "output": [ + "core.reactElement", + ], + }, + ], + "nav": [ + { + "id": "core.nav", + "output": [ + "core.reactElement", + ], + }, + ], + }, + "id": "core.layout", + "output": [ + "core.reactElement", + ], + }, + ], + }, + "id": "root", + } + `); + }); }); diff --git a/packages/frontend-app-api/src/wiring/createExtensionInstance.ts b/packages/frontend-app-api/src/wiring/createExtensionInstance.ts index 18551eb8da..9aec3078bc 100644 --- a/packages/frontend-app-api/src/wiring/createExtensionInstance.ts +++ b/packages/frontend-app-api/src/wiring/createExtensionInstance.ts @@ -90,6 +90,68 @@ function resolveInputs( }); } +function indent(str: string) { + return str.replace(/^/gm, ' '); +} + +class ExtensionInstanceImpl implements ExtensionInstance { + readonly $$type = '@backstage/ExtensionInstance'; + + readonly id: string; + readonly #extensionData: Map; + readonly attachments: Map; + readonly source?: BackstagePlugin; + + constructor( + id: string, + extensionData: Map, + attachments: Map, + source: BackstagePlugin | undefined, + ) { + this.id = id; + this.#extensionData = extensionData; + this.attachments = attachments; + this.source = source; + } + + getData(ref: ExtensionDataRef): T | undefined { + return this.#extensionData.get(ref.id) as T | undefined; + } + + toJSON() { + return { + id: this.id, + output: + this.#extensionData.size > 0 + ? [...this.#extensionData.keys()] + : undefined, + attachments: + this.attachments.size > 0 + ? Object.fromEntries(this.attachments) + : undefined, + }; + } + + toString() { + const out = + this.#extensionData.size > 0 + ? ` out=[${[...this.#extensionData.keys()].join(', ')}]` + : ''; + + if (this.attachments.size === 0) { + return `<${this.id}${out} />`; + } + + return [ + `<${this.id}${out}>`, + ...[...this.attachments.entries()].map(([k, v]) => + indent([`${k} [`, ...v.map(e => indent(e.toString())), `]`].join('\n')), + ), + ``, + ].join('\n'); + } +} + /** @internal */ export function createExtensionInstance(options: { extension: Extension; @@ -137,13 +199,10 @@ export function createExtensionInstance(options: { ); } - return { - $$type: '@backstage/ExtensionInstance', - id: options.extension.id, - getData(ref: ExtensionDataRef): T | undefined { - return extensionData.get(ref.id) as T | undefined; - }, - source, + return new ExtensionInstanceImpl( + options.extension.id, + extensionData, attachments, - }; + source, + ); }