diff --git a/.changeset/heavy-tools-hang.md b/.changeset/heavy-tools-hang.md new file mode 100644 index 0000000000..91a843b0a7 --- /dev/null +++ b/.changeset/heavy-tools-hang.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-permission-node': patch +--- + +New package containing common permission and authorization utilities for backend plugins. For more information, see the [authorization PRFC](https://github.com/backstage/backstage/pull/7761). diff --git a/.changeset/many-waves-visit.md b/.changeset/many-waves-visit.md new file mode 100644 index 0000000000..4f4711f371 --- /dev/null +++ b/.changeset/many-waves-visit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Providing an empty values array in an EntityFilter will now return no matches. diff --git a/.changeset/shiny-insects-remember.md b/.changeset/shiny-insects-remember.md new file mode 100644 index 0000000000..a26882b65d --- /dev/null +++ b/.changeset/shiny-insects-remember.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Fix bug where there was error log lines written when failing to `JSON.parse` things that were not `JSON` values. diff --git a/.changeset/slow-olives-confess.md b/.changeset/slow-olives-confess.md new file mode 100644 index 0000000000..68b2539157 --- /dev/null +++ b/.changeset/slow-olives-confess.md @@ -0,0 +1,7 @@ +--- +'@backstage/core-plugin-api': patch +--- + +Deprecated `register` option of `createPlugin` and the `outputs` methods of the plugin instance. + +Introduces the `featureFlags` property to define your feature flags instead. diff --git a/.changeset/strong-seahorses-scream.md b/.changeset/strong-seahorses-scream.md new file mode 100644 index 0000000000..bbd1d44378 --- /dev/null +++ b/.changeset/strong-seahorses-scream.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Added Azure DevOps discovery processor diff --git a/.changeset/thirty-fireants-occur.md b/.changeset/thirty-fireants-occur.md new file mode 100644 index 0000000000..dc9348ac6e --- /dev/null +++ b/.changeset/thirty-fireants-occur.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Detect a duplicate entities when adding locations through dry run diff --git a/.changeset/thirty-tigers-refuse.md b/.changeset/thirty-tigers-refuse.md new file mode 100644 index 0000000000..0797d36747 --- /dev/null +++ b/.changeset/thirty-tigers-refuse.md @@ -0,0 +1,6 @@ +--- +'@backstage/create-app': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Bump gitbeaker to the latest version diff --git a/.changeset/yellow-apes-brush.md b/.changeset/yellow-apes-brush.md new file mode 100644 index 0000000000..06b33e97d1 --- /dev/null +++ b/.changeset/yellow-apes-brush.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Adjusted some styles in the OpenAPI definition, for elements which were barely readable in dark mode. diff --git a/.changeset/young-hats-shop.md b/.changeset/young-hats-shop.md new file mode 100644 index 0000000000..4db46cf9d9 --- /dev/null +++ b/.changeset/young-hats-shop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +Supply featureFlags using featureFlag config option. diff --git a/ADOPTERS.md b/ADOPTERS.md index 3f32a22c23..c4c84d11be 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -71,3 +71,4 @@ | [Epic Games](https://www.epicgames.com) | [Brian Jung](https://github.com/brian-at-epic), [Jeff Goldian](https://github.com/jeffgoldian-Epic) | Developer Portal: Service Catalog, Documentation, Software Templates and more making our internal teams' lives easier! | | [Globo](https://globo.com) | [Carlos Gusmão](https://github.com/caeugusmao), [Guilherme Vierno](https://github.com/vierno), [Denis Aoki](https://github.com/dnsaoki2), [Maycon Dionisio](https://github.com/MayconDionisio), | Reduce the friction of accessing the information engineers need about Globo's digital services through a coherent and centralized experience. | | [QBE](https://www.qbe.com/) | [Daniel Steel](https://github.com/danielsteelqbe), [Pete Jespers](https://github.com/petejespersqbe) | Developer portal allowing our global teams to explore and create applications, documentation and cloud infrastructure easily and quickly 🚀 | +| [LogMeIn](https://www.logmein.com) | [Lorenzo Orsatti](https://github.com/lorsatti) | Improve onboarding experience of new developers. Discover faster and painlessly developer documentation, API definitions and team information. Provide useful dev metrics in a central place. Provide easy-to-use templates for new services. | diff --git a/contrib/chart/backstage/templates/ingress.yaml b/contrib/chart/backstage/templates/ingress.yaml index 7231afda13..9340467800 100644 --- a/contrib/chart/backstage/templates/ingress.yaml +++ b/contrib/chart/backstage/templates/ingress.yaml @@ -1,7 +1,13 @@ {{- $frontendUrl := urlParse .Values.appConfig.app.baseUrl}} {{- $backendUrl := urlParse .Values.appConfig.backend.baseUrl}} {{- $lighthouseUrl := urlParse .Values.appConfig.lighthouse.baseUrl}} + +{{/* Determine the api type for the ingress */}} +{{- if lt .Capabilities.KubeVersion.Minor "19" }} apiVersion: networking.k8s.io/v1beta1 +{{- else if ge .Capabilities.KubeVersion.Minor "19" }} +apiVersion: networking.k8s.io/v1 +{{- end }} kind: Ingress metadata: name: {{ include "backstage.fullname" . }}-ingress diff --git a/docs/integrations/azure/discovery.md b/docs/integrations/azure/discovery.md new file mode 100644 index 0000000000..ad452f47af --- /dev/null +++ b/docs/integrations/azure/discovery.md @@ -0,0 +1,49 @@ +--- +id: discovery +title: Azure DevOps Discovery +sidebar_label: Discovery +# prettier-ignore +description: Automatically discovering catalog entities from repositories in an Azure DevOps organization +--- + +The Azure DevOps integration has a special discovery processor for discovering +catalog entities within an Azure DevOps. The processor will crawl the Azure +DevOps organization and register entities matching the configured path. This can +be useful as an alternative to static locations or manually adding things to the +catalog. + +To use the discovery processor, you'll need a GitHub integration +[set up](locations.md) with a `AZURE_TOKEN`. Then you can add a location target +to the catalog configuration: + +```yaml +catalog: + locations: + # Scan all repositories for a catalog-info.yaml in the root of the default branch + - type: azure-discovery + target: https://dev.azure.com/myorg/myproject + # Or use a custom pattern for a subset of all repositories with default repository + - type: azure-discovery + target: https://dev.azure.com/myorg/myproject/_git/service-* + # Or use a custom file format and location + - type: azure-discovery + target: https://dev.azure.com/myorg/myproject/_git/*?path=/src/*/catalog-info.yaml +``` + +Note the `azure-discovery` type, as this is not a regular `url` processor. + +When using a custom pattern, the target is composed of five parts: + +- The base instance URL, `https://dev.azure.com` in this case +- The organization name which is required, `myorg` in this case +- The project name which is required, `myproject` in this case +- The repository blob to scan, which accepts \* wildcard tokens and must be + added after `_git/`. This can simply be `*` to scan all repositories in the + project. +- The path within each repository to find the catalog YAML file. This will + usually be `/catalog-info.yaml`, `/src/*/catalog-info.yaml` or a similar + variation for catalog files stored in the root directory of each repository. + +_Note:_ the path parameter follows the same rules as the search on Azure DevOps +web interface. For more details visit the +[official search documentation](https://docs.microsoft.com/en-us/azure/devops/project/search/get-started-search?view=azure-devops) diff --git a/docs/plugins/analytics.md b/docs/plugins/analytics.md index 7adf2650df..ace9a89968 100644 --- a/docs/plugins/analytics.md +++ b/docs/plugins/analytics.md @@ -261,10 +261,13 @@ analytics events captured. Use it like this: ```tsx -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; -import { analyticsApiRef } from '@backstage/core-plugin-api'; -import { MockAnalyticsApi, wrapInTestApp } from '@backstage/test-utils'; import { render, fireEvent, waitFor } from '@testing-library/react'; +import { analyticsApiRef } from '@backstage/core-plugin-api'; +import { + MockAnalyticsApi, + TestApiProvider, + wrapInTestApp, +} from '@backstage/test-utils'; describe('SomeComponent', () => { it('should capture event on click', () => { @@ -274,9 +277,9 @@ describe('SomeComponent', () => { // Render the component being tested const { getByText } = render( wrapInTestApp( - + - , + , ), ); diff --git a/microsite/sidebars.json b/microsite/sidebars.json index a24d5897ab..89eb282d14 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -125,7 +125,11 @@ { "type": "subcategory", "label": "Azure", - "ids": ["integrations/azure/locations", "integrations/azure/org"] + "ids": [ + "integrations/azure/locations", + "integrations/azure/discovery", + "integrations/azure/org" + ] }, { "type": "subcategory", diff --git a/mkdocs.yml b/mkdocs.yml index 48df25afef..2eed377819 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -88,6 +88,7 @@ nav: - Discovery: 'integrations/aws-s3/discovery.md' - Azure: - Locations: 'integrations/azure/locations.md' + - Discovery: 'integrations/azure/discovery.md' - Org Data: 'integrations/azure/org.md' - Bitbucket: - Locations: 'integrations/bitbucket/locations.md' diff --git a/packages/app/package.json b/packages/app/package.json index 762a09fb47..4f6f6a2ea2 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -67,7 +67,7 @@ "devDependencies": { "@backstage/test-utils": "^0.1.22", "@rjsf/core": "^3.2.1", - "@testing-library/cypress": "^7.0.1", + "@testing-library/cypress": "^8.0.2", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", diff --git a/packages/app/src/components/catalog/EntityPage.test.tsx b/packages/app/src/components/catalog/EntityPage.test.tsx index 089cb9c7cf..a6878cd28b 100644 --- a/packages/app/src/components/catalog/EntityPage.test.tsx +++ b/packages/app/src/components/catalog/EntityPage.test.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { EntityLayout } from '@backstage/plugin-catalog'; import { DefaultStarredEntitiesApi, @@ -22,7 +21,11 @@ import { starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; import { githubActionsApiRef } from '@backstage/plugin-github-actions'; -import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; +import { + MockStorageApi, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; import React from 'react'; import { cicdContent } from './EntityPage'; @@ -45,22 +48,22 @@ describe('EntityPage Test', () => { const mockedApi = { listWorkflowRuns: jest.fn().mockResolvedValue([]), - getWorkflow: jest.fn(), - getWorkflowRun: jest.fn(), - reRunWorkflow: jest.fn(), - listJobsForWorkflowRun: jest.fn(), - downloadJobLogsForWorkflowRun: jest.fn(), - } as jest.Mocked; - - const apis = ApiRegistry.with(githubActionsApiRef, mockedApi).with( - starredEntitiesApiRef, - new DefaultStarredEntitiesApi({ storageApi: MockStorageApi.create() }), - ); + }; describe('cicdContent', () => { it('Should render GitHub Actions View', async () => { const rendered = await renderInTestApp( - + @@ -68,7 +71,7 @@ describe('EntityPage Test', () => { - , + , ); expect(rendered.getByText('ExampleComponent')).toBeInTheDocument(); diff --git a/packages/backend/package.json b/packages/backend/package.json index 4fb692f040..1cef99c6a0 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -52,7 +52,7 @@ "@backstage/plugin-tech-insights-node": "^0.1.0", "@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.0", "@backstage/plugin-todo-backend": "^0.1.13", - "@gitbeaker/node": "^30.2.0", + "@gitbeaker/node": "^34.6.0", "@octokit/rest": "^18.5.3", "azure-devops-node-api": "^11.0.1", "dockerode": "^3.3.1", diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index 07401660e8..20872b26f0 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -349,6 +349,78 @@ describe('Integration Test', () => { }); }); + it('getFeatureFlags should return feature flags', async () => { + const storageFlags = new LocalStorageFeatureFlags(); + jest.spyOn(storageFlags, 'registerFlag'); + + const apis = [ + noOpAnalyticsApi, + createApiFactory({ + api: featureFlagsApiRef, + deps: { configApi: configApiRef }, + factory() { + return storageFlags; + }, + }), + ]; + + const app = new AppManager({ + apis, + defaultApis: [], + themes: [ + { + id: 'light', + title: 'Light Theme', + variant: 'light', + Provider: ({ children }) => <>{children}, + }, + ], + icons, + plugins: [ + createPlugin({ + id: 'test', + featureFlags: [ + { + name: 'foo', + }, + ], + register: p => p.featureFlags.register('name'), + }), + ], + components, + configLoader: async () => [], + bindRoutes: ({ bind }) => { + bind(plugin1.externalRoutes, { + extRouteRef1: plugin1RouteRef, + extRouteRef2: plugin2RouteRef, + }); + }, + }); + + const Provider = app.getProvider(); + const Router = app.getRouter(); + + await renderWithEffects( + + + + } /> + } /> + + + , + ); + + expect(storageFlags.registerFlag).toHaveBeenCalledWith({ + name: 'name', + pluginId: 'test', + }); + expect(storageFlags.registerFlag).toHaveBeenCalledWith({ + name: 'foo', + pluginId: 'test', + }); + }); + it('should track route changes via analytics api', async () => { const mockAnalyticsApi = new MockAnalyticsApi(); const apis = [createApiFactory(analyticsApiRef, mockAnalyticsApi)]; diff --git a/packages/core-app-api/src/app/AppManager.tsx b/packages/core-app-api/src/app/AppManager.tsx index 950f57bb7d..5903a8c874 100644 --- a/packages/core-app-api/src/app/AppManager.tsx +++ b/packages/core-app-api/src/app/AppManager.tsx @@ -272,17 +272,26 @@ export class AppManager implements BackstageApp { const featureFlagsApi = this.getApiHolder().get(featureFlagsApiRef)!; for (const plugin of this.plugins.values()) { - for (const output of plugin.output()) { - switch (output.type) { - case 'feature-flag': { - featureFlagsApi.registerFlag({ - name: output.name, - pluginId: plugin.getId(), - }); - break; + if ('getFeatureFlags' in plugin) { + for (const flag of plugin.getFeatureFlags()) { + featureFlagsApi.registerFlag({ + name: flag.name, + pluginId: plugin.getId(), + }); + } + } else { + for (const output of plugin.output()) { + switch (output.type) { + case 'feature-flag': { + featureFlagsApi.registerFlag({ + name: output.name, + pluginId: plugin.getId(), + }); + break; + } + default: + break; } - default: - break; } } } diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index 27f3480930..560071e1fa 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -258,6 +258,7 @@ export type BackstagePlugin< getId(): string; output(): PluginOutput[]; getApis(): Iterable; + getFeatureFlags(): Iterable; provide(extension: Extension): T; routes: Routes; externalRoutes: ExternalRoutes; @@ -463,7 +464,7 @@ export type FeatureFlag = { pluginId: string; }; -// @public +// @public @deprecated export type FeatureFlagOutput = { type: 'feature-flag'; name: string; @@ -682,14 +683,20 @@ export type PluginConfig< register?(hooks: PluginHooks): void; routes?: Routes; externalRoutes?: ExternalRoutes; + featureFlags?: PluginFeatureFlagConfig[]; }; // @public +export type PluginFeatureFlagConfig = { + name: string; +}; + +// @public @deprecated export type PluginHooks = { featureFlags: FeatureFlagsHooks; }; -// @public +// @public @deprecated export type PluginOutput = FeatureFlagOutput; // @public diff --git a/packages/core-plugin-api/src/plugin/Plugin.test.tsx b/packages/core-plugin-api/src/plugin/Plugin.test.tsx new file mode 100644 index 0000000000..d3758543a1 --- /dev/null +++ b/packages/core-plugin-api/src/plugin/Plugin.test.tsx @@ -0,0 +1,89 @@ +/* + * Copyright 2021 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 { createPlugin } from './Plugin'; + +describe('Plugin Feature Flag', () => { + it('should be able to register and receive feature flags', () => { + expect( + createPlugin({ + id: 'test', + featureFlags: [{ name: 'test' }], + }).getFeatureFlags(), + ).toEqual([{ name: 'test' }]); + + expect( + createPlugin({ + id: 'test', + register({ featureFlags }) { + featureFlags.register('blob'); + }, + }).getFeatureFlags(), + ).toEqual([{ name: 'blob' }]); + + expect( + createPlugin({ + id: 'test', + register({ featureFlags }) { + featureFlags.register('blob'); + }, + featureFlags: [{ name: 'test' }], + }).getFeatureFlags(), + ).toEqual([{ name: 'test' }, { name: 'blob' }]); + + expect( + createPlugin({ + id: 'test', + }).getFeatureFlags(), + ).toEqual([]); + + /* deprecated tests */ + + expect( + createPlugin({ + id: 'test', + featureFlags: [{ name: 'test' }], + }).output(), + ).toEqual([{ name: 'test', type: 'feature-flag' }]); + + expect( + createPlugin({ + id: 'test', + register({ featureFlags }) { + featureFlags.register('blob'); + }, + }).output(), + ).toEqual([{ name: 'blob', type: 'feature-flag' }]); + expect( + createPlugin({ + id: 'test', + register({ featureFlags }) { + featureFlags.register('blob'); + }, + featureFlags: [{ name: 'test' }], + }).output(), + ).toEqual([ + { name: 'test', type: 'feature-flag' }, + { name: 'blob', type: 'feature-flag' }, + ]); + + expect( + createPlugin({ + id: 'test', + }).output(), + ).toEqual([]); + }); +}); diff --git a/packages/core-plugin-api/src/plugin/Plugin.tsx b/packages/core-plugin-api/src/plugin/Plugin.tsx index 52ca0e76e3..0fb7574cd7 100644 --- a/packages/core-plugin-api/src/plugin/Plugin.tsx +++ b/packages/core-plugin-api/src/plugin/Plugin.tsx @@ -21,6 +21,7 @@ import { Extension, AnyRoutes, AnyExternalRoutes, + PluginFeatureFlagConfig, } from './types'; import { AnyApiFactory } from '../apis'; @@ -44,6 +45,14 @@ export class PluginImpl< return this.config.apis ?? []; } + getFeatureFlags(): Iterable { + const registeredFlags = this.output() + .filter(({ type }) => type === 'feature-flag') + .map(({ name }) => ({ name })); + + return registeredFlags; + } + get routes(): Routes { return this.config.routes ?? ({} as Routes); } @@ -56,11 +65,18 @@ export class PluginImpl< if (this.storedOutput) { return this.storedOutput; } - if (!this.config.register) { - return []; + const outputs = new Array(); + this.storedOutput = outputs; + + if (this.config.featureFlags) { + for (const flag of this.config.featureFlags) { + outputs.push({ type: 'feature-flag', name: flag.name }); + } } - const outputs = new Array(); + if (!this.config.register) { + return outputs; + } this.config.register({ featureFlags: { @@ -70,7 +86,6 @@ export class PluginImpl< }, }); - this.storedOutput = outputs; return this.storedOutput; } diff --git a/packages/core-plugin-api/src/plugin/index.ts b/packages/core-plugin-api/src/plugin/index.ts index d3272607ef..4f7609fd9c 100644 --- a/packages/core-plugin-api/src/plugin/index.ts +++ b/packages/core-plugin-api/src/plugin/index.ts @@ -25,4 +25,5 @@ export type { PluginConfig, PluginHooks, PluginOutput, + PluginFeatureFlagConfig, } from './types'; diff --git a/packages/core-plugin-api/src/plugin/types.ts b/packages/core-plugin-api/src/plugin/types.ts index aeb7037c51..0f8d3404d8 100644 --- a/packages/core-plugin-api/src/plugin/types.ts +++ b/packages/core-plugin-api/src/plugin/types.ts @@ -19,7 +19,7 @@ import { AnyApiFactory } from '../apis/system'; /** * Replace with using {@link RouteRef}s. - * + * @deprecated will be removed * @public */ export type FeatureFlagOutput = { @@ -31,6 +31,7 @@ export type FeatureFlagOutput = { * {@link FeatureFlagOutput} type. * * @public + * @deprecated Use {@link BackstagePlugin.getFeatureFlags} instead. */ export type PluginOutput = FeatureFlagOutput; @@ -71,13 +72,30 @@ export type BackstagePlugin< ExternalRoutes extends AnyExternalRoutes = {}, > = { getId(): string; + /** + * @deprecated use getFeatureFlags instead. + * */ output(): PluginOutput[]; getApis(): Iterable; + /** + * Returns all registered feature flags for this plugin. + */ + getFeatureFlags(): Iterable; provide(extension: Extension): T; routes: Routes; externalRoutes: ExternalRoutes; }; +/** + * Plugin feature flag configuration. + * + * @public + */ +export type PluginFeatureFlagConfig = { + /** Feature flag name */ + name: string; +}; + /** * Plugin descriptor type. * @@ -89,14 +107,17 @@ export type PluginConfig< > = { id: string; apis?: Iterable; + /** @deprecated use featureFlags property instead for defining feature flags */ register?(hooks: PluginHooks): void; routes?: Routes; externalRoutes?: ExternalRoutes; + featureFlags?: PluginFeatureFlagConfig[]; }; /** * Holds hooks registered by the plugin. * + * @deprecated - feature flags are now registered in plugin config under featureFlags * @public */ export type PluginHooks = { diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index 8f2de6a8ae..6f0726b0f8 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -27,7 +27,7 @@ "@backstage/plugin-search-backend": "^{{version '@backstage/plugin-search-backend'}}", "@backstage/plugin-search-backend-node": "^{{version '@backstage/plugin-search-backend-node'}}", "@backstage/plugin-techdocs-backend": "^{{version '@backstage/plugin-techdocs-backend'}}", - "@gitbeaker/node": "^30.2.0", + "@gitbeaker/node": "^34.6.0", "@octokit/rest": "^18.5.3", "dockerode": "^3.3.1", "express": "^4.17.1", diff --git a/packages/dev-utils/src/devApp/SidebarThemeSwitcher.test.tsx b/packages/dev-utils/src/devApp/SidebarThemeSwitcher.test.tsx index 3907dc1007..43eaa6218c 100644 --- a/packages/dev-utils/src/devApp/SidebarThemeSwitcher.test.tsx +++ b/packages/dev-utils/src/devApp/SidebarThemeSwitcher.test.tsx @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; + import { AppThemeApi, appThemeApiRef } from '@backstage/core-plugin-api'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { BackstageTheme } from '@backstage/theme'; import userEvent from '@testing-library/user-event'; import React from 'react'; @@ -24,7 +24,6 @@ import { SidebarThemeSwitcher } from './SidebarThemeSwitcher'; describe('SidebarThemeSwitcher', () => { let appThemeApi: jest.Mocked; - let apiRegistry: ApiRegistry; beforeEach(() => { appThemeApi = { @@ -51,15 +50,13 @@ describe('SidebarThemeSwitcher', () => { theme: {} as unknown as BackstageTheme, }, ]); - - apiRegistry = ApiRegistry.with(appThemeApiRef, appThemeApi); }); it('should display current theme', async () => { const { getByLabelText, getByRole, getByText } = await renderInTestApp( - + - , + , ); const button = getByLabelText('Switch Theme'); @@ -76,9 +73,9 @@ describe('SidebarThemeSwitcher', () => { it('should select different theme', async () => { const { getByLabelText, getByRole, getByText } = await renderInTestApp( - + - , + , ); const button = getByLabelText('Switch Theme'); diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js index 3b7f6119ef..2750324c17 100644 --- a/packages/storybook/.storybook/apis.js +++ b/packages/storybook/.storybook/apis.js @@ -1,6 +1,5 @@ import { AlertApiForwarder, - ApiRegistry, ErrorAlerter, ErrorApiForwarder, GithubAuth, @@ -27,78 +26,57 @@ import { configApiRef, } from '@backstage/core-plugin-api'; -const builder = ApiRegistry.builder(); - -builder.add(configApiRef, new ConfigReader({})); - -const alertApi = builder.add(alertApiRef, new AlertApiForwarder()); - -builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder())); - -builder.add(identityApiRef, { +const configApi = new ConfigReader({}); +const alertApi = new AlertApiForwarder(); +const errorApi = new ErrorAlerter(alertApi, new ErrorApiForwarder()); +const identityApi = { getUserId: () => 'guest', getProfile: () => ({ email: 'guest@example.com' }), getIdToken: () => undefined, signOut: async () => {}, +}; +const oauthRequestApi = new OAuthRequestManager(); +const googleAuthApi = GoogleAuth.create({ + apiOrigin: 'http://localhost:7007', + basePath: '/auth/', + oauthRequestApi, +}); +const githubAuthApi = GithubAuth.create({ + apiOrigin: 'http://localhost:7007', + basePath: '/auth/', + oauthRequestApi, +}); +const gitlabAuthApi = GitlabAuth.create({ + apiOrigin: 'http://localhost:7007', + basePath: '/auth/', + oauthRequestApi, +}); +const oktaAuthApi = OktaAuth.create({ + apiOrigin: 'http://localhost:7007', + basePath: '/auth/', + oauthRequestApi, +}); +const auth0AuthApi = Auth0Auth.create({ + apiOrigin: 'http://localhost:7007', + basePath: '/auth/', + oauthRequestApi, +}); +const oauth2Api = OAuth2.create({ + apiOrigin: 'http://localhost:7007', + basePath: '/auth/', + oauthRequestApi, }); -const oauthRequestApi = builder.add( - oauthRequestApiRef, - new OAuthRequestManager(), -); - -builder.add( - googleAuthApiRef, - GoogleAuth.create({ - apiOrigin: 'http://localhost:7007', - basePath: '/auth/', - oauthRequestApi, - }), -); - -builder.add( - githubAuthApiRef, - GithubAuth.create({ - apiOrigin: 'http://localhost:7007', - basePath: '/auth/', - oauthRequestApi, - }), -); - -builder.add( - gitlabAuthApiRef, - GitlabAuth.create({ - apiOrigin: 'http://localhost:7007', - basePath: '/auth/', - oauthRequestApi, - }), -); - -builder.add( - oktaAuthApiRef, - OktaAuth.create({ - apiOrigin: 'http://localhost:7007', - basePath: '/auth/', - oauthRequestApi, - }), -); - -builder.add( - auth0AuthApiRef, - Auth0Auth.create({ - apiOrigin: 'http://localhost:7007', - basePath: '/auth/', - oauthRequestApi, - }), -); - -builder.add( - oauth2ApiRef, - OAuth2.create({ - apiOrigin: 'http://localhost:7007', - basePath: '/auth/', - oauthRequestApi, - }), -); - -export const apis = builder.build(); +export const apis = [ + [configApiRef, configApi], + [alertApiRef, alertApi], + [errorApiRef, errorApi], + [identityApiRef, identityApi], + [oauthRequestApiRef, oauthRequestApi], + [googleAuthApiRef, googleAuthApi], + [githubAuthApiRef, githubAuthApi], + [gitlabAuthApiRef, gitlabAuthApi], + [oktaAuthApiRef, oktaAuthApi], + [auth0AuthApiRef, auth0AuthApi], + [oauth2ApiRef, oauth2Api], +]; diff --git a/packages/storybook/.storybook/preview.js b/packages/storybook/.storybook/preview.js index 6186b82ca5..2b1a4af329 100644 --- a/packages/storybook/.storybook/preview.js +++ b/packages/storybook/.storybook/preview.js @@ -6,17 +6,17 @@ import { useDarkMode } from 'storybook-dark-mode'; import { apis } from './apis'; import { Content, AlertDisplay } from '@backstage/core-components'; -import { ApiProvider } from '@backstage/core-app-api'; +import { TestApiProvider } from '@backstage/test-utils'; addDecorator(story => ( - + {story()} - + )); addParameters({ diff --git a/packages/techdocs-cli/.snyk b/packages/techdocs-cli/.snyk new file mode 100644 index 0000000000..cfb30e5aa7 --- /dev/null +++ b/packages/techdocs-cli/.snyk @@ -0,0 +1,17 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.22.1 +# ignores vulnerabilities until expiry date; change duration by modifying expiry date +ignore: + SNYK-JS-BROWSERSLIST-1090194: + - '*': + reason: Developer tools are not a valid target for ReDoS attacks + expires: 2022-05-20T00:00:00.000Z + created: 2021-11-20T00:00:00.000Z + + SNYK-JS-IMMER-1540542: + - '*': + reason: Prototype pollution is not an effective attack against a CLI as it already executes arbitrary code + expires: 2022-05-20T00:00:00.000Z + created: 2021-11-20T00:00:00.000Z + +patch: {} diff --git a/packages/test-utils/src/testUtils/appWrappers.test.tsx b/packages/test-utils/src/testUtils/appWrappers.test.tsx index 25809912ec..39e5da3df2 100644 --- a/packages/test-utils/src/testUtils/appWrappers.test.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.test.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { createExternalRouteRef, createRouteRef, @@ -29,6 +28,7 @@ import React, { useEffect } from 'react'; import { Route, Routes } from 'react-router'; import { MockErrorApi } from './apis'; import { renderInTestApp, wrapInTestApp } from './appWrappers'; +import { TestApiProvider } from './TestApiProvider'; describe('wrapInTestApp', () => { it('should provide routing and warn about missing act()', async () => { @@ -111,9 +111,9 @@ describe('wrapInTestApp', () => { }; const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText('foo')).toBeInTheDocument(); diff --git a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx index 7b0ffac58e..32a6ec0f77 100644 --- a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx +++ b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx @@ -16,13 +16,12 @@ import { ApiEntity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { ApiDocsConfig, apiDocsConfigRef } from '../../config'; import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget'; import { ApiDefinitionCard } from './ApiDefinitionCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const apiDocsConfig: jest.Mocked = { @@ -31,10 +30,10 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(apiDocsConfigRef, apiDocsConfig); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/api-docs/src/components/ApiDefinitionCard/ApiTypeTitle.test.tsx b/plugins/api-docs/src/components/ApiDefinitionCard/ApiTypeTitle.test.tsx index 450e9e57ac..5476857a06 100644 --- a/plugins/api-docs/src/components/ApiDefinitionCard/ApiTypeTitle.test.tsx +++ b/plugins/api-docs/src/components/ApiDefinitionCard/ApiTypeTitle.test.tsx @@ -15,11 +15,10 @@ */ import { ApiEntity } from '@backstage/catalog-model'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { ApiDocsConfig, apiDocsConfigRef } from '../../config'; import { ApiTypeTitle } from './ApiTypeTitle'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const apiDocsConfig: jest.Mocked = { @@ -28,10 +27,10 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(apiDocsConfigRef, apiDocsConfig); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx index cf40e5160c..3629110d47 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/ApiExplorerPage.test.tsx @@ -15,11 +15,7 @@ */ import { Entity, RELATION_MEMBER_OF } from '@backstage/catalog-model'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ConfigReader } from '@backstage/core-app-api'; import { TableColumn, TableProps } from '@backstage/core-components'; import { ConfigApi, @@ -34,7 +30,11 @@ import { entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; -import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; +import { + MockStorageApi, + TestApiProvider, + wrapInTestApp, +} from '@backstage/test-utils'; import DashboardIcon from '@material-ui/icons/Dashboard'; import { render } from '@testing-library/react'; import React from 'react'; @@ -88,8 +88,8 @@ describe('ApiCatalogPage', () => { const renderWrapped = (children: React.ReactNode) => render( wrapInTestApp( - { new DefaultStarredEntitiesApi({ storageApi }), ], [apiDocsConfigRef, apiDocsConfig], - ])} + ]} > {children} - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx index ac09b717f7..5e7b22524a 100644 --- a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx @@ -21,12 +21,11 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { ApiDocsConfig, apiDocsConfigRef } from '../../config'; import { ConsumedApisCard } from './ConsumedApisCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const apiDocsConfig: jest.Mocked = { @@ -43,13 +42,15 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi).with( - apiDocsConfigRef, - apiDocsConfig, - ); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx index ac833251e3..4102bebf0e 100644 --- a/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx @@ -21,12 +21,11 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { ApiDocsConfig, apiDocsConfigRef } from '../../config'; import { HasApisCard } from './HasApisCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const apiDocsConfig: jest.Mocked = { @@ -43,13 +42,15 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi).with( - apiDocsConfigRef, - apiDocsConfig, - ); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx index f2719d0f4b..b29075aee9 100644 --- a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx @@ -21,12 +21,11 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { ApiDocsConfig, apiDocsConfigRef } from '../../config'; import { ProvidedApisCard } from './ProvidedApisCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const apiDocsConfig: jest.Mocked = { @@ -43,13 +42,15 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi).with( - apiDocsConfigRef, - apiDocsConfig, - ); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx index 274962c346..06022eec69 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx @@ -21,11 +21,10 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { ConsumingComponentsCard } from './ConsumingComponentsCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const catalogApi: jest.Mocked = { @@ -39,10 +38,10 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx index 879944749a..275338d2f9 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx @@ -21,11 +21,10 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { ProvidingComponentsCard } from './ProvidingComponentsCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const catalogApi: jest.Mocked = { @@ -39,10 +38,10 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx index 4a74b238f7..8d0accb254 100644 --- a/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx +++ b/plugins/api-docs/src/components/OpenApiDefinitionWidget/OpenApiDefinition.tsx @@ -61,12 +61,13 @@ const useStyles = makeStyles(theme => ({ [`& .opblock .opblock-summary-operation-id, .opblock .opblock-summary-path, .opblock .opblock-summary-path__deprecated, - .opblock .opblock-section-header h4, + .opblock h4, + .opblock h5, + .opblock a, + .opblock li, .parameter__name, .response-col_status, .response-col_links, - .responses-inner h4, - .responses-inner h5, .opblock-section-header .btn, .tab li, .info li, @@ -89,6 +90,7 @@ const useStyles = makeStyles(theme => ({ color: theme.palette.text.disabled, }, [`& .opblock-description-wrapper p, + .opblock-description-wrapper li, .opblock-external-docs-wrapper p, .opblock-title_normal p, .response-control-media-type__accept-message, diff --git a/plugins/badges/src/components/EntityBadgesDialog.test.tsx b/plugins/badges/src/components/EntityBadgesDialog.test.tsx index 1f5ee94e04..ea44eb20fc 100644 --- a/plugins/badges/src/components/EntityBadgesDialog.test.tsx +++ b/plugins/badges/src/components/EntityBadgesDialog.test.tsx @@ -16,12 +16,11 @@ import React from 'react'; import { Entity } from '@backstage/catalog-model'; -import { renderWithEffects } from '@backstage/test-utils'; +import { renderWithEffects, TestApiProvider } from '@backstage/test-utils'; import { BadgesApi, badgesApiRef } from '../api'; import { EntityBadgesDialog } from './EntityBadgesDialog'; import { EntityProvider } from '@backstage/plugin-catalog-react'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { ErrorApi, errorApiRef } from '@backstage/core-plugin-api'; describe('EntityBadgesDialog', () => { @@ -42,16 +41,16 @@ describe('EntityBadgesDialog', () => { const mockEntity = { metadata: { name: 'mock' } } as Entity; const rendered = await renderWithEffects( - - , + , ); await expect( diff --git a/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.test.tsx b/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.test.tsx index a206e28de6..82faa3e39c 100644 --- a/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.test.tsx +++ b/plugins/bitrise/src/components/BitriseBuildsTableComponent/BitriseBuildsTableComponent.test.tsx @@ -21,14 +21,11 @@ import { setupServer } from 'msw/node'; import { setupRequestMockHandlers, renderInTestApp, + TestApiRegistry, } from '@backstage/test-utils'; import { useBitriseBuilds } from '../../hooks/useBitriseBuilds'; import { BitriseBuildsTable } from './BitriseBuildsTableComponent'; -import { - ApiProvider, - ApiRegistry, - UrlPatternDiscovery, -} from '@backstage/core-app-api'; +import { ApiProvider, UrlPatternDiscovery } from '@backstage/core-app-api'; jest.mock('../../hooks/useBitriseBuilds', () => ({ useBitriseBuilds: jest.fn(), @@ -40,10 +37,13 @@ describe('BitriseBuildsFetchComponent', () => { setupRequestMockHandlers(server); const mockBaseUrl = 'http://backstage:9191'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); - let apis: ApiRegistry; + let apis: TestApiRegistry; beforeEach(() => { - apis = ApiRegistry.with(bitriseApiRef, new BitriseClientApi(discoveryApi)); + apis = TestApiRegistry.from([ + bitriseApiRef, + new BitriseClientApi(discoveryApi), + ]); }); it('should display `no records` message if there are no builds', async () => { diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 5008cf02f1..7363a803c8 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -174,6 +174,26 @@ export class AwsS3DiscoveryProcessor implements CatalogProcessor { ): Promise; } +// Warning: (ae-missing-release-tag) "AzureDevOpsDiscoveryProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { + constructor(options: { integrations: ScmIntegrations; logger: Logger_2 }); + // (undocumented) + static fromConfig( + config: Config, + options: { + logger: Logger_2; + }, + ): AzureDevOpsDiscoveryProcessor; + // (undocumented) + readLocation( + location: LocationSpec, + _optional: boolean, + emit: CatalogProcessorEmit, + ): Promise; +} + // Warning: (ae-missing-release-tag) "BitbucketDiscoveryProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts new file mode 100644 index 0000000000..ab053e8748 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts @@ -0,0 +1,277 @@ +/* + * Copyright 2021 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 { getVoidLogger } from '@backstage/backend-common'; +import { LocationSpec } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import { codeSearch } from './azure'; +import { + AzureDevOpsDiscoveryProcessor, + parseUrl, +} from './AzureDevOpsDiscoveryProcessor'; + +jest.mock('./azure'); +const mockCodeSearch = codeSearch as jest.MockedFunction; + +describe('AzureDevOpsDiscoveryProcessor', () => { + describe('parseUrl', () => { + it('parses well formed URLs', () => { + expect(parseUrl('https://dev.azure.com/my-org/my-proj')).toEqual({ + baseUrl: 'https://dev.azure.com', + org: 'my-org', + project: 'my-proj', + repo: '', + catalogPath: '/catalog-info.yaml', + }); + + expect( + parseUrl( + 'https://dev.azure.com/spotify/engineering/_git/backstage?path=/catalog.yaml', + ), + ).toEqual({ + baseUrl: 'https://dev.azure.com', + org: 'spotify', + project: 'engineering', + repo: 'backstage', + catalogPath: '/catalog.yaml', + }); + + expect( + parseUrl( + 'https://azuredevops.mycompany.com/spotify/engineering/_git/backstage?path=/src/*/catalog.yaml', + ), + ).toEqual({ + baseUrl: 'https://azuredevops.mycompany.com', + org: 'spotify', + project: 'engineering', + repo: 'backstage', + catalogPath: '/src/*/catalog.yaml', + }); + }); + + it('throws on incorrectly formed URLs', () => { + expect(() => parseUrl('https://dev.azure.com')).toThrow(); + expect(() => parseUrl('https://dev.azure.com//')).toThrow(); + expect(() => parseUrl('https://dev.azure.com//foo')).toThrow(); + }); + }); + + describe('reject unrelated entries', () => { + it('rejects unknown types', async () => { + const processor = AzureDevOpsDiscoveryProcessor.fromConfig( + new ConfigReader({ + integrations: { + azure: [{ host: 'dev.azure.com', token: 'blob' }], + }, + }), + { logger: getVoidLogger() }, + ); + const location: LocationSpec = { + type: 'not-azure-discovery', + target: 'https://dev.azure.com', + }; + await expect( + processor.readLocation(location, false, () => {}), + ).resolves.toBeFalsy(); + }); + }); + + it('rejects unknown targets', async () => { + const processor = AzureDevOpsDiscoveryProcessor.fromConfig( + new ConfigReader({ + integrations: { + github: [ + { host: 'dev.azure.com', token: 'blob' }, + { host: 'azure.myorg.com', token: 'blob' }, + ], + }, + }), + { logger: getVoidLogger() }, + ); + const location: LocationSpec = { + type: 'azure-discovery', + target: 'https://not.azure.com/org/project', + }; + await expect( + processor.readLocation(location, false, () => {}), + ).rejects.toThrow( + /There is no Azure integration that matches https:\/\/not.azure.com\/org\/project. Please add a configuration entry for it under integrations.azure/, + ); + }); + + describe('handles repositories', () => { + const processor = AzureDevOpsDiscoveryProcessor.fromConfig( + new ConfigReader({ + integrations: { + github: [{ host: 'dev.azure.com', token: 'blob' }], + }, + }), + { logger: getVoidLogger() }, + ); + + beforeEach(() => { + mockCodeSearch.mockClear(); + }); + + it('output all locations found on from code search', async () => { + const location: LocationSpec = { + type: 'azure-discovery', + target: 'https://dev.azure.com/shopify/engineering', + }; + mockCodeSearch.mockResolvedValueOnce([ + { + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + repository: { + name: 'backstage', + }, + }, + { + fileName: 'catalog-info.yaml', + path: '/src/catalog-info.yaml', + repository: { + name: 'ios-app', + }, + }, + ]); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(mockCodeSearch).toHaveBeenCalledWith( + { host: 'dev.azure.com' }, + 'shopify', + 'engineering', + '', + '/catalog-info.yaml', + ); + expect(emitter).toHaveBeenCalledTimes(2); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://dev.azure.com/shopify/engineering/_git/backstage?path=/catalog-info.yaml', + }, + optional: true, + }); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://dev.azure.com/shopify/engineering/_git/ios-app?path=/src/catalog-info.yaml', + }, + optional: true, + }); + }); + + it('output single locations from code search', async () => { + const location: LocationSpec = { + type: 'azure-discovery', + target: 'https://dev.azure.com/shopify/engineering/_git/backstage', + }; + mockCodeSearch.mockResolvedValueOnce([ + { + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + repository: { + name: 'backstage', + }, + }, + ]); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(mockCodeSearch).toHaveBeenCalledWith( + { host: 'dev.azure.com' }, + 'shopify', + 'engineering', + 'backstage', + '/catalog-info.yaml', + ); + expect(emitter).toHaveBeenCalledTimes(1); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://dev.azure.com/shopify/engineering/_git/backstage?path=/catalog-info.yaml', + }, + optional: true, + }); + }); + + it('output single locations with different file name from code search', async () => { + const location: LocationSpec = { + type: 'azure-discovery', + target: + 'https://dev.azure.com/shopify/engineering?path=/src/*/catalog.yaml', + }; + mockCodeSearch.mockResolvedValueOnce([ + { + fileName: 'catalog.yaml', + path: '/src/main/catalog.yaml', + repository: { + name: 'backstage', + }, + }, + ]); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(mockCodeSearch).toHaveBeenCalledWith( + { host: 'dev.azure.com' }, + 'shopify', + 'engineering', + '', + '/src/*/catalog.yaml', + ); + expect(emitter).toHaveBeenCalledTimes(1); + expect(emitter).toHaveBeenCalledWith({ + type: 'location', + location: { + type: 'url', + target: + 'https://dev.azure.com/shopify/engineering/_git/backstage?path=/src/main/catalog.yaml', + }, + optional: true, + }); + }); + + it('output nothing when code search does not find anything', async () => { + const location: LocationSpec = { + type: 'azure-discovery', + target: 'https://dev.azure.com/shopify/engineering/_git/backstage', + }; + mockCodeSearch.mockResolvedValueOnce([]); + const emitter = jest.fn(); + + await processor.readLocation(location, false, emitter); + + expect(mockCodeSearch).toHaveBeenCalledWith( + { host: 'dev.azure.com' }, + 'shopify', + 'engineering', + 'backstage', + '/catalog-info.yaml', + ); + expect(emitter).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts new file mode 100644 index 0000000000..58ee2155e9 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts @@ -0,0 +1,151 @@ +/* + * Copyright 2021 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 { LocationSpec } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import { Logger } from 'winston'; +import * as results from './results'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; +import { codeSearch } from './azure'; + +/** + * Extracts repositories out of an Azure DevOps org. + * + * The following will create locations for all projects which have a catalog-info.yaml + * on the default branch. The first is shorthand for the second. + * + * target: "https://dev.azure.com/org/project" + * or + * target: https://dev.azure.com/org/project?path=/catalog-info.yaml + * + * You may also explicitly specify a single repo: + * + * target: https://dev.azure.com/org/project/_git/repo + **/ +export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { + private readonly integrations: ScmIntegrations; + private readonly logger: Logger; + + static fromConfig(config: Config, options: { logger: Logger }) { + const integrations = ScmIntegrations.fromConfig(config); + + return new AzureDevOpsDiscoveryProcessor({ + ...options, + integrations, + }); + } + + constructor(options: { integrations: ScmIntegrations; logger: Logger }) { + this.integrations = options.integrations; + this.logger = options.logger; + } + + async readLocation( + location: LocationSpec, + _optional: boolean, + emit: CatalogProcessorEmit, + ): Promise { + if (location.type !== 'azure-discovery') { + return false; + } + + const azureConfig = this.integrations.azure.byUrl(location.target)?.config; + if (!azureConfig) { + throw new Error( + `There is no Azure integration that matches ${location.target}. Please add a configuration entry for it under integrations.azure`, + ); + } + + const { baseUrl, org, project, repo, catalogPath } = parseUrl( + location.target, + ); + this.logger.info( + `Reading Azure DevOps repositories from ${location.target}`, + ); + + const files = await codeSearch( + azureConfig, + org, + project, + repo, + catalogPath, + ); + + this.logger.debug( + `Found ${files.length} files in Azure DevOps from ${location.target}.`, + ); + + for (const file of files) { + emit( + results.location( + { + type: 'url', + target: `${baseUrl}/${org}/${project}/_git/${file.repository.name}?path=${file.path}`, + }, + // Not all locations may actually exist, since the user defined them as a wildcard pattern. + // Thus, we emit them as optional and let the downstream processor find them while not outputting + // an error if it couldn't. + true, + ), + ); + } + + return true; + } +} + +/** + * parseUrl extracts segments from the Azure DevOps URL. + **/ +export function parseUrl(urlString: string): { + baseUrl: string; + org: string; + project: string; + repo: string; + catalogPath: string; +} { + const url = new URL(urlString); + const path = url.pathname.substr(1).split('/'); + + const catalogPath = url.searchParams.get('path') || '/catalog-info.yaml'; + + if (path.length === 2 && path[0].length && path[1].length) { + return { + baseUrl: url.origin, + org: decodeURIComponent(path[0]), + project: decodeURIComponent(path[1]), + repo: '', + catalogPath, + }; + } else if ( + path.length === 4 && + path[0].length && + path[1].length && + path[2].length && + path[3].length + ) { + return { + baseUrl: url.origin, + org: decodeURIComponent(path[0]), + project: decodeURIComponent(path[1]), + repo: decodeURIComponent(path[3]), + catalogPath, + }; + } + + throw new Error(`Failed to parse ${urlString}`); +} diff --git a/plugins/catalog-backend/src/ingestion/processors/azure/azure.test.ts b/plugins/catalog-backend/src/ingestion/processors/azure/azure.test.ts new file mode 100644 index 0000000000..67cc120bab --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/azure/azure.test.ts @@ -0,0 +1,232 @@ +/* + * Copyright 2021 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 { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { rest } from 'msw'; +import { codeSearch, CodeSearchResponse } from './azure'; + +describe('azure', () => { + const server = setupServer(); + setupRequestMockHandlers(server); + + describe('codeSearch', () => { + it('returns empty when nothing is found', async () => { + const response: CodeSearchResponse = { count: 0, results: [] }; + + server.use( + rest.post( + `https://almsearch.dev.azure.com/shopify/engineering/_apis/search/codesearchresults`, + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); + expect(req.body).toEqual({ + searchText: 'path:/catalog-info.yaml repo:*', + $skip: 0, + $top: 1000, + }); + return res(ctx.json(response)); + }, + ), + ); + + await expect( + codeSearch( + { host: 'dev.azure.com', token: 'ABC' }, + 'shopify', + 'engineering', + '', + '/catalog-info.yaml', + ), + ).resolves.toEqual([]); + }); + }); + + it('returns entries when request matches some files', async () => { + const response: CodeSearchResponse = { + count: 2, + results: [ + { + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + repository: { + name: 'backstage', + }, + }, + { + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + repository: { + name: 'ios-app', + }, + }, + ], + }; + + server.use( + rest.post( + `https://almsearch.dev.azure.com/shopify/engineering/_apis/search/codesearchresults`, + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); + expect(req.body).toEqual({ + searchText: 'path:/catalog-info.yaml repo:*', + $skip: 0, + $top: 1000, + }); + return res(ctx.json(response)); + }, + ), + ); + + await expect( + codeSearch( + { host: 'dev.azure.com', token: 'ABC' }, + 'shopify', + 'engineering', + '', + '/catalog-info.yaml', + ), + ).resolves.toEqual(response.results); + }); + + it('searches in specific repo if parameter is set', async () => { + const response: CodeSearchResponse = { + count: 1, + results: [ + { + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + repository: { + name: 'backstage', + }, + }, + ], + }; + + server.use( + rest.post( + `https://almsearch.dev.azure.com/shopify/engineering/_apis/search/codesearchresults`, + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); + expect(req.body).toEqual({ + searchText: 'path:/catalog-info.yaml repo:backstage', + $skip: 0, + $top: 1000, + }); + return res(ctx.json(response)); + }, + ), + ); + + await expect( + codeSearch( + { host: 'dev.azure.com', token: 'ABC' }, + 'shopify', + 'engineering', + 'backstage', + '/catalog-info.yaml', + ), + ).resolves.toEqual(response.results); + }); + + it('can search using onpremise api', async () => { + const response: CodeSearchResponse = { + count: 1, + results: [ + { + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + repository: { + name: 'backstage', + }, + }, + ], + }; + + server.use( + rest.post( + `https://azuredevops.mycompany.com/shopify/engineering/_apis/search/codesearchresults`, + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); + expect(req.body).toEqual({ + searchText: 'path:/catalog-info.yaml repo:*', + $skip: 0, + $top: 1000, + }); + return res(ctx.json(response)); + }, + ), + ); + + await expect( + codeSearch( + { host: 'azuredevops.mycompany.com', token: 'ABC' }, + 'shopify', + 'engineering', + '', + '/catalog-info.yaml', + ), + ).resolves.toEqual(response.results); + }); + + it('searches multiple pages if response contains many items', async () => { + const totalCount = 2401; + const generateItems = (count: number) => { + return Array.from(Array(count).keys()).map(_ => ({ + fileName: 'catalog-info.yaml', + path: '/catalog-info.yaml', + repository: { + name: 'backstage', + }, + })); + }; + + server.use( + rest.post( + `https://almsearch.dev.azure.com/shopify/engineering/_apis/search/codesearchresults`, + (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe('Basic OkFCQw=='); + expect(req.body).toMatchObject({ + searchText: 'path:/catalog-info.yaml repo:backstage', + $top: 1000, + }); + + const body = req.body as { $skip: number; $top: number }; + const countItemsToReturn = + body.$top + body.$skip > totalCount + ? totalCount - body.$skip + : body.$top; + + return res( + ctx.json({ + count: totalCount, + results: generateItems(countItemsToReturn), + }), + ); + }, + ), + ); + + await expect( + codeSearch( + { host: 'dev.azure.com', token: 'ABC' }, + 'shopify', + 'engineering', + 'backstage', + '/catalog-info.yaml', + ), + ).resolves.toHaveLength(totalCount); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/azure/azure.ts b/plugins/catalog-backend/src/ingestion/processors/azure/azure.ts new file mode 100644 index 0000000000..0c7b17483a --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/azure/azure.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2021 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 fetch from 'cross-fetch'; +import { + AzureIntegrationConfig, + getAzureRequestOptions, +} from '@backstage/integration'; + +export interface CodeSearchResponse { + count: number; + results: CodeSearchResultItem[]; +} + +export interface CodeSearchResultItem { + fileName: string; + path: string; + repository: { + name: string; + }; +} + +const isCloud = (host: string) => host === 'dev.azure.com'; +const PAGE_SIZE = 1000; + +// codeSearch returns all files that matches the given search path. +export async function codeSearch( + azureConfig: AzureIntegrationConfig, + org: string, + project: string, + repo: string, + path: string, +): Promise { + const searchBaseUrl = isCloud(azureConfig.host) + ? 'https://almsearch.dev.azure.com' + : `https://${azureConfig.host}`; + const searchUrl = `${searchBaseUrl}/${org}/${project}/_apis/search/codesearchresults?api-version=6.0-preview.1`; + + let items: CodeSearchResultItem[] = []; + let hasMorePages = true; + + do { + const response = await fetch(searchUrl, { + ...getAzureRequestOptions(azureConfig, { + 'Content-Type': 'application/json', + }), + method: 'POST', + body: JSON.stringify({ + searchText: `path:${path} repo:${repo || '*'}`, + $skip: items.length, + $top: PAGE_SIZE, + }), + }); + + if (response.status !== 200) { + throw new Error( + `Azure DevOps search failed with response status ${response.status}`, + ); + } + + const body: CodeSearchResponse = await response.json(); + items = [...items, ...body.results]; + hasMorePages = body.count > items.length; + } while (hasMorePages); + + return items; +} diff --git a/plugins/catalog-backend/src/ingestion/processors/azure/index.ts b/plugins/catalog-backend/src/ingestion/processors/azure/index.ts new file mode 100644 index 0000000000..d4ff56c4c0 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/azure/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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 * from './azure'; diff --git a/plugins/catalog-backend/src/ingestion/processors/index.ts b/plugins/catalog-backend/src/ingestion/processors/index.ts index 4f3c502a8e..63feac51af 100644 --- a/plugins/catalog-backend/src/ingestion/processors/index.ts +++ b/plugins/catalog-backend/src/ingestion/processors/index.ts @@ -26,6 +26,7 @@ export { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor'; export { CodeOwnersProcessor } from './CodeOwnersProcessor'; export { FileReaderProcessor } from './FileReaderProcessor'; export { GithubDiscoveryProcessor } from './GithubDiscoveryProcessor'; +export { AzureDevOpsDiscoveryProcessor } from './AzureDevOpsDiscoveryProcessor'; export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor'; export { GithubMultiOrgReaderProcessor } from './GithubMultiOrgReaderProcessor'; export { GitLabDiscoveryProcessor } from './GitLabDiscoveryProcessor'; diff --git a/plugins/catalog-backend/src/legacy/service/CatalogBuilder.ts b/plugins/catalog-backend/src/legacy/service/CatalogBuilder.ts index b8e45c7cf3..6fb52e245e 100644 --- a/plugins/catalog-backend/src/legacy/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/legacy/service/CatalogBuilder.ts @@ -42,6 +42,7 @@ import { CodeOwnersProcessor, FileReaderProcessor, GithubDiscoveryProcessor, + AzureDevOpsDiscoveryProcessor, GithubOrgReaderProcessor, GitLabDiscoveryProcessor, LocationEntityProcessor, @@ -317,6 +318,7 @@ export class CatalogBuilder { new FileReaderProcessor(), BitbucketDiscoveryProcessor.fromConfig(config, { logger }), GithubDiscoveryProcessor.fromConfig(config, { logger }), + AzureDevOpsDiscoveryProcessor.fromConfig(config, { logger }), GithubOrgReaderProcessor.fromConfig(config, { logger }), GitLabDiscoveryProcessor.fromConfig(config, { logger }), new UrlReaderProcessor({ reader, logger }), diff --git a/plugins/catalog-backend/src/service/DefaultLocationService.test.ts b/plugins/catalog-backend/src/service/DefaultLocationService.test.ts index a089989d20..562bae4867 100644 --- a/plugins/catalog-backend/src/service/DefaultLocationService.test.ts +++ b/plugins/catalog-backend/src/service/DefaultLocationService.test.ts @@ -148,6 +148,57 @@ describe('DefaultLocationServiceTest', () => { expect(result.exists).toBe(true); }); + it('should fail when there are duplicate entities using dry run', async () => { + store.listLocations.mockResolvedValueOnce([]); + orchestrator.process.mockResolvedValueOnce({ + ok: true, + state: {}, + completedEntity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + name: 'foo', + }, + }, + deferredEntities: [ + { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + name: 'foo', + }, + }, + locationKey: 'file:///tmp/mock.yaml', + }, + ], + relations: [], + errors: [], + }); + + orchestrator.process.mockResolvedValueOnce({ + ok: true, + state: {}, + completedEntity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Location', + metadata: { + name: 'foo', + }, + }, + deferredEntities: [], + relations: [], + errors: [], + }); + + await expect( + locationService.createLocation( + { type: 'url', target: 'https://backstage.io/catalog-info.yaml' }, + true, + ), + ).rejects.toThrowError('Duplicate nested entity: location:default/foo'); + }); + it('should return exists false when the location does not exist beforehand', async () => { orchestrator.process.mockResolvedValueOnce({ ok: true, diff --git a/plugins/catalog-backend/src/service/DefaultLocationService.ts b/plugins/catalog-backend/src/service/DefaultLocationService.ts index 2166341935..8dd2274842 100644 --- a/plugins/catalog-backend/src/service/DefaultLocationService.ts +++ b/plugins/catalog-backend/src/service/DefaultLocationService.ts @@ -19,6 +19,7 @@ import { LocationSpec, LOCATION_ANNOTATION, ORIGIN_LOCATION_ANNOTATION, + stringifyEntityRef, } from '@backstage/catalog-model'; import { CatalogProcessingOrchestrator, @@ -54,6 +55,43 @@ export class DefaultLocationService implements LocationService { return this.store.deleteLocation(id); } + private async processEntities( + unprocessedEntities: DeferredEntity[], + ): Promise { + const entities: Entity[] = []; + while (unprocessedEntities.length) { + const currentEntity = unprocessedEntities.pop(); + if (!currentEntity) { + continue; + } + const processed = await this.orchestrator.process({ + entity: currentEntity.entity, + state: {}, // we process without the existing cache + }); + + if (processed.ok) { + if ( + entities.some( + e => + stringifyEntityRef(e) === + stringifyEntityRef(processed.completedEntity), + ) + ) { + throw new Error( + `Duplicate nested entity: ${stringifyEntityRef( + processed.completedEntity, + )}`, + ); + } + unprocessedEntities.push(...processed.deferredEntities); + entities.push(processed.completedEntity); + } else { + throw Error(processed.errors.map(String).join(', ')); + } + } + return entities; + } + private async dryRunCreateLocation( spec: LocationSpec, ): Promise<{ location: Location; entities: Entity[]; exists?: boolean }> { @@ -86,24 +124,7 @@ export class DefaultLocationService implements LocationService { const unprocessedEntities: DeferredEntity[] = [ { entity, locationKey: `${spec.type}:${spec.target}` }, ]; - const entities: Entity[] = []; - while (unprocessedEntities.length) { - const currentEntity = unprocessedEntities.pop(); - if (!currentEntity) { - continue; - } - const processed = await this.orchestrator.process({ - entity: currentEntity.entity, - state: {}, // we process without the existing cache - }); - - if (processed.ok) { - unprocessedEntities.push(...processed.deferredEntities); - entities.push(processed.completedEntity); - } else { - throw Error(processed.errors.map(String).join(', ')); - } - } + const entities: Entity[] = await this.processEntities(unprocessedEntities); return { exists: await existsPromise, diff --git a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts index eed420cd53..c9ebc8f723 100644 --- a/plugins/catalog-backend/src/service/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts @@ -44,6 +44,7 @@ import { CatalogProcessorParser, CodeOwnersProcessor, FileReaderProcessor, + AzureDevOpsDiscoveryProcessor, GithubDiscoveryProcessor, GithubOrgReaderProcessor, GitLabDiscoveryProcessor, @@ -292,6 +293,7 @@ export class NextCatalogBuilder { return [ new FileReaderProcessor(), BitbucketDiscoveryProcessor.fromConfig(config, { logger }), + AzureDevOpsDiscoveryProcessor.fromConfig(config, { logger }), GithubDiscoveryProcessor.fromConfig(config, { logger }), GithubOrgReaderProcessor.fromConfig(config, { logger }), GitLabDiscoveryProcessor.fromConfig(config, { logger }), diff --git a/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts index ee86e78c6b..d0f284c11c 100644 --- a/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/NextEntitiesCatalog.test.ts @@ -435,5 +435,37 @@ describe('NextEntitiesCatalog', () => { expect(entities).toContainEqual(entity1); }, ); + + it.each(databases.eachSupportedId())( + 'should return no matches for an empty values array', + // NOTE: An empty values array is not a sensible input in a realistic scenario. + async databaseId => { + const { knex } = await createDatabase(databaseId); + const entity1: Entity = { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'one' }, + spec: {}, + }; + const entity2: Entity = { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'two' }, + spec: {}, + }; + await addEntityToSearch(knex, entity1); + await addEntityToSearch(knex, entity2); + const catalog = new NextEntitiesCatalog(knex); + + const testFilter = { + key: 'kind', + values: [], + }; + const request = { filter: testFilter }; + const { entities } = await catalog.entities(request); + + expect(entities.length).toBe(0); + }, + ); }); }); diff --git a/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts b/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts index 8c2de9d7c1..f9b3abf190 100644 --- a/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/NextEntitiesCatalog.ts @@ -91,7 +91,7 @@ function addCondition( if (filter.values) { if (filter.values.length === 1) { this.where({ value: filter.values[0].toLowerCase() }); - } else if (filter.values.length > 1) { + } else { this.andWhere( 'value', 'in', diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx index 9fe482a30b..3c167946d4 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx @@ -14,14 +14,19 @@ * limitations under the License. */ import { Entity } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { analyticsApiRef } from '@backstage/core-plugin-api'; import { CatalogApi, catalogApiRef, EntityProvider, } from '@backstage/plugin-catalog-react'; -import { MockAnalyticsApi, renderInTestApp } from '@backstage/test-utils'; +import { + MockAnalyticsApi, + renderInTestApp, + TestApiProvider, + TestApiRegistry, +} from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { catalogEntityRouteRef, catalogGraphRouteRef } from '../../routes'; @@ -31,7 +36,7 @@ describe('', () => { let entity: Entity; let wrapper: JSX.Element; let catalog: jest.Mocked; - let apis: ApiRegistry; + let apis: TestApiRegistry; beforeAll(() => { Object.defineProperty(window.SVGElement.prototype, 'getBBox', { @@ -61,7 +66,7 @@ describe('', () => { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), }; - apis = ApiRegistry.with(catalogApiRef, catalog); + apis = TestApiRegistry.from([catalogApiRef, catalog]); wrapper = ( @@ -123,9 +128,9 @@ describe('', () => { test('captures analytics event on click', async () => { const analyticsSpy = new MockAnalyticsApi(); const { findByText } = await renderInTestApp( - + {wrapper} - , + , { mountedRoutes: { '/entity/{kind}/{namespace}/{name}': catalogEntityRouteRef, diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx index b992d0c976..7bd1045fff 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx @@ -14,10 +14,13 @@ * limitations under the License. */ import { RELATION_HAS_PART, RELATION_PART_OF } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { analyticsApiRef } from '@backstage/core-plugin-api'; import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; -import { MockAnalyticsApi, renderInTestApp } from '@backstage/test-utils'; +import { + MockAnalyticsApi, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { catalogEntityRouteRef } from '../../routes'; @@ -90,10 +93,9 @@ describe('', () => { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), }; - const apis = ApiRegistry.with(catalogApiRef, catalog); wrapper = ( - + ', () => { selectedKinds: ['b'], }} /> - + ); }); @@ -172,9 +174,9 @@ describe('', () => { test('should capture analytics event when selecting other entity', async () => { const analyticsSpy = new MockAnalyticsApi(); const { getByText, findAllByTestId } = await renderInTestApp( - + {wrapper} - , + , { mountedRoutes: { '/entity/{kind}/{namespace}/{name}': catalogEntityRouteRef, @@ -195,9 +197,9 @@ describe('', () => { test('should capture analytics event when navigating to entity', async () => { const analyticsSpy = new MockAnalyticsApi(); const { getByText, findAllByTestId } = await renderInTestApp( - + {wrapper} - , + , { mountedRoutes: { '/entity/{kind}/{namespace}/{name}': catalogEntityRouteRef, diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx index 2d6c5cc93d..be0c4dc5e5 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx @@ -21,9 +21,8 @@ import { RELATION_PART_OF, stringifyEntityRef, } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import React, { FunctionComponent } from 'react'; import { EntityRelationsGraph } from './EntityRelationsGraph'; @@ -158,10 +157,11 @@ describe('', () => { refreshEntity: jest.fn(), getEntityAncestors: jest.fn(), }; - const apis = ApiRegistry.with(catalogApiRef, catalog); Wrapper = ({ children }) => ( - {children} + + {children} + ); }); diff --git a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx index 69ae38207d..784cbfbe9e 100644 --- a/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx +++ b/plugins/catalog-import/src/components/DefaultImportPage/DefaultImportPage.test.tsx @@ -15,14 +15,10 @@ */ import { CatalogClient } from '@backstage/catalog-client'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { configApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import React from 'react'; import { catalogImportApiRef, CatalogImportClient } from '../../api'; import { DefaultImportPage } from './DefaultImportPage'; @@ -43,15 +39,13 @@ describe('', () => { }, }; - let apis: ApiRegistry; + let apis: TestApiRegistry; beforeEach(() => { - apis = ApiRegistry.with( - configApiRef, - new ConfigReader({ integrations: {} }), - ) - .with(catalogApiRef, new CatalogClient({ discoveryApi: {} as any })) - .with( + apis = TestApiRegistry.from( + [configApiRef, new ConfigReader({ integrations: {} })], + [catalogApiRef, new CatalogClient({ discoveryApi: {} as any })], + [ catalogImportApiRef, new CatalogImportClient({ discoveryApi: {} as any, @@ -63,7 +57,8 @@ describe('', () => { catalogApi: {} as any, configApi: {} as any, }), - ); + ], + ); }); it('renders without exploding', async () => { diff --git a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.test.tsx b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.test.tsx index 4e0c3694cd..9a27532c57 100644 --- a/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.test.tsx +++ b/plugins/catalog-import/src/components/ImportInfoCard/ImportInfoCard.test.tsx @@ -14,19 +14,19 @@ * limitations under the License. */ -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { configApiRef } from '@backstage/core-plugin-api'; -import { renderInTestApp } from '@backstage/test-utils'; +import { + renderInTestApp, + TestApiProvider, + TestApiRegistry, +} from '@backstage/test-utils'; import React from 'react'; import { CatalogImportApi, catalogImportApiRef } from '../../api'; import { ImportInfoCard } from './ImportInfoCard'; describe('', () => { - let apis: ApiRegistry; + let apis: TestApiRegistry; let catalogImportApi: jest.Mocked; beforeEach(() => { @@ -35,26 +35,29 @@ describe('', () => { submitPullRequest: jest.fn(), }; - apis = ApiRegistry.with( - configApiRef, - new ConfigReader({ - integrations: { - github: [{ token: 'my-token' }], - }, - }), - ).with(catalogImportApiRef, catalogImportApi); + apis = TestApiRegistry.from( + [ + configApiRef, + new ConfigReader({ + integrations: { + github: [{ token: 'my-token' }], + }, + }), + ], + [catalogImportApiRef, catalogImportApi], + ); }); it('renders without exploding', async () => { - apis = ApiRegistry.with( - configApiRef, - new ConfigReader({ integrations: {} }), - ).with(catalogImportApiRef, catalogImportApi); - const { getByText } = await renderInTestApp( - + - , + , ); expect(getByText('Register an existing component')).toBeInTheDocument(); diff --git a/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx b/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx index 92a6077792..1778af73dc 100644 --- a/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx +++ b/plugins/catalog-import/src/components/ImportPage/ImportPage.test.tsx @@ -15,14 +15,10 @@ */ import { CatalogClient } from '@backstage/catalog-client'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { configApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import React from 'react'; import { useOutlet } from 'react-router'; import { catalogImportApiRef, CatalogImportClient } from '../../api'; @@ -49,15 +45,13 @@ describe('', () => { }, }; - let apis: ApiRegistry; + let apis: TestApiRegistry; beforeEach(() => { - apis = ApiRegistry.with( - configApiRef, - new ConfigReader({ integrations: {} }), - ) - .with(catalogApiRef, new CatalogClient({ discoveryApi: {} as any })) - .with( + apis = TestApiRegistry.from( + [configApiRef, new ConfigReader({ integrations: {} })], + [catalogApiRef, new CatalogClient({ discoveryApi: {} as any })], + [ catalogImportApiRef, new CatalogImportClient({ discoveryApi: {} as any, @@ -67,7 +61,8 @@ describe('', () => { catalogApi: {} as any, configApi: new ConfigReader({}), }), - ); + ], + ); }); afterEach(() => jest.resetAllMocks()); diff --git a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx index 4a72a653ea..8d28fb8604 100644 --- a/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx +++ b/plugins/catalog-import/src/components/StepInitAnalyzeUrl/StepInitAnalyzeUrl.test.tsx @@ -14,8 +14,8 @@ * limitations under the License. */ -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { errorApiRef } from '@backstage/core-plugin-api'; +import { TestApiProvider } from '@backstage/test-utils'; import { act, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; @@ -34,14 +34,14 @@ describe('', () => { }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} - + ); const location = { diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx index 0a55d6337b..3b6f353155 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx @@ -14,9 +14,9 @@ * limitations under the License. */ -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { errorApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { TestApiProvider } from '@backstage/test-utils'; import { TextField } from '@material-ui/core'; import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -54,13 +54,15 @@ describe('', () => { }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} - + ); const onPrepareFn = jest.fn(); diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx index 8eba11aeca..f6e9d296df 100644 --- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx +++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx @@ -17,16 +17,15 @@ import React from 'react'; import { fireEvent, waitFor } from '@testing-library/react'; import { capitalize } from 'lodash'; -import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { EntityTypePicker } from './EntityTypePicker'; import { MockEntityListContextProvider } from '../../testUtils/providers'; import { catalogApiRef } from '../../api'; import { EntityKindFilter, EntityTypeFilter } from '../../filters'; -import { AlertApi, alertApiRef } from '@backstage/core-plugin-api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; -import { renderWithEffects } from '@backstage/test-utils'; +import { alertApiRef } from '@backstage/core-plugin-api'; +import { ApiProvider } from '@backstage/core-app-api'; +import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils'; const entities: Entity[] = [ { @@ -61,11 +60,20 @@ const entities: Entity[] = [ }, ]; -const apis = ApiRegistry.with(catalogApiRef, { - getEntities: jest.fn().mockResolvedValue({ items: entities }), -} as unknown as CatalogApi).with(alertApiRef, { - post: jest.fn(), -} as unknown as AlertApi); +const apis = TestApiRegistry.from( + [ + catalogApiRef, + { + getEntities: jest.fn().mockResolvedValue({ items: entities }), + }, + ], + [ + alertApiRef, + { + post: jest.fn(), + }, + ], +); describe('', () => { it('renders available entity types', async () => { diff --git a/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx index eccf824c2e..2b29bba4fd 100644 --- a/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx @@ -24,7 +24,7 @@ import { CatalogClient } from '@backstage/catalog-client'; import { catalogApiRef } from '../../api'; import { entityRouteRef } from '../../routes'; import { screen, waitFor } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import * as state from './useUnregisterEntityDialogState'; import { @@ -32,7 +32,6 @@ import { alertApiRef, DiscoveryApi, } from '@backstage/core-plugin-api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('UnregisterEntityDialog', () => { const discoveryApi: DiscoveryApi = { @@ -49,11 +48,6 @@ describe('UnregisterEntityDialog', () => { }, }; - const apis = ApiRegistry.with( - catalogApiRef, - new CatalogClient({ discoveryApi }), - ).with(alertApiRef, alertApi); - const entity = { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -68,7 +62,14 @@ describe('UnregisterEntityDialog', () => { }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); const stateSpy = jest.spyOn(state, 'useUnregisterEntityDialogState'); diff --git a/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx b/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx index 8b4f436aed..8d6083f111 100644 --- a/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx @@ -31,7 +31,7 @@ import { UseUnregisterEntityDialogState, useUnregisterEntityDialogState, } from './useUnregisterEntityDialogState'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { TestApiProvider } from '@backstage/test-utils'; function defer(): { promise: Promise; resolve: (value: T) => void } { let resolve: (value: T) => void = () => {}; @@ -51,9 +51,9 @@ describe('useUnregisterEntityDialogState', () => { const catalogApi = catalogApiMock as Partial as CatalogApi; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - + {children} - + ); let entity: Entity; diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index 4662e58dd2..f6a4c59374 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -26,9 +26,9 @@ import { MockEntityListContextProvider } from '../../testUtils/providers'; import { EntityTagFilter, UserListFilter } from '../../filters'; import { CatalogApi } from '@backstage/catalog-client'; import { catalogApiRef } from '../../api'; -import { MockStorageApi } from '@backstage/test-utils'; +import { MockStorageApi, TestApiRegistry } from '@backstage/test-utils'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { ConfigApi, configApiRef, @@ -62,12 +62,12 @@ const mockIdentityApi = { getIdToken: async () => undefined, } as Partial; -const apis = ApiRegistry.from([ +const apis = TestApiRegistry.from( [configApiRef, mockConfigApi], [catalogApiRef, mockCatalogApi], [identityApiRef, mockIdentityApi], [storageApiRef, MockStorageApi.create()], -]); +); const mockIsOwnedEntity = (entity: Entity) => entity.metadata.name === 'component-1'; diff --git a/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx b/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx index d801e508a6..cdcac8c194 100644 --- a/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityKinds.test.tsx @@ -15,12 +15,12 @@ */ import React, { PropsWithChildren } from 'react'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; import { catalogApiRef } from '../api'; import { renderHook } from '@testing-library/react-hooks'; import { useEntityKinds } from './useEntityKinds'; +import { TestApiProvider } from '@backstage/test-utils'; const entities: Entity[] = [ { @@ -59,9 +59,9 @@ const mockCatalogApi: Partial = { const wrapper = ({ children }: PropsWithChildren<{}>) => { return ( - + {children} - + ); }; diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index e448ee73f1..b96f63304a 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -16,7 +16,6 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { ConfigApi, configApiRef, @@ -24,7 +23,7 @@ import { identityApiRef, storageApiRef, } from '@backstage/core-plugin-api'; -import { MockStorageApi } from '@backstage/test-utils'; +import { MockStorageApi, TestApiProvider } from '@backstage/test-utils'; import { act, renderHook } from '@testing-library/react-hooks'; import qs from 'qs'; import React, { PropsWithChildren } from 'react'; @@ -76,16 +75,6 @@ const mockCatalogApi: Partial = { getEntities: jest.fn().mockImplementation(async () => ({ items: entities })), getEntityByName: async () => undefined, }; -const apis = ApiRegistry.from([ - [configApiRef, mockConfigApi], - [catalogApiRef, mockCatalogApi], - [identityApiRef, mockIdentityApi], - [storageApiRef, MockStorageApi.create()], - [ - starredEntitiesApiRef, - new DefaultStarredEntitiesApi({ storageApi: MockStorageApi.create() }), - ], -]); const wrapper = ({ userFilter, @@ -94,13 +83,26 @@ const wrapper = ({ userFilter?: UserListFilterKind; }>) => { return ( - + - + ); }; diff --git a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx index 4edad98c12..f01c13839d 100644 --- a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx @@ -21,8 +21,8 @@ import { RELATION_OWNED_BY, UserEntity, } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; +import { TestApiProvider } from '@backstage/test-utils'; import { renderHook } from '@testing-library/react-hooks'; import React from 'react'; import { catalogApiRef } from '../api'; @@ -50,14 +50,14 @@ describe('useEntityOwnership', () => { const catalogApi = mockCatalogApi as unknown as CatalogApi; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} - + ); const ownedEntity: ComponentEntity = { diff --git a/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx b/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx index 4f41ef090f..aef876a1b2 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx +++ b/plugins/catalog-react/src/hooks/useStarredEntities.test.tsx @@ -15,9 +15,8 @@ */ import { Entity } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { StorageApi } from '@backstage/core-plugin-api'; -import { MockStorageApi } from '@backstage/test-utils'; +import { MockStorageApi, TestApiProvider } from '@backstage/test-utils'; import { act, renderHook } from '@testing-library/react-hooks'; import React, { PropsWithChildren } from 'react'; import { DefaultStarredEntitiesApi, starredEntitiesApiRef } from '../apis'; @@ -47,14 +46,16 @@ describe('useStarredEntities', () => { beforeEach(() => { mockStorage = MockStorageApi.create(); wrapper = ({ children }: PropsWithChildren<{}>) => ( - {children} - + ); }); diff --git a/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx b/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx index b8577aec0c..8ffc891092 100644 --- a/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx +++ b/plugins/catalog-react/src/hooks/useStarredEntity.test.tsx @@ -15,7 +15,7 @@ */ import { Entity, EntityName } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { TestApiProvider } from '@backstage/test-utils'; import { renderHook } from '@testing-library/react-hooks'; import React, { PropsWithChildren } from 'react'; import Observable from 'zen-observable'; @@ -31,11 +31,9 @@ describe('useStarredEntity', () => { beforeEach(() => { wrapper = ({ children }: PropsWithChildren<{}>) => ( - + {children} - + ); }); diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx index d6f13ed22d..afbed3bdc7 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx @@ -15,11 +15,7 @@ */ import { RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ConfigReader } from '@backstage/core-app-api'; import { ScmIntegrationsApi, scmIntegrationsApiRef, @@ -30,7 +26,7 @@ import { CatalogApi, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { viewTechDocRouteRef } from '../../routes'; @@ -71,21 +67,25 @@ describe('', () => { }, ], }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig( - new ConfigReader({ - integrations: {}, - }), - ), - ).with(catalogApiRef, catalogApi); const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, @@ -116,28 +116,32 @@ describe('', () => { lifecycle: 'production', }, }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig( - new ConfigReader({ - integrations: { - github: [ - { - host: 'github.com', - token: '...', - }, - ], - }, - }), - ), - ).with(catalogApiRef, catalogApi); const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, @@ -167,28 +171,32 @@ describe('', () => { lifecycle: 'production', }, }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig( - new ConfigReader({ - integrations: { - github: [ - { - host: 'github.com', - token: '...', - }, - ], - }, - }), - ), - ).with(catalogApiRef, catalogApi); const { getByTitle } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, @@ -216,17 +224,21 @@ describe('', () => { lifecycle: 'production', }, }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig(new ConfigReader({})), - ).with(catalogApiRef, catalogApi); const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, @@ -253,17 +265,21 @@ describe('', () => { lifecycle: 'production', }, }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig(new ConfigReader({})), - ).with(catalogApiRef, catalogApi); const { getByTitle } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, @@ -295,17 +311,21 @@ describe('', () => { lifecycle: 'production', }, }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig(new ConfigReader({})), - ).with(catalogApiRef, catalogApi); const { queryByTitle } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, @@ -332,28 +352,32 @@ describe('', () => { lifecycle: 'production', }, }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig( - new ConfigReader({ - integrations: { - github: [ - { - host: 'github.com', - token: '...', - }, - ], - }, - }), - ), - ).with(catalogApiRef, catalogApi); const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/docs/:namespace/:kind/:name': viewTechDocRouteRef, @@ -381,28 +405,32 @@ describe('', () => { lifecycle: 'production', }, }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig( - new ConfigReader({ - integrations: { - github: [ - { - host: 'github.com', - token: '...', - }, - ], - }, - }), - ), - ).with(catalogApiRef, catalogApi); const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, @@ -429,28 +457,32 @@ describe('', () => { lifecycle: 'production', }, }; - const apis = ApiRegistry.with( - scmIntegrationsApiRef, - ScmIntegrationsApi.fromConfig( - new ConfigReader({ - integrations: { - github: [ - { - host: 'github.com', - token: '...', - }, - ], - }, - }), - ), - ).with(catalogApiRef, catalogApi); const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, diff --git a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx index 2730665fb6..b320d6d220 100644 --- a/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx +++ b/plugins/catalog/src/components/CatalogKindHeader/CatalogKindHeader.test.tsx @@ -18,13 +18,12 @@ import React from 'react'; import { fireEvent } from '@testing-library/react'; import { Entity } from '@backstage/catalog-model'; import { - CatalogApi, catalogApiRef, EntityKindFilter, MockEntityListContextProvider, } from '@backstage/plugin-catalog-react'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; -import { renderWithEffects } from '@backstage/test-utils'; +import { ApiProvider } from '@backstage/core-app-api'; +import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils'; import { CatalogKindHeader } from './CatalogKindHeader'; const entities: Entity[] = [ @@ -58,9 +57,12 @@ const entities: Entity[] = [ }, ]; -const apis = ApiRegistry.with(catalogApiRef, { - getEntities: jest.fn().mockResolvedValue({ items: entities }), -} as Partial); +const apis = TestApiRegistry.from([ + catalogApiRef, + { + getEntities: jest.fn().mockResolvedValue({ items: entities }), + }, +]); describe('', () => { it('renders available kinds', async () => { diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 6b33b33b08..0a46580c12 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -20,7 +20,6 @@ import { RELATION_MEMBER_OF, RELATION_OWNED_BY, } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { TableColumn, TableProps } from '@backstage/core-components'; import { IdentityApi, @@ -38,6 +37,7 @@ import { mockBreakpoint, MockStorageApi, renderWithEffects, + TestApiProvider, wrapInTestApp, } from '@backstage/test-utils'; import DashboardIcon from '@material-ui/icons/Dashboard'; @@ -129,8 +129,8 @@ describe('CatalogPage', () => { const renderWrapped = (children: React.ReactNode) => renderWithEffects( wrapInTestApp( - { starredEntitiesApiRef, new DefaultStarredEntitiesApi({ storageApi }), ], - ])} + ]} > {children} - , + , { mountedRoutes: { '/create': createComponentRouteRef, diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index 822913a385..4c6adcf400 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -19,7 +19,7 @@ import { Entity, VIEW_URL_ANNOTATION, } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { entityRouteRef, DefaultStarredEntitiesApi, @@ -27,7 +27,11 @@ import { starredEntitiesApiRef, UserListFilter, } from '@backstage/plugin-catalog-react'; -import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; +import { + MockStorageApi, + renderInTestApp, + TestApiRegistry, +} from '@backstage/test-utils'; import { act, fireEvent } from '@testing-library/react'; import * as React from 'react'; import { CatalogTable } from './CatalogTable'; @@ -51,10 +55,10 @@ const entities: Entity[] = [ ]; describe('CatalogTable component', () => { - const mockApis = ApiRegistry.with( + const mockApis = TestApiRegistry.from([ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ storageApi: MockStorageApi.create() }), - ); + ]); beforeEach(() => { window.open = jest.fn(); diff --git a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx index 7dd823a8f0..e43104b67d 100644 --- a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx +++ b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.test.tsx @@ -21,28 +21,20 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { DependencyOfComponentsCard } from './DependencyOfComponentsCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { - const catalogApi: jest.Mocked = { - getLocationById: jest.fn(), - getEntityByName: jest.fn(), - getEntities: jest.fn(), - addLocation: jest.fn(), - getLocationByEntity: jest.fn(), - removeEntityByUid: jest.fn(), - } as any; + const getEntities: jest.MockedFunction = jest.fn(); let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); @@ -97,7 +89,7 @@ describe('', () => { }, ], }; - catalogApi.getEntities.mockResolvedValue({ + getEntities.mockResolvedValue({ items: [ { apiVersion: 'v1', diff --git a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx index b9fd75def9..b654136d18 100644 --- a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx +++ b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.test.tsx @@ -21,28 +21,20 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { DependsOnComponentsCard } from './DependsOnComponentsCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { - const catalogApi: jest.Mocked = { - getLocationById: jest.fn(), - getEntityByName: jest.fn(), - getEntities: jest.fn(), - addLocation: jest.fn(), - getLocationByEntity: jest.fn(), - removeEntityByUid: jest.fn(), - } as any; + const getEntities: jest.MockedFunction = jest.fn(); let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); @@ -97,7 +89,7 @@ describe('', () => { }, ], }; - catalogApi.getEntities.mockResolvedValue({ + getEntities.mockResolvedValue({ items: [ { apiVersion: 'v1', diff --git a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx index bee759254c..40685705aa 100644 --- a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx +++ b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.test.tsx @@ -21,28 +21,20 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { DependsOnResourcesCard } from './DependsOnResourcesCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { - const catalogApi: jest.Mocked = { - getLocationById: jest.fn(), - getEntityByName: jest.fn(), - getEntities: jest.fn(), - addLocation: jest.fn(), - getLocationByEntity: jest.fn(), - removeEntityByUid: jest.fn(), - } as any; + const getEntities: jest.MockedFunction = jest.fn(); let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); @@ -97,7 +89,7 @@ describe('', () => { }, ], }; - catalogApi.getEntities.mockResolvedValue({ + getEntities.mockResolvedValue({ items: [ { apiVersion: 'v1', diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx index 2216070984..3f3fc804e1 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx @@ -16,7 +16,7 @@ import { CatalogApi } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { AlertApi, alertApiRef } from '@backstage/core-plugin-api'; import { AsyncEntityProvider, @@ -26,7 +26,11 @@ import { entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; -import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; +import { + MockStorageApi, + renderInTestApp, + TestApiRegistry, +} from '@backstage/test-utils'; import { fireEvent } from '@testing-library/react'; import React from 'react'; import { act } from 'react-dom/test-utils'; @@ -40,12 +44,14 @@ const mockEntity = { }, } as Entity; -const mockApis = ApiRegistry.with(catalogApiRef, {} as CatalogApi) - .with(alertApiRef, {} as AlertApi) - .with( +const mockApis = TestApiRegistry.from( + [catalogApiRef, {} as CatalogApi], + [alertApiRef, {} as AlertApi], + [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ storageApi: MockStorageApi.create() }), - ); + ], +); describe('EntityLayout', () => { it('renders simplest case', async () => { diff --git a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx index 862ef9504b..f58e6b9b66 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/DeleteEntityDialog.test.tsx @@ -20,10 +20,9 @@ import { ORIGIN_LOCATION_ANNOTATION } from '@backstage/catalog-model'; import { CatalogApi } from '@backstage/catalog-client'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { screen, waitFor } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { AlertApi, alertApiRef } from '@backstage/core-plugin-api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('DeleteEntityDialog', () => { const alertApi: jest.Mocked = { @@ -34,10 +33,6 @@ describe('DeleteEntityDialog', () => { const catalogClient: jest.Mocked = { removeEntityByUid: jest.fn(), } as any; - const apis = ApiRegistry.with(catalogApiRef, catalogClient).with( - alertApiRef, - alertApi, - ); const entity = { apiVersion: 'backstage.io/v1alpha1', @@ -54,7 +49,14 @@ describe('DeleteEntityDialog', () => { }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); afterEach(() => { diff --git a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx index 9e26184a8d..a8aac405fe 100644 --- a/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx +++ b/plugins/catalog/src/components/EntityOrphanWarning/EntityOrphanWarning.test.tsx @@ -15,23 +15,16 @@ */ import { - CatalogApi, catalogApiRef, catalogRouteRef, EntityProvider, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { EntityOrphanWarning } from './EntityOrphanWarning'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { - const catalogClient: jest.Mocked = { - removeEntityByUid: jest.fn(), - } as any; - const apis = ApiRegistry.with(catalogApiRef, catalogClient); - it('renders EntityOrphanWarning if the entity is orphan', async () => { const entity = { apiVersion: 'v1', @@ -50,11 +43,20 @@ describe('', () => { }; const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/create': catalogRouteRef, diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx index c406d0ed3c..fcc4475f6a 100644 --- a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx @@ -21,17 +21,17 @@ import { entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import React from 'react'; import { EntityProcessingErrorsPanel } from './EntityProcessingErrorsPanel'; import { Entity, getEntityName } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; describe('', () => { - const catalogClient: jest.Mocked = { - getEntityAncestors: jest.fn(), - } as any; - const apis = ApiRegistry.with(catalogApiRef, catalogClient); + const getEntityAncestors: jest.MockedFunction< + CatalogApi['getEntityAncestors'] + > = jest.fn(); + const apis = TestApiRegistry.from([catalogApiRef, { getEntityAncestors }]); it('renders EntityProcessErrors if the entity has errors', async () => { const entity: Entity = { @@ -97,7 +97,7 @@ describe('', () => { }, }; - catalogClient.getEntityAncestors.mockResolvedValue({ + getEntityAncestors.mockResolvedValue({ root: getEntityName(entity), items: [{ entity, parents: [] }], }); @@ -198,7 +198,7 @@ describe('', () => { ], }, }; - catalogClient.getEntityAncestors.mockResolvedValue({ + getEntityAncestors.mockResolvedValue({ root: getEntityName(entity), items: [ { entity, parents: [getEntityName(parent)] }, diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx index 7f8bf35b74..8768e9728e 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx @@ -21,17 +21,14 @@ import React from 'react'; import { isKind } from './conditions'; import { EntitySwitch } from './EntitySwitch'; import { featureFlagsApiRef } from '@backstage/core-plugin-api'; -import { - LocalStorageFeatureFlags, - ApiProvider, - ApiRegistry, -} from '@backstage/core-app-api'; +import { LocalStorageFeatureFlags } from '@backstage/core-app-api'; +import { TestApiProvider } from '@backstage/test-utils'; const mockFeatureFlagsApi = new LocalStorageFeatureFlags(); const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - + {children} - + ); describe('EntitySwitch', () => { diff --git a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx index d484ab026a..45f7561d6e 100644 --- a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx +++ b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx @@ -21,28 +21,20 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { HasComponentsCard } from './HasComponentsCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { - const catalogApi: jest.Mocked = { - getLocationById: jest.fn(), - getEntityByName: jest.fn(), - getEntities: jest.fn(), - addLocation: jest.fn(), - getLocationByEntity: jest.fn(), - removeEntityByUid: jest.fn(), - } as any; + const getEntities: jest.MockedFunction = jest.fn(); let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); @@ -97,7 +89,7 @@ describe('', () => { }, ], }; - catalogApi.getEntities.mockResolvedValue({ + getEntities.mockResolvedValue({ items: [ { apiVersion: 'v1', diff --git a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx index 45604ceb56..a01922d988 100644 --- a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx +++ b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.test.tsx @@ -21,28 +21,20 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { HasResourcesCard } from './HasResourcesCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { - const catalogApi: jest.Mocked = { - getLocationById: jest.fn(), - getEntityByName: jest.fn(), - getEntities: jest.fn(), - addLocation: jest.fn(), - getLocationByEntity: jest.fn(), - removeEntityByUid: jest.fn(), - } as any; + const getEntities: jest.MockedFunction = jest.fn(); let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); @@ -92,7 +84,7 @@ describe('', () => { }, ], }; - catalogApi.getEntities.mockResolvedValue({ + getEntities.mockResolvedValue({ items: [ { apiVersion: 'v1', diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx index 431e757656..e517bd38ae 100644 --- a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx +++ b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx @@ -21,28 +21,20 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { HasSubcomponentsCard } from './HasSubcomponentsCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { - const catalogApi: jest.Mocked = { - getLocationById: jest.fn(), - getEntityByName: jest.fn(), - getEntities: jest.fn(), - addLocation: jest.fn(), - getLocationByEntity: jest.fn(), - removeEntityByUid: jest.fn(), - } as any; + const getEntities: jest.MockedFunction = jest.fn(); let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); @@ -97,7 +89,7 @@ describe('', () => { }, ], }; - catalogApi.getEntities.mockResolvedValue({ + getEntities.mockResolvedValue({ items: [ { apiVersion: 'v1', diff --git a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx index 3fcfed6099..050550101f 100644 --- a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx +++ b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx @@ -21,28 +21,20 @@ import { EntityProvider, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { HasSystemsCard } from './HasSystemsCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { - const catalogApi: jest.Mocked = { - getLocationById: jest.fn(), - getEntityByName: jest.fn(), - getEntities: jest.fn(), - addLocation: jest.fn(), - getLocationByEntity: jest.fn(), - removeEntityByUid: jest.fn(), - } as any; + const getEntities: jest.MockedFunction = jest.fn(); let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); @@ -95,7 +87,7 @@ describe('', () => { }, ], }; - catalogApi.getEntities.mockResolvedValue({ + getEntities.mockResolvedValue({ items: [ { apiVersion: 'v1', diff --git a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx index 2b35d0f3b6..e0ec48fbed 100644 --- a/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx +++ b/plugins/catalog/src/components/SystemDiagramCard/SystemDiagramCard.test.tsx @@ -21,10 +21,9 @@ import { entityRouteRef, } from '@backstage/plugin-catalog-react'; import { Entity, RELATION_PART_OF } from '@backstage/catalog-model'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { SystemDiagramCard } from './SystemDiagramCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { beforeAll(() => { @@ -55,11 +54,11 @@ describe('', () => { }; const { queryByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, @@ -114,11 +113,11 @@ describe('', () => { }; const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, @@ -173,11 +172,11 @@ describe('', () => { }; const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, diff --git a/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.test.tsx b/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.test.tsx index 4a38af3dbc..e29bb78f2f 100644 --- a/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.test.tsx +++ b/plugins/cost-insights/src/components/CostInsightsHeader/CostInsightsHeader.test.tsx @@ -15,10 +15,10 @@ */ import { CostInsightsHeader } from './CostInsightsHeader'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import React from 'react'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; describe('', () => { @@ -29,7 +29,7 @@ describe('', () => { }), }; - const apis = ApiRegistry.from([[identityApiRef, identityApi]]); + const apis = TestApiRegistry.from([identityApiRef, identityApi]); it('Shows nothing to do when no alerts exist', async () => { const rendered = await renderInTestApp( diff --git a/plugins/cost-insights/src/plugin.ts b/plugins/cost-insights/src/plugin.ts index 1714c5fbd5..a422098e08 100644 --- a/plugins/cost-insights/src/plugin.ts +++ b/plugins/cost-insights/src/plugin.ts @@ -34,9 +34,7 @@ export const unlabeledDataflowAlertRef = createRouteRef({ export const costInsightsPlugin = createPlugin({ id: 'cost-insights', - register({ featureFlags }) { - featureFlags.register('cost-insights-currencies'); - }, + featureFlags: [{ name: 'cost-insights-currencies' }], routes: { root: rootRouteRef, growthAlerts: projectGrowthAlertRef, diff --git a/plugins/cost-insights/src/testUtils/providers.tsx b/plugins/cost-insights/src/testUtils/providers.tsx index 75450c0bb7..09d2feb71b 100644 --- a/plugins/cost-insights/src/testUtils/providers.tsx +++ b/plugins/cost-insights/src/testUtils/providers.tsx @@ -30,8 +30,9 @@ import { Group, Duration } from '../types'; // TODO(Rugvip): Could be good to have a clear place to put test utils that is linted accordingly // eslint-disable-next-line import/no-extraneous-dependencies -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { TestApiProvider } from '@backstage/test-utils'; type PartialPropsWithChildren = PropsWithChildren>; @@ -199,16 +200,17 @@ export const MockCostInsightsApiProvider = ({ getUserGroups: jest.fn(), }; - // TODO: defaultConfigApiRef: ConfigApiRef - - const defaultContext = ApiRegistry.from([ - [identityApiRef, { ...defaultIdentityApi, ...context.identityApi }], - [ - costInsightsApiRef, - { ...defaultCostInsightsApi, ...context.costInsightsApi }, - ], - // [configApiRef, { ...defaultConfigApiRef, ...context.configApiRef }] - ]); - - return {children}; + return ( + + {children} + + ); }; diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx index 8927cf088b..d10c262357 100644 --- a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx @@ -15,11 +15,10 @@ */ import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor, getByText } from '@testing-library/react'; import React from 'react'; import { DefaultExplorePage } from './DefaultExplorePage'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const catalogApi: jest.Mocked = { @@ -36,9 +35,9 @@ describe('', () => { }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - + {children} - + ); beforeEach(() => { diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx index d78fc0428d..46a8f1adf2 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx @@ -16,12 +16,11 @@ import { DomainEntity } from '@backstage/catalog-model'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { catalogEntityRouteRef } from '../../routes'; import { DomainExplorerContent } from './DomainExplorerContent'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const catalogApi: jest.Mocked = { @@ -38,9 +37,9 @@ describe('', () => { }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - + {children} - + ); const mountedRoutes = { diff --git a/plugins/explore/src/components/ExploreLayout/ExploreLayout.test.tsx b/plugins/explore/src/components/ExploreLayout/ExploreLayout.test.tsx index 80944628da..7574c685a4 100644 --- a/plugins/explore/src/components/ExploreLayout/ExploreLayout.test.tsx +++ b/plugins/explore/src/components/ExploreLayout/ExploreLayout.test.tsx @@ -14,16 +14,12 @@ * limitations under the License. */ -import { - ApiProvider, - ApiRegistry, - FeatureFlagged, -} from '@backstage/core-app-api'; +import { FeatureFlagged } from '@backstage/core-app-api'; import { FeatureFlagsApi, featureFlagsApiRef, } from '@backstage/core-plugin-api'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { ExploreLayout } from './ExploreLayout'; @@ -35,11 +31,11 @@ const featureFlagsApi: jest.Mocked = { registerFlag: jest.fn(), }; -const mockApis = ApiRegistry.with(featureFlagsApiRef, featureFlagsApi); - describe('', () => { const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); afterEach(() => { diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx index 66f54900c9..7c57de84bc 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsDiagram.test.tsx @@ -20,10 +20,9 @@ import { entityRouteRef, } from '@backstage/plugin-catalog-react'; import { Entity } from '@backstage/catalog-model'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { GroupsDiagram } from './GroupsDiagram'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { beforeAll(() => { @@ -57,9 +56,9 @@ describe('', () => { }; const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef, diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx index f0815e0116..5d3f7c358b 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx @@ -16,11 +16,10 @@ import { Entity } from '@backstage/catalog-model'; import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { GroupsExplorerContent } from '../GroupsExplorerContent'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const catalogApi: jest.Mocked = { @@ -37,9 +36,9 @@ describe('', () => { }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - + {children} - + ); const mountedRoutes = { diff --git a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx index 8accf8fd30..da35c9fa39 100644 --- a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx +++ b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx @@ -18,13 +18,12 @@ import { ExploreTool, exploreToolsConfigRef, } from '@backstage/plugin-explore-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; import { waitFor } from '@testing-library/react'; import React from 'react'; import { ToolExplorerContent } from './ToolExplorerContent'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const exploreToolsConfigApi: jest.Mocked = { @@ -33,11 +32,9 @@ describe('', () => { const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - + {children} - + ); diff --git a/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.test.tsx b/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.test.tsx index 1123f014da..20dbb77e86 100644 --- a/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.test.tsx +++ b/plugins/firehydrant/src/components/ServiceDetailsCard/ServiceDetailsCard.test.tsx @@ -14,19 +14,19 @@ * limitations under the License. */ import React from 'react'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { fireHydrantApiRef } from '../../api'; import { screen } from '@testing-library/react'; import { ServiceDetailsCard } from './ServiceDetailsCard'; import { Service, Incident } from '../types'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; const mockFireHydrantApi = { - getServiceDetails: () => {}, - getServiceAnalytics: () => {}, + getServiceDetails: jest.fn(), + getServiceAnalytics: jest.fn(), }; -const apis = ApiRegistry.from([[fireHydrantApiRef, mockFireHydrantApi]]); +const apis = TestApiRegistry.from([fireHydrantApiRef, mockFireHydrantApi]); jest.mock('@backstage/plugin-catalog-react', () => ({ useEntity: () => { diff --git a/plugins/fossa/src/components/FossaCard/FossaCard.test.tsx b/plugins/fossa/src/components/FossaCard/FossaCard.test.tsx index 79b401e4a8..046e3e9860 100644 --- a/plugins/fossa/src/components/FossaCard/FossaCard.test.tsx +++ b/plugins/fossa/src/components/FossaCard/FossaCard.test.tsx @@ -16,11 +16,10 @@ import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { FossaApi, fossaApiRef } from '../../api'; import { FossaCard } from './FossaCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const fossaApi: jest.Mocked = { @@ -30,10 +29,10 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(fossaApiRef, fossaApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx index 778f5ee9c6..421ff4431f 100644 --- a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx +++ b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx @@ -20,11 +20,10 @@ import { catalogApiRef, entityRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { FossaApi, fossaApiRef } from '../../api'; import { FossaPage } from './FossaPage'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { const catalogApi: jest.Mocked = { @@ -46,13 +45,15 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(fossaApiRef, fossaApi).with( - catalogApiRef, - catalogApi, - ); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx index 4a4452be5c..0f9573e051 100644 --- a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx @@ -24,16 +24,13 @@ import { useWorkflowRuns } from '../useWorkflowRuns'; import type { Props as RecentWorkflowRunsCardProps } from './RecentWorkflowRunsCard'; import { RecentWorkflowRunsCard } from './RecentWorkflowRunsCard'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ConfigReader } from '@backstage/core-app-api'; import { errorApiRef, configApiRef, ConfigApi, } from '@backstage/core-plugin-api'; +import { TestApiProvider } from '@backstage/test-utils'; jest.mock('../useWorkflowRuns', () => ({ useWorkflowRuns: jest.fn(), @@ -82,16 +79,16 @@ describe('', () => { render( - - + , ); diff --git a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx index 08eeb4a3ce..dba5bbfe1c 100644 --- a/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx +++ b/plugins/github-deployments/src/components/GithubDeploymentsCard.test.tsx @@ -19,6 +19,7 @@ import { fireEvent } from '@testing-library/react'; import { setupRequestMockHandlers, renderInTestApp, + TestApiRegistry, } from '@backstage/test-utils'; import { GithubDeployment, @@ -42,11 +43,7 @@ import { Entity } from '@backstage/catalog-model'; import { GithubDeploymentsTable } from './GithubDeploymentsTable'; import { Box } from '@material-ui/core'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { errorApiRef, configApiRef, @@ -95,14 +92,14 @@ const githubAuthApi: OAuthApi = { getAccessToken: async _ => 'access_token', }; -const apis = ApiRegistry.from([ +const apis = TestApiRegistry.from( [configApiRef, configApi], [errorApiRef, errorApiMock], [ githubDeploymentsApiRef, new GithubDeploymentsApiClient({ scmIntegrationsApi, githubAuthApi }), ], -]); +); const assertFetchedData = async () => { const rendered = await renderInTestApp( diff --git a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx index 8dba498507..e158bcf6ad 100644 --- a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx +++ b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; import React from 'react'; @@ -23,7 +23,6 @@ import ProfileCatalog from './ProfileCatalog'; import { ApiProvider, - ApiRegistry, GithubAuth, OAuthRequestManager, UrlPatternDiscovery, @@ -34,7 +33,7 @@ import { githubAuthApiRef } from '@backstage/core-plugin-api'; describe('ProfileCatalog', () => { it('should render', async () => { const oauthRequestApi = new OAuthRequestManager(); - const apis = ApiRegistry.from([ + const apis = TestApiRegistry.from( [gitOpsApiRef, new GitOpsRestApi('http://localhost:3008')], [ githubAuthApiRef, @@ -45,7 +44,7 @@ describe('ProfileCatalog', () => { oauthRequestApi, }), ], - ]); + ); const { getByText } = await renderInTestApp( diff --git a/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx b/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx index 4e426d4364..ae84b549ec 100644 --- a/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx +++ b/plugins/graphiql/src/components/GraphiQLPage/GraphiQLPage.test.tsx @@ -19,14 +19,10 @@ import { GraphiQLPage } from './GraphiQLPage'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; import { act } from 'react-dom/test-utils'; -import { renderWithEffects } from '@backstage/test-utils'; +import { renderWithEffects, TestApiProvider } from '@backstage/test-utils'; import { GraphQLBrowseApi, graphQlBrowseApiRef } from '../../lib/api'; import { configApiRef } from '@backstage/core-plugin-api'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ConfigReader } from '@backstage/core-app-api'; jest.mock('../GraphiQLBrowser', () => ({ GraphiQLBrowser: () => '', @@ -43,17 +39,17 @@ describe('GraphiQLPage', () => { }; const rendered = await renderWithEffects( - , - , + , ); act(() => { jest.advanceTimersByTime(250); @@ -71,16 +67,16 @@ describe('GraphiQLPage', () => { }; const rendered = await renderWithEffects( - - , + , ); rendered.getByText('GraphiQL'); @@ -95,16 +91,16 @@ describe('GraphiQLPage', () => { }; const rendered = await renderWithEffects( - - , + , ); rendered.getByText('GraphiQL'); diff --git a/plugins/jenkins/src/components/Cards/Cards.test.tsx b/plugins/jenkins/src/components/Cards/Cards.test.tsx index 2508461710..0af91da179 100644 --- a/plugins/jenkins/src/components/Cards/Cards.test.tsx +++ b/plugins/jenkins/src/components/Cards/Cards.test.tsx @@ -15,11 +15,10 @@ */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { LatestRunCard } from './Cards'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { JenkinsApi, jenkinsApiRef } from '../../api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { Project } from '../../api/JenkinsApi'; describe('', () => { @@ -41,14 +40,12 @@ describe('', () => { }; it('should show success status of latest build', async () => { - const apis = ApiRegistry.from([[jenkinsApiRef, jenkinsApi]]); - const { getByText } = await renderInTestApp( - + - , + , ); expect(getByText('Completed')).toBeInTheDocument(); @@ -59,14 +56,12 @@ describe('', () => { getProjects: () => Promise.reject(new Error('Unauthorized')), }; - const apis = ApiRegistry.from([[jenkinsApiRef, jenkinsApiWithError]]); - const { getByText } = await renderInTestApp( - + - , + , ); expect(getByText("Error: Can't connect to Jenkins")).toBeInTheDocument(); @@ -82,14 +77,12 @@ describe('', () => { }), }; - const apis = ApiRegistry.from([[jenkinsApiRef, jenkinsApiWithError]]); - const { getByText } = await renderInTestApp( - + - , + , ); expect(getByText("Error: Can't find Jenkins project")).toBeInTheDocument(); diff --git a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx index a90d3d608e..0be9cabacd 100644 --- a/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx +++ b/plugins/kafka/src/components/ConsumerGroupOffsets/useConsumerGroupsOffsetsForEntity.test.tsx @@ -26,8 +26,8 @@ import { import { useConsumerGroupsOffsetsForEntity } from './useConsumerGroupsOffsetsForEntity'; import * as data from './__fixtures__/consumer-group-offsets.json'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { errorApiRef } from '@backstage/core-plugin-api'; +import { TestApiProvider } from '@backstage/test-utils'; const consumerGroupOffsets = data as ConsumerGroupOffsetsResponse; @@ -59,14 +59,14 @@ describe('useConsumerGroupOffsets', () => { const wrapper = ({ children }: PropsWithChildren<{}>) => { return ( - {children} - + ); }; diff --git a/plugins/kubernetes/dev/index.tsx b/plugins/kubernetes/dev/index.tsx index a2c1114c2d..12de5f098f 100644 --- a/plugins/kubernetes/dev/index.tsx +++ b/plugins/kubernetes/dev/index.tsx @@ -29,7 +29,7 @@ import { } from '@backstage/plugin-kubernetes-common'; import fixture1 from '../src/__fixtures__/1-deployments.json'; import fixture2 from '../src/__fixtures__/2-deployments.json'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { TestApiProvider } from '@backstage/test-utils'; const mockEntity: Entity = { apiVersion: 'backstage.io/v1alpha1', @@ -80,32 +80,26 @@ createDevApp() path: '/fixture-1', title: 'Fixture 1', element: ( - - + ), }) .addPage({ path: '/fixture-2', title: 'Fixture 2', element: ( - - + ), }) .registerPlugin(kubernetesPlugin) diff --git a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx index 1433a186fb..53de33d21b 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListForEntity.test.tsx @@ -30,8 +30,9 @@ import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity'; import * as data from '../../__fixtures__/website-list-response.json'; import { AuditListForEntity } from './AuditListForEntity'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { errorApiRef } from '@backstage/core-plugin-api'; +import { TestApiRegistry } from '@backstage/test-utils'; jest.mock('../../hooks/useWebsiteForEntity', () => ({ useWebsiteForEntity: jest.fn(), @@ -41,7 +42,7 @@ const websiteListResponse = data as WebsiteListResponse; const entityWebsite = websiteListResponse.items[0]; describe('', () => { - let apis: ApiRegistry; + let apis: TestApiRegistry; const mockErrorApi: jest.Mocked = { post: jest.fn(), @@ -49,10 +50,10 @@ describe('', () => { }; beforeEach(() => { - apis = ApiRegistry.from([ + apis = TestApiRegistry.from( [lighthouseApiRef, new LighthouseRestApi('http://lighthouse')], [errorApiRef, mockErrorApi], - ]); + ); (useWebsiteForEntity as jest.Mock).mockReturnValue({ value: entityWebsite, diff --git a/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx b/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx index 4c92997755..d8a54928b2 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListTable.test.tsx @@ -16,7 +16,11 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { wrapInTestApp, setupRequestMockHandlers } from '@backstage/test-utils'; +import { + wrapInTestApp, + setupRequestMockHandlers, + TestApiRegistry, +} from '@backstage/test-utils'; import AuditListTable from './AuditListTable'; import { @@ -28,19 +32,20 @@ import { formatTime } from '../../utils'; import { setupServer } from 'msw/node'; import * as data from '../../__fixtures__/website-list-response.json'; -import { ApiRegistry, ApiProvider } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; const websiteListResponse = data as WebsiteListResponse; describe('AuditListTable', () => { - let apis: ApiRegistry; + let apis: TestApiRegistry; const server = setupServer(); setupRequestMockHandlers(server); beforeEach(() => { - apis = ApiRegistry.from([ - [lighthouseApiRef, new LighthouseRestApi('http://lighthouse')], + apis = TestApiRegistry.from([ + lighthouseApiRef, + new LighthouseRestApi('http://lighthouse'), ]); }); diff --git a/plugins/lighthouse/src/components/AuditList/index.test.tsx b/plugins/lighthouse/src/components/AuditList/index.test.tsx index ea3587fea9..2b422cce7a 100644 --- a/plugins/lighthouse/src/components/AuditList/index.test.tsx +++ b/plugins/lighthouse/src/components/AuditList/index.test.tsx @@ -23,7 +23,11 @@ jest.mock('react-router-dom', () => { }; }); -import { setupRequestMockHandlers, wrapInTestApp } from '@backstage/test-utils'; +import { + setupRequestMockHandlers, + TestApiRegistry, + wrapInTestApp, +} from '@backstage/test-utils'; import { fireEvent, render } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -35,20 +39,21 @@ import { } from '../../api'; import * as data from '../../__fixtures__/website-list-response.json'; import AuditList from './index'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; const { useNavigate } = jest.requireMock('react-router-dom'); const websiteListResponse = data as WebsiteListResponse; describe('AuditList', () => { - let apis: ApiRegistry; + let apis: TestApiRegistry; const server = setupServer(); setupRequestMockHandlers(server); beforeEach(() => { - apis = ApiRegistry.from([ - [lighthouseApiRef, new LighthouseRestApi('http://lighthouse')], + apis = TestApiRegistry.from([ + lighthouseApiRef, + new LighthouseRestApi('http://lighthouse'), ]); }); diff --git a/plugins/lighthouse/src/components/AuditView/index.test.tsx b/plugins/lighthouse/src/components/AuditView/index.test.tsx index ed1bbbcaf9..00b2843a50 100644 --- a/plugins/lighthouse/src/components/AuditView/index.test.tsx +++ b/plugins/lighthouse/src/components/AuditView/index.test.tsx @@ -26,7 +26,11 @@ jest.mock('react-router-dom', () => { }; }); -import { setupRequestMockHandlers, wrapInTestApp } from '@backstage/test-utils'; +import { + setupRequestMockHandlers, + TestApiRegistry, + wrapInTestApp, +} from '@backstage/test-utils'; import { render } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -35,14 +39,14 @@ import { Audit, lighthouseApiRef, LighthouseRestApi, Website } from '../../api'; import { formatTime } from '../../utils'; import * as data from '../../__fixtures__/website-response.json'; import AuditView from './index'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; const { useParams }: { useParams: jest.Mock } = jest.requireMock('react-router-dom'); const websiteResponse = data as Website; describe('AuditView', () => { - let apis: ApiRegistry; + let apis: TestApiRegistry; let id: string; const server = setupServer(); @@ -55,8 +59,9 @@ describe('AuditView', () => { ), ); - apis = ApiRegistry.from([ - [lighthouseApiRef, new LighthouseRestApi('https://lighthouse')], + apis = TestApiRegistry.from([ + lighthouseApiRef, + new LighthouseRestApi('https://lighthouse'), ]); id = websiteResponse.audits.find(a => a.status === 'COMPLETED') ?.id as string; diff --git a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx index 84493e243f..59847b516e 100644 --- a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx +++ b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx @@ -23,7 +23,11 @@ jest.mock('react-router-dom', () => { }; }); -import { setupRequestMockHandlers, wrapInTestApp } from '@backstage/test-utils'; +import { + setupRequestMockHandlers, + TestApiRegistry, + wrapInTestApp, +} from '@backstage/test-utils'; import { fireEvent, render, waitFor } from '@testing-library/react'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; @@ -32,7 +36,7 @@ import { Audit, lighthouseApiRef, LighthouseRestApi } from '../../api'; import * as data from '../../__fixtures__/create-audit-response.json'; import CreateAudit from './index'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { ErrorApi, errorApiRef } from '@backstage/core-plugin-api'; const { useNavigate }: { useNavigate: jest.Mock } = @@ -41,17 +45,17 @@ const createAuditResponse = data as Audit; // TODO add act() to these tests without breaking them! describe('CreateAudit', () => { - let apis: ApiRegistry; + let apis: TestApiRegistry; let errorApi: ErrorApi; const server = setupServer(); setupRequestMockHandlers(server); beforeEach(() => { errorApi = { post: jest.fn(), error$: jest.fn() }; - apis = ApiRegistry.from([ + apis = TestApiRegistry.from( [lighthouseApiRef, new LighthouseRestApi('http://lighthouse')], [errorApiRef, errorApi], - ]); + ); }); it('renders the form', () => { diff --git a/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx index 64f7483081..13da0f6f4f 100644 --- a/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx +++ b/plugins/lighthouse/src/hooks/useWebsiteForEntity.test.tsx @@ -21,8 +21,8 @@ import { lighthouseApiRef, WebsiteListResponse } from '../api'; import * as data from '../__fixtures__/website-list-response.json'; import { useWebsiteForEntity } from './useWebsiteForEntity'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { errorApiRef } from '@backstage/core-plugin-api'; +import { TestApiProvider } from '@backstage/test-utils'; const websiteListResponse = data as WebsiteListResponse; const website = websiteListResponse.items[0]; @@ -55,14 +55,14 @@ describe('useWebsiteForEntity', () => { const wrapper = ({ children }: PropsWithChildren<{}>) => { return ( - {children} - + ); }; diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx index 0a5a0cfef1..b6b549d934 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx @@ -15,8 +15,8 @@ */ import { Entity, GroupEntity } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { catalogApiRef, EntityProvider } from '@backstage/plugin-catalog-react'; +import { TestApiProvider } from '@backstage/test-utils'; import { Grid } from '@material-ui/core'; import React from 'react'; import { MemoryRouter } from 'react-router'; @@ -99,12 +99,9 @@ const catalogApi = (items: Entity[]) => ({ getEntities: () => Promise.resolve({ items }), }); -const apiRegistry = (items: Entity[]) => - ApiRegistry.from([[catalogApiRef, catalogApi(items)]]); - export const Default = () => ( - + @@ -112,13 +109,13 @@ export const Default = () => ( - + ); export const Empty = () => ( - + @@ -126,6 +123,6 @@ export const Empty = () => ( - + ); diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx index 75cb9efd7f..b0c68d9a83 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx @@ -20,10 +20,13 @@ import { catalogApiRef, EntityProvider, } from '@backstage/plugin-catalog-react'; -import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import { + renderWithEffects, + TestApiProvider, + wrapInTestApp, +} from '@backstage/test-utils'; import React from 'react'; import { MembersListCard } from './MembersListCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('MemberTab Test', () => { const groupEntity: GroupEntity = { @@ -103,17 +106,15 @@ describe('MemberTab Test', () => { }), }; - const apis = ApiRegistry.from([[catalogApiRef, catalogApi]]); - it('Display Profile Card', async () => { const rendered = await renderWithEffects( wrapInTestApp( - + , - , + , ), ); diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx index 14fed985b6..6a20035dfa 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx @@ -15,14 +15,14 @@ */ import { GroupEntity } from '@backstage/catalog-model'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { CatalogApi, catalogApiRef, catalogRouteRef, EntityProvider, } from '@backstage/plugin-catalog-react'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { BackstageTheme, createTheme, @@ -86,11 +86,11 @@ const catalogApi: Partial = { getEntities: () => Promise.resolve({ items: [serviceA, serviceB, websiteA] }), }; -const apiRegistry = ApiRegistry.from([[catalogApiRef, catalogApi]]); +const apis = TestApiRegistry.from([catalogApiRef, catalogApi]); export const Default = () => wrapInTestApp( - + @@ -123,7 +123,7 @@ const monochromeTheme = (outer: BackstageTheme) => export const Themed = () => wrapInTestApp( - + diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx index b5ffd37083..8d6fc1a002 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx @@ -21,11 +21,10 @@ import { EntityProvider, catalogRouteRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { queryByText } from '@testing-library/react'; import React from 'react'; import { OwnershipCard } from './OwnershipCard'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('OwnershipCard', () => { const groupEntity: GroupEntity = { @@ -121,11 +120,11 @@ describe('OwnershipCard', () => { }); const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/create': catalogRouteRef, @@ -157,11 +156,11 @@ describe('OwnershipCard', () => { }); const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/create': catalogRouteRef, @@ -205,11 +204,11 @@ describe('OwnershipCard', () => { }); const { getByText } = await renderInTestApp( - + - , + , { mountedRoutes: { '/create': catalogRouteRef, diff --git a/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.test.tsx b/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.test.tsx index 446c867f35..723e7b7b19 100644 --- a/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.test.tsx +++ b/plugins/pagerduty/src/components/ChangeEvents/ChangeEvents.test.tsx @@ -16,15 +16,15 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { ChangeEvent } from '../types'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { pagerDutyApiRef } from '../../api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { ChangeEvents } from './ChangeEvents'; const mockPagerDutyApi = { - getChangeEventsByServiceId: () => [], + getChangeEventsByServiceId: jest.fn(), }; -const apis = ApiRegistry.from([[pagerDutyApiRef, mockPagerDutyApi]]); +const apis = TestApiRegistry.from([pagerDutyApiRef, mockPagerDutyApi]); describe('Incidents', () => { it('Renders an empty state when there are no change events', async () => { diff --git a/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx b/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx index 52aa9a860f..47f18002fe 100644 --- a/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx +++ b/plugins/pagerduty/src/components/Escalation/Escalation.test.tsx @@ -16,15 +16,15 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { EscalationPolicy } from './EscalationPolicy'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { User } from '../types'; import { pagerDutyApiRef } from '../../api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; const mockPagerDutyApi = { - getOnCallByPolicyId: () => [], + getOnCallByPolicyId: jest.fn(), }; -const apis = ApiRegistry.from([[pagerDutyApiRef, mockPagerDutyApi]]); +const apis = TestApiRegistry.from([pagerDutyApiRef, mockPagerDutyApi]); describe('Escalation', () => { it('Handles an empty response', async () => { diff --git a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx index d66acc0879..74e7d82e8b 100644 --- a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx +++ b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx @@ -16,15 +16,15 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { Incidents } from './Incidents'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { pagerDutyApiRef } from '../../api'; import { Incident } from '../types'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; const mockPagerDutyApi = { - getIncidentsByServiceId: () => [], + getIncidentsByServiceId: jest.fn(), }; -const apis = ApiRegistry.from([[pagerDutyApiRef, mockPagerDutyApi]]); +const apis = TestApiRegistry.from([pagerDutyApiRef, mockPagerDutyApi]); describe('Incidents', () => { it('Renders an empty state when there are no incidents', async () => { diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx index d644aa019a..e0a857ccc7 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx @@ -18,12 +18,12 @@ import { render, waitFor, fireEvent, act } from '@testing-library/react'; import { PagerDutyCard } from '../PagerDutyCard'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { pagerDutyApiRef, UnauthorizedError, PagerDutyClient } from '../../api'; import { Service, User } from '../types'; -import { alertApiRef, createApiRef } from '@backstage/core-plugin-api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { alertApiRef } from '@backstage/core-plugin-api'; +import { ApiProvider } from '@backstage/core-app-api'; const mockPagerDutyApi: Partial = { getServiceByIntegrationKey: async () => [], @@ -31,16 +31,10 @@ const mockPagerDutyApi: Partial = { getIncidentsByServiceId: async () => [], }; -const apis = ApiRegistry.from([ +const apis = TestApiRegistry.from( [pagerDutyApiRef, mockPagerDutyApi], - [ - alertApiRef, - createApiRef({ - id: 'core.alert', - description: 'Used to report alerts and forward them to the app', - }), - ], -]); + [alertApiRef, {}], +); const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', diff --git a/plugins/pagerduty/src/components/TriggerButton/index.test.tsx b/plugins/pagerduty/src/components/TriggerButton/index.test.tsx index 21f0dd5f1d..3776bcaf1c 100644 --- a/plugins/pagerduty/src/components/TriggerButton/index.test.tsx +++ b/plugins/pagerduty/src/components/TriggerButton/index.test.tsx @@ -15,16 +15,15 @@ */ import React from 'react'; import { act, fireEvent, screen, waitFor } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { pagerDutyApiRef } from '../../api'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { TriggerButton } from './'; -import { ApiRegistry, ApiProvider } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { alertApiRef, - createApiRef, IdentityApi, identityApiRef, } from '@backstage/core-plugin-api'; @@ -39,17 +38,11 @@ describe('TriggerButton', () => { triggerAlarm: mockTriggerAlarmFn, }; - const apis = ApiRegistry.from([ - [ - alertApiRef, - createApiRef({ - id: 'core.alert', - description: 'Used to report alerts and forward them to the app', - }), - ], + const apis = TestApiRegistry.from( + [alertApiRef, {}], [identityApiRef, mockIdentityApi], [pagerDutyApiRef, mockPagerDutyApi], - ]); + ); it('renders the trigger button, opens and closes dialog', async () => { const entity: Entity = { diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx index 444e820b98..d6f378b4bf 100644 --- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx +++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.test.tsx @@ -15,16 +15,15 @@ */ import React from 'react'; import { fireEvent, act } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { pagerDutyApiRef } from '../../api'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; import { TriggerDialog } from './TriggerDialog'; -import { ApiRegistry, ApiProvider } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { alertApiRef, - createApiRef, IdentityApi, identityApiRef, } from '@backstage/core-plugin-api'; @@ -39,17 +38,11 @@ describe('TriggerDialog', () => { triggerAlarm: mockTriggerAlarmFn, }; - const apis = ApiRegistry.from([ - [ - alertApiRef, - createApiRef({ - id: 'core.alert', - description: 'Used to report alerts and forward them to the app', - }), - ], + const apis = TestApiRegistry.from( + [alertApiRef, {}], [identityApiRef, mockIdentityApi], [pagerDutyApiRef, mockPagerDutyApi], - ]); + ); it('open the dialog and trigger an alarm', async () => { const entity: Entity = { diff --git a/plugins/permission-node/.eslintrc.js b/plugins/permission-node/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/plugins/permission-node/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/plugins/permission-node/README.md b/plugins/permission-node/README.md new file mode 100644 index 0000000000..82dfe54f9c --- /dev/null +++ b/plugins/permission-node/README.md @@ -0,0 +1,7 @@ +# @backstage/plugin-permission-node + +> NOTE: THIS PACKAGE IS EXPERIMENTAL, HERE BE DRAGONS + +Common permission and authorization utilities for backend plugins. For more +information, see the [authorization +PRFC](https://github.com/backstage/backstage/pull/7761). diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md new file mode 100644 index 0000000000..e5fe9a7087 --- /dev/null +++ b/plugins/permission-node/api-report.md @@ -0,0 +1,130 @@ +## API Report File for "@backstage/plugin-permission-node" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { AuthorizeRequest } from '@backstage/plugin-permission-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; +import { BackstageIdentity } from '@backstage/plugin-auth-backend'; +import { PermissionCondition } from '@backstage/plugin-permission-common'; +import { PermissionCriteria } from '@backstage/plugin-permission-common'; +import { Router } from 'express'; + +// @public +export type ApplyConditionsRequest = { + resourceRef: string; + resourceType: string; + conditions: PermissionCriteria; +}; + +// @public +export type ApplyConditionsResponse = { + result: AuthorizeResult.ALLOW | AuthorizeResult.DENY; +}; + +// @public +export type Condition = TRule extends PermissionRule< + any, + any, + infer TParams +> + ? (...params: TParams) => PermissionCondition + : never; + +// @public +export type ConditionalPolicyResult = { + result: AuthorizeResult.CONDITIONAL; + conditions: { + pluginId: string; + resourceType: string; + conditions: PermissionCriteria; + }; +}; + +// @public +export type Conditions< + TRules extends Record>, +> = { + [Name in keyof TRules]: Condition; +}; + +// @public +export type ConditionTransformer = ( + conditions: PermissionCriteria, +) => PermissionCriteria; + +// @public +export const createConditionExports: < + TResource, + TRules extends Record>, +>(options: { + pluginId: string; + resourceType: string; + rules: TRules; +}) => { + conditions: Conditions; + createConditions: (conditions: PermissionCriteria) => { + pluginId: string; + resourceType: string; + conditions: PermissionCriteria; + }; +}; + +// @public +export const createConditionFactory: ( + rule: PermissionRule, +) => (...params: TParams) => { + rule: string; + params: TParams; +}; + +// @public +export const createConditionTransformer: < + TQuery, + TRules extends PermissionRule[], +>( + permissionRules: [...TRules], +) => ConditionTransformer; + +// @public +export const createPermissionIntegrationRouter: ({ + resourceType, + rules, + getResource, +}: { + resourceType: string; + rules: PermissionRule[]; + getResource: (resourceRef: string) => Promise; +}) => Router; + +// @public +export interface PermissionPolicy { + // (undocumented) + handle( + request: PolicyAuthorizeRequest, + user?: BackstageIdentity, + ): Promise; +} + +// @public +export type PermissionRule< + TResource, + TQuery, + TParams extends unknown[] = unknown[], +> = { + name: string; + description: string; + apply(resource: TResource, ...params: TParams): boolean; + toQuery(...params: TParams): PermissionCriteria; +}; + +// @public +export type PolicyAuthorizeRequest = Omit; + +// @public +export type PolicyResult = + | { + result: AuthorizeResult.ALLOW | AuthorizeResult.DENY; + } + | ConditionalPolicyResult; +``` diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json new file mode 100644 index 0000000000..e22e2d2de4 --- /dev/null +++ b/plugins/permission-node/package.json @@ -0,0 +1,46 @@ +{ + "name": "@backstage/plugin-permission-node", + "description": "Common permission and authorization utilities for backend plugins", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/permission-node" + }, + "keywords": [ + "backstage", + "permissions" + ], + "scripts": { + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/plugin-auth-backend": "^0.4.6", + "@backstage/plugin-permission-common": "^0.1.0", + "@types/express": "^4.17.6", + "express": "^4.17.1", + "zod": "^3.11.6" + }, + "devDependencies": { + "@backstage/cli": "^0.9.0", + "@types/supertest": "^2.0.8", + "supertest": "^6.1.3" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/permission-node/src/index.ts b/plugins/permission-node/src/index.ts new file mode 100644 index 0000000000..39527bac71 --- /dev/null +++ b/plugins/permission-node/src/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2021 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. + */ + +/** + * Common permission and authorization utilities for backend plugins + * + * @packageDocumentation + */ +export * from './integration'; +export * from './policy'; +export * from './types'; diff --git a/plugins/permission-node/src/integration/createConditionExports.test.ts b/plugins/permission-node/src/integration/createConditionExports.test.ts new file mode 100644 index 0000000000..6f0f8f8632 --- /dev/null +++ b/plugins/permission-node/src/integration/createConditionExports.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright 2021 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 { createConditionExports } from './createConditionExports'; + +const testIntegration = () => + createConditionExports({ + pluginId: 'test-plugin', + resourceType: 'test-resource', + rules: { + testRule1: { + name: 'testRule1', + description: 'Test rule 1', + apply: jest.fn( + (_resource: any, _firstParam: string, _secondParam: number) => true, + ), + toQuery: jest.fn((firstParam: string, secondParam: number) => ({ + query: 'testRule1', + params: [firstParam, secondParam], + })), + }, + testRule2: { + name: 'testRule2', + description: 'Test rule 2', + apply: jest.fn((_firstParam: object) => false), + toQuery: jest.fn((firstParam: object) => ({ + query: 'testRule2', + params: [firstParam], + })), + }, + }, + }); + +describe('createConditionExports', () => { + describe('conditions', () => { + it('creates condition factories for the supplied rules', () => { + const { conditions } = testIntegration(); + + expect(conditions.testRule1('a', 1)).toEqual({ + rule: 'testRule1', + params: ['a', 1], + }); + + expect(conditions.testRule2({ baz: 'quux' })).toEqual({ + rule: 'testRule2', + params: [{ baz: 'quux' }], + }); + }); + }); + + describe('createConditions', () => { + it('wraps conditions in an object with resourceType and pluginId', () => { + const { createConditions } = testIntegration(); + + expect( + createConditions({ allOf: [{ rule: 'testRule1', params: ['a', 1] }] }), + ).toEqual({ + pluginId: 'test-plugin', + resourceType: 'test-resource', + conditions: { + allOf: [{ rule: 'testRule1', params: ['a', 1] }], + }, + }); + }); + }); +}); diff --git a/plugins/permission-node/src/integration/createConditionExports.ts b/plugins/permission-node/src/integration/createConditionExports.ts new file mode 100644 index 0000000000..30cff6628a --- /dev/null +++ b/plugins/permission-node/src/integration/createConditionExports.ts @@ -0,0 +1,100 @@ +/* + * Copyright 2021 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 { + PermissionCondition, + PermissionCriteria, +} from '@backstage/plugin-permission-common'; +import { PermissionRule } from '../types'; +import { createConditionFactory } from './createConditionFactory'; + +/** + * A utility type for mapping a single {@link PermissionRule} to its + * corresponding {@link @backstage/plugin-permission-common#PermissionCondition}. + * + * @public + */ +export type Condition = TRule extends PermissionRule< + any, + any, + infer TParams +> + ? (...params: TParams) => PermissionCondition + : never; + +/** + * A utility type for mapping {@link PermissionRule}s to their corresponding + * {@link @backstage/plugin-permission-common#PermissionCondition}s. + * + * @public + */ +export type Conditions< + TRules extends Record>, +> = { + [Name in keyof TRules]: Condition; +}; + +/** + * Creates the recommended condition-related exports for a given plugin based on the built-in + * {@link PermissionRule}s it supports. + * + * @remarks + * + * The function returns a `conditions` object containing a + * {@link @backstage/plugin-permission-common#PermissionCondition} factory for each of the + * supplied {@link PermissionRule}s, along with a `createConditions` function which builds the + * wrapper object needed to enclose conditions when authoring {@link PermissionPolicy} implementations. + * + * Plugin authors should generally call this method with all the built-in {@link PermissionRule}s + * the plugin supports, and export the resulting `conditions` object and `createConditions` + * function so that they can be used by {@link PermissionPolicy} authors. + * + * @public + */ +export const createConditionExports = < + TResource, + TRules extends Record>, +>(options: { + pluginId: string; + resourceType: string; + rules: TRules; +}): { + conditions: Conditions; + createConditions: (conditions: PermissionCriteria) => { + pluginId: string; + resourceType: string; + conditions: PermissionCriteria; + }; +} => { + const { pluginId, resourceType, rules } = options; + + return { + conditions: Object.entries(rules).reduce( + (acc, [key, rule]) => ({ + ...acc, + [key]: createConditionFactory(rule), + }), + {} as Conditions, + ), + createConditions: ( + conditions: PermissionCriteria, + ) => ({ + pluginId, + resourceType, + conditions, + }), + }; +}; diff --git a/plugins/permission-node/src/integration/createConditionFactory.test.ts b/plugins/permission-node/src/integration/createConditionFactory.test.ts new file mode 100644 index 0000000000..8dd43f5da6 --- /dev/null +++ b/plugins/permission-node/src/integration/createConditionFactory.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2021 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 { createConditionFactory } from './createConditionFactory'; + +describe('createConditionFactory', () => { + const testRule = { + name: 'test-rule', + description: 'test-description', + apply: jest.fn(), + toQuery: jest.fn(), + }; + + it('returns a function', () => { + expect(createConditionFactory(testRule)).toEqual(expect.any(Function)); + }); + + describe('return value', () => { + it('constructs a condition with the rule name and supplied params', () => { + const conditionFactory = createConditionFactory(testRule); + expect(conditionFactory('a', 'b', 1, 2)).toEqual({ + rule: 'test-rule', + params: ['a', 'b', 1, 2], + }); + }); + }); +}); diff --git a/plugins/permission-node/src/integration/createConditionFactory.ts b/plugins/permission-node/src/integration/createConditionFactory.ts new file mode 100644 index 0000000000..84a8dc86a6 --- /dev/null +++ b/plugins/permission-node/src/integration/createConditionFactory.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2021 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 { PermissionRule } from '../types'; + +/** + * Creates a condition factory function for a given authorization rule and parameter types. + * + * @remarks + * + * For example, an isEntityOwner rule for catalog entities might take an array of entityRef strings. + * The rule itself defines _how_ to check a given resource, whereas a condition also includes _what_ + * to verify. + * + * Plugin authors should generally use the {@link createConditionExports} in order to efficiently + * create multiple condition factories. This helper should generally only be used to construct + * condition factories for third-party rules that aren't part of the backend plugin with which + * they're intended to integrate. + * + * @public + */ +export const createConditionFactory = + (rule: PermissionRule) => + (...params: TParams) => ({ + rule: rule.name, + params, + }); diff --git a/plugins/permission-node/src/integration/createConditionTransformer.test.ts b/plugins/permission-node/src/integration/createConditionTransformer.test.ts new file mode 100644 index 0000000000..8a93a4ea27 --- /dev/null +++ b/plugins/permission-node/src/integration/createConditionTransformer.test.ts @@ -0,0 +1,158 @@ +/* + * Copyright 2021 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 { + PermissionCondition, + PermissionCriteria, +} from '@backstage/plugin-permission-common'; +import { createConditionTransformer } from './createConditionTransformer'; + +const transformConditions = createConditionTransformer([ + { + name: 'test-rule-1', + description: 'Test rule 1', + apply: jest.fn(), + toQuery: jest.fn( + (firstParam: string, secondParam: number) => + `test-rule-1:${firstParam}/${secondParam}`, + ), + }, + { + name: 'test-rule-2', + description: 'Test rule 2', + apply: jest.fn(), + toQuery: jest.fn( + (firstParam: object) => `test-rule-2:${JSON.stringify(firstParam)}`, + ), + }, +]); + +describe('createConditionTransformer', () => { + const testCases: { + conditions: PermissionCriteria; + expectedResult: PermissionCriteria; + }[] = [ + { + conditions: { rule: 'test-rule-1', params: ['abc', 123] }, + expectedResult: 'test-rule-1:abc/123', + }, + { + conditions: { rule: 'test-rule-2', params: [{ foo: 0 }] }, + expectedResult: 'test-rule-2:{"foo":0}', + }, + { + conditions: { + anyOf: [ + { rule: 'test-rule-1', params: ['a', 1] }, + { rule: 'test-rule-2', params: [{}] }, + ], + }, + expectedResult: { + anyOf: ['test-rule-1:a/1', 'test-rule-2:{}'], + }, + }, + { + conditions: { + allOf: [ + { rule: 'test-rule-1', params: ['a', 1] }, + { rule: 'test-rule-2', params: [{}] }, + ], + }, + expectedResult: { + allOf: ['test-rule-1:a/1', 'test-rule-2:{}'], + }, + }, + { + conditions: { + not: { rule: 'test-rule-2', params: [{}] }, + }, + expectedResult: { + not: 'test-rule-2:{}', + }, + }, + { + conditions: { + allOf: [ + { + anyOf: [ + { rule: 'test-rule-1', params: ['a', 1] }, + { rule: 'test-rule-2', params: [{}] }, + ], + }, + { + not: { + allOf: [ + { rule: 'test-rule-1', params: ['b', 2] }, + { rule: 'test-rule-2', params: [{ c: 3 }] }, + ], + }, + }, + ], + }, + expectedResult: { + allOf: [ + { + anyOf: ['test-rule-1:a/1', 'test-rule-2:{}'], + }, + { + not: { + allOf: ['test-rule-1:b/2', 'test-rule-2:{"c":3}'], + }, + }, + ], + }, + }, + { + conditions: { + allOf: [ + { + anyOf: [ + { rule: 'test-rule-1', params: ['a', 1] }, + { rule: 'test-rule-2', params: [{ b: 2 }] }, + ], + }, + { + not: { + allOf: [ + { rule: 'test-rule-1', params: ['c', 3] }, + { not: { rule: 'test-rule-2', params: [{ d: 4 }] } }, + ], + }, + }, + ], + }, + expectedResult: { + allOf: [ + { + anyOf: ['test-rule-1:a/1', 'test-rule-2:{"b":2}'], + }, + { + not: { + allOf: ['test-rule-1:c/3', { not: 'test-rule-2:{"d":4}' }], + }, + }, + ], + }, + }, + ]; + + it.each(testCases)( + 'works with criteria %#', + ({ conditions, expectedResult }) => { + expect(transformConditions(conditions)).toEqual(expectedResult); + }, + ); +}); diff --git a/plugins/permission-node/src/integration/createConditionTransformer.ts b/plugins/permission-node/src/integration/createConditionTransformer.ts new file mode 100644 index 0000000000..0e736b832c --- /dev/null +++ b/plugins/permission-node/src/integration/createConditionTransformer.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2021 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 { + PermissionCondition, + PermissionCriteria, +} from '@backstage/plugin-permission-common'; +import { PermissionRule } from '../types'; +import { + createGetRule, + isAndCriteria, + isNotCriteria, + isOrCriteria, +} from './util'; + +const mapConditions = ( + criteria: PermissionCriteria, + getRule: (name: string) => PermissionRule, +): PermissionCriteria => { + if (isAndCriteria(criteria)) { + return { + allOf: criteria.allOf.map(child => mapConditions(child, getRule)), + }; + } else if (isOrCriteria(criteria)) { + return { + anyOf: criteria.anyOf.map(child => mapConditions(child, getRule)), + }; + } else if (isNotCriteria(criteria)) { + return { + not: mapConditions(criteria.not, getRule), + }; + } + + return getRule(criteria.rule).toQuery(...criteria.params); +}; + +/** + * A function which accepts {@link @backstage/plugin-permission-common#PermissionCondition}s + * logically grouped in a {@link @backstage/plugin-permission-common#PermissionCriteria} + * object, and transforms the {@link @backstage/plugin-permission-common#PermissionCondition}s + * into plugin specific query fragments while retaining the enclosing criteria shape. + * + * @public + */ +export type ConditionTransformer = ( + conditions: PermissionCriteria, +) => PermissionCriteria; + +/** + * A higher-order helper function which accepts an array of + * {@link PermissionRule}s, and returns a {@link ConditionTransformer} + * which transforms input conditions into equivalent plugin-specific + * query fragments using the supplied rules. + * + * @public + */ +export const createConditionTransformer = < + TQuery, + TRules extends PermissionRule[], +>( + permissionRules: [...TRules], +): ConditionTransformer => { + const getRule = createGetRule(permissionRules); + + return conditions => mapConditions(conditions, getRule); +}; diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts new file mode 100644 index 0000000000..af216dfcb8 --- /dev/null +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts @@ -0,0 +1,210 @@ +/* + * Copyright 2021 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 { AuthorizeResult } from '@backstage/plugin-permission-common'; +import express, { Express, Router } from 'express'; +import request from 'supertest'; +import { createPermissionIntegrationRouter } from './createPermissionIntegrationRouter'; + +const mockGetResource: jest.MockedFunction< + (resourceRef: string) => Promise +> = jest.fn((resourceRef: string) => + Promise.resolve({ + resourceRef, + }), +); + +const testRule1 = { + name: 'test-rule-1', + description: 'Test rule 1', + apply: jest.fn( + (_resource: any, _firstParam: string, _secondParam: number) => true, + ), + toQuery: jest.fn(), +}; + +const testRule2 = { + name: 'test-rule-2', + description: 'Test rule 2', + apply: jest.fn((_firstParam: object) => false), + toQuery: jest.fn(), +}; + +describe('createPermissionIntegrationRouter', () => { + let app: Express; + let router: Router; + + beforeEach(() => { + router = createPermissionIntegrationRouter({ + resourceType: 'test-resource', + getResource: mockGetResource, + rules: [testRule1, testRule2], + }); + + app = express().use(router); + }); + + it('works', async () => { + expect(router).toBeDefined(); + }); + + describe('POST /permissions/apply-conditions', () => { + it.each([ + { rule: 'test-rule-1', params: ['abc', 123] }, + { + anyOf: [ + { rule: 'test-rule-1', params: ['a', 1] }, + { rule: 'test-rule-2', params: [{}] }, + ], + }, + + { + not: { rule: 'test-rule-2', params: [{}] }, + }, + { + allOf: [ + { + anyOf: [ + { rule: 'test-rule-1', params: ['a', 1] }, + { rule: 'test-rule-2', params: [{}] }, + ], + }, + { + not: { + allOf: [ + { rule: 'test-rule-1', params: ['b', 2] }, + { rule: 'test-rule-2', params: [{ c: 3 }] }, + ], + }, + }, + ], + }, + ])('returns 200/ALLOW when criteria match (case %#)', async conditions => { + const response = await request(app) + .post('/permissions/apply-conditions') + .send({ + resourceRef: 'default:test/resource', + resourceType: 'test-resource', + conditions, + }); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ result: AuthorizeResult.ALLOW }); + }); + + it.each([ + { rule: 'test-rule-2', params: [{ foo: 0 }] }, + { + allOf: [ + { rule: 'test-rule-1', params: ['a', 1] }, + { rule: 'test-rule-2', params: [{}] }, + ], + }, + { + allOf: [ + { + anyOf: [ + { rule: 'test-rule-1', params: ['a', 1] }, + { rule: 'test-rule-2', params: [{ b: 2 }] }, + ], + }, + { + not: { + allOf: [ + { rule: 'test-rule-1', params: ['c', 3] }, + { not: { rule: 'test-rule-2', params: [{ d: 4 }] } }, + ], + }, + }, + ], + }, + ])( + 'returns 200/DENY when criteria do not match (case %#)', + async conditions => { + const response = await request(app) + .post('/permissions/apply-conditions') + .send({ + resourceRef: 'default:test/resource', + resourceType: 'test-resource', + conditions, + }); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ result: AuthorizeResult.DENY }); + }, + ); + + it('returns 400 when called with incorrect resource type', async () => { + const response = await request(app) + .post('/permissions/apply-conditions') + .send({ + resourceRef: 'default:test/resource', + resourceType: 'test-incorrect-resource', + conditions: { + anyOf: [], + }, + }); + + expect(response.status).toEqual(400); + expect(response.error && response.error.text).toMatch( + /unexpected resource type: test-incorrect-resource/i, + ); + }); + + it('returns 400 when resource is not found', async () => { + mockGetResource.mockReturnValueOnce(Promise.resolve(undefined)); + + const response = await request(app) + .post('/permissions/apply-conditions') + .send({ + resourceRef: 'default:test/resource', + resourceType: 'test-resource', + conditions: { + not: { + rule: 'testRule1', + params: ['a', 1], + }, + }, + }); + + expect(response.status).toEqual(400); + expect(response.error && response.error.text).toMatch( + /resource for ref default:test\/resource not found/i, + ); + }); + + it.each([ + undefined, + {}, + { resourceType: 'test-resource-type' }, + { resourceRef: 'test/resource-ref' }, + { + resourceType: 'test-resource-type', + resourceRef: 'test/resource-ref', + }, + { conditions: { anyOf: [] } }, + ])(`returns 400 for invalid input %#`, async input => { + const response = await request(app) + .post('/permissions/apply-conditions') + .send(input); + + expect(response.status).toEqual(400); + expect(response.error && response.error.text).toMatch( + /invalid request body/i, + ); + }); + }); +}); diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts new file mode 100644 index 0000000000..99e282b144 --- /dev/null +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -0,0 +1,177 @@ +/* + * Copyright 2021 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 express, { Response, Router } from 'express'; +import { z } from 'zod'; +import { + AuthorizeResult, + PermissionCondition, + PermissionCriteria, +} from '@backstage/plugin-permission-common'; +import { PermissionRule } from '../types'; +import { + createGetRule, + isAndCriteria, + isNotCriteria, + isOrCriteria, +} from './util'; + +const permissionCriteriaSchema: z.ZodSchema< + PermissionCriteria +> = z.lazy(() => + z.union([ + z.object({ anyOf: z.array(permissionCriteriaSchema) }), + z.object({ allOf: z.array(permissionCriteriaSchema) }), + z.object({ not: permissionCriteriaSchema }), + z.object({ + rule: z.string(), + params: z.array(z.unknown()), + }), + ]), +); + +const applyConditionsRequestSchema = z.object({ + resourceRef: z.string(), + resourceType: z.string(), + conditions: permissionCriteriaSchema, +}); + +/** + * A request to load the referenced resource and apply conditions in order to + * finalize a conditional authorization response. + * + * @public + */ +export type ApplyConditionsRequest = { + resourceRef: string; + resourceType: string; + conditions: PermissionCriteria; +}; + +/** + * The result of applying the conditions, expressed as a definitive authorize + * result of ALLOW or DENY. + * + * @public + */ +export type ApplyConditionsResponse = { + result: AuthorizeResult.ALLOW | AuthorizeResult.DENY; +}; + +const applyConditions = ( + criteria: PermissionCriteria, + resource: TResource, + getRule: (name: string) => PermissionRule, +): boolean => { + if (isAndCriteria(criteria)) { + return criteria.allOf.every(child => + applyConditions(child, resource, getRule), + ); + } else if (isOrCriteria(criteria)) { + return criteria.anyOf.some(child => + applyConditions(child, resource, getRule), + ); + } else if (isNotCriteria(criteria)) { + return !applyConditions(criteria.not, resource, getRule); + } + + return getRule(criteria.rule).apply(resource, ...criteria.params); +}; + +/** + * Create an express Router which provides an authorization route to allow integration between the + * permission backend and other Backstage backend plugins. Plugin owners that wish to support + * conditional authorization for their resources should add the router created by this function + * to their express app inside their `createRouter` implementation. + * + * @remarks + * + * To make this concrete, we can use the Backstage software catalog as an example. The catalog has + * conditional rules around access to specific _entities_ in the catalog. The _type_ of resource is + * captured here as `resourceType`, a string identifier (`catalog-entity` in this example) that can + * be provided with permission definitions. This is merely a _type_ to verify that conditions in an + * authorization policy are constructed correctly, not a reference to a specific resource. + * + * The `rules` parameter is an array of {@link PermissionRule}s that introduce conditional + * filtering logic for resources; for the catalog, these are things like `isEntityOwner` or + * `hasAnnotation`. Rules describe how to filter a list of resources, and the `conditions` returned + * allow these rules to be applied with specific parameters (such as 'group:default/team-a', or + * 'backstage.io/edit-url'). + * + * The `getResource` argument should load a resource by reference. For the catalog, this is an + * {@link @backstage/catalog-model#EntityRef}. For other plugins, this can be any serialized format. + * This is used to construct the `createPermissionIntegrationRouter`, a function to add an + * authorization route to your backend plugin. This route will be called by the `permission-backend` + * when authorization conditions relating to this plugin need to be evaluated. + * @public + */ +export const createPermissionIntegrationRouter = ({ + resourceType, + rules, + getResource, +}: { + resourceType: string; + rules: PermissionRule[]; + getResource: (resourceRef: string) => Promise; +}): Router => { + const router = Router(); + + const getRule = createGetRule(rules); + + router.post( + '/permissions/apply-conditions', + express.json(), + async ( + req, + res: Response< + | { + result: Omit; + } + | string + >, + ) => { + const parseResult = applyConditionsRequestSchema.safeParse(req.body); + + if (!parseResult.success) { + return res.status(400).send(`Invalid request body.`); + } + + const { data: body } = parseResult; + + if (body.resourceType !== resourceType) { + return res + .status(400) + .send(`Unexpected resource type: ${body.resourceType}.`); + } + + const resource = await getResource(body.resourceRef); + + if (!resource) { + return res + .status(400) + .send(`Resource for ref ${body.resourceRef} not found.`); + } + + return res.status(200).json({ + result: applyConditions(body.conditions, resource, getRule) + ? AuthorizeResult.ALLOW + : AuthorizeResult.DENY, + }); + }, + ); + + return router; +}; diff --git a/plugins/permission-node/src/integration/index.ts b/plugins/permission-node/src/integration/index.ts new file mode 100644 index 0000000000..f070d57c8f --- /dev/null +++ b/plugins/permission-node/src/integration/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2021 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 * from './createConditionFactory'; +export * from './createConditionExports'; +export * from './createConditionTransformer'; +export * from './createPermissionIntegrationRouter'; diff --git a/plugins/permission-node/src/integration/util.test.ts b/plugins/permission-node/src/integration/util.test.ts new file mode 100644 index 0000000000..d27c0a4243 --- /dev/null +++ b/plugins/permission-node/src/integration/util.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright 2021 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 { + createGetRule, + isAndCriteria, + isNotCriteria, + isOrCriteria, +} from './util'; + +describe('permission integration utils', () => { + describe('createGetRule', () => { + let getRule: ReturnType; + + const testRule1 = { + name: 'test-rule-1', + description: 'Test rule 1', + apply: jest.fn(), + toQuery: jest.fn(), + }; + + const testRule2 = { + name: 'test-rule-2', + description: 'Test rule 2', + apply: jest.fn(), + toQuery: jest.fn(), + }; + + beforeEach(() => { + getRule = createGetRule([testRule1, testRule2]); + }); + + it('returns the rule matching the supplied name', () => { + expect(getRule('test-rule-1')).toBe(testRule1); + }); + + it('throws if there is no rule for the supplied name', () => { + expect(() => getRule('test-rule-3')).toThrowError( + /unexpected permission rule/i, + ); + }); + }); + + describe('isOrCriteria', () => { + it('returns true if input has a top-level "anyOf" property', () => { + expect(isOrCriteria({ anyOf: { not: { allOf: [] } } })).toEqual(true); + }); + + it('returns false if input does not have a top-level "anyOf" property', () => { + expect(isOrCriteria({ allOf: { not: { anyOf: [] } } })).toEqual(false); + }); + }); + + describe('isAndCriteria', () => { + it('returns true if input has a top-level "allOf" property', () => { + expect(isAndCriteria({ allOf: { not: { anyOf: [] } } })).toEqual(true); + }); + + it('returns false if input does not have a top-level "allOf" property', () => { + expect(isAndCriteria({ anyOf: { not: { allOf: [] } } })).toEqual(false); + }); + }); + + describe('isNotCriteria', () => { + it('returns true if input has a top-level "not" property', () => { + expect(isNotCriteria({ not: { allOf: [{ anyOf: [] }] } })).toEqual(true); + }); + + it('returns false if input does not have a top-level "not" property', () => { + expect(isNotCriteria({ anyOf: { not: { allOf: [] } } })).toEqual(false); + }); + }); +}); diff --git a/plugins/permission-node/src/integration/util.ts b/plugins/permission-node/src/integration/util.ts new file mode 100644 index 0000000000..d9457cfefc --- /dev/null +++ b/plugins/permission-node/src/integration/util.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2021 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 { PermissionCriteria } from '@backstage/plugin-permission-common'; +import { PermissionRule } from '../types'; + +export const isAndCriteria = ( + filter: PermissionCriteria, +): filter is { allOf: PermissionCriteria[] } => + Object.prototype.hasOwnProperty.call(filter, 'allOf'); + +export const isOrCriteria = ( + filter: PermissionCriteria, +): filter is { anyOf: PermissionCriteria[] } => + Object.prototype.hasOwnProperty.call(filter, 'anyOf'); + +export const isNotCriteria = ( + filter: PermissionCriteria, +): filter is { not: PermissionCriteria } => + Object.prototype.hasOwnProperty.call(filter, 'not'); + +export const createGetRule = ( + rules: PermissionRule[], +) => { + const rulesMap = new Map(Object.values(rules).map(rule => [rule.name, rule])); + + return (name: string): PermissionRule => { + const rule = rulesMap.get(name); + + if (!rule) { + throw new Error(`Unexpected permission rule: ${name}`); + } + + return rule; + }; +}; diff --git a/plugins/permission-node/src/policy/index.ts b/plugins/permission-node/src/policy/index.ts new file mode 100644 index 0000000000..f04c2a094c --- /dev/null +++ b/plugins/permission-node/src/policy/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2021 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 type { + ConditionalPolicyResult, + PermissionPolicy, + PolicyAuthorizeRequest, + PolicyResult, +} from './types'; diff --git a/plugins/permission-node/src/policy/types.ts b/plugins/permission-node/src/policy/types.ts new file mode 100644 index 0000000000..44380bad26 --- /dev/null +++ b/plugins/permission-node/src/policy/types.ts @@ -0,0 +1,90 @@ +/* + * Copyright 2021 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 { + AuthorizeRequest, + AuthorizeResult, + PermissionCondition, + PermissionCriteria, +} from '@backstage/plugin-permission-common'; +import { BackstageIdentity } from '@backstage/plugin-auth-backend'; + +/** + * An authorization request to be evaluated by the {@link PermissionPolicy}. + * + * @remarks + * + * This differs from {@link @backstage/permission-common#AuthorizeRequest} in that `resourceRef` + * should never be provided. This forces policies to be written in a way that's compatible with + * filtering collections of resources at data load time. + * + * @public + */ +export type PolicyAuthorizeRequest = Omit; + +/** + * A conditional result to an authorization request, returned by the {@link PermissionPolicy}. + * + * @remarks + * + * This indicates that the policy allows authorization for the request, given that the returned + * conditions hold when evaluated. The conditions will be evaluated by the corresponding plugin + * which knows about the referenced permission rules. + * + * Similar to {@link @backstage/permission-common#AuthorizeResult}, but with the plugin and resource + * identifiers needed to evaluate the returned conditions. + * @public + */ +export type ConditionalPolicyResult = { + result: AuthorizeResult.CONDITIONAL; + conditions: { + pluginId: string; + resourceType: string; + conditions: PermissionCriteria; + }; +}; + +/** + * The result of evaluating an authorization request with a {@link PermissionPolicy}. + * + * @public + */ +export type PolicyResult = + | { result: AuthorizeResult.ALLOW | AuthorizeResult.DENY } + | ConditionalPolicyResult; + +/** + * A policy to evaluate authorization requests for any permissioned action performed in Backstage. + * + * @remarks + * + * This takes as input a permission and an optional Backstage identity, and should return ALLOW if + * the user is permitted to execute that action; otherwise DENY. For permissions relating to + * resources, such a catalog entities, a conditional response can also be returned. This states + * that the action is allowed if the conditions provided hold true. + * + * Conditions are a rule, and parameters to evaluate against that rule. For example, the rule might + * be `isOwner` and the parameters a collection of entityRefs; if one of the entityRefs matches + * the `owner` field on a catalog entity, this would resolve to ALLOW. + * + * @public + */ +export interface PermissionPolicy { + handle( + request: PolicyAuthorizeRequest, + user?: BackstageIdentity, + ): Promise; +} diff --git a/plugins/permission-node/src/setupTests.ts b/plugins/permission-node/src/setupTests.ts new file mode 100644 index 0000000000..a330613afb --- /dev/null +++ b/plugins/permission-node/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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 {}; diff --git a/plugins/permission-node/src/types.ts b/plugins/permission-node/src/types.ts new file mode 100644 index 0000000000..678befc99c --- /dev/null +++ b/plugins/permission-node/src/types.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2021 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 type { PermissionCriteria } from '@backstage/plugin-permission-common'; + +/** + * A conditional rule that can be provided in an + * {@link @backstage/permission-common#AuthorizeResult} response to an authorization request. + * + * @remarks + * + * Rules can either be evaluated against a resource loaded in memory, or used as filters when + * loading a collection of resources from a data source. The `apply` and `toQuery` methods implement + * these two concepts. + * + * The two operations should always have the same logical result. If they don’t, the effective + * outcome of an authorization operation will sometimes differ depending on how the authorization + * check was performed. + * + * @public + */ +export type PermissionRule< + TResource, + TQuery, + TParams extends unknown[] = unknown[], +> = { + name: string; + description: string; + + /** + * Apply this rule to a resource already loaded from a backing data source. The params are + * arguments supplied for the rule; for example, a rule could be `isOwner` with entityRefs as the + * params. + */ + apply(resource: TResource, ...params: TParams): boolean; + + /** + * Translate this rule to criteria suitable for use in querying a backing data store. The criteria + * can be used for loading a collection of resources efficiently with conditional criteria already + * applied. + */ + toQuery(...params: TParams): PermissionCriteria; +}; diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 374cfeffd6..36ac76c95f 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -40,8 +40,8 @@ "@backstage/plugin-scaffolder-common": "^0.1.1", "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.4", "@backstage/types": "^0.1.1", - "@gitbeaker/core": "^30.2.0", - "@gitbeaker/node": "^30.2.0", + "@gitbeaker/core": "^34.6.0", + "@gitbeaker/node": "^34.6.0", "@octokit/rest": "^18.5.3", "@octokit/webhooks": "^9.14.1", "@types/express": "^4.17.6", diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts similarity index 95% rename from plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts rename to plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 1f7877791c..b2e2e85a6c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts @@ -270,6 +270,31 @@ describe('DefaultWorkflowRunner', () => { ); }); + it('should not try and parse something that is not parsable', async () => { + jest.spyOn(logger, 'error'); + const task = createMockTaskWithSpec({ + apiVersion: 'scaffolder.backstage.io/v1beta3', + steps: [ + { + id: 'test', + name: 'name', + action: 'jest-mock-action', + input: { + foo: 'bob', + }, + }, + ], + output: {}, + parameters: { + input: 'BACKSTAGE', + }, + }); + + await runner.execute(task); + + expect(logger.error).not.toHaveBeenCalled(); + }); + it('should keep the original types for the input and not parse things that arent meant to be parsed', async () => { const task = createMockTaskWithSpec({ apiVersion: 'scaffolder.backstage.io/v1beta3', diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index c66d967103..5b42eef459 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -114,9 +114,10 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { private isSingleTemplateString(input: string) { const { parser, nodes } = require('nunjucks'); const parsed = parser.parse(input, {}, this.nunjucksOptions); + return ( parsed.children.length === 1 && - !(parsed.children[0] instanceof nodes.TemplateData) + !(parsed.children[0]?.children?.[0] instanceof nodes.TemplateData) ); } diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx index 77c8240974..ce0354b817 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx @@ -17,8 +17,8 @@ import React from 'react'; import { ScaffolderApi, scaffolderApiRef } from '../../api'; import { ActionsPage } from './ActionsPage'; import { rootRouteRef } from '../../routes'; -import { renderInTestApp } from '@backstage/test-utils'; -import { ApiRegistry, ApiProvider } from '@backstage/core-app-api'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; +import { ApiProvider } from '@backstage/core-app-api'; const scaffolderApiMock: jest.Mocked = { scaffold: jest.fn(), @@ -29,7 +29,7 @@ const scaffolderApiMock: jest.Mocked = { listActions: jest.fn(), }; -const apis = ApiRegistry.from([[scaffolderApiRef, scaffolderApiMock]]); +const apis = TestApiRegistry.from([scaffolderApiRef, scaffolderApiMock]); describe('TemplatePage', () => { beforeEach(() => jest.resetAllMocks()); diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx index e42bcbdbdc..60e9ee8840 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx @@ -13,7 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { renderInTestApp, renderWithEffects } from '@backstage/test-utils'; +import { + renderInTestApp, + renderWithEffects, + TestApiRegistry, +} from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; import { fireEvent, within } from '@testing-library/react'; @@ -24,7 +28,7 @@ import { ScaffolderApi, scaffolderApiRef } from '../../api'; import { rootRouteRef } from '../../routes'; import { TemplatePage } from './TemplatePage'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { errorApiRef } from '@backstage/core-plugin-api'; jest.mock('react-router-dom', () => { @@ -47,10 +51,10 @@ const scaffolderApiMock: jest.Mocked = { const errorApiMock = { post: jest.fn(), error$: jest.fn() }; -const apis = ApiRegistry.from([ +const apis = TestApiRegistry.from( [scaffolderApiRef, scaffolderApiMock], [errorApiRef, errorApiMock], -]); +); describe('TemplatePage', () => { beforeEach(() => jest.resetAllMocks()); diff --git a/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.test.tsx b/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.test.tsx index 3d0267ff1f..c70f251eac 100644 --- a/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.test.tsx +++ b/plugins/scaffolder/src/components/TemplateTypePicker/TemplateTypePicker.test.tsx @@ -26,8 +26,8 @@ import { MockEntityListContextProvider, } from '@backstage/plugin-catalog-react'; import { AlertApi, alertApiRef } from '@backstage/core-plugin-api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; -import { renderWithEffects } from '@backstage/test-utils'; +import { ApiProvider } from '@backstage/core-app-api'; +import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils'; const entities: Entity[] = [ { @@ -62,7 +62,7 @@ const entities: Entity[] = [ }, ]; -const apis = ApiRegistry.from([ +const apis = TestApiRegistry.from( [ catalogApiRef, { @@ -77,7 +77,7 @@ const apis = ApiRegistry.from([ post: jest.fn(), } as unknown as AlertApi, ], -]); +); describe('', () => { it('renders available entity types', async () => { diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx index f293969dff..b7e72e3583 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx @@ -16,12 +16,11 @@ import { Entity } from '@backstage/catalog-model'; import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { FieldProps } from '@rjsf/core'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { EntityPicker } from './EntityPicker'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; const makeEntity = (kind: string, namespace: string, name: string): Entity => ({ apiVersion: 'backstage.io/v1beta1', @@ -53,14 +52,15 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); entities = [ makeEntity('Group', 'default', 'team-a'), makeEntity('Group', 'default', 'squad-b'), ]; Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx index 18d6fd7e5c..31992fbc8a 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx @@ -16,11 +16,10 @@ import { Entity } from '@backstage/catalog-model'; import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { FieldProps } from '@rjsf/core'; import React from 'react'; import { OwnerPicker } from './OwnerPicker'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; const makeEntity = (kind: string, namespace: string, name: string): Entity => ({ apiVersion: 'backstage.io/v1beta1', @@ -50,14 +49,15 @@ describe('', () => { let Wrapper: React.ComponentType; beforeEach(() => { - const apis = ApiRegistry.with(catalogApiRef, catalogApi); entities = [ makeEntity('Group', 'default', 'team-a'), makeEntity('Group', 'default', 'squad-b'), ]; Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/search/src/components/SearchBar/SearchBar.test.tsx b/plugins/search/src/components/SearchBar/SearchBar.test.tsx index 3a11daa9fa..81f90640c1 100644 --- a/plugins/search/src/components/SearchBar/SearchBar.test.tsx +++ b/plugins/search/src/components/SearchBar/SearchBar.test.tsx @@ -21,12 +21,9 @@ import { SearchContextProvider } from '../SearchContext'; import { SearchBar } from './SearchBar'; import { configApiRef } from '@backstage/core-plugin-api'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { searchApiRef } from '../../apis'; +import { TestApiRegistry } from '@backstage/test-utils'; jest.mock('@backstage/core-plugin-api', () => ({ ...jest.requireActual('@backstage/core-plugin-api'), @@ -42,10 +39,10 @@ describe('SearchBar', () => { const query = jest.fn().mockResolvedValue({}); - const apiRegistry = ApiRegistry.from([ + const apiRegistry = TestApiRegistry.from( [configApiRef, new ConfigReader({ app: { title: 'Mock title' } })], [searchApiRef, { query }], - ]); + ); const name = 'Search'; const term = 'term'; diff --git a/plugins/search/src/components/SearchModal/SearchModal.test.tsx b/plugins/search/src/components/SearchModal/SearchModal.test.tsx index d7f873abf3..ff9622dfb9 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.test.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.test.tsx @@ -15,14 +15,10 @@ */ import React from 'react'; import { screen } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import { configApiRef } from '@backstage/core-plugin-api'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { rootRouteRef } from '../../plugin'; import { searchApiRef } from '../../apis'; @@ -38,10 +34,10 @@ jest.mock('../SearchContext', () => ({ describe('SearchModal', () => { const query = jest.fn().mockResolvedValue({}); - const apiRegistry = ApiRegistry.from([ + const apiRegistry = TestApiRegistry.from( [configApiRef, new ConfigReader({ app: { title: 'Mock app' } })], [searchApiRef, { query }], - ]); + ); const toggleModal = jest.fn(); diff --git a/plugins/shortcuts/src/Shortcuts.test.tsx b/plugins/shortcuts/src/Shortcuts.test.tsx index 9182bcb47d..e0a70d6069 100644 --- a/plugins/shortcuts/src/Shortcuts.test.tsx +++ b/plugins/shortcuts/src/Shortcuts.test.tsx @@ -15,25 +15,31 @@ */ import React from 'react'; -import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; +import { + MockStorageApi, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; import { screen, waitFor } from '@testing-library/react'; import { Shortcuts } from './Shortcuts'; import { LocalStoredShortcuts, shortcutsApiRef } from './api'; import { SidebarContext } from '@backstage/core-components'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; - -const apis = ApiRegistry.from([ - [shortcutsApiRef, new LocalStoredShortcuts(MockStorageApi.create())], -]); describe('Shortcuts', () => { it('displays an add button', async () => { await renderInTestApp( - + - + , ); await waitFor(() => !screen.queryByTestId('progress')); diff --git a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx index 63d617aff2..eb5c99d4e7 100644 --- a/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx +++ b/plugins/splunk-on-call/src/components/EntitySplunkOnCallCard.test.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { act, fireEvent, render, waitFor } from '@testing-library/react'; import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { splunkOnCallApiRef, SplunkOnCallClient, @@ -37,13 +37,8 @@ import { alertApiRef, ConfigApi, configApiRef, - createApiRef, } from '@backstage/core-plugin-api'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; const mockSplunkOnCallApi: Partial = { getUsers: async () => [], @@ -59,17 +54,11 @@ const configApi: ConfigApi = new ConfigReader({ }, }); -const apis = ApiRegistry.from([ +const apis = TestApiRegistry.from( [splunkOnCallApiRef, mockSplunkOnCallApi], [configApiRef, configApi], - [ - alertApiRef, - createApiRef({ - id: 'core.alert', - description: 'Used to report alerts and forward them to the app', - }), - ], -]); + [alertApiRef, {}], +); const mockEntity = { apiVersion: 'backstage.io/v1alpha1', diff --git a/plugins/splunk-on-call/src/components/Escalation/Escalation.test.tsx b/plugins/splunk-on-call/src/components/Escalation/Escalation.test.tsx index 1319493742..046c6a37f9 100644 --- a/plugins/splunk-on-call/src/components/Escalation/Escalation.test.tsx +++ b/plugins/splunk-on-call/src/components/Escalation/Escalation.test.tsx @@ -16,15 +16,15 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { EscalationPolicy } from './EscalationPolicy'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { splunkOnCallApiRef } from '../../api'; import { MOCKED_ON_CALL, MOCKED_USER } from '../../api/mocks'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; const mockSplunkOnCallApi = { - getOnCallUsers: () => [], + getOnCallUsers: jest.fn(), }; -const apis = ApiRegistry.from([[splunkOnCallApiRef, mockSplunkOnCallApi]]); +const apis = TestApiRegistry.from([splunkOnCallApiRef, mockSplunkOnCallApi]); describe('Escalation', () => { it('Handles an empty response', async () => { diff --git a/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx b/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx index df4254d3ad..181c2fda77 100644 --- a/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx +++ b/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx @@ -16,43 +16,39 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import { Incidents } from './Incidents'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { splunkOnCallApiRef } from '../../api'; import { MOCK_TEAM, MOCK_INCIDENT } from '../../api/mocks'; import { alertApiRef, - createApiRef, IdentityApi, identityApiRef, } from '@backstage/core-plugin-api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; const mockIdentityApi: Partial = { getUserId: () => 'test', }; const mockSplunkOnCallApi = { - getIncidents: () => [], - getTeams: () => [], + getIncidents: jest.fn(), + getTeams: jest.fn(), }; -const apis = ApiRegistry.from([ - [ - alertApiRef, - createApiRef({ - id: 'core.alert', - description: 'Used to report alerts and forward them to the app', - }), - ], +const apis = TestApiRegistry.from( + [alertApiRef, {}], [identityApiRef, mockIdentityApi], [splunkOnCallApiRef, mockSplunkOnCallApi], -]); +); describe('Incidents', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + it('Renders an empty state when there are no incidents', async () => { - mockSplunkOnCallApi.getTeams = jest - .fn() - .mockImplementationOnce(async () => [MOCK_TEAM]); + mockSplunkOnCallApi.getIncidents.mockResolvedValue([]); + mockSplunkOnCallApi.getTeams.mockResolvedValue([MOCK_TEAM]); const { getByText, queryByTestId } = render( wrapInTestApp( @@ -69,13 +65,9 @@ describe('Incidents', () => { }); it('Renders all incidents', async () => { - mockSplunkOnCallApi.getIncidents = jest - .fn() - .mockImplementationOnce(async () => [MOCK_INCIDENT]); + mockSplunkOnCallApi.getIncidents.mockResolvedValue([MOCK_INCIDENT]); + mockSplunkOnCallApi.getTeams.mockResolvedValue([MOCK_TEAM]); - mockSplunkOnCallApi.getTeams = jest - .fn() - .mockImplementationOnce(async () => [MOCK_TEAM]); const { getByText, getByTitle, @@ -108,9 +100,10 @@ describe('Incidents', () => { }); it('Handle errors', async () => { - mockSplunkOnCallApi.getIncidents = jest - .fn() - .mockRejectedValueOnce(new Error('Error occurred')); + mockSplunkOnCallApi.getIncidents.mockRejectedValueOnce( + new Error('Error occurred'), + ); + mockSplunkOnCallApi.getTeams.mockResolvedValue([]); const { getByText, queryByTestId } = render( wrapInTestApp( diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx index 94fd40bfe5..0b64a137af 100644 --- a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx +++ b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx @@ -15,12 +15,12 @@ */ import React from 'react'; import { render, fireEvent, act } from '@testing-library/react'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { splunkOnCallApiRef } from '../../api'; import { TriggerDialog } from './TriggerDialog'; -import { ApiRegistry, ApiProvider } from '@backstage/core-app-api'; -import { alertApiRef, createApiRef } from '@backstage/core-plugin-api'; +import { ApiProvider } from '@backstage/core-app-api'; +import { alertApiRef } from '@backstage/core-plugin-api'; describe('TriggerDialog', () => { const mockTriggerAlarmFn = jest.fn(); @@ -28,16 +28,10 @@ describe('TriggerDialog', () => { incidentAction: mockTriggerAlarmFn, }; - const apis = ApiRegistry.from([ - [ - alertApiRef, - createApiRef({ - id: 'core.alert', - description: 'Used to report alerts and forward them to the app', - }), - ], + const apis = TestApiRegistry.from( + [alertApiRef, {}], [splunkOnCallApiRef, mockSplunkOnCallApi], - ]); + ); it('open the dialog and trigger an alarm', async () => { const { getByText, getByRole, getByTestId } = render( diff --git a/plugins/tech-radar/src/components/RadarComponent.test.tsx b/plugins/tech-radar/src/components/RadarComponent.test.tsx index cce411e897..2b237c8d83 100644 --- a/plugins/tech-radar/src/components/RadarComponent.test.tsx +++ b/plugins/tech-radar/src/components/RadarComponent.test.tsx @@ -19,13 +19,12 @@ import { render, waitForElement } from '@testing-library/react'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; import { act } from 'react-dom/test-utils'; -import { withLogCollector } from '@backstage/test-utils'; +import { TestApiProvider, withLogCollector } from '@backstage/test-utils'; import GetBBoxPolyfill from '../utils/polyfills/getBBox'; import { RadarComponent } from './RadarComponent'; import { TechRadarLoaderResponse, techRadarApiRef, TechRadarApi } from '../api'; -import { ApiRegistry, ApiProvider } from '@backstage/core-app-api'; import { errorApiRef } from '@backstage/core-plugin-api'; describe('RadarComponent', () => { @@ -55,18 +54,18 @@ describe('RadarComponent', () => { const errorApi = { post: () => {} }; const { getByTestId, queryByTestId } = render( - - + , ); @@ -87,18 +86,18 @@ describe('RadarComponent', () => { const { queryByTestId } = render( - - + , ); @@ -115,13 +114,13 @@ describe('RadarComponent', () => { expect(() => { render( - + - + , ); }).toThrow(); diff --git a/plugins/tech-radar/src/components/RadarPage.test.tsx b/plugins/tech-radar/src/components/RadarPage.test.tsx index 7215fc40df..7f4e69e388 100644 --- a/plugins/tech-radar/src/components/RadarPage.test.tsx +++ b/plugins/tech-radar/src/components/RadarPage.test.tsx @@ -17,6 +17,7 @@ import { MockErrorApi, renderInTestApp, + TestApiProvider, wrapInTestApp, } from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; @@ -28,7 +29,6 @@ import GetBBoxPolyfill from '../utils/polyfills/getBBox'; import { RadarPage } from './RadarPage'; import { TechRadarLoaderResponse, techRadarApiRef, TechRadarApi } from '../api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { errorApiRef } from '@backstage/core-plugin-api'; describe('RadarPage', () => { @@ -63,9 +63,9 @@ describe('RadarPage', () => { const { getByTestId, queryByTestId } = render( wrapInTestApp( - + - + , ), ); @@ -89,9 +89,9 @@ describe('RadarPage', () => { const { getByText, getByTestId } = await renderInTestApp( - + - + , ); @@ -115,9 +115,9 @@ describe('RadarPage', () => { const { getByTestId } = await renderInTestApp( - + - + , ); @@ -142,14 +142,14 @@ describe('RadarPage', () => { const { queryByTestId } = await renderInTestApp( - - + , ); diff --git a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx index 1d8d7d2b04..8618c4432f 100644 --- a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx +++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx @@ -14,11 +14,7 @@ * limitations under the License. */ -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { ConfigApi, configApiRef, @@ -30,7 +26,11 @@ import { DefaultStarredEntitiesApi, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; -import { MockStorageApi, renderInTestApp } from '@backstage/test-utils'; +import { + MockStorageApi, + renderInTestApp, + TestApiRegistry, +} from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; import { rootDocsRouteRef } from '../../routes'; @@ -69,12 +69,12 @@ describe('TechDocs Home', () => { const storageApi = MockStorageApi.create(); - const apiRegistry = ApiRegistry.from([ + const apiRegistry = TestApiRegistry.from( [catalogApiRef, mockCatalogApi], [configApiRef, configApi], [storageApiRef, storageApi], [starredEntitiesApiRef, new DefaultStarredEntitiesApi({ storageApi })], - ]); + ); it('should render a TechDocs home page', async () => { await renderInTestApp( diff --git a/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx index d9d2c6a345..a8fd472f7c 100644 --- a/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx +++ b/plugins/techdocs/src/home/components/LegacyTechDocsHome.test.tsx @@ -15,16 +15,12 @@ */ import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; import { LegacyTechDocsHome } from './LegacyTechDocsHome'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { ConfigApi, configApiRef } from '@backstage/core-plugin-api'; import { rootDocsRouteRef } from '../../routes'; @@ -59,10 +55,10 @@ describe('Legacy TechDocs Home', () => { }, }); - const apiRegistry = ApiRegistry.from([ + const apiRegistry = TestApiRegistry.from( [catalogApiRef, mockCatalogApi], [configApiRef, configApi], - ]); + ); it('should render a TechDocs home page', async () => { await renderInTestApp( diff --git a/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx b/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx index bcbfdeed86..e9c3a55b15 100644 --- a/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx +++ b/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx @@ -15,11 +15,11 @@ */ import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; import { TechDocsCustomHome, PanelType } from './TechDocsCustomHome'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { rootDocsRouteRef } from '../../routes'; jest.mock('@backstage/plugin-catalog-react', () => { @@ -47,7 +47,7 @@ const mockCatalogApi = { } as Partial; describe('TechDocsCustomHome', () => { - const apiRegistry = ApiRegistry.with(catalogApiRef, mockCatalogApi); + const apiRegistry = TestApiRegistry.from([catalogApiRef, mockCatalogApi]); it('should render a TechDocs home page', async () => { const tabsConfig = [ diff --git a/plugins/techdocs/src/reader/components/Reader.test.tsx b/plugins/techdocs/src/reader/components/Reader.test.tsx index 8b5dbced3f..6e8609694d 100644 --- a/plugins/techdocs/src/reader/components/Reader.test.tsx +++ b/plugins/techdocs/src/reader/components/Reader.test.tsx @@ -19,12 +19,12 @@ import { ScmIntegrationsApi, scmIntegrationsApiRef, } from '@backstage/integration-react'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { act, render } from '@testing-library/react'; import React from 'react'; import { TechDocsStorageApi, techdocsStorageApiRef } from '../../api'; import { Reader } from './Reader'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { searchApiRef } from '@backstage/plugin-search'; jest.mock('react-router-dom', () => { @@ -57,11 +57,11 @@ describe('', () => { results: [], }), }; - const apiRegistry = ApiRegistry.from([ + const apiRegistry = TestApiRegistry.from( [scmIntegrationsApiRef, scmIntegrationsApi], [techdocsStorageApiRef, techdocsStorageApi], [searchApiRef, searchApi], - ]); + ); await act(async () => { const rendered = render( diff --git a/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx index 6a5af982fd..b297466a62 100644 --- a/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx @@ -21,7 +21,7 @@ import { ScmIntegrationsApi, scmIntegrationsApiRef, } from '@backstage/integration-react'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { Header } from '@backstage/core-components'; import { techdocsApiRef, @@ -29,7 +29,7 @@ import { techdocsStorageApiRef, TechDocsStorageApi, } from '../../api'; -import { ApiRegistry, ApiProvider } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { searchApiRef } from '@backstage/plugin-search'; jest.mock('react-router-dom', () => { @@ -90,12 +90,12 @@ describe('', () => { results: [], }), }; - const apiRegistry = ApiRegistry.from([ + const apiRegistry = TestApiRegistry.from( [scmIntegrationsApiRef, scmIntegrationsApi], [techdocsApiRef, techdocsApi], [techdocsStorageApiRef, techdocsStorageApi], [searchApiRef, searchApi], - ]); + ); await act(async () => { const rendered = render( @@ -147,12 +147,12 @@ describe('', () => { results: [], }), }; - const apiRegistry = ApiRegistry.from([ + const apiRegistry = TestApiRegistry.from( [scmIntegrationsApiRef, scmIntegrationsApi], [techdocsApiRef, techdocsApi], [techdocsStorageApiRef, techdocsStorageApi], [searchApiRef, searchApi], - ]); + ); await act(async () => { const rendered = render( diff --git a/plugins/techdocs/src/reader/components/TechDocsSearch.test.tsx b/plugins/techdocs/src/reader/components/TechDocsSearch.test.tsx index 602a94e758..5b3d714a04 100644 --- a/plugins/techdocs/src/reader/components/TechDocsSearch.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsSearch.test.tsx @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { ApiProvider } from '@backstage/core-app-api'; import { searchApiRef } from '@backstage/plugin-search'; -import { wrapInTestApp } from '@backstage/test-utils'; +import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils'; import { act, fireEvent, @@ -54,7 +54,7 @@ describe('', () => { const querySpy = jest.fn(query); const searchApi = { query: querySpy }; - const apiRegistry = ApiRegistry.from([[searchApiRef, searchApi]]); + const apiRegistry = TestApiRegistry.from([searchApiRef, searchApi]); await act(async () => { const rendered = render( @@ -75,7 +75,7 @@ describe('', () => { const querySpy = jest.fn(query); const searchApi = { query: querySpy }; - const apiRegistry = ApiRegistry.from([[searchApiRef, searchApi]]); + const apiRegistry = TestApiRegistry.from([searchApiRef, searchApi]); await act(async () => { const rendered = render( diff --git a/plugins/techdocs/src/reader/components/useReaderState.test.tsx b/plugins/techdocs/src/reader/components/useReaderState.test.tsx index cce8282b3d..e41a114dbe 100644 --- a/plugins/techdocs/src/reader/components/useReaderState.test.tsx +++ b/plugins/techdocs/src/reader/components/useReaderState.test.tsx @@ -14,8 +14,8 @@ * limitations under the License. */ -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { NotFoundError } from '@backstage/errors'; +import { TestApiProvider } from '@backstage/test-utils'; import { act, renderHook } from '@testing-library/react-hooks'; import React from 'react'; import { techdocsStorageApiRef } from '../../api'; @@ -38,10 +38,10 @@ describe('useReaderState', () => { }; beforeEach(() => { - const apis = ApiRegistry.with(techdocsStorageApiRef, techdocsStorageApi); - Wrapper = ({ children }: { children?: React.ReactNode }) => ( - {children} + + {children} + ); }); diff --git a/plugins/todo/dev/index.tsx b/plugins/todo/dev/index.tsx index a4221dc710..fe547812ec 100644 --- a/plugins/todo/dev/index.tsx +++ b/plugins/todo/dev/index.tsx @@ -22,8 +22,8 @@ import OfflineIcon from '@material-ui/icons/Storage'; import React from 'react'; import { EntityTodoContent, todoApiRef, todoPlugin } from '../src'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { Content, Header, HeaderLabel, Page } from '@backstage/core-components'; +import { TestApiProvider } from '@backstage/test-utils'; const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', @@ -60,7 +60,7 @@ createDevApp() .registerPlugin(todoPlugin) .addPage({ element: ( - +
@@ -71,7 +71,7 @@ createDevApp() - + ), title: 'Entity Todo Content', icon: OfflineIcon, diff --git a/plugins/todo/src/components/TodoList/TodoList.test.tsx b/plugins/todo/src/components/TodoList/TodoList.test.tsx index 0e4d64172d..2759fe40f8 100644 --- a/plugins/todo/src/components/TodoList/TodoList.test.tsx +++ b/plugins/todo/src/components/TodoList/TodoList.test.tsx @@ -16,11 +16,10 @@ import { Entity } from '@backstage/catalog-model'; import { EntityProvider } from '@backstage/plugin-catalog-react'; -import { renderWithEffects } from '@backstage/test-utils'; +import { renderWithEffects, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { TodoApi, todoApiRef } from '../../api'; import { TodoList } from './TodoList'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('TodoList', () => { it('should render', async () => { @@ -42,11 +41,11 @@ describe('TodoList', () => { const mockEntity = { metadata: { name: 'mock' } } as Entity; const rendered = await renderWithEffects( - + - , + , ); await expect(rendered.findByText('FIXME')).resolves.toBeInTheDocument(); diff --git a/plugins/user-settings/src/components/AuthProviders/UserSettingsAuthProviders.test.tsx b/plugins/user-settings/src/components/AuthProviders/UserSettingsAuthProviders.test.tsx index 085a884f2e..4e14dec2ef 100644 --- a/plugins/user-settings/src/components/AuthProviders/UserSettingsAuthProviders.test.tsx +++ b/plugins/user-settings/src/components/AuthProviders/UserSettingsAuthProviders.test.tsx @@ -14,22 +14,24 @@ * limitations under the License. */ -import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import { + renderWithEffects, + TestApiRegistry, + wrapInTestApp, +} from '@backstage/test-utils'; import { fireEvent } from '@testing-library/react'; import React from 'react'; import { UserSettingsAuthProviders } from './UserSettingsAuthProviders'; -import { - ApiProvider, - ApiRegistry, - ConfigReader, -} from '@backstage/core-app-api'; +import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; import { configApiRef, googleAuthApiRef } from '@backstage/core-plugin-api'; const mockSignInHandler = jest.fn().mockReturnValue(''); const mockGoogleAuth = { sessionState$: () => ({ + [Symbol.observable]: jest.fn(), subscribe: () => ({ + closed: false, unsubscribe: () => null, }), }), @@ -47,10 +49,10 @@ const createConfig = () => const config = createConfig(); -const apiRegistry = ApiRegistry.from([ +const apiRegistry = TestApiRegistry.from( [configApiRef, config], [googleAuthApiRef, mockGoogleAuth], -]); +); describe('', () => { it('displays a provider and calls its sign-in handler on click', async () => { diff --git a/plugins/user-settings/src/components/General/UserSettingsThemeToggle.test.tsx b/plugins/user-settings/src/components/General/UserSettingsThemeToggle.test.tsx index caae4d69bc..83d9e23f83 100644 --- a/plugins/user-settings/src/components/General/UserSettingsThemeToggle.test.tsx +++ b/plugins/user-settings/src/components/General/UserSettingsThemeToggle.test.tsx @@ -15,16 +15,16 @@ */ import { AppTheme, appThemeApiRef } from '@backstage/core-plugin-api'; -import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; +import { + renderWithEffects, + TestApiRegistry, + wrapInTestApp, +} from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; import { fireEvent } from '@testing-library/react'; import React from 'react'; import { UserSettingsThemeToggle } from './UserSettingsThemeToggle'; -import { - ApiProvider, - ApiRegistry, - AppThemeSelector, -} from '@backstage/core-app-api'; +import { ApiProvider, AppThemeSelector } from '@backstage/core-app-api'; const mockTheme: AppTheme = { id: 'light-theme', @@ -33,8 +33,9 @@ const mockTheme: AppTheme = { theme: lightTheme, }; -const apiRegistry = ApiRegistry.from([ - [appThemeApiRef, AppThemeSelector.createWithStorage([mockTheme])], +const apiRegistry = TestApiRegistry.from([ + appThemeApiRef, + AppThemeSelector.createWithStorage([mockTheme]), ]); describe('', () => { diff --git a/plugins/xcmetrics/src/components/BuildDetails/BuildDetails.test.tsx b/plugins/xcmetrics/src/components/BuildDetails/BuildDetails.test.tsx index 2d949d7abc..7083d890d6 100644 --- a/plugins/xcmetrics/src/components/BuildDetails/BuildDetails.test.tsx +++ b/plugins/xcmetrics/src/components/BuildDetails/BuildDetails.test.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { BuildDetails, withRequest } from './BuildDetails'; import { xcmetricsApiRef } from '../../api'; @@ -35,11 +34,9 @@ jest.mock('../BuildTimeline', () => ({ describe('BuildDetails', () => { it('should render', async () => { const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText('accordion-Host')).toBeInTheDocument(); @@ -61,11 +58,9 @@ describe('BuildDetails with request', () => { it('should fetch the build and render', async () => { const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText(client.mockBuild.id)).toBeInTheDocument(); @@ -78,11 +73,9 @@ describe('BuildDetails with request', () => { .mockRejectedValue({ message: errorMessage }); const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText(errorMessage)).toBeInTheDocument(); @@ -92,11 +85,9 @@ describe('BuildDetails with request', () => { client.XcmetricsClient.getBuild = jest.fn().mockReturnValue(undefined); const rendered = await renderInTestApp( - + - , + , ); expect( diff --git a/plugins/xcmetrics/src/components/BuildList/BuildList.test.tsx b/plugins/xcmetrics/src/components/BuildList/BuildList.test.tsx index eb141209e7..53471f5e9f 100644 --- a/plugins/xcmetrics/src/components/BuildList/BuildList.test.tsx +++ b/plugins/xcmetrics/src/components/BuildList/BuildList.test.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { BuildList } from './BuildList'; import { xcmetricsApiRef } from '../../api'; import userEvent from '@testing-library/user-event'; @@ -35,11 +34,9 @@ jest.mock('../BuildDetails', () => ({ describe('BuildList', () => { it('should render', async () => { const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText('Builds')).toBeInTheDocument(); @@ -50,11 +47,9 @@ describe('BuildList', () => { it('should show build details', async () => { const rendered = await renderInTestApp( - + - , + , ); userEvent.click( @@ -70,11 +65,9 @@ describe('BuildList', () => { .mockRejectedValue({ message }); const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText(message)).toBeInTheDocument(); diff --git a/plugins/xcmetrics/src/components/BuildListFilter/BuildListFilter.test.tsx b/plugins/xcmetrics/src/components/BuildListFilter/BuildListFilter.test.tsx index b997de68e0..06f4aed1b7 100644 --- a/plugins/xcmetrics/src/components/BuildListFilter/BuildListFilter.test.tsx +++ b/plugins/xcmetrics/src/components/BuildListFilter/BuildListFilter.test.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import { BuildListFilter } from './BuildListFilter'; import { BuildFilters, xcmetricsApiRef } from '../../api'; @@ -37,14 +36,12 @@ const renderWithFiltersVisible = async ( callback?: (filters: BuildFilters) => void, ) => { const rendered = await renderInTestApp( - + - , + , ); userEvent.click(rendered.getByLabelText('show filters')); @@ -67,14 +64,12 @@ const setProjectFilter = async (rendered: RenderResult, option: string) => { describe('BuildListFilter', () => { it('should render', async () => { const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText('Filters (0)')).toBeInTheDocument(); diff --git a/plugins/xcmetrics/src/components/Overview/Overview.test.tsx b/plugins/xcmetrics/src/components/Overview/Overview.test.tsx index 3d24d2a422..42e3772d04 100644 --- a/plugins/xcmetrics/src/components/Overview/Overview.test.tsx +++ b/plugins/xcmetrics/src/components/Overview/Overview.test.tsx @@ -14,9 +14,8 @@ * limitations under the License. */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { xcmetricsApiRef } from '../../api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import { Overview } from './Overview'; jest.mock('../../api/XcmetricsClient'); @@ -33,11 +32,9 @@ jest.mock('../StatusMatrix', () => ({ describe('Overview', () => { it('should render', async () => { const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText('XCMetrics Dashboard')).toBeInTheDocument(); @@ -49,9 +46,9 @@ describe('Overview', () => { api.getBuilds = jest.fn().mockResolvedValue([]); const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText('No builds to show')).toBeInTheDocument(); @@ -64,9 +61,9 @@ describe('Overview', () => { api.getBuilds = jest.fn().mockRejectedValue({ message: errorMessage }); const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText(errorMessage)).toBeInTheDocument(); diff --git a/plugins/xcmetrics/src/components/OverviewTrends/OverviewTrends.test.tsx b/plugins/xcmetrics/src/components/OverviewTrends/OverviewTrends.test.tsx index 76b47f4943..db7904a8d1 100644 --- a/plugins/xcmetrics/src/components/OverviewTrends/OverviewTrends.test.tsx +++ b/plugins/xcmetrics/src/components/OverviewTrends/OverviewTrends.test.tsx @@ -15,9 +15,8 @@ */ import React from 'react'; import { OverviewTrends } from './OverviewTrends'; -import { renderInTestApp } from '@backstage/test-utils'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { xcmetricsApiRef } from '../../api'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; import userEvent from '@testing-library/user-event'; jest.mock('../../api/XcmetricsClient'); @@ -26,11 +25,9 @@ const client = require('../../api/XcmetricsClient'); describe('OverviewTrends', () => { it('should render', async () => { const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText('Trends for')).toBeInTheDocument(); expect(rendered.getAllByText('Build Count').length).toEqual(3); @@ -42,20 +39,18 @@ describe('OverviewTrends', () => { api.getBuildCounts = jest.fn().mockResolvedValue([]); const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText('--')).toBeInTheDocument(); }); it('should change number of days when select is changed', async () => { const rendered = await renderInTestApp( - + - , + , ); userEvent.click(rendered.getByText('14 days')); @@ -76,9 +71,9 @@ describe('OverviewTrends', () => { .mockRejectedValue({ message: buildTimesError }); const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText(buildCountError)).toBeInTheDocument(); expect(rendered.getByText(buildTimesError)).toBeInTheDocument(); diff --git a/plugins/xcmetrics/src/components/StatusCell/StatusCell.test.tsx b/plugins/xcmetrics/src/components/StatusCell/StatusCell.test.tsx index cac111d13e..6e5b7f73b9 100644 --- a/plugins/xcmetrics/src/components/StatusCell/StatusCell.test.tsx +++ b/plugins/xcmetrics/src/components/StatusCell/StatusCell.test.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import userEvent from '@testing-library/user-event'; import { StatusCell } from './StatusCell'; import { xcmetricsApiRef } from '../../api'; @@ -27,9 +26,7 @@ const client = require('../../api/XcmetricsClient'); describe('StatusCell', () => { it('should render', async () => { const rendered = await renderInTestApp( - + { size={10} spacing={10} /> - , + , ); userEvent.hover(rendered.getByTestId(client.mockBuild.id)); diff --git a/plugins/xcmetrics/src/components/StatusMatrix/StatusMatrix.test.tsx b/plugins/xcmetrics/src/components/StatusMatrix/StatusMatrix.test.tsx index 7d3854bbda..f18d322b43 100644 --- a/plugins/xcmetrics/src/components/StatusMatrix/StatusMatrix.test.tsx +++ b/plugins/xcmetrics/src/components/StatusMatrix/StatusMatrix.test.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { StatusMatrix } from './StatusMatrix'; import { xcmetricsApiRef } from '../../api'; @@ -25,11 +24,9 @@ const client = require('../../api/XcmetricsClient'); describe('StatusMatrix', () => { it('should render', async () => { const rendered = await renderInTestApp( - + - , + , ); const cell = rendered.getByTestId(client.mockBuild.id); diff --git a/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.test.tsx b/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.test.tsx index 84bd25624c..29d6407b03 100644 --- a/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.test.tsx +++ b/plugins/xcmetrics/src/components/XcmetricsLayout/XcmetricsLayout.test.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ import React from 'react'; -import { renderInTestApp } from '@backstage/test-utils'; -import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { XcmetricsLayout } from './XcmetricsLayout'; import { xcmetricsApiRef } from '../../api'; import userEvent from '@testing-library/user-event'; @@ -34,11 +33,9 @@ jest.mock('../BuildList', () => ({ describe('XcmetricsLayout', () => { it('should render', async () => { const rendered = await renderInTestApp( - + - , + , ); expect(rendered.getByText('Overview')).toBeInTheDocument(); @@ -49,11 +46,9 @@ describe('XcmetricsLayout', () => { it('should show a list of builds when the Builds tab is selected', async () => { const rendered = await renderInTestApp( - + - , + , ); userEvent.click(rendered.getByText('Builds')); diff --git a/yarn.lock b/yarn.lock index 07a5febcb8..1e4d402072 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2078,10 +2078,10 @@ core-js-pure "^3.16.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.15.4" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a" - integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw== +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.6", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.16.3" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.3.tgz#b86f0db02a04187a3c17caa77de69840165d42d5" + integrity sha512-WBwekcqacdY2e9AF/Q7WLFUWmdJGJTkbjqTjoMDgXkVZ3ZRUvOPsLb5KdwISoQVsbP+DQzVZW4Zhci0DvpbNTQ== dependencies: regenerator-runtime "^0.13.4" @@ -2879,35 +2879,36 @@ resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210" integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw== -"@gitbeaker/core@^30.2.0", "@gitbeaker/core@^30.3.0": - version "30.3.0" - resolved "https://registry.npmjs.org/@gitbeaker/core/-/core-30.3.0.tgz#d005891d47cfacb41d4a3cc8bf3ee8c68c53d378" - integrity sha512-j7GHsFo6AOuRmLaK4F4Kx967jTy6LFZh5vjmRxjlRvX0qlfl3NJKiQIl30fZz1rPXWdQLAq+kwl1i0BKrWhOpA== +"@gitbeaker/core@^34.6.0": + version "34.6.0" + resolved "https://registry.npmjs.org/@gitbeaker/core/-/core-34.6.0.tgz#f774ea98ac079ba2edf495fdef738ac3f741178b" + integrity sha512-yKF+oxffPyzOnyuHCqLGJrBHhcFHuGHtcmqKhGKtnYPfqcNYA8rt4INAHaE5wMz4ILua9b4sB8p42fki+xn6WA== dependencies: - "@gitbeaker/requester-utils" "^30.3.0" + "@gitbeaker/requester-utils" "^34.6.0" form-data "^4.0.0" li "^1.3.0" + mime "^3.0.0" query-string "^7.0.0" xcase "^2.0.1" -"@gitbeaker/node@^30.2.0": - version "30.3.0" - resolved "https://registry.npmjs.org/@gitbeaker/node/-/node-30.3.0.tgz#ceebde08a13d3f655fa614a4ced56eb1f0a71ec1" - integrity sha512-Ythbadb1+yMO2hSgvp3nvaroHTkI4+mdLg1rdV3YNIF1n+kUYXQD2GY7wpAjH0KjAnBAmgxw/JsFmlfnRu3KAg== +"@gitbeaker/node@^34.6.0": + version "34.6.0" + resolved "https://registry.npmjs.org/@gitbeaker/node/-/node-34.6.0.tgz#104f122433b65ceb45b0e645001d15cbcc9b1280" + integrity sha512-gVV4Wuev43Jbyoy1fszC885+bkvWH4zWiUhtIu0PSAm628j/OxO7idLIqUEMV0hDf6wm/PE/vOSP6PhjE0N+fA== dependencies: - "@gitbeaker/core" "^30.3.0" - "@gitbeaker/requester-utils" "^30.3.0" + "@gitbeaker/core" "^34.6.0" + "@gitbeaker/requester-utils" "^34.6.0" delay "^5.0.0" got "^11.8.2" xcase "^2.0.1" -"@gitbeaker/requester-utils@^30.3.0": - version "30.3.0" - resolved "https://registry.npmjs.org/@gitbeaker/requester-utils/-/requester-utils-30.3.0.tgz#fbbaae20263ff90952da116d8782f74f47b29c81" - integrity sha512-b5vCaUqP2Jrqdpt5CkmFET4VFHwJYjnIovwZGYd5H0aKmiPw4NmeW1JtP+65J4xf0c4AOQQj6XSf7TZLuPNAqw== +"@gitbeaker/requester-utils@^34.6.0": + version "34.6.0" + resolved "https://registry.npmjs.org/@gitbeaker/requester-utils/-/requester-utils-34.6.0.tgz#4489009b759ca6f9a83f244453f4f610f1ac7349" + integrity sha512-H8utxbSP1kEdX0KcyVYrTDTT0A3UcPwrIV1ahyufX9ZLybYSUsA56B8Wx5kJSbWGFT1ffu2f8H2YDMwNCKKsBg== dependencies: form-data "^4.0.0" - query-string "^7.0.0" + qs "^6.10.1" xcase "^2.0.1" "@google-cloud/common@^3.7.0": @@ -3654,6 +3655,17 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@jest/types@^27.2.5": + version "27.2.5" + resolved "https://registry.npmjs.org/@jest/types/-/types-27.2.5.tgz#420765c052605e75686982d24b061b4cbba22132" + integrity sha512-nmuM4VuDtCZcY+eTpw+0nvstwReMsjPoj7ZR80/BbixulhLaiX+fbv8oeLW8WZlJMcsGQsTmMKT/iTZu1Uy/lQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^16.0.0" + chalk "^4.0.0" + "@josephg/resolvable@^1.0.0": version "1.0.1" resolved "https://registry.npmjs.org/@josephg/resolvable/-/resolvable-1.0.1.tgz#69bc4db754d79e1a2f17a650d3466e038d94a5eb" @@ -5556,16 +5568,16 @@ util-deprecate "^1.0.2" "@storybook/addon-actions@^6.1.11": - version "6.3.7" - resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.3.7.tgz#b25434972bef351aceb3f7ec6fd66e210f256aac" - integrity sha512-CEAmztbVt47Gw1o6Iw0VP20tuvISCEKk9CS/rCjHtb4ubby6+j/bkp3pkEUQIbyLdHiLWFMz0ZJdyA/U6T6jCw== + version "6.3.12" + resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.3.12.tgz#69eb5f8f780f1b00456051da6290d4b959ba24a0" + integrity sha512-mzuN4Ano4eyicwycM2PueGzzUCAEzt9/6vyptWEIVJu0sjK0J9KtBRlqFi1xGQxmCfimDR/n/vWBBkc7fp2uJA== dependencies: - "@storybook/addons" "6.3.7" - "@storybook/api" "6.3.7" - "@storybook/client-api" "6.3.7" - "@storybook/components" "6.3.7" - "@storybook/core-events" "6.3.7" - "@storybook/theming" "6.3.7" + "@storybook/addons" "6.3.12" + "@storybook/api" "6.3.12" + "@storybook/client-api" "6.3.12" + "@storybook/components" "6.3.12" + "@storybook/core-events" "6.3.12" + "@storybook/theming" "6.3.12" core-js "^3.8.2" fast-deep-equal "^3.1.3" global "^4.4.0" @@ -5841,19 +5853,6 @@ qs "^6.10.0" telejson "^5.3.2" -"@storybook/channel-postmessage@6.3.7": - version "6.3.7" - resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.3.7.tgz#bd4edf84a29aa2cd4a22d26115c60194d289a840" - integrity sha512-Cmw8HRkeSF1yUFLfEIUIkUICyCXX8x5Ol/5QPbiW9HPE2hbZtYROCcg4bmWqdq59N0Tp9FQNSn+9ZygPgqQtNw== - dependencies: - "@storybook/channels" "6.3.7" - "@storybook/client-logger" "6.3.7" - "@storybook/core-events" "6.3.7" - core-js "^3.8.2" - global "^4.4.0" - qs "^6.10.0" - telejson "^5.3.2" - "@storybook/channels@6.3.11": version "6.3.11" resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.3.11.tgz#a14ce233367a9072bd1cffef3825f125c27bb0ae" @@ -5929,30 +5928,6 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-api@6.3.7": - version "6.3.7" - resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.3.7.tgz#cb1dca05467d777bd09aadbbdd1dd22ca537ce14" - integrity sha512-8wOH19cMIwIIYhZy5O5Wl8JT1QOL5kNuamp9GPmg5ff4DtnG+/uUslskRvsnKyjPvl+WbIlZtBVWBiawVdd/yQ== - dependencies: - "@storybook/addons" "6.3.7" - "@storybook/channel-postmessage" "6.3.7" - "@storybook/channels" "6.3.7" - "@storybook/client-logger" "6.3.7" - "@storybook/core-events" "6.3.7" - "@storybook/csf" "0.0.1" - "@types/qs" "^6.9.5" - "@types/webpack-env" "^1.16.0" - core-js "^3.8.2" - global "^4.4.0" - lodash "^4.17.20" - memoizerific "^1.11.3" - qs "^6.10.0" - regenerator-runtime "^0.13.7" - stable "^0.1.8" - store2 "^2.12.0" - ts-dedent "^2.0.0" - util-deprecate "^1.0.2" - "@storybook/client-logger@6.3.11": version "6.3.11" resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.3.11.tgz#d2e0e17f35ed4c5ee282d818b6db3a015ce6b833" @@ -6651,13 +6626,13 @@ dependencies: defer-to-connect "^2.0.0" -"@testing-library/cypress@^7.0.1": - version "7.0.6" - resolved "https://registry.npmjs.org/@testing-library/cypress/-/cypress-7.0.6.tgz#5445dac4f4852c26901c356e9d3a69371bd20ccf" - integrity sha512-atnjqlkEt6spU4Mv7evvpA8fMXeRw7AN2uTKOR1dP6WBvBixVwAYMZY+1fMOaZULWAj9vGLCXXvmw++u3TxuCQ== +"@testing-library/cypress@^8.0.2": + version "8.0.2" + resolved "https://registry.npmjs.org/@testing-library/cypress/-/cypress-8.0.2.tgz#b13f0ff2424dec4368b6670dfbfb7e43af8eefc9" + integrity sha512-KVdm7n37sg/A4e3wKMD4zUl0NpzzVhx06V9Tf0hZHZ7nrZ4yFva6Zwg2EFF1VzHkEfN/ahUzRtT1qiW+vuWnJw== dependencies: - "@babel/runtime" "^7.12.5" - "@testing-library/dom" "^7.28.1" + "@babel/runtime" "^7.14.6" + "@testing-library/dom" "^8.1.0" "@testing-library/dom@^7.28.1": version "7.29.6" @@ -6673,6 +6648,20 @@ lz-string "^1.4.4" pretty-format "^26.6.2" +"@testing-library/dom@^8.1.0": + version "8.11.1" + resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.11.1.tgz#03fa2684aa09ade589b460db46b4c7be9fc69753" + integrity sha512-3KQDyx9r0RKYailW2MiYrSSKEfH0GTkI51UGEvJenvcoDoeRYs0PZpi2SXqtnMClQvCqdtTTpOfFETDTVADpAg== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^4.2.0" + aria-query "^5.0.0" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.4.4" + pretty-format "^27.0.2" + "@testing-library/jest-dom@^5.10.1": version "5.14.1" resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.14.1.tgz#8501e16f1e55a55d675fe73eecee32cdaddb9766" @@ -6742,6 +6731,11 @@ axios-cached-dns-resolve "0.5.2" file-type "16.5.3" +"@trysound/sax@0.2.0": + version "0.2.0" + resolved "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== + "@tsconfig/node10@^1.0.7": version "1.0.8" resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" @@ -8317,6 +8311,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yargs@^16.0.0": + version "16.0.4" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977" + integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw== + dependencies: + "@types/yargs-parser" "*" + "@types/yarnpkg__lockfile@^1.1.4": version "1.1.4" resolved "https://registry.npmjs.org/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.4.tgz#445251eb00bd9c1e751f82c7c6bf4f714edfd464" @@ -8938,7 +8939,7 @@ ajv@^8.0.1: require-from-string "^2.0.2" uri-js "^4.2.2" -alphanum-sort@^1.0.0: +alphanum-sort@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= @@ -9011,7 +9012,7 @@ ansi-regex@^4.0.0, ansi-regex@^4.1.0: resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== -ansi-regex@^5.0.0: +ansi-regex@^5.0.0, ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== @@ -9041,6 +9042,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: "@types/color-name" "^1.1.1" color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + ansi-to-html@^0.6.11: version "0.6.14" resolved "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.6.14.tgz#65fe6d08bba5dd9db33f44a20aec331e0010dad8" @@ -9362,6 +9368,11 @@ aria-query@^4.2.2: "@babel/runtime" "^7.10.2" "@babel/runtime-corejs3" "^7.10.2" +aria-query@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c" + integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg== + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -10388,7 +10399,7 @@ browserslist@4.14.2: escalade "^3.0.2" node-releases "^1.1.61" -browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.16.6: +browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.16.0, browserslist@^4.16.6: version "4.18.1" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.18.1.tgz#60d3920f25b6860eb917c6c7b185576f4d8b017f" integrity sha512-8ScCzdpPwR2wQh8IT82CA2VgDwjHyqMovPBZSNH54+tm4Jk2pCuv90gmAdH6J84OCRWi0b4gMe6O6XPXuJnjgQ== @@ -10670,25 +10681,6 @@ call-me-maybe@^1.0.1: resolved "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= -caller-callsite@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" - integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= - dependencies: - callsites "^2.0.0" - -caller-path@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" - integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= - dependencies: - caller-callsite "^2.0.0" - -callsites@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" - integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= - callsites@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -11332,7 +11324,7 @@ color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.5.2, color-string@^1.5.4, color-string@^1.6.0: +color-string@^1.5.2, color-string@^1.6.0: version "1.6.0" resolved "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz#c3915f61fe267672cb7e1e064c9d692219f6c312" integrity sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA== @@ -11348,14 +11340,6 @@ color@3.0.x: color-convert "^1.9.1" color-string "^1.5.2" -color@^3.0.0: - version "3.1.3" - resolved "https://registry.npmjs.org/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e" - integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ== - dependencies: - color-convert "^1.9.1" - color-string "^1.5.4" - color@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/color/-/color-4.0.1.tgz#21df44cd10245a91b1ccf5ba031609b0e10e7d67" @@ -11364,6 +11348,11 @@ color@^4.0.1: color-convert "^2.0.1" color-string "^1.6.0" +colord@^2.9.1: + version "2.9.1" + resolved "https://registry.npmjs.org/colord/-/colord-2.9.1.tgz#c961ea0efeb57c9f0f4834458f26cb9cc4a3f90e" + integrity sha512-4LBMSt09vR0uLnPVkOUBnmxgoaeN4ewRbx801wY/bXcltXfpR/G46OdWn96XpYmCWuYvO46aBZP4NgX8HpNAcw== + colorette@1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" @@ -11878,16 +11867,6 @@ cosmiconfig@7.0.0, cosmiconfig@^7.0.0: path-type "^4.0.0" yaml "^1.10.0" -cosmiconfig@^5.0.0: - version "5.2.1" - resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" - integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== - dependencies: - import-fresh "^2.0.0" - is-directory "^0.3.1" - js-yaml "^3.13.1" - parse-json "^4.0.0" - cosmiconfig@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" @@ -12084,17 +12063,11 @@ css-color-converter@^2.0.0: color-name "^1.1.4" css-unit-converter "^1.1.2" -css-color-names@0.0.4, css-color-names@^0.0.4: - version "0.0.4" - resolved "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz#808adc2e79cf84738069b646cb20ec27beb629e0" - integrity sha1-gIrcLnnPhHOAabZGyyDsJ762KeA= - -css-declaration-sorter@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz#c198940f63a76d7e36c1e71018b001721054cb22" - integrity sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA== +css-declaration-sorter@^6.0.3: + version "6.1.3" + resolved "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.1.3.tgz#e9852e4cf940ba79f509d9425b137d1f94438dc2" + integrity sha512-SvjQjNRZgh4ULK1LDJ2AduPKUKxIqmtU7ZAyi47BTV+M90Qvxr9AB6lKlLbDUfXqI9IQeYA8LbAsCZPpJEV3aA== dependencies: - postcss "^7.0.1" timsort "^0.3.0" css-in-js-utils@^2.0.0: @@ -12182,6 +12155,14 @@ css-tree@^1.1.2: mdn-data "2.0.14" source-map "^0.6.1" +css-tree@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + css-unit-converter@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21" @@ -12229,73 +12210,55 @@ cssfilter@0.0.10: resolved "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz#c6d2672632a2e5c83e013e6864a42ce8defd20ae" integrity sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4= -cssnano-preset-default@^4.0.7: - version "4.0.7" - resolved "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.7.tgz#51ec662ccfca0f88b396dcd9679cdb931be17f76" - integrity sha512-x0YHHx2h6p0fCl1zY9L9roD7rnlltugGu7zXSKQx6k2rYw0Hi3IqxcoAGF7u9Q5w1nt7vK0ulxV8Lo+EvllGsA== +cssnano-preset-default@^5.1.7: + version "5.1.7" + resolved "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.1.7.tgz#68c3ad1ec6a810482ec7d06b2d70fc34b6b0d70c" + integrity sha512-bWDjtTY+BOqrqBtsSQIbN0RLGD2Yr2CnecpP0ydHNafh9ZUEre8c8VYTaH9FEbyOt0eIfEUAYYk5zj92ioO8LA== dependencies: - css-declaration-sorter "^4.0.1" - cssnano-util-raw-cache "^4.0.1" - postcss "^7.0.0" - postcss-calc "^7.0.1" - postcss-colormin "^4.0.3" - postcss-convert-values "^4.0.1" - postcss-discard-comments "^4.0.2" - postcss-discard-duplicates "^4.0.2" - postcss-discard-empty "^4.0.1" - postcss-discard-overridden "^4.0.1" - postcss-merge-longhand "^4.0.11" - postcss-merge-rules "^4.0.3" - postcss-minify-font-values "^4.0.2" - postcss-minify-gradients "^4.0.2" - postcss-minify-params "^4.0.2" - postcss-minify-selectors "^4.0.2" - postcss-normalize-charset "^4.0.1" - postcss-normalize-display-values "^4.0.2" - postcss-normalize-positions "^4.0.2" - postcss-normalize-repeat-style "^4.0.2" - postcss-normalize-string "^4.0.2" - postcss-normalize-timing-functions "^4.0.2" - postcss-normalize-unicode "^4.0.1" - postcss-normalize-url "^4.0.1" - postcss-normalize-whitespace "^4.0.2" - postcss-ordered-values "^4.1.2" - postcss-reduce-initial "^4.0.3" - postcss-reduce-transforms "^4.0.2" - postcss-svgo "^4.0.2" - postcss-unique-selectors "^4.0.1" + css-declaration-sorter "^6.0.3" + cssnano-utils "^2.0.1" + postcss-calc "^8.0.0" + postcss-colormin "^5.2.1" + postcss-convert-values "^5.0.2" + postcss-discard-comments "^5.0.1" + postcss-discard-duplicates "^5.0.1" + postcss-discard-empty "^5.0.1" + postcss-discard-overridden "^5.0.1" + postcss-merge-longhand "^5.0.4" + postcss-merge-rules "^5.0.3" + postcss-minify-font-values "^5.0.1" + postcss-minify-gradients "^5.0.3" + postcss-minify-params "^5.0.2" + postcss-minify-selectors "^5.1.0" + postcss-normalize-charset "^5.0.1" + postcss-normalize-display-values "^5.0.1" + postcss-normalize-positions "^5.0.1" + postcss-normalize-repeat-style "^5.0.1" + postcss-normalize-string "^5.0.1" + postcss-normalize-timing-functions "^5.0.1" + postcss-normalize-unicode "^5.0.1" + postcss-normalize-url "^5.0.3" + postcss-normalize-whitespace "^5.0.1" + postcss-ordered-values "^5.0.2" + postcss-reduce-initial "^5.0.1" + postcss-reduce-transforms "^5.0.1" + postcss-svgo "^5.0.3" + postcss-unique-selectors "^5.0.2" -cssnano-util-get-arguments@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz#ed3a08299f21d75741b20f3b81f194ed49cc150f" - integrity sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8= +cssnano-utils@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-2.0.1.tgz#8660aa2b37ed869d2e2f22918196a9a8b6498ce2" + integrity sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ== -cssnano-util-get-match@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz#c0e4ca07f5386bb17ec5e52250b4f5961365156d" - integrity sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0= - -cssnano-util-raw-cache@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz#b26d5fd5f72a11dfe7a7846fb4c67260f96bf282" - integrity sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA== +cssnano@^5.0.1: + version "5.0.11" + resolved "https://registry.npmjs.org/cssnano/-/cssnano-5.0.11.tgz#743397a05e04cb87e9df44b7659850adfafc3646" + integrity sha512-5SHM31NAAe29jvy0MJqK40zZ/8dGlnlzcfHKw00bWMVFp8LWqtuyPSFwbaoIoxvt71KWJOfg8HMRGrBR3PExCg== dependencies: - postcss "^7.0.0" - -cssnano-util-same-parent@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz#574082fb2859d2db433855835d9a8456ea18bbf3" - integrity sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q== - -cssnano@^4.1.10: - version "4.1.10" - resolved "https://registry.npmjs.org/cssnano/-/cssnano-4.1.10.tgz#0ac41f0b13d13d465487e111b778d42da631b8b2" - integrity sha512-5wny+F6H4/8RgNlaqab4ktc3e0/blKutmq8yNlBFXA//nSFFAqAngjNVRzUvCgYROULmZZUoosL/KSoZo5aUaQ== - dependencies: - cosmiconfig "^5.0.0" - cssnano-preset-default "^4.0.7" - is-resolvable "^1.0.0" - postcss "^7.0.0" + cssnano-preset-default "^5.1.7" + is-resolvable "^1.1.0" + lilconfig "^2.0.3" + yaml "^1.10.2" csso@^4.0.2: version "4.0.2" @@ -12304,6 +12267,13 @@ csso@^4.0.2: dependencies: css-tree "1.0.0-alpha.37" +csso@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + cssom@^0.4.4: version "0.4.4" resolved "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10" @@ -13162,6 +13132,11 @@ dom-accessibility-api@^0.5.4, dom-accessibility-api@^0.5.6: resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.6.tgz#3f5d43b52c7a3bd68b5fb63fa47b4e4c1fdf65a9" integrity sha512-DplGLZd8L1lN64jlT27N9TVSESFR5STaEJvX+thCby7fuCHonfPpAlodYc3vuUYbDuDec5w8AMP7oCM5TWFsqw== +dom-accessibility-api@^0.5.9: + version "0.5.10" + resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.10.tgz#caa6d08f60388d0bb4539dd75fe458a9a1d0014c" + integrity sha512-Xu9mD0UjrJisTmv7lmVSDMagQcU9R5hwAbxsaAE/35XPnPLJobbuREfV/rraiSaEj/UOvgrzQs66zyTWTlyd+g== + dom-converter@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" @@ -16055,7 +16030,7 @@ has-yarn@^2.1.0: resolved "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== -has@^1.0.0, has@^1.0.3: +has@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== @@ -16179,11 +16154,6 @@ helmet@^4.0.0: resolved "https://registry.npmjs.org/helmet/-/helmet-4.4.1.tgz#a17e1444d81d7a83ddc6e6f9bc6e2055b994efe7" integrity sha512-G8tp0wUMI7i8wkMk2xLcEvESg5PiCitFMYgGRc/PwULB0RVhTP5GFdxOwvJwp9XVha8CuS8mnhmE8I/8dx/pbw== -hex-color-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" - integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== - highlight.js@^10.1.0, highlight.js@^10.1.1, highlight.js@^10.4.1, highlight.js@^10.6.0: version "10.7.2" resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.2.tgz#89319b861edc66c48854ed1e6da21ea89f847360" @@ -16256,16 +16226,6 @@ hpagent@^0.1.1: resolved "https://registry.npmjs.org/hpagent/-/hpagent-0.1.2.tgz#cab39c66d4df2d4377dbd212295d878deb9bdaa9" integrity sha512-ePqFXHtSQWAFXYmj+JtOTHr84iNrII4/QRlAAPPE+zqnKy4xJo7Ie1Y4kC7AdB+LxLxSTTzBMASsEcy0q8YyvQ== -hsl-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" - integrity sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4= - -hsla-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz#c1ce7a3168c8c6614033a4b5f7877f3b225f9c38" - integrity sha1-wc56MWjIxmFAM6S194d/OyJfnDg= - html-encoding-sniffer@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz#42a6dc4fd33f00281176e8b23759ca4e4fa185f3" @@ -16639,14 +16599,6 @@ import-cwd@^3.0.0: dependencies: import-from "^3.0.0" -import-fresh@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" - integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= - dependencies: - caller-path "^2.0.0" - resolve-from "^3.0.0" - import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: version "3.2.1" resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" @@ -16916,10 +16868,10 @@ ipaddr.js@^2.0.1: resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz#eca256a7a877e917aeb368b0a7497ddf42ef81c0" integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== -is-absolute-url@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6" - integrity sha1-UFMN+4T8yap9vnhS6Do3uTufKqY= +is-absolute-url@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" + integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q== is-absolute@^1.0.0: version "1.0.0" @@ -17039,18 +16991,6 @@ is-ci@^3.0.0: dependencies: ci-info "^3.1.1" -is-color-stop@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" - integrity sha1-z/9HGu5N1cnhWFmPvhKWe1za00U= - dependencies: - css-color-names "^0.0.4" - hex-color-regex "^1.1.0" - hsl-regex "^1.0.0" - hsla-regex "^1.0.0" - rgb-regex "^1.0.1" - rgba-regex "^1.0.0" - is-core-module@^2.1.0, is-core-module@^2.2.0: version "2.4.0" resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1" @@ -17105,11 +17045,6 @@ is-descriptor@^1.0.0, is-descriptor@^1.0.2: is-data-descriptor "^1.0.0" kind-of "^6.0.2" -is-directory@^0.3.1: - version "0.3.1" - resolved "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" - integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= - is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" @@ -17423,7 +17358,7 @@ is-relative@^1.0.0: dependencies: is-unc-path "^1.0.0" -is-resolvable@^1.0.0: +is-resolvable@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== @@ -19001,6 +18936,11 @@ libnpmpublish@^4.0.0: semver "^7.1.3" ssri "^8.0.0" +lilconfig@^2.0.3: + version "2.0.4" + resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz#f4507d043d7058b380b6a8f5cb7bcd4b34cee082" + integrity sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA== + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" @@ -20547,6 +20487,11 @@ mime@^2.2.0, mime@^2.4.4, mime@^2.4.6: resolved "https://registry.npmjs.org/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" @@ -21361,7 +21306,7 @@ normalize-range@^0.1.2: resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= -normalize-url@^3.0.0, normalize-url@^3.3.0: +normalize-url@^3.3.0: version "3.3.0" resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559" integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== @@ -21371,6 +21316,11 @@ normalize-url@^4.1.0: resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== +normalize-url@^6.0.1: + version "6.1.0" + resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== + npm-bundled@^1.0.1, npm-bundled@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" @@ -22820,61 +22770,50 @@ posix-character-classes@^0.1.0: resolved "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= -postcss-calc@^7.0.1: - version "7.0.2" - resolved "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.2.tgz#504efcd008ca0273120568b0792b16cdcde8aac1" - integrity sha512-rofZFHUg6ZIrvRwPeFktv06GdbDYLcGqh9EwiMutZg+a0oePCCw1zHOEiji6LCpyRcjTREtPASuUqeAvYlEVvQ== +postcss-calc@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.0.0.tgz#a05b87aacd132740a5db09462a3612453e5df90a" + integrity sha512-5NglwDrcbiy8XXfPM11F3HeC6hoT9W7GUH/Zi5U/p7u3Irv4rHhdDcIZwG0llHXV4ftsBjpfWMXAnXNl4lnt8g== dependencies: - postcss "^7.0.27" postcss-selector-parser "^6.0.2" postcss-value-parser "^4.0.2" -postcss-colormin@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz#ae060bce93ed794ac71264f08132d550956bd381" - integrity sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw== +postcss-colormin@^5.2.1: + version "5.2.1" + resolved "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.2.1.tgz#6e444a806fd3c578827dbad022762df19334414d" + integrity sha512-VVwMrEYLcHYePUYV99Ymuoi7WhKrMGy/V9/kTS0DkCoJYmmjdOMneyhzYUxcNgteKDVbrewOkSM7Wje/MFwxzA== dependencies: - browserslist "^4.0.0" - color "^3.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + browserslist "^4.16.6" + caniuse-api "^3.0.0" + colord "^2.9.1" + postcss-value-parser "^4.1.0" -postcss-convert-values@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz#ca3813ed4da0f812f9d43703584e449ebe189a7f" - integrity sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ== +postcss-convert-values@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.0.2.tgz#879b849dc3677c7d6bc94b6a2c1a3f0808798059" + integrity sha512-KQ04E2yadmfa1LqXm7UIDwW1ftxU/QWZmz6NKnHnUvJ3LEYbbcX6i329f/ig+WnEByHegulocXrECaZGLpL8Zg== dependencies: - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + postcss-value-parser "^4.1.0" -postcss-discard-comments@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz#1fbabd2c246bff6aaad7997b2b0918f4d7af4033" - integrity sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg== - dependencies: - postcss "^7.0.0" +postcss-discard-comments@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz#9eae4b747cf760d31f2447c27f0619d5718901fe" + integrity sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg== -postcss-discard-duplicates@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz#3fe133cd3c82282e550fc9b239176a9207b784eb" - integrity sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ== - dependencies: - postcss "^7.0.0" +postcss-discard-duplicates@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz#68f7cc6458fe6bab2e46c9f55ae52869f680e66d" + integrity sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA== -postcss-discard-empty@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz#c8c951e9f73ed9428019458444a02ad90bb9f765" - integrity sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w== - dependencies: - postcss "^7.0.0" +postcss-discard-empty@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz#ee136c39e27d5d2ed4da0ee5ed02bc8a9f8bf6d8" + integrity sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw== -postcss-discard-overridden@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz#652aef8a96726f029f5e3e00146ee7a4e755ff57" - integrity sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg== - dependencies: - postcss "^7.0.0" +postcss-discard-overridden@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz#454b41f707300b98109a75005ca4ab0ff2743ac6" + integrity sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q== postcss-flexbugs-fixes@^4.2.1: version "4.2.1" @@ -22902,67 +22841,57 @@ postcss-loader@^4.2.0: schema-utils "^3.0.0" semver "^7.3.4" -postcss-merge-longhand@^4.0.11: - version "4.0.11" - resolved "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz#62f49a13e4a0ee04e7b98f42bb16062ca2549e24" - integrity sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw== +postcss-merge-longhand@^5.0.4: + version "5.0.4" + resolved "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.0.4.tgz#41f4f3270282ea1a145ece078b7679f0cef21c32" + integrity sha512-2lZrOVD+d81aoYkZDpWu6+3dTAAGkCKbV5DoRhnIR7KOULVrI/R7bcMjhrH9KTRy6iiHKqmtG+n/MMj1WmqHFw== dependencies: - css-color-names "0.0.4" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - stylehacks "^4.0.0" + postcss-value-parser "^4.1.0" + stylehacks "^5.0.1" -postcss-merge-rules@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz#362bea4ff5a1f98e4075a713c6cb25aefef9a650" - integrity sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ== +postcss-merge-rules@^5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.0.3.tgz#b5cae31f53129812a77e3eb1eeee448f8cf1a1db" + integrity sha512-cEKTMEbWazVa5NXd8deLdCnXl+6cYG7m2am+1HzqH0EnTdy8fRysatkaXb2dEnR+fdaDxTvuZ5zoBdv6efF6hg== dependencies: - browserslist "^4.0.0" + browserslist "^4.16.6" caniuse-api "^3.0.0" - cssnano-util-same-parent "^4.0.0" - postcss "^7.0.0" - postcss-selector-parser "^3.0.0" - vendors "^1.0.0" + cssnano-utils "^2.0.1" + postcss-selector-parser "^6.0.5" -postcss-minify-font-values@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz#cd4c344cce474343fac5d82206ab2cbcb8afd5a6" - integrity sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg== +postcss-minify-font-values@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.0.1.tgz#a90cefbfdaa075bd3dbaa1b33588bb4dc268addf" + integrity sha512-7JS4qIsnqaxk+FXY1E8dHBDmraYFWmuL6cgt0T1SWGRO5bzJf8sUoelwa4P88LEWJZweHevAiDKxHlofuvtIoA== dependencies: - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + postcss-value-parser "^4.1.0" -postcss-minify-gradients@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz#93b29c2ff5099c535eecda56c4aa6e665a663471" - integrity sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q== +postcss-minify-gradients@^5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.0.3.tgz#f970a11cc71e08e9095e78ec3a6b34b91c19550e" + integrity sha512-Z91Ol22nB6XJW+5oe31+YxRsYooxOdFKcbOqY/V8Fxse1Y3vqlNRpi1cxCqoACZTQEhl+xvt4hsbWiV5R+XI9Q== dependencies: - cssnano-util-get-arguments "^4.0.0" - is-color-stop "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + colord "^2.9.1" + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" -postcss-minify-params@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz#6b9cef030c11e35261f95f618c90036d680db874" - integrity sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg== +postcss-minify-params@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.0.2.tgz#1b644da903473fbbb18fbe07b8e239883684b85c" + integrity sha512-qJAPuBzxO1yhLad7h2Dzk/F7n1vPyfHfCCh5grjGfjhi1ttCnq4ZXGIW77GSrEbh9Hus9Lc/e/+tB4vh3/GpDg== dependencies: - alphanum-sort "^1.0.0" - browserslist "^4.0.0" - cssnano-util-get-arguments "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - uniqs "^2.0.0" + alphanum-sort "^1.0.2" + browserslist "^4.16.6" + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" -postcss-minify-selectors@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz#e2e5eb40bfee500d0cd9243500f5f8ea4262fbd8" - integrity sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g== +postcss-minify-selectors@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.1.0.tgz#4385c845d3979ff160291774523ffa54eafd5a54" + integrity sha512-NzGBXDa7aPsAcijXZeagnJBKBPMYLaJJzB8CQh6ncvyl2sIndLVWfbcDi0SBjRWk5VqEjXvf8tYwzoKf4Z07og== dependencies: - alphanum-sort "^1.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-selector-parser "^3.0.0" + alphanum-sort "^1.0.2" + postcss-selector-parser "^6.0.5" postcss-modules-extract-imports@^2.0.0: version "2.0.0" @@ -23039,124 +22968,96 @@ postcss-modules@^4.0.0: postcss-modules-values "^4.0.0" string-hash "^1.1.1" -postcss-normalize-charset@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz#8b35add3aee83a136b0471e0d59be58a50285dd4" - integrity sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g== - dependencies: - postcss "^7.0.0" +postcss-normalize-charset@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz#121559d1bebc55ac8d24af37f67bd4da9efd91d0" + integrity sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg== -postcss-normalize-display-values@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz#0dbe04a4ce9063d4667ed2be476bb830c825935a" - integrity sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ== +postcss-normalize-display-values@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz#62650b965981a955dffee83363453db82f6ad1fd" + integrity sha512-uupdvWk88kLDXi5HEyI9IaAJTE3/Djbcrqq8YgjvAVuzgVuqIk3SuJWUisT2gaJbZm1H9g5k2w1xXilM3x8DjQ== dependencies: - cssnano-util-get-match "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" -postcss-normalize-positions@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz#05f757f84f260437378368a91f8932d4b102917f" - integrity sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA== +postcss-normalize-positions@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.0.1.tgz#868f6af1795fdfa86fbbe960dceb47e5f9492fe5" + integrity sha512-rvzWAJai5xej9yWqlCb1OWLd9JjW2Ex2BCPzUJrbaXmtKtgfL8dBMOOMTX6TnvQMtjk3ei1Lswcs78qKO1Skrg== dependencies: - cssnano-util-get-arguments "^4.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + postcss-value-parser "^4.1.0" -postcss-normalize-repeat-style@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz#c4ebbc289f3991a028d44751cbdd11918b17910c" - integrity sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q== +postcss-normalize-repeat-style@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.1.tgz#cbc0de1383b57f5bb61ddd6a84653b5e8665b2b5" + integrity sha512-syZ2itq0HTQjj4QtXZOeefomckiV5TaUO6ReIEabCh3wgDs4Mr01pkif0MeVwKyU/LHEkPJnpwFKRxqWA/7O3w== dependencies: - cssnano-util-get-arguments "^4.0.0" - cssnano-util-get-match "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" -postcss-normalize-string@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz#cd44c40ab07a0c7a36dc5e99aace1eca4ec2690c" - integrity sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA== +postcss-normalize-string@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.0.1.tgz#d9eafaa4df78c7a3b973ae346ef0e47c554985b0" + integrity sha512-Ic8GaQ3jPMVl1OEn2U//2pm93AXUcF3wz+OriskdZ1AOuYV25OdgS7w9Xu2LO5cGyhHCgn8dMXh9bO7vi3i9pA== dependencies: - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + postcss-value-parser "^4.1.0" -postcss-normalize-timing-functions@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz#8e009ca2a3949cdaf8ad23e6b6ab99cb5e7d28d9" - integrity sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A== +postcss-normalize-timing-functions@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.1.tgz#8ee41103b9130429c6cbba736932b75c5e2cb08c" + integrity sha512-cPcBdVN5OsWCNEo5hiXfLUnXfTGtSFiBU9SK8k7ii8UD7OLuznzgNRYkLZow11BkQiiqMcgPyh4ZqXEEUrtQ1Q== dependencies: - cssnano-util-get-match "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" -postcss-normalize-unicode@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz#841bd48fdcf3019ad4baa7493a3d363b52ae1cfb" - integrity sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg== +postcss-normalize-unicode@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.1.tgz#82d672d648a411814aa5bf3ae565379ccd9f5e37" + integrity sha512-kAtYD6V3pK0beqrU90gpCQB7g6AOfP/2KIPCVBKJM2EheVsBQmx/Iof+9zR9NFKLAx4Pr9mDhogB27pmn354nA== dependencies: - browserslist "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + browserslist "^4.16.0" + postcss-value-parser "^4.1.0" -postcss-normalize-url@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz#10e437f86bc7c7e58f7b9652ed878daaa95faae1" - integrity sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA== +postcss-normalize-url@^5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.0.3.tgz#42eca6ede57fe69075fab0f88ac8e48916ef931c" + integrity sha512-qWiUMbvkRx3kc1Dp5opzUwc7MBWZcSDK2yofCmdvFBCpx+zFPkxBC1FASQ59Pt+flYfj/nTZSkmF56+XG5elSg== dependencies: - is-absolute-url "^2.0.0" - normalize-url "^3.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + is-absolute-url "^3.0.3" + normalize-url "^6.0.1" + postcss-value-parser "^4.1.0" -postcss-normalize-whitespace@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82" - integrity sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA== +postcss-normalize-whitespace@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.1.tgz#b0b40b5bcac83585ff07ead2daf2dcfbeeef8e9a" + integrity sha512-iPklmI5SBnRvwceb/XH568yyzK0qRVuAG+a1HFUsFRf11lEJTiQQa03a4RSCQvLKdcpX7XsI1Gen9LuLoqwiqA== dependencies: - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + postcss-value-parser "^4.1.0" -postcss-ordered-values@^4.1.2: - version "4.1.2" - resolved "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz#0cf75c820ec7d5c4d280189559e0b571ebac0eee" - integrity sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw== +postcss-ordered-values@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.0.2.tgz#1f351426977be00e0f765b3164ad753dac8ed044" + integrity sha512-8AFYDSOYWebJYLyJi3fyjl6CqMEG/UVworjiyK1r573I56kb3e879sCJLGvR3merj+fAdPpVplXKQZv+ey6CgQ== dependencies: - cssnano-util-get-arguments "^4.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" -postcss-reduce-initial@^4.0.3: - version "4.0.3" - resolved "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz#7fd42ebea5e9c814609639e2c2e84ae270ba48df" - integrity sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA== +postcss-reduce-initial@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz#9d6369865b0f6f6f6b165a0ef5dc1a4856c7e946" + integrity sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw== dependencies: - browserslist "^4.0.0" + browserslist "^4.16.0" caniuse-api "^3.0.0" - has "^1.0.0" - postcss "^7.0.0" -postcss-reduce-transforms@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz#17efa405eacc6e07be3414a5ca2d1074681d4e29" - integrity sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg== +postcss-reduce-transforms@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.1.tgz#93c12f6a159474aa711d5269923e2383cedcf640" + integrity sha512-a//FjoPeFkRuAguPscTVmRQUODP+f3ke2HqFNgGPwdYnpeC29RZdCBvGRGTsKpMURb/I3p6jdKoBQ2zI+9Q7kA== dependencies: - cssnano-util-get-match "^4.0.0" - has "^1.0.0" - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - -postcss-selector-parser@^3.0.0: - version "3.1.2" - resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz#b310f5c4c0fdaf76f94902bbaa30db6aa84f5270" - integrity sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA== - dependencies: - dot-prop "^5.2.0" - indexes-of "^1.0.1" - uniq "^1.0.1" + cssnano-utils "^2.0.1" + postcss-value-parser "^4.1.0" postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: version "6.0.4" @@ -23168,35 +23069,36 @@ postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2, postcss-selector uniq "^1.0.1" util-deprecate "^1.0.2" -postcss-svgo@^4.0.2: - version "4.0.3" - resolved "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.3.tgz#343a2cdbac9505d416243d496f724f38894c941e" - integrity sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw== +postcss-selector-parser@^6.0.5: + version "6.0.6" + resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz#2c5bba8174ac2f6981ab631a42ab0ee54af332ea" + integrity sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg== dependencies: - postcss "^7.0.0" - postcss-value-parser "^3.0.0" - svgo "^1.0.0" + cssesc "^3.0.0" + util-deprecate "^1.0.2" -postcss-unique-selectors@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz#9446911f3289bfd64c6d680f073c03b1f9ee4bac" - integrity sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg== +postcss-svgo@^5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.0.3.tgz#d945185756e5dfaae07f9edb0d3cae7ff79f9b30" + integrity sha512-41XZUA1wNDAZrQ3XgWREL/M2zSw8LJPvb5ZWivljBsUQAGoEKMYm6okHsTjJxKYI4M75RQEH4KYlEM52VwdXVA== dependencies: - alphanum-sort "^1.0.0" - postcss "^7.0.0" - uniqs "^2.0.0" + postcss-value-parser "^4.1.0" + svgo "^2.7.0" -postcss-value-parser@^3.0.0: - version "3.3.1" - resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" - integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== +postcss-unique-selectors@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.0.2.tgz#5d6893daf534ae52626708e0d62250890108c0c1" + integrity sha512-w3zBVlrtZm7loQWRPVC0yjUwwpty7OM6DnEHkxcSQXO1bMS3RJ+JUS5LFMSDZHJcvGsRwhZinCWVqn8Kej4EDA== + dependencies: + alphanum-sort "^1.0.2" + postcss-selector-parser "^6.0.5" postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== -"postcss@5 - 7", postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: +"postcss@5 - 7", postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: version "7.0.32" resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz#4310d6ee347053da3433db2be492883d62cec59d" integrity sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw== @@ -23347,6 +23249,16 @@ pretty-format@^26.0.0, pretty-format@^26.6.2: ansi-styles "^4.0.0" react-is "^17.0.1" +pretty-format@^27.0.2: + version "27.3.1" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-27.3.1.tgz#7e9486365ccdd4a502061fa761d3ab9ca1b78df5" + integrity sha512-DR/c+pvFc52nLimLROYjnXPtolawm+uWDxr4FjuLDLUn+ktWnSN851KoHwHzzqq6rfCOjkzN8FLgDrSub6UDuA== + dependencies: + "@jest/types" "^27.2.5" + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + pretty-hrtime@^1.0.3: version "1.0.3" resolved "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" @@ -25072,11 +24984,6 @@ resolve-from@5.0.0, resolve-from@^5.0.0: resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve-from@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" - integrity sha1-six699nWiBvItuZTM17rywoYh0g= - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -25188,16 +25095,6 @@ rfc4648@^1.3.0: resolved "https://registry.npmjs.org/rfc4648/-/rfc4648-1.4.0.tgz#c75b2856ad2e2d588b6ddb985d556f1f7f2a2abd" integrity sha512-3qIzGhHlMHA6PoT6+cdPKZ+ZqtxkIvg8DZGKA5z6PQ33/uuhoJ+Ws/D/J9rXW6gXodgH8QYlz2UCl+sdUDmNIg== -rgb-regex@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" - integrity sha1-wODWiC3w4jviVKR16O3UGRX+rrE= - -rgba-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" - integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= - rifm@^0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz#debe951a9c83549ca6b33e5919f716044c2230be" @@ -25265,13 +25162,13 @@ rollup-plugin-peer-deps-external@^2.2.2: integrity sha512-AWdukIM1+k5JDdAqV/Cxd+nejvno2FVLVeZ74NKggm3Q5s9cbbcOgUPGdbxPi4BXu7xGaZ8HG12F+thImYu/0g== rollup-plugin-postcss@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-4.0.0.tgz#2131fb6db0d5dce01a37235e4f6ad4523c681cea" - integrity sha512-OQzT+YspV01/6dxfyEw8lBO2px3hyL8Xn+k2QGctL7V/Yx2Z1QaMKdYVslP1mqv7RsKt6DROIlnbpmgJ3yxf6g== + version "4.0.2" + resolved "https://registry.npmjs.org/rollup-plugin-postcss/-/rollup-plugin-postcss-4.0.2.tgz#15e9462f39475059b368ce0e49c800fa4b1f7050" + integrity sha512-05EaY6zvZdmvPUDi3uCcAQoESDcYnv8ogJJQRp6V5kZ6J6P7uAVJlrTZcaaA20wTH527YTnKfkAoPxWI/jPp4w== dependencies: chalk "^4.1.0" concat-with-sourcemaps "^1.1.0" - cssnano "^4.1.10" + cssnano "^5.0.1" import-cwd "^3.0.0" p-queue "^6.6.2" pify "^5.0.0" @@ -26757,14 +26654,13 @@ style-to-object@0.3.0, style-to-object@^0.3.0: dependencies: inline-style-parser "0.1.1" -stylehacks@^4.0.0: - version "4.0.3" - resolved "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5" - integrity sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g== +stylehacks@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/stylehacks/-/stylehacks-5.0.1.tgz#323ec554198520986806388c7fdaebc38d2c06fb" + integrity sha512-Es0rVnHIqbWzveU1b24kbw92HsebBepxfcqe5iix7t9j0PQqhs0IxXVXv0pY2Bxa08CgMkzD6OWql7kbGOuEdA== dependencies: - browserslist "^4.0.0" - postcss "^7.0.0" - postcss-selector-parser "^3.0.0" + browserslist "^4.16.0" + postcss-selector-parser "^6.0.4" stylis@^4.0.6: version "4.0.7" @@ -26896,7 +26792,7 @@ svg-parser@^2.0.2: resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5" integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== -svgo@^1.0.0, svgo@^1.2.2: +svgo@^1.2.2: version "1.3.2" resolved "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== @@ -26915,6 +26811,19 @@ svgo@^1.0.0, svgo@^1.2.2: unquote "~1.1.1" util.promisify "~1.0.0" +svgo@^2.7.0: + version "2.8.0" + resolved "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" + integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^4.1.3" + css-tree "^1.1.3" + csso "^4.2.0" + picocolors "^1.0.0" + stable "^0.1.8" + swagger-client@3.16.1, swagger-client@^3.16.1: version "3.16.1" resolved "https://registry.npmjs.org/swagger-client/-/swagger-client-3.16.1.tgz#df86c9d407ab52c00cb356e714b0ec732bb3ad40" @@ -28072,11 +27981,6 @@ uniq@^1.0.1: resolved "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" integrity sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8= -uniqs@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" - integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI= - unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" @@ -28616,11 +28520,6 @@ vasync@^2.2.0: dependencies: verror "1.10.0" -vendors@^1.0.0: - version "1.0.4" - resolved "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" - integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w== - verror@1.10.0, verror@^1.8.1: version "1.10.0" resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" @@ -29269,9 +29168,9 @@ write-pkg@^4.0.0: write-json-file "^3.2.0" ws@7.4.5, ws@^7.4.6: - version "7.5.5" - resolved "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" - integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w== + version "7.5.6" + resolved "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" + integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== ws@7.4.6: version "7.4.6"