From 29ef695410e42a7ffc7af79369a0f9717a27a8ec Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 17 Nov 2021 18:56:22 +0100 Subject: [PATCH 01/51] packages,plugins: migrate to using TestApiProvider and Registry Signed-off-by: Patrik Oldsberg --- docs/plugins/analytics.md | 13 +- .../components/catalog/EntityPage.test.tsx | 33 ++- .../src/devApp/SidebarThemeSwitcher.test.tsx | 15 +- packages/storybook/.storybook/apis.js | 118 ++++---- packages/storybook/.storybook/preview.js | 6 +- .../src/testUtils/TestApiProvider.tsx | 2 +- .../src/testUtils/appWrappers.test.tsx | 6 +- .../ApiDefinitionCard.test.tsx | 9 +- .../ApiDefinitionCard/ApiTypeTitle.test.tsx | 9 +- .../ApiExplorerPage/ApiExplorerPage.test.tsx | 20 +- .../ApisCards/ConsumedApisCard.test.tsx | 17 +- .../components/ApisCards/HasApisCard.test.tsx | 17 +- .../ApisCards/ProvidedApisCard.test.tsx | 17 +- .../ConsumingComponentsCard.test.tsx | 9 +- .../ProvidingComponentsCard.test.tsx | 9 +- .../components/EntityBadgesDialog.test.tsx | 15 +- .../BitriseBuildsTableComponent.test.tsx | 14 +- .../CatalogGraphCard.test.tsx | 17 +- .../CatalogGraphPage.test.tsx | 20 +- .../EntityRelationsGraph.test.tsx | 8 +- .../DefaultImportPage.test.tsx | 23 +- .../ImportInfoCard/ImportInfoCard.test.tsx | 47 +-- .../components/ImportPage/ImportPage.test.tsx | 23 +- .../StepInitAnalyzeUrl.test.tsx | 14 +- .../StepPrepareCreatePullRequest.test.tsx | 14 +- .../EntityTypePicker.test.tsx | 26 +- .../UnregisterEntityDialog.test.tsx | 17 +- .../useUnregisterEntityDialogState.test.tsx | 6 +- .../UserListPicker/UserListPicker.test.tsx | 8 +- .../src/hooks/useEntityKinds.test.tsx | 6 +- .../src/hooks/useEntityListProvider.test.tsx | 30 +- .../src/hooks/useEntityOwnership.test.tsx | 14 +- .../src/hooks/useStarredEntities.test.tsx | 17 +- .../src/hooks/useStarredEntity.test.tsx | 8 +- .../components/AboutCard/AboutCard.test.tsx | 270 ++++++++++-------- .../CatalogKindHeader.test.tsx | 14 +- .../CatalogPage/CatalogPage.test.tsx | 10 +- .../CatalogTable/CatalogTable.test.tsx | 12 +- .../DependencyOfComponentsCard.test.tsx | 20 +- .../DependsOnComponentsCard.test.tsx | 20 +- .../DependsOnResourcesCard.test.tsx | 20 +- .../EntityLayout/EntityLayout.test.tsx | 18 +- .../DeleteEntityDialog.test.tsx | 16 +- .../EntityOrphanWarning.test.tsx | 22 +- .../EntityProcessingErrorsPanel.test.tsx | 16 +- .../EntitySwitch/EntitySwitch.test.tsx | 11 +- .../HasComponentsCard.test.tsx | 20 +- .../HasResourcesCard.test.tsx | 20 +- .../HasSubcomponentsCard.test.tsx | 20 +- .../HasSystemsCard/HasSystemsCard.test.tsx | 20 +- .../SystemDiagramCard.test.tsx | 15 +- .../CostInsightsHeader.test.tsx | 6 +- .../cost-insights/src/testUtils/providers.tsx | 28 +- .../DefaultExplorePage.test.tsx | 7 +- .../DomainExplorerContent.test.tsx | 7 +- .../ExploreLayout/ExploreLayout.test.tsx | 14 +- .../GroupsDiagram.test.tsx | 7 +- .../GroupsExplorerContent.test.tsx | 7 +- .../ToolExplorerContent.test.tsx | 9 +- .../ServiceDetailsCard.test.tsx | 10 +- .../components/FossaCard/FossaCard.test.tsx | 9 +- .../components/FossaPage/FossaPage.test.tsx | 17 +- .../Cards/RecentWorkflowRunsCard.test.tsx | 19 +- .../components/GithubDeploymentsCard.test.tsx | 11 +- .../ProfileCatalog/ProfileCatalog.test.tsx | 7 +- .../GraphiQLPage/GraphiQLPage.test.tsx | 32 +-- .../src/components/Cards/Cards.test.tsx | 21 +- ...useConsumerGroupsOffsetsForEntity.test.tsx | 14 +- plugins/kubernetes/dev/index.tsx | 20 +- .../AuditList/AuditListForEntity.test.tsx | 9 +- .../AuditList/AuditListTable.test.tsx | 15 +- .../src/components/AuditList/index.test.tsx | 15 +- .../src/components/AuditView/index.test.tsx | 15 +- .../src/components/CreateAudit/index.test.tsx | 14 +- .../src/hooks/useWebsiteForEntity.test.tsx | 14 +- .../MembersList/MembersListCard.stories.tsx | 13 +- .../MembersList/MembersListCard.test.tsx | 13 +- .../OwnershipCard/OwnershipCard.stories.tsx | 10 +- .../OwnershipCard/OwnershipCard.test.tsx | 15 +- .../ChangeEvents/ChangeEvents.test.tsx | 8 +- .../components/Escalation/Escalation.test.tsx | 8 +- .../components/Incident/Incidents.test.tsx | 8 +- .../components/PagerDutyCard/index.test.tsx | 18 +- .../components/TriggerButton/index.test.tsx | 17 +- .../TriggerDialog/TriggerDialog.test.tsx | 17 +- .../ActionsPage/ActionsPage.test.tsx | 6 +- .../TemplatePage/TemplatePage.test.tsx | 12 +- .../TemplateTypePicker.test.tsx | 8 +- .../fields/EntityPicker/EntityPicker.test.tsx | 8 +- .../fields/OwnerPicker/OwnerPicker.test.tsx | 8 +- .../components/SearchBar/SearchBar.test.tsx | 11 +- .../SearchModal/SearchModal.test.tsx | 12 +- plugins/shortcuts/src/Shortcuts.test.tsx | 22 +- .../EntitySplunkOnCallCard.test.tsx | 21 +- .../components/Escalation/Escalation.test.tsx | 8 +- .../components/Incident/Incidents.test.tsx | 45 ++- .../TriggerDialog/TriggerDialog.test.tsx | 18 +- .../src/components/RadarComponent.test.tsx | 23 +- .../src/components/RadarPage.test.tsx | 22 +- .../components/DefaultTechDocsHome.test.tsx | 16 +- .../components/LegacyTechDocsHome.test.tsx | 12 +- .../components/TechDocsCustomHome.test.tsx | 6 +- .../src/reader/components/Reader.test.tsx | 8 +- .../reader/components/TechDocsPage.test.tsx | 12 +- .../reader/components/TechDocsSearch.test.tsx | 8 +- .../reader/components/useReaderState.test.tsx | 8 +- plugins/todo/dev/index.tsx | 6 +- .../src/components/TodoList/TodoList.test.tsx | 7 +- .../UserSettingsAuthProviders.test.tsx | 18 +- .../General/UserSettingsThemeToggle.test.tsx | 17 +- .../BuildDetails/BuildDetails.test.tsx | 27 +- .../components/BuildList/BuildList.test.tsx | 21 +- .../BuildListFilter/BuildListFilter.test.tsx | 15 +- .../src/components/Overview/Overview.test.tsx | 17 +- .../OverviewTrends/OverviewTrends.test.tsx | 23 +- .../components/StatusCell/StatusCell.test.tsx | 9 +- .../StatusMatrix/StatusMatrix.test.tsx | 9 +- .../XcmetricsLayout/XcmetricsLayout.test.tsx | 15 +- 118 files changed, 991 insertions(+), 1112 deletions(-) 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/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/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 41b8433594..ca3914dd0c 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:7000', + basePath: '/auth/', + oauthRequestApi, +}); +const githubAuthApi = GithubAuth.create({ + apiOrigin: 'http://localhost:7000', + basePath: '/auth/', + oauthRequestApi, +}); +const gitlabAuthApi = GitlabAuth.create({ + apiOrigin: 'http://localhost:7000', + basePath: '/auth/', + oauthRequestApi, +}); +const oktaAuthApi = OktaAuth.create({ + apiOrigin: 'http://localhost:7000', + basePath: '/auth/', + oauthRequestApi, +}); +const auth0AuthApi = Auth0Auth.create({ + apiOrigin: 'http://localhost:7000', + basePath: '/auth/', + oauthRequestApi, +}); +const oauth2Api = OAuth2.create({ + apiOrigin: 'http://localhost:7000', + basePath: '/auth/', + oauthRequestApi, }); -const oauthRequestApi = builder.add( - oauthRequestApiRef, - new OAuthRequestManager(), -); - -builder.add( - googleAuthApiRef, - GoogleAuth.create({ - apiOrigin: 'http://localhost:7000', - basePath: '/auth/', - oauthRequestApi, - }), -); - -builder.add( - githubAuthApiRef, - GithubAuth.create({ - apiOrigin: 'http://localhost:7000', - basePath: '/auth/', - oauthRequestApi, - }), -); - -builder.add( - gitlabAuthApiRef, - GitlabAuth.create({ - apiOrigin: 'http://localhost:7000', - basePath: '/auth/', - oauthRequestApi, - }), -); - -builder.add( - oktaAuthApiRef, - OktaAuth.create({ - apiOrigin: 'http://localhost:7000', - basePath: '/auth/', - oauthRequestApi, - }), -); - -builder.add( - auth0AuthApiRef, - Auth0Auth.create({ - apiOrigin: 'http://localhost:7000', - basePath: '/auth/', - oauthRequestApi, - }), -); - -builder.add( - oauth2ApiRef, - OAuth2.create({ - apiOrigin: 'http://localhost:7000', - 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/test-utils/src/testUtils/TestApiProvider.tsx b/packages/test-utils/src/testUtils/TestApiProvider.tsx index 4408fc5b36..b1499b0cde 100644 --- a/packages/test-utils/src/testUtils/TestApiProvider.tsx +++ b/packages/test-utils/src/testUtils/TestApiProvider.tsx @@ -54,7 +54,7 @@ export class TestApiRegistry implements ApiHolder { * * @example * ```ts - * const apis = TestApiRegistry.with( + * const apis = TestApiRegistry.from( * [configApiRef, new ConfigReader({})], * [identityApiRef, { getUserId: () => 'tester' }], * ); 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/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-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/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/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')); From 8866b62f3ddb2a9e1cd842feea7002b30b5276bb Mon Sep 17 00:00:00 2001 From: Nicolas Arnold Date: Thu, 18 Nov 2021 17:42:17 +0000 Subject: [PATCH 02/51] Fail when adding duplicate locations using the dry run flag This change here adds in logic to the DefaultLocationService that will prematurely terminate the dry run addition of locations if there is a duplicate entity in the unprocessed entities. Adding this logic here will avoid an infinite loop if a monorepo arch is used (for example if a target references itself). It will also enforce some best practices. I have also added tests. You can test this out using this catalog-info.yaml: https://github.com/RoadieHQ/monorepo-infinte-loop/blob/main/catalog-info.yaml Signed-off-by: Nicolas Arnold --- .changeset/thirty-fireants-occur.md | 5 ++ .../service/DefaultLocationService.test.ts | 51 +++++++++++++++++++ .../src/service/DefaultLocationService.ts | 50 +++++++++++------- 3 files changed, 88 insertions(+), 18 deletions(-) create mode 100644 .changeset/thirty-fireants-occur.md 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/plugins/catalog-backend/src/service/DefaultLocationService.test.ts b/plugins/catalog-backend/src/service/DefaultLocationService.test.ts index a089989d20..e2b82ad2c0 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: 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..d339064d33 100644 --- a/plugins/catalog-backend/src/service/DefaultLocationService.ts +++ b/plugins/catalog-backend/src/service/DefaultLocationService.ts @@ -54,6 +54,34 @@ export class DefaultLocationService implements LocationService { return this.store.deleteLocation(id); } + private async getUnprocessedEntities( + unprocessed: DeferredEntity[], + entities: Entity[], + ): Promise { + const currentEntity = unprocessed.pop(); + if (!currentEntity) { + return []; + } + const processed = await this.orchestrator.process({ + entity: currentEntity.entity, + state: {}, // we process without the existing cache + }); + + if (processed.ok) { + const { metadata, kind } = processed.completedEntity; + if ( + entities.some(e => e.metadata.name === metadata.name && e.kind === kind) + ) { + throw new Error(`Duplicate nested entity: ${metadata.name}`); + } + return await this.getUnprocessedEntities( + [...unprocessed, ...processed.deferredEntities], + [...entities, processed.completedEntity], + ); + } + throw Error(processed.errors.map(String).join(', ')); + } + private async dryRunCreateLocation( spec: LocationSpec, ): Promise<{ location: Location; entities: Entity[]; exists?: boolean }> { @@ -86,24 +114,10 @@ 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.getUnprocessedEntities( + unprocessedEntities, + [], + ); return { exists: await existsPromise, From 3c6141c37ab0af1cba9d7a803b0d058b33370f9d Mon Sep 17 00:00:00 2001 From: Nicolas Arnold Date: Fri, 19 Nov 2021 11:09:37 +0000 Subject: [PATCH 03/51] Fix return type Signed-off-by: Nicolas Arnold --- plugins/catalog-backend/src/service/DefaultLocationService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-backend/src/service/DefaultLocationService.ts b/plugins/catalog-backend/src/service/DefaultLocationService.ts index d339064d33..0e5f2380f8 100644 --- a/plugins/catalog-backend/src/service/DefaultLocationService.ts +++ b/plugins/catalog-backend/src/service/DefaultLocationService.ts @@ -60,7 +60,7 @@ export class DefaultLocationService implements LocationService { ): Promise { const currentEntity = unprocessed.pop(); if (!currentEntity) { - return []; + return entities; } const processed = await this.orchestrator.process({ entity: currentEntity.entity, From 0179bd1f139b9e7383a965b0adf53075ad0eaf7a Mon Sep 17 00:00:00 2001 From: goenning Date: Sat, 20 Nov 2021 09:37:57 +0000 Subject: [PATCH 04/51] draft implementation for AzureDevOps Discovery Signed-off-by: goenning --- .../AzureDevOpsDiscoveryProcessor.ts | 128 ++++++++++++++++++ .../src/ingestion/processors/index.ts | 1 + .../src/legacy/service/CatalogBuilder.ts | 2 + .../src/service/NextCatalogBuilder.ts | 2 + 4 files changed, 133 insertions(+) create mode 100644 plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts 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..8300b503f3 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts @@ -0,0 +1,128 @@ +/* + * 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 fetch from 'cross-fetch'; +import { Config } from '@backstage/config'; +import { + getAzureRequestOptions, + ScmIntegrations, +} from '@backstage/integration'; +import { Logger } from 'winston'; +import * as results from './results'; +import { CatalogProcessor, CatalogProcessorEmit } from './types'; + +/** + * TODO + **/ +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`, + ); + } + + // TODO: extract this from configured URL + const { org, project } = { org: 'myOrg', project: 'myProject' }; + + // TODO: What's the search URL for self hosted DevOps? + const searchUrl = `https://almsearch.dev.azure.com/${org}/${project}/_apis/search/codesearchresults?api-version=6.0-preview.1`; + const opts = getAzureRequestOptions(azureConfig); + + const response = await fetch(searchUrl, { + method: 'POST', + headers: { + ...opts.headers, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + searchText: 'catalog-info.yaml', + $top: 1000, + }), + }); + + // TODO: more logging + if (response.status === 200) { + const responseBody: AzureDevOpsCodeSearchResults = await response.json(); + + // TODO: should we support different file names? + const matches = responseBody.results.filter( + r => r.fileName === 'catalog-info.yaml', + ); + + for (const match of matches) { + emit( + results.location( + { + type: 'url', + // TODO: Do we need to support non-default branches? + target: `${location.target}/_git/${match.repository.name}?path=${match.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; + } +} + +interface AzureDevOpsCodeSearchResults { + count: number; + results: Array<{ + fileName: string; + path: string; + project: { + id: string; + name: string; + }; + repository: { + id: string; + name: string; + }; + }>; +} 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/NextCatalogBuilder.ts b/plugins/catalog-backend/src/service/NextCatalogBuilder.ts index bbda6f070c..a694030701 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, @@ -291,6 +292,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 }), From 51e42bc2c81846765b11ca9288259225a0b4346e Mon Sep 17 00:00:00 2001 From: goenning Date: Sat, 20 Nov 2021 21:20:47 +0000 Subject: [PATCH 05/51] add support for search queries Signed-off-by: goenning --- docs/integrations/azure/discovery.md | 49 ++++++ .../AzureDevOpsDiscoveryProcessor.test.ts | 63 +++++++ .../AzureDevOpsDiscoveryProcessor.ts | 160 +++++++++++++----- 3 files changed, 230 insertions(+), 42 deletions(-) create mode 100644 docs/integrations/azure/discovery.md create mode 100644 plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts 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/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..9d468acf15 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts @@ -0,0 +1,63 @@ +/* + * 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 { parseUrl } from './AzureDevOpsDiscoveryProcessor'; + +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(); + }); + }); + + // TODO: add tests for AzureDevOpsDiscoveryProcessor +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts index 8300b503f3..a938e230e7 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts @@ -18,6 +18,7 @@ import { LocationSpec } from '@backstage/catalog-model'; import fetch from 'cross-fetch'; import { Config } from '@backstage/config'; import { + AzureIntegrationConfig, getAzureRequestOptions, ScmIntegrations, } from '@backstage/integration'; @@ -26,7 +27,18 @@ import * as results from './results'; import { CatalogProcessor, CatalogProcessorEmit } from './types'; /** - * TODO + * 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; @@ -62,13 +74,49 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { ); } - // TODO: extract this from configured URL - const { org, project } = { org: 'myOrg', project: 'myProject' }; + const { baseUrl, org, project, repo, catalogPath } = parseUrl( + location.target, + ); + this.logger.info( + `Reading Azure DevOps repositories from ${location.target}`, + ); - // TODO: What's the search URL for self hosted DevOps? + const files = await this.search( + azureConfig, + org, + project, + repo, + catalogPath, + ); + 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; + } + + // search returns all files that matches the given search path. + async search( + azureConfig: AzureIntegrationConfig, + org: string, + project: string, + repo: string, + path: string, + ): Promise { + // TODO: What's the search URL for onpremises DevOps? const searchUrl = `https://almsearch.dev.azure.com/${org}/${project}/_apis/search/codesearchresults?api-version=6.0-preview.1`; const opts = getAzureRequestOptions(azureConfig); - const response = await fetch(searchUrl, { method: 'POST', headers: { @@ -76,53 +124,81 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { 'Content-Type': 'application/json', }, body: JSON.stringify({ - searchText: 'catalog-info.yaml', + searchText: `path:${path} repo:${repo || '*'}`, $top: 1000, }), }); - // TODO: more logging - if (response.status === 200) { - const responseBody: AzureDevOpsCodeSearchResults = await response.json(); - - // TODO: should we support different file names? - const matches = responseBody.results.filter( - r => r.fileName === 'catalog-info.yaml', + if (response.status !== 200) { + this.logger.warn( + `Azure DevOps search failed with response status ${response.status}`, ); - - for (const match of matches) { - emit( - results.location( - { - type: 'url', - // TODO: Do we need to support non-default branches? - target: `${location.target}/_git/${match.repository.name}?path=${match.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 []; } - return true; + const responseBody: CodeSearchResponse = await response.json(); + this.logger.info(`Azure DevOps search found ${responseBody.count} files`); + + return responseBody.results; } } -interface AzureDevOpsCodeSearchResults { - count: number; - results: Array<{ - fileName: string; - path: string; - project: { - id: string; - name: string; +/** + * 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, }; - repository: { - id: string; - name: string; + } 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}`); +} + +interface CodeSearchResponse { + count: number; + results: CodeSearchResultItem[]; +} + +interface CodeSearchResultItem { + fileName: string; + path: string; + project: { + id: string; + name: string; + }; + repository: { + id: string; + name: string; + }; } From 5919df85d15832fbba75122f5fa624a13399c9ec Mon Sep 17 00:00:00 2001 From: Colton Padden Date: Sun, 21 Nov 2021 09:39:02 -0500 Subject: [PATCH 06/51] Add .snyk ignore file for @techdocs/cli Ignores with 6-month duration for vulnerabilities that have also been ignored for @backstage/cli: - SNYK-JS-BROWSERSLIST-1090194 - SNYK-JS-IMMER-1540542 Signed-off-by: Colton Padden --- packages/techdocs-cli/.snyk | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 packages/techdocs-cli/.snyk 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: {} From ac724ea8aee28c2deec0099fa46347853113be5a Mon Sep 17 00:00:00 2001 From: goenning Date: Sun, 21 Nov 2021 14:45:41 +0000 Subject: [PATCH 07/51] adding tests Signed-off-by: goenning --- .../AzureDevOpsDiscoveryProcessor.test.ts | 218 +++++++++++++++++- .../AzureDevOpsDiscoveryProcessor.ts | 67 +----- .../ingestion/processors/azure/azure.test.ts | 140 +++++++++++ .../src/ingestion/processors/azure/azure.ts | 67 ++++++ .../src/ingestion/processors/azure/index.ts | 17 ++ 5 files changed, 446 insertions(+), 63 deletions(-) create mode 100644 plugins/catalog-backend/src/ingestion/processors/azure/azure.test.ts create mode 100644 plugins/catalog-backend/src/ingestion/processors/azure/azure.ts create mode 100644 plugins/catalog-backend/src/ingestion/processors/azure/index.ts diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts index 9d468acf15..ab053e8748 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts @@ -14,7 +14,17 @@ * limitations under the License. */ -import { parseUrl } from './AzureDevOpsDiscoveryProcessor'; +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', () => { @@ -59,5 +69,209 @@ describe('AzureDevOpsDiscoveryProcessor', () => { }); }); - // TODO: add tests for AzureDevOpsDiscoveryProcessor + 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 index a938e230e7..b86e3f9c82 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts @@ -15,16 +15,12 @@ */ import { LocationSpec } from '@backstage/catalog-model'; -import fetch from 'cross-fetch'; import { Config } from '@backstage/config'; -import { - AzureIntegrationConfig, - getAzureRequestOptions, - ScmIntegrations, -} from '@backstage/integration'; +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. @@ -81,13 +77,16 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { `Reading Azure DevOps repositories from ${location.target}`, ); - const files = await this.search( + const files = await codeSearch( azureConfig, org, project, repo, catalogPath, ); + + this.logger.info(`Found ${files.length} files in Azure DevOps.`); + for (const file of files) { emit( results.location( @@ -105,42 +104,6 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { return true; } - - // search returns all files that matches the given search path. - async search( - azureConfig: AzureIntegrationConfig, - org: string, - project: string, - repo: string, - path: string, - ): Promise { - // TODO: What's the search URL for onpremises DevOps? - const searchUrl = `https://almsearch.dev.azure.com/${org}/${project}/_apis/search/codesearchresults?api-version=6.0-preview.1`; - const opts = getAzureRequestOptions(azureConfig); - const response = await fetch(searchUrl, { - method: 'POST', - headers: { - ...opts.headers, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - searchText: `path:${path} repo:${repo || '*'}`, - $top: 1000, - }), - }); - - if (response.status !== 200) { - this.logger.warn( - `Azure DevOps search failed with response status ${response.status}`, - ); - return []; - } - - const responseBody: CodeSearchResponse = await response.json(); - this.logger.info(`Azure DevOps search found ${responseBody.count} files`); - - return responseBody.results; - } } /** @@ -184,21 +147,3 @@ export function parseUrl(urlString: string): { throw new Error(`Failed to parse ${urlString}`); } - -interface CodeSearchResponse { - count: number; - results: CodeSearchResultItem[]; -} - -interface CodeSearchResultItem { - fileName: string; - path: string; - project: { - id: string; - name: string; - }; - repository: { - id: string; - name: string; - }; -} 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..d9dfe5d1f4 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/azure/azure.test.ts @@ -0,0 +1,140 @@ +/* + * 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:*', + $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:*', + $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', + $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); + }); +}); 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..ec1b9ba0fb --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/azure/azure.ts @@ -0,0 +1,67 @@ +/* + * 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; + }; +} + +// 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 { + // TODO: What's the search URL for onpremises DevOps? + const searchUrl = `https://almsearch.dev.azure.com/${org}/${project}/_apis/search/codesearchresults?api-version=6.0-preview.1`; + const opts = getAzureRequestOptions(azureConfig); + const response = await fetch(searchUrl, { + method: 'POST', + headers: { + ...opts.headers, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + searchText: `path:${path} repo:${repo || '*'}`, + $top: 1000, + }), + }); + + if (response.status !== 200) { + throw new Error( + `Azure DevOps search failed with response status ${response.status}`, + ); + } + + const responseBody: CodeSearchResponse = await response.json(); + return responseBody.results; +} 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'; From 563b039f0b6ea67ef7adc04912ef728af8117fd9 Mon Sep 17 00:00:00 2001 From: goenning Date: Sun, 21 Nov 2021 14:51:33 +0000 Subject: [PATCH 08/51] add api-report and changeset Signed-off-by: goenning --- .changeset/strong-seahorses-scream.md | 5 +++++ plugins/catalog-backend/api-report.md | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 .changeset/strong-seahorses-scream.md diff --git a/.changeset/strong-seahorses-scream.md b/.changeset/strong-seahorses-scream.md new file mode 100644 index 0000000000..445ed65c81 --- /dev/null +++ b/.changeset/strong-seahorses-scream.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Added Azure DevOps discovery processor diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 6660912dda..ece074fe94 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) From f9c0ad85d9741cb90b57d668577bb93a69a7f171 Mon Sep 17 00:00:00 2001 From: Nicolas Arnold Date: Mon, 22 Nov 2021 09:12:38 +0000 Subject: [PATCH 09/51] Addressing comments Signed-off-by: Nicolas Arnold --- .../src/service/DefaultLocationService.ts | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/plugins/catalog-backend/src/service/DefaultLocationService.ts b/plugins/catalog-backend/src/service/DefaultLocationService.ts index 0e5f2380f8..d28112eda3 100644 --- a/plugins/catalog-backend/src/service/DefaultLocationService.ts +++ b/plugins/catalog-backend/src/service/DefaultLocationService.ts @@ -54,32 +54,36 @@ export class DefaultLocationService implements LocationService { return this.store.deleteLocation(id); } - private async getUnprocessedEntities( - unprocessed: DeferredEntity[], - entities: Entity[], + private async processEntities( + unprocessedEntities: DeferredEntity[], ): Promise { - const currentEntity = unprocessed.pop(); - if (!currentEntity) { - return entities; - } - const processed = await this.orchestrator.process({ - entity: currentEntity.entity, - state: {}, // we process without the existing cache - }); - - if (processed.ok) { - const { metadata, kind } = processed.completedEntity; - if ( - entities.some(e => e.metadata.name === metadata.name && e.kind === kind) - ) { - throw new Error(`Duplicate nested entity: ${metadata.name}`); + 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) { + const { metadata, kind } = processed.completedEntity; + if ( + entities.some( + e => e.metadata.name === metadata.name && e.kind === kind, + ) + ) { + throw new Error(`Duplicate nested entity: ${metadata.name}`); + } + unprocessedEntities.push(...processed.deferredEntities); + entities.push(processed.completedEntity); + } else { + throw Error(processed.errors.map(String).join(', ')); } - return await this.getUnprocessedEntities( - [...unprocessed, ...processed.deferredEntities], - [...entities, processed.completedEntity], - ); } - throw Error(processed.errors.map(String).join(', ')); + return entities; } private async dryRunCreateLocation( @@ -114,10 +118,7 @@ export class DefaultLocationService implements LocationService { const unprocessedEntities: DeferredEntity[] = [ { entity, locationKey: `${spec.type}:${spec.target}` }, ]; - const entities: Entity[] = await this.getUnprocessedEntities( - unprocessedEntities, - [], - ); + const entities: Entity[] = await this.processEntities(unprocessedEntities); return { exists: await existsPromise, From a1e2972ddaf6bc2fec733d53c6fdc453510bbd0c Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Wed, 10 Nov 2021 16:00:21 -0700 Subject: [PATCH 10/51] Add permission-node package Signed-off-by: Tim Hansen Co-authored-by: Mike Lewis Co-authored-by: Himanshu Mishra Co-authored-by: Joe Porpeglia Co-authored-by: Vincenzo Scamporlino --- plugins/permission-node/.eslintrc.js | 3 + plugins/permission-node/README.md | 7 + plugins/permission-node/api-report.md | 126 +++++++++++ plugins/permission-node/package.json | 33 +++ plugins/permission-node/src/index.ts | 25 +++ .../src/integration/conditionFor.ts | 24 +++ .../createPermissionIntegration.ts | 203 ++++++++++++++++++ .../permission-node/src/integration/index.ts | 18 ++ plugins/permission-node/src/policy/index.ts | 21 ++ plugins/permission-node/src/policy/types.ts | 81 +++++++ plugins/permission-node/src/setupTests.ts | 17 ++ plugins/permission-node/src/types.ts | 49 +++++ 12 files changed, 607 insertions(+) create mode 100644 plugins/permission-node/.eslintrc.js create mode 100644 plugins/permission-node/README.md create mode 100644 plugins/permission-node/api-report.md create mode 100644 plugins/permission-node/package.json create mode 100644 plugins/permission-node/src/index.ts create mode 100644 plugins/permission-node/src/integration/conditionFor.ts create mode 100644 plugins/permission-node/src/integration/createPermissionIntegration.ts create mode 100644 plugins/permission-node/src/integration/index.ts create mode 100644 plugins/permission-node/src/policy/index.ts create mode 100644 plugins/permission-node/src/policy/types.ts create mode 100644 plugins/permission-node/src/setupTests.ts create mode 100644 plugins/permission-node/src/types.ts 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..1285207e61 --- /dev/null +++ b/plugins/permission-node/api-report.md @@ -0,0 +1,126 @@ +## 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 { AuthorizeResult } from '@backstage/permission-common'; +import { BackstageIdentity } from '@backstage/plugin-auth-backend'; +import { DefinitiveAuthorizeResult } from '@backstage/permission-common'; +import { OpaqueAuthorizeRequest } from '@backstage/permission-common'; +import { PermissionCondition } from '@backstage/permission-common'; +import { PermissionCriteria } from '@backstage/permission-common'; +import { Router } from 'express'; + +// Warning: (ae-missing-release-tag) "AllowAllPermissionPolicy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class AllowAllPermissionPolicy implements PermissionPolicy { + // (undocumented) + handle( + _request: OpaqueAuthorizeRequest, + _user?: BackstageIdentity, + ): Promise; +} + +// Warning: (ae-missing-release-tag) "ApplyConditionsRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ApplyConditionsRequest = { + resourceRef: string; + resourceType: string; + conditions: PermissionCriteria>; +}; + +// Warning: (ae-missing-release-tag) "ConditionalPolicyResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type ConditionalPolicyResult = { + result: AuthorizeResult.MAYBE; + conditions: { + pluginId: string; + resourceType: string; + conditions: PermissionCriteria; + }; +}; + +// Warning: (ae-missing-release-tag) "conditionFor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const conditionFor: ( + rule: PermissionRule, +) => (...params: TParams) => { + rule: string; + params: TParams; +}; + +// Warning: (ae-missing-release-tag) "createPermissionIntegration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const createPermissionIntegration: < + TResource, + TRules extends { + [key: string]: PermissionRule; + }, + TGetResourceParams extends any[] = [], +>({ + pluginId, + resourceType, + rules, + getResource, +}: { + pluginId: string; + resourceType: string; + rules: TRules; + getResource: ( + resourceRef: string, + ...params: TGetResourceParams + ) => Promise; +}) => { + createPermissionIntegrationRouter: (...params: TGetResourceParams) => Router; + toQuery: ( + conditions: PermissionCriteria, + ) => PermissionCriteria>; + conditions: Conditions; + createConditions: (conditions: PermissionCriteria) => { + pluginId: string; + resourceType: string; + conditions: PermissionCriteria; + }; + registerPermissionRule: ( + rule: PermissionRule, any>, + ) => void; +}; + +// Warning: (ae-missing-release-tag) "PermissionPolicy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface PermissionPolicy { + // (undocumented) + handle( + request: OpaqueAuthorizeRequest, + user?: BackstageIdentity, + ): Promise; +} + +// Warning: (ae-missing-release-tag) "PermissionRule" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PermissionRule = { + name: string; + description: string; + apply(resource: TResource, ...params: TParams): boolean; + toQuery(...params: TParams): TQuery | PermissionCriteria; +}; + +// Warning: (ae-missing-release-tag) "PolicyResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PolicyResult = DefinitiveAuthorizeResult | ConditionalPolicyResult; + +// Warnings were encountered during analysis: +// +// src/integration/createPermissionIntegration.d.ts:23:5 - (ae-forgotten-export) The symbol "QueryType" needs to be exported by the entry point index.d.ts +// src/integration/createPermissionIntegration.d.ts:24:5 - (ae-forgotten-export) The symbol "Conditions" needs to be exported by the entry point index.d.ts + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json new file mode 100644 index 0000000000..112976b78b --- /dev/null +++ b/plugins/permission-node/package.json @@ -0,0 +1,33 @@ +{ + "name": "@backstage/plugin-permission-node", + "description": "Common permission and authorization utilities for backend plugins", + "version": "0.1.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" + }, + "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/permission-common": "^0.1.0", + "@backstage/plugin-auth-backend": "^0.4.6", + "@types/express": "*", + "express": "^4.17.1" + }, + "devDependencies": { + "@backstage/cli": "^0.8.1" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/permission-node/src/index.ts b/plugins/permission-node/src/index.ts new file mode 100644 index 0000000000..697c02ab22 --- /dev/null +++ b/plugins/permission-node/src/index.ts @@ -0,0 +1,25 @@ +/* + * 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/conditionFor.ts b/plugins/permission-node/src/integration/conditionFor.ts new file mode 100644 index 0000000000..6cead51716 --- /dev/null +++ b/plugins/permission-node/src/integration/conditionFor.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. + */ + +import { PermissionRule } from '../types'; + +export const conditionFor = + (rule: PermissionRule) => + (...params: TParams) => ({ + rule: rule.name, + params, + }); diff --git a/plugins/permission-node/src/integration/createPermissionIntegration.ts b/plugins/permission-node/src/integration/createPermissionIntegration.ts new file mode 100644 index 0000000000..88a05a7cf8 --- /dev/null +++ b/plugins/permission-node/src/integration/createPermissionIntegration.ts @@ -0,0 +1,203 @@ +/* + * 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, + PermissionCondition, + PermissionCriteria, +} from '@backstage/permission-common'; +import express, { Response, Router } from 'express'; +import { PermissionRule } from '../types'; +import { conditionFor } from './conditionFor'; + +/** + * A request to load the referenced resource and apply conditions, to finalize a conditional + * authorization response. + * @public + */ +export type ApplyConditionsRequest = { + resourceRef: string; + resourceType: string; + conditions: PermissionCriteria>; +}; + +type Condition = TRule extends PermissionRule + ? (...params: TParams) => PermissionCondition + : never; + +type Conditions>> = { + [Name in keyof TRules]: Condition; +}; + +type QueryType = TRules extends Record< + string, + PermissionRule +> + ? TQuery + : never; + +// TODO(permission-node): this is no longer correct +function isPermissionCriteria( + criteria: unknown, +): criteria is PermissionCriteria { + return Object.prototype.hasOwnProperty.call(criteria, 'anyOf'); +} + +export const createPermissionIntegration = < + TResource, + TRules extends { [key: string]: PermissionRule }, + TGetResourceParams extends any[] = [], +>({ + pluginId, + resourceType, + rules, + getResource, +}: { + pluginId: string; + resourceType: string; + rules: TRules; + getResource: ( + resourceRef: string, + ...params: TGetResourceParams + ) => Promise; +}): { + createPermissionIntegrationRouter: (...params: TGetResourceParams) => Router; + toQuery: ( + conditions: PermissionCriteria>>, + ) => PermissionCriteria>; + conditions: Conditions; + createConditions: ( + conditions: PermissionCriteria>>, + ) => { + pluginId: string; + resourceType: string; + conditions: PermissionCriteria>>; + }; + registerPermissionRule: ( + rule: PermissionRule>, + ) => void; +} => { + const rulesMap = new Map(Object.values(rules).map(rule => [rule.name, rule])); + + const getRule = ( + name: string, + ): PermissionRule> => { + const rule = rulesMap.get(name); + + if (!rule) { + throw new Error(`Unexpected permission rule: ${name}`); + } + + return rule; + }; + + return { + createPermissionIntegrationRouter: ( + ...getResourceParams: TGetResourceParams + ) => { + const router = Router(); + + router.use('/permissions/', express.json()); + + router.post( + '/permissions/apply-conditions', + async ( + req, + res: Response>, + ) => { + // TODO(authorization-framework): validate input + const body = req.body as ApplyConditionsRequest; + + if (body.resourceType !== resourceType) { + throw new Error(`Unexpected resource type: ${body.resourceType}`); + } + + const resource = await getResource( + body.resourceRef, + ...getResourceParams, + ); + + if (!resource) { + return res.status(400).end(); + } + + const resolveCriteria = ( + criteria: PermissionCriteria< + PermissionCondition> + >, + ): boolean => { + return criteria.anyOf.some(({ allOf }) => + allOf.every(child => + isPermissionCriteria(child) + ? resolveCriteria(child) + : getRule(child.rule).apply(resource, ...child.params), + ), + ); + }; + + return res.status(200).json({ + result: resolveCriteria(body.conditions) + ? AuthorizeResult.ALLOW + : AuthorizeResult.DENY, + }); + }, + ); + + return router; + }, + toQuery: ( + conditions: PermissionCriteria>>, + ): PermissionCriteria> => { + const mapCriteria = ( + criteria: PermissionCriteria>>, + mapFn: ( + condition: PermissionCondition>, + ) => QueryType | PermissionCriteria>, + ): PermissionCriteria> => { + return { + anyOf: criteria.anyOf.map(({ allOf }) => ({ + allOf: allOf.map(child => + isPermissionCriteria(child) + ? mapCriteria(child, mapFn) + : mapFn(child), + ), + })), + }; + }; + + return mapCriteria(conditions, condition => + getRule(condition.rule).toQuery(...condition.params), + ); + }, + conditions: Object.entries(rules).reduce( + (acc, [key, rule]) => ({ + ...acc, + [key]: conditionFor(rule), + }), + {} as Conditions, + ), + createConditions: ( + conditions: PermissionCriteria>>, + ) => ({ + pluginId, + resourceType, + conditions, + }), + registerPermissionRule: rule => { + rulesMap.set(rule.name, rule); + }, + }; +}; diff --git a/plugins/permission-node/src/integration/index.ts b/plugins/permission-node/src/integration/index.ts new file mode 100644 index 0000000000..697d25f65d --- /dev/null +++ b/plugins/permission-node/src/integration/index.ts @@ -0,0 +1,18 @@ +/* + * 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 './conditionFor'; +export * from './createPermissionIntegration'; diff --git a/plugins/permission-node/src/policy/index.ts b/plugins/permission-node/src/policy/index.ts new file mode 100644 index 0000000000..226d50df21 --- /dev/null +++ b/plugins/permission-node/src/policy/index.ts @@ -0,0 +1,21 @@ +/* + * 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, + 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..b1b3881c3e --- /dev/null +++ b/plugins/permission-node/src/policy/types.ts @@ -0,0 +1,81 @@ +/* + * 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/permission-common'; +import { BackstageIdentity } from '@backstage/plugin-auth-backend'; + +/** + * An authorization request to be evaluated by the {@link PermissionPolicy}. + * + * This differs from {@link 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}. + * + * 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 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. + * + * 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..6d2622119b --- /dev/null +++ b/plugins/permission-node/src/types.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 type { PermissionCriteria } from '@backstage/permission-common'; + +/** + * A conditional rule that can be provided in an {@link AuthorizeResult} response to an + * authorization request. + * + * 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 = { + 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): TQuery | PermissionCriteria; +}; From a82709c38f4225331fd8f6fc91e81f91fd64a5d3 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Thu, 11 Nov 2021 09:53:50 +0000 Subject: [PATCH 11/51] authz: update unused type params in conditionFor to unknown Signed-off-by: Mike Lewis --- plugins/permission-node/src/integration/conditionFor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/permission-node/src/integration/conditionFor.ts b/plugins/permission-node/src/integration/conditionFor.ts index 6cead51716..3a13a3afe3 100644 --- a/plugins/permission-node/src/integration/conditionFor.ts +++ b/plugins/permission-node/src/integration/conditionFor.ts @@ -17,7 +17,7 @@ import { PermissionRule } from '../types'; export const conditionFor = - (rule: PermissionRule) => + (rule: PermissionRule) => (...params: TParams) => ({ rule: rule.name, params, From 4083fcdb5e5dfd1bf7712c8e3c75204d3da1d859 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Thu, 11 Nov 2021 13:54:29 +0000 Subject: [PATCH 12/51] authz: fix PermissionCondition type parameter `unknown` doesn't satisfy the (recently added) `extends unknown[]` constraint. In these two cases, we can remove the parameter entirely and rely instead on the default of `unknown[]`. Signed-off-by: Mike Lewis --- .../src/integration/createPermissionIntegration.ts | 2 +- plugins/permission-node/src/policy/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/permission-node/src/integration/createPermissionIntegration.ts b/plugins/permission-node/src/integration/createPermissionIntegration.ts index 88a05a7cf8..bbd1c8af2a 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegration.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegration.ts @@ -31,7 +31,7 @@ import { conditionFor } from './conditionFor'; export type ApplyConditionsRequest = { resourceRef: string; resourceType: string; - conditions: PermissionCriteria>; + conditions: PermissionCriteria; }; type Condition = TRule extends PermissionRule diff --git a/plugins/permission-node/src/policy/types.ts b/plugins/permission-node/src/policy/types.ts index b1b3881c3e..e81757d59c 100644 --- a/plugins/permission-node/src/policy/types.ts +++ b/plugins/permission-node/src/policy/types.ts @@ -48,7 +48,7 @@ export type ConditionalPolicyResult = { conditions: { pluginId: string; resourceType: string; - conditions: PermissionCriteria>; + conditions: PermissionCriteria; }; }; From f3d2ccfeb883b6e79dc9e3177e4ca9a6b36a7082 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Thu, 11 Nov 2021 13:56:07 +0000 Subject: [PATCH 13/51] authz: switch to unknown[] in PermissionRule type parameter Signed-off-by: Mike Lewis --- plugins/permission-node/src/types.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/permission-node/src/types.ts b/plugins/permission-node/src/types.ts index 6d2622119b..308eafe481 100644 --- a/plugins/permission-node/src/types.ts +++ b/plugins/permission-node/src/types.ts @@ -29,7 +29,11 @@ import type { PermissionCriteria } from '@backstage/permission-common'; * check was performed. * @public */ -export type PermissionRule = { +export type PermissionRule< + TResource, + TQuery, + TParams extends unknown[] = unknown[], +> = { name: string; description: string; From 652af0834ae886da9310bf60b9e84be0bb0f46ba Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Thu, 11 Nov 2021 13:58:05 +0000 Subject: [PATCH 14/51] authz: add test suite for conditionFor Signed-off-by: Mike Lewis --- .../src/integration/conditionFor.test.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 plugins/permission-node/src/integration/conditionFor.test.ts diff --git a/plugins/permission-node/src/integration/conditionFor.test.ts b/plugins/permission-node/src/integration/conditionFor.test.ts new file mode 100644 index 0000000000..d1099e7555 --- /dev/null +++ b/plugins/permission-node/src/integration/conditionFor.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 { conditionFor } from './conditionFor'; + +describe('conditionFor', () => { + const testRule = { + name: 'test-rule', + description: 'test-description', + apply: jest.fn(), + toQuery: jest.fn(), + }; + + it('returns a function', () => { + expect(conditionFor(testRule)).toEqual(expect.any(Function)); + }); + + describe('return value', () => { + it('constructs a condition with the rule name and supplied params', () => { + const conditionFactory = conditionFor(testRule); + expect(conditionFactory('a', 'b', 1, 2)).toEqual({ + rule: 'test-rule', + params: ['a', 'b', 1, 2], + }); + }); + }); +}); From 64b6ff368f1e5081a8739cbad6c162c8bb402ce7 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Thu, 11 Nov 2021 13:58:28 +0000 Subject: [PATCH 15/51] authz: fix expected response type for /apply-conditions route Signed-off-by: Mike Lewis --- .../src/integration/createPermissionIntegration.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/permission-node/src/integration/createPermissionIntegration.ts b/plugins/permission-node/src/integration/createPermissionIntegration.ts index bbd1c8af2a..d70da5f7df 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegration.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegration.ts @@ -116,7 +116,9 @@ export const createPermissionIntegration = < '/permissions/apply-conditions', async ( req, - res: Response>, + res: Response<{ + result: Omit; + }>, ) => { // TODO(authorization-framework): validate input const body = req.body as ApplyConditionsRequest; From d0a99e3219e67fb1b81e205d2cf864c61fc8a81c Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Thu, 11 Nov 2021 14:12:38 +0000 Subject: [PATCH 16/51] authz: update permission-node api-report Signed-off-by: Mike Lewis --- plugins/permission-node/api-report.md | 74 ++++++++++++--------------- 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 1285207e61..fac7e8d7fa 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -3,39 +3,25 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AuthorizeRequest } from '@backstage/permission-common'; import { AuthorizeResult } from '@backstage/permission-common'; import { BackstageIdentity } from '@backstage/plugin-auth-backend'; -import { DefinitiveAuthorizeResult } from '@backstage/permission-common'; -import { OpaqueAuthorizeRequest } from '@backstage/permission-common'; import { PermissionCondition } from '@backstage/permission-common'; import { PermissionCriteria } from '@backstage/permission-common'; import { Router } from 'express'; -// Warning: (ae-missing-release-tag) "AllowAllPermissionPolicy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export class AllowAllPermissionPolicy implements PermissionPolicy { - // (undocumented) - handle( - _request: OpaqueAuthorizeRequest, - _user?: BackstageIdentity, - ): Promise; -} - -// Warning: (ae-missing-release-tag) "ApplyConditionsRequest" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export type ApplyConditionsRequest = { resourceRef: string; resourceType: string; - conditions: PermissionCriteria>; + conditions: PermissionCriteria; }; -// Warning: (ae-missing-release-tag) "ConditionalPolicyResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-permission-node" does not have an export "AuthorizeResult" // -// @public (undocumented) +// @public export type ConditionalPolicyResult = { - result: AuthorizeResult.MAYBE; + result: AuthorizeResult.CONDITIONAL; conditions: { pluginId: string; resourceType: string; @@ -47,7 +33,7 @@ export type ConditionalPolicyResult = { // // @public (undocumented) export const conditionFor: ( - rule: PermissionRule, + rule: PermissionRule, ) => (...params: TParams) => { rule: string; params: TParams; @@ -59,7 +45,7 @@ export const conditionFor: ( export const createPermissionIntegration: < TResource, TRules extends { - [key: string]: PermissionRule; + [key: string]: PermissionRule; }, TGetResourceParams extends any[] = [], >({ @@ -78,49 +64,55 @@ export const createPermissionIntegration: < }) => { createPermissionIntegrationRouter: (...params: TGetResourceParams) => Router; toQuery: ( - conditions: PermissionCriteria, + conditions: PermissionCriteria>>, ) => PermissionCriteria>; conditions: Conditions; - createConditions: (conditions: PermissionCriteria) => { + createConditions: ( + conditions: PermissionCriteria>>, + ) => { pluginId: string; resourceType: string; - conditions: PermissionCriteria; + conditions: PermissionCriteria>>; }; registerPermissionRule: ( - rule: PermissionRule, any>, + rule: PermissionRule, unknown[]>, ) => void; }; -// Warning: (ae-missing-release-tag) "PermissionPolicy" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface PermissionPolicy { + // Warning: (ae-forgotten-export) The symbol "PolicyAuthorizeRequest" needs to be exported by the entry point index.d.ts + // // (undocumented) handle( - request: OpaqueAuthorizeRequest, + request: PolicyAuthorizeRequest, user?: BackstageIdentity, ): Promise; } -// Warning: (ae-missing-release-tag) "PermissionRule" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-permission-node" does not have an export "AuthorizeResult" // -// @public (undocumented) -export type PermissionRule = { +// @public +export type PermissionRule< + TResource, + TQuery, + TParams extends unknown[] = unknown[], +> = { name: string; description: string; apply(resource: TResource, ...params: TParams): boolean; toQuery(...params: TParams): TQuery | PermissionCriteria; }; -// Warning: (ae-missing-release-tag) "PolicyResult" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export type PolicyResult = DefinitiveAuthorizeResult | ConditionalPolicyResult; +// @public +export type PolicyResult = + | { + result: AuthorizeResult.ALLOW | AuthorizeResult.DENY; + } + | ConditionalPolicyResult; // Warnings were encountered during analysis: // -// src/integration/createPermissionIntegration.d.ts:23:5 - (ae-forgotten-export) The symbol "QueryType" needs to be exported by the entry point index.d.ts -// src/integration/createPermissionIntegration.d.ts:24:5 - (ae-forgotten-export) The symbol "Conditions" needs to be exported by the entry point index.d.ts - -// (No @packageDocumentation comment for this package) +// src/integration/createPermissionIntegration.d.ts:28:5 - (ae-forgotten-export) The symbol "QueryType" needs to be exported by the entry point index.d.ts +// src/integration/createPermissionIntegration.d.ts:29:5 - (ae-forgotten-export) The symbol "Conditions" needs to be exported by the entry point index.d.ts ``` From 22f5de984007ecd6a4d24316e816a64b9c092d4b Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Thu, 11 Nov 2021 14:13:26 +0000 Subject: [PATCH 17/51] authz: update createPermissionIntegration to work with new PermissionCriteria type Signed-off-by: Mike Lewis --- plugins/permission-node/api-report.md | 12 +- .../createPermissionIntegration.ts | 68 ++---- .../src/integration/util.test.ts | 217 ++++++++++++++++++ .../permission-node/src/integration/util.ts | 68 ++++++ 4 files changed, 304 insertions(+), 61 deletions(-) create mode 100644 plugins/permission-node/src/integration/util.test.ts create mode 100644 plugins/permission-node/src/integration/util.ts diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index fac7e8d7fa..407ea3dc84 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -64,15 +64,13 @@ export const createPermissionIntegration: < }) => { createPermissionIntegrationRouter: (...params: TGetResourceParams) => Router; toQuery: ( - conditions: PermissionCriteria>>, + conditions: PermissionCriteria, ) => PermissionCriteria>; conditions: Conditions; - createConditions: ( - conditions: PermissionCriteria>>, - ) => { + createConditions: (conditions: PermissionCriteria) => { pluginId: string; resourceType: string; - conditions: PermissionCriteria>>; + conditions: PermissionCriteria; }; registerPermissionRule: ( rule: PermissionRule, unknown[]>, @@ -113,6 +111,6 @@ export type PolicyResult = // Warnings were encountered during analysis: // -// src/integration/createPermissionIntegration.d.ts:28:5 - (ae-forgotten-export) The symbol "QueryType" needs to be exported by the entry point index.d.ts -// src/integration/createPermissionIntegration.d.ts:29:5 - (ae-forgotten-export) The symbol "Conditions" needs to be exported by the entry point index.d.ts +// src/integration/createPermissionIntegration.d.ts:38:5 - (ae-forgotten-export) The symbol "QueryType" needs to be exported by the entry point index.d.ts +// src/integration/createPermissionIntegration.d.ts:39:5 - (ae-forgotten-export) The symbol "Conditions" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/permission-node/src/integration/createPermissionIntegration.ts b/plugins/permission-node/src/integration/createPermissionIntegration.ts index d70da5f7df..35d6f0da54 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegration.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegration.ts @@ -14,14 +14,15 @@ * limitations under the License. */ +import express, { Response, Router } from 'express'; import { AuthorizeResult, PermissionCondition, PermissionCriteria, } from '@backstage/permission-common'; -import express, { Response, Router } from 'express'; import { PermissionRule } from '../types'; import { conditionFor } from './conditionFor'; +import { applyConditions, mapConditions } from './util'; /** * A request to load the referenced resource and apply conditions, to finalize a conditional @@ -49,13 +50,6 @@ type QueryType = TRules extends Record< ? TQuery : never; -// TODO(permission-node): this is no longer correct -function isPermissionCriteria( - criteria: unknown, -): criteria is PermissionCriteria { - return Object.prototype.hasOwnProperty.call(criteria, 'anyOf'); -} - export const createPermissionIntegration = < TResource, TRules extends { [key: string]: PermissionRule }, @@ -76,15 +70,13 @@ export const createPermissionIntegration = < }): { createPermissionIntegrationRouter: (...params: TGetResourceParams) => Router; toQuery: ( - conditions: PermissionCriteria>>, + conditions: PermissionCriteria, ) => PermissionCriteria>; conditions: Conditions; - createConditions: ( - conditions: PermissionCriteria>>, - ) => { + createConditions: (conditions: PermissionCriteria) => { pluginId: string; resourceType: string; - conditions: PermissionCriteria>>; + conditions: PermissionCriteria; }; registerPermissionRule: ( rule: PermissionRule>, @@ -92,9 +84,7 @@ export const createPermissionIntegration = < } => { const rulesMap = new Map(Object.values(rules).map(rule => [rule.name, rule])); - const getRule = ( - name: string, - ): PermissionRule> => { + const getRule = (name: string) => { const rule = rulesMap.get(name); if (!rule) { @@ -110,10 +100,9 @@ export const createPermissionIntegration = < ) => { const router = Router(); - router.use('/permissions/', express.json()); - router.post( '/permissions/apply-conditions', + express.json(), async ( req, res: Response<{ @@ -136,22 +125,10 @@ export const createPermissionIntegration = < return res.status(400).end(); } - const resolveCriteria = ( - criteria: PermissionCriteria< - PermissionCondition> - >, - ): boolean => { - return criteria.anyOf.some(({ allOf }) => - allOf.every(child => - isPermissionCriteria(child) - ? resolveCriteria(child) - : getRule(child.rule).apply(resource, ...child.params), - ), - ); - }; - return res.status(200).json({ - result: resolveCriteria(body.conditions) + result: applyConditions(body.conditions, ({ rule, params }) => + getRule(rule).apply(resource, ...params), + ) ? AuthorizeResult.ALLOW : AuthorizeResult.DENY, }); @@ -161,27 +138,10 @@ export const createPermissionIntegration = < return router; }, toQuery: ( - conditions: PermissionCriteria>>, + conditions: PermissionCriteria, ): PermissionCriteria> => { - const mapCriteria = ( - criteria: PermissionCriteria>>, - mapFn: ( - condition: PermissionCondition>, - ) => QueryType | PermissionCriteria>, - ): PermissionCriteria> => { - return { - anyOf: criteria.anyOf.map(({ allOf }) => ({ - allOf: allOf.map(child => - isPermissionCriteria(child) - ? mapCriteria(child, mapFn) - : mapFn(child), - ), - })), - }; - }; - - return mapCriteria(conditions, condition => - getRule(condition.rule).toQuery(...condition.params), + return mapConditions(conditions, ({ rule, params }) => + getRule(rule).toQuery(...params), ); }, conditions: Object.entries(rules).reduce( @@ -192,7 +152,7 @@ export const createPermissionIntegration = < {} as Conditions, ), createConditions: ( - conditions: PermissionCriteria>>, + conditions: PermissionCriteria, ) => ({ pluginId, resourceType, 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..44a000d395 --- /dev/null +++ b/plugins/permission-node/src/integration/util.test.ts @@ -0,0 +1,217 @@ +/* + * 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/permission-common'; +import { applyConditions, mapConditions } from './util'; + +describe('integration utils', () => { + const testCases: { + criteria: PermissionCriteria; + expectedResult: { + applyConditions: boolean; + mapConditions: PermissionCriteria<{ query: string; params: unknown[] }>; + }; + }[] = [ + { + criteria: { rule: 'test-rule-1', params: [] }, + expectedResult: { + applyConditions: true, + mapConditions: { query: 'test-rule-1', params: [] }, + }, + }, + { + criteria: { rule: 'test-rule-2', params: [] }, + expectedResult: { + applyConditions: false, + mapConditions: { query: 'test-rule-2', params: [] }, + }, + }, + { + criteria: { + anyOf: [ + { rule: 'test-rule-1', params: ['a', 'b'] }, + { rule: 'test-rule-2', params: ['c', 'd'] }, + ], + }, + expectedResult: { + applyConditions: true, + mapConditions: { + anyOf: [ + { query: 'test-rule-1', params: ['a', 'b'] }, + { query: 'test-rule-2', params: ['c', 'd'] }, + ], + }, + }, + }, + { + criteria: { + allOf: [ + { rule: 'test-rule-1', params: ['e', 'f'] }, + { rule: 'test-rule-2', params: ['g', 'h'] }, + ], + }, + expectedResult: { + applyConditions: false, + mapConditions: { + allOf: [ + { query: 'test-rule-1', params: ['e', 'f'] }, + { query: 'test-rule-2', params: ['g', 'h'] }, + ], + }, + }, + }, + { + criteria: { + not: { rule: 'test-rule-2', params: ['i'] }, + }, + expectedResult: { + applyConditions: true, + mapConditions: { + not: { query: 'test-rule-2', params: ['i'] }, + }, + }, + }, + { + criteria: { + allOf: [ + { + anyOf: [ + { rule: 'test-rule-1', params: ['j'] }, + { rule: 'test-rule-2', params: ['k'] }, + ], + }, + { + not: { + allOf: [ + { rule: 'test-rule-1', params: ['l'] }, + { rule: 'test-rule-2', params: ['m'] }, + ], + }, + }, + ], + }, + expectedResult: { + applyConditions: true, + mapConditions: { + allOf: [ + { + anyOf: [ + { query: 'test-rule-1', params: ['j'] }, + { query: 'test-rule-2', params: ['k'] }, + ], + }, + { + not: { + allOf: [ + { query: 'test-rule-1', params: ['l'] }, + { query: 'test-rule-2', params: ['m'] }, + ], + }, + }, + ], + }, + }, + }, + { + criteria: { + allOf: [ + { + anyOf: [ + { rule: 'test-rule-1', params: ['j'] }, + { rule: 'test-rule-2', params: ['k'] }, + ], + }, + { + not: { + allOf: [ + { rule: 'test-rule-1', params: ['l'] }, + { not: { rule: 'test-rule-2', params: ['m'] } }, + ], + }, + }, + ], + }, + expectedResult: { + applyConditions: false, + mapConditions: { + allOf: [ + { + anyOf: [ + { query: 'test-rule-1', params: ['j'] }, + { query: 'test-rule-2', params: ['k'] }, + ], + }, + { + not: { + allOf: [ + { query: 'test-rule-1', params: ['l'] }, + { not: { query: 'test-rule-2', params: ['m'] } }, + ], + }, + }, + ], + }, + }, + }, + ]; + + describe('applyConditions', () => { + const applyFn = jest.fn(condition => condition.rule === 'test-rule-1'); + + it('invokes applyFn with rules to determine result', () => { + applyConditions({ rule: 'test-rule-1', params: ['foo', 'bar'] }, applyFn); + + expect(applyFn).toHaveBeenCalledWith({ + rule: 'test-rule-1', + params: ['foo', 'bar'], + }); + }); + + it.each(testCases)( + 'works with criteria %#', + ({ criteria, expectedResult }) => { + expect(applyConditions(criteria, applyFn)).toEqual( + expectedResult.applyConditions, + ); + }, + ); + }); + + describe('mapConditions', () => { + const mapFn = jest.fn(({ rule, params }) => ({ query: rule, params })); + + it('invokes mapFn to transform conditions', () => { + mapConditions({ rule: 'test-rule-1', params: ['foo', 'bar'] }, mapFn); + + expect(mapFn).toHaveBeenCalledWith({ + rule: 'test-rule-1', + params: ['foo', 'bar'], + }); + }); + + it.each(testCases)( + 'works with criteria %#', + ({ criteria, expectedResult }) => { + expect(mapConditions(criteria, mapFn)).toEqual( + expectedResult.mapConditions, + ); + }, + ); + }); +}); diff --git a/plugins/permission-node/src/integration/util.ts b/plugins/permission-node/src/integration/util.ts new file mode 100644 index 0000000000..355964d573 --- /dev/null +++ b/plugins/permission-node/src/integration/util.ts @@ -0,0 +1,68 @@ +/* + * 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/permission-common'; + +const isAndCriteria = ( + filter: PermissionCriteria, +): filter is { allOf: PermissionCriteria[] } => + Object.prototype.hasOwnProperty.call(filter, 'allOf'); + +const isOrCriteria = ( + filter: PermissionCriteria, +): filter is { anyOf: PermissionCriteria[] } => + Object.prototype.hasOwnProperty.call(filter, 'anyOf'); + +const isNotCriteria = ( + filter: PermissionCriteria, +): filter is { not: PermissionCriteria } => + Object.prototype.hasOwnProperty.call(filter, 'not'); + +export const mapConditions = ( + criteria: PermissionCriteria, + mapFn: (condition: TQueryIn) => TQueryOut, +): PermissionCriteria => { + if (isAndCriteria(criteria)) { + return { + allOf: criteria.allOf.map(child => mapConditions(child, mapFn)), + }; + } else if (isOrCriteria(criteria)) { + return { + anyOf: criteria.anyOf.map(child => mapConditions(child, mapFn)), + }; + } else if (isNotCriteria(criteria)) { + return { + not: mapConditions(criteria.not, mapFn), + }; + } + + return mapFn(criteria); +}; + +export const applyConditions = ( + criteria: PermissionCriteria, + applyFn: (condition: TQuery) => boolean, +): boolean => { + if (isAndCriteria(criteria)) { + return criteria.allOf.every(child => applyConditions(child, applyFn)); + } else if (isOrCriteria(criteria)) { + return criteria.anyOf.some(child => applyConditions(child, applyFn)); + } else if (isNotCriteria(criteria)) { + return !applyConditions(criteria.not, applyFn); + } + + return applyFn(criteria); +}; From 2b668f2eb886970a73b7f9498d5641a5fbe5d45b Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Thu, 11 Nov 2021 18:26:46 +0000 Subject: [PATCH 18/51] build(deps): add dependency on zod in permission-node Signed-off-by: Mike Lewis --- plugins/permission-node/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index 112976b78b..85e21049ed 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -22,7 +22,8 @@ "@backstage/permission-common": "^0.1.0", "@backstage/plugin-auth-backend": "^0.4.6", "@types/express": "*", - "express": "^4.17.1" + "express": "^4.17.1", + "zod": "^3.11.6" }, "devDependencies": { "@backstage/cli": "^0.8.1" From 5c75c89106f824ae2d202cc2ee3eaef396160d28 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Thu, 11 Nov 2021 18:27:27 +0000 Subject: [PATCH 19/51] authz: improve error handling in permission-node apply-conditions route Signed-off-by: Mike Lewis --- plugins/permission-node/api-report.md | 4 +- .../createPermissionIntegration.ts | 47 ++++++++++++++++--- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 407ea3dc84..fe26dc2a1a 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -111,6 +111,6 @@ export type PolicyResult = // Warnings were encountered during analysis: // -// src/integration/createPermissionIntegration.d.ts:38:5 - (ae-forgotten-export) The symbol "QueryType" needs to be exported by the entry point index.d.ts -// src/integration/createPermissionIntegration.d.ts:39:5 - (ae-forgotten-export) The symbol "Conditions" needs to be exported by the entry point index.d.ts +// src/integration/createPermissionIntegration.d.ts:28:5 - (ae-forgotten-export) The symbol "QueryType" needs to be exported by the entry point index.d.ts +// src/integration/createPermissionIntegration.d.ts:29:5 - (ae-forgotten-export) The symbol "Conditions" needs to be exported by the entry point index.d.ts ``` diff --git a/plugins/permission-node/src/integration/createPermissionIntegration.ts b/plugins/permission-node/src/integration/createPermissionIntegration.ts index 35d6f0da54..cbdcb72041 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegration.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegration.ts @@ -15,6 +15,7 @@ */ import express, { Response, Router } from 'express'; +import { z } from 'zod'; import { AuthorizeResult, PermissionCondition, @@ -24,6 +25,26 @@ import { PermissionRule } from '../types'; import { conditionFor } from './conditionFor'; import { applyConditions, mapConditions } 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, to finalize a conditional * authorization response. @@ -105,15 +126,25 @@ export const createPermissionIntegration = < express.json(), async ( req, - res: Response<{ - result: Omit; - }>, + res: Response< + | { + result: Omit; + } + | string + >, ) => { - // TODO(authorization-framework): validate input - const body = req.body as ApplyConditionsRequest; + 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) { - throw new Error(`Unexpected resource type: ${body.resourceType}`); + return res + .status(400) + .send(`Unexpected resource type: ${body.resourceType}.`); } const resource = await getResource( @@ -122,7 +153,9 @@ export const createPermissionIntegration = < ); if (!resource) { - return res.status(400).end(); + return res + .status(400) + .send(`Resource for ref ${body.resourceRef} not found.`); } return res.status(200).json({ From f72d7bba0e1a93343b76568b351220c0a608e040 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Thu, 11 Nov 2021 18:27:46 +0000 Subject: [PATCH 20/51] authz: add testsuite for createPermissionIntegration Signed-off-by: Mike Lewis --- plugins/permission-node/package.json | 4 +- .../createPermissionIntegration.test.ts | 293 ++++++++++++++++++ 2 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 plugins/permission-node/src/integration/createPermissionIntegration.test.ts diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index 85e21049ed..a23cb0fae2 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -26,7 +26,9 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.8.1" + "@backstage/cli": "^0.8.1", + "@types/supertest": "^2.0.8", + "supertest": "^6.1.3" }, "files": [ "dist" diff --git a/plugins/permission-node/src/integration/createPermissionIntegration.test.ts b/plugins/permission-node/src/integration/createPermissionIntegration.test.ts new file mode 100644 index 0000000000..6b0ca948bf --- /dev/null +++ b/plugins/permission-node/src/integration/createPermissionIntegration.test.ts @@ -0,0 +1,293 @@ +/* + * 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/permission-common'; +import express, { Express, Router } from 'express'; +import request from 'supertest'; +import { createPermissionIntegration } from './createPermissionIntegration'; + +const mockGetResource: jest.MockedFunction< + (resourceRef: string) => Promise +> = jest.fn((resourceRef: string) => + Promise.resolve({ + resourceRef, + }), +); + +const testIntegration = () => + createPermissionIntegration({ + pluginId: 'test-plugin', + resourceType: 'test-resource', + getResource: mockGetResource, + 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('createPermissionIntegration', () => { + describe('createPermissionIntegrationRouter', () => { + let app: Express; + let router: Router; + + beforeEach(() => { + const { createPermissionIntegrationRouter } = testIntegration(); + + router = createPermissionIntegrationRouter(); + app = express().use(router); + }); + + it('works', async () => { + expect(router).toBeDefined(); + }); + + describe('POST /permissions/apply-conditions', () => { + it('returns 200/ALLOW when criteria match', async () => { + const response = await request(app) + .post('/permissions/apply-conditions') + .send({ + resourceRef: 'default:test/resource', + resourceType: 'test-resource', + conditions: { + rule: 'testRule1', + params: ['a', 1], + }, + }); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ result: AuthorizeResult.ALLOW }); + }); + + it('returns 200/DENY when criteria do not match', async () => { + const response = await request(app) + .post('/permissions/apply-conditions') + .send({ + resourceRef: 'default:test/resource', + resourceType: 'test-resource', + conditions: { + anyOf: [], + }, + }); + + 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, + ); + }); + }); + }); + + describe('toQuery', () => { + it('converts conditions to plugin-specific queries using rule toQuery methods', () => { + const { toQuery } = testIntegration(); + + expect( + toQuery({ + anyOf: [ + { + allOf: [ + { rule: 'testRule1', params: ['a', 1] }, + { rule: 'testRule2', params: [{ foo: 'bar' }] }, + ], + }, + { + not: { rule: 'testRule1', params: ['b', 2] }, + }, + ], + }), + ).toEqual({ + anyOf: [ + { + allOf: [ + { query: 'testRule1', params: ['a', 1] }, + { query: 'testRule2', params: [{ foo: 'bar' }] }, + ], + }, + { + not: { query: 'testRule1', params: ['b', 2] }, + }, + ], + }); + }); + }); + + 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] }], + }, + }); + }); + }); + + describe('registerPermissionRule', () => { + it('adds support for the new rule in toQuery', () => { + const { registerPermissionRule, toQuery } = testIntegration(); + + registerPermissionRule({ + name: 'testRule3', + description: 'Test rule 3', + apply: jest.fn((_resource: any, _firstParam: string) => false), + toQuery: jest.fn((firstParam: string) => ({ + query: 'testRule3', + params: [firstParam], + })), + }); + + expect( + toQuery({ + rule: 'testRule3', + params: ['abc'], + }), + ).toEqual({ + query: 'testRule3', + params: ['abc'], + }); + }); + + it('adds support for the new rule in the apply-conditions endpoint', async () => { + const { registerPermissionRule, createPermissionIntegrationRouter } = + testIntegration(); + + const app = express().use(createPermissionIntegrationRouter()); + + registerPermissionRule({ + name: 'testRule3', + description: 'Test rule 3', + apply: jest.fn((_resource: any, _firstParam: string) => false), + toQuery: jest.fn((firstParam: string) => ({ + query: 'testRule3', + params: [firstParam], + })), + }); + + const response = await request(app) + .post('/permissions/apply-conditions') + .send({ + resourceRef: 'default:test/resource', + resourceType: 'test-resource', + conditions: { + not: { + rule: 'testRule3', + params: ['a'], + }, + }, + }); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ result: AuthorizeResult.ALLOW }); + }); + }); +}); From e2088541ddc81dbeff9331ae7711ed452233021f Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Fri, 12 Nov 2021 12:30:04 +0000 Subject: [PATCH 21/51] authz: update references to permission-common after move to plugins Signed-off-by: Mike Lewis --- plugins/permission-node/api-report.md | 8 ++++---- plugins/permission-node/package.json | 2 +- .../src/integration/createPermissionIntegration.test.ts | 2 +- .../src/integration/createPermissionIntegration.ts | 2 +- plugins/permission-node/src/integration/util.test.ts | 2 +- plugins/permission-node/src/integration/util.ts | 2 +- plugins/permission-node/src/policy/types.ts | 2 +- plugins/permission-node/src/types.ts | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index fe26dc2a1a..0044807857 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -3,11 +3,11 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { AuthorizeRequest } from '@backstage/permission-common'; -import { AuthorizeResult } from '@backstage/permission-common'; +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/permission-common'; -import { PermissionCriteria } from '@backstage/permission-common'; +import { PermissionCondition } from '@backstage/plugin-permission-common'; +import { PermissionCriteria } from '@backstage/plugin-permission-common'; import { Router } from 'express'; // @public diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index a23cb0fae2..eaf9efdbba 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -19,8 +19,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/permission-common": "^0.1.0", "@backstage/plugin-auth-backend": "^0.4.6", + "@backstage/plugin-permission-common": "^0.1.0", "@types/express": "*", "express": "^4.17.1", "zod": "^3.11.6" diff --git a/plugins/permission-node/src/integration/createPermissionIntegration.test.ts b/plugins/permission-node/src/integration/createPermissionIntegration.test.ts index 6b0ca948bf..1ef310954f 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegration.test.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegration.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { AuthorizeResult } from '@backstage/permission-common'; +import { AuthorizeResult } from '@backstage/plugin-permission-common'; import express, { Express, Router } from 'express'; import request from 'supertest'; import { createPermissionIntegration } from './createPermissionIntegration'; diff --git a/plugins/permission-node/src/integration/createPermissionIntegration.ts b/plugins/permission-node/src/integration/createPermissionIntegration.ts index cbdcb72041..7a7ae0ea4b 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegration.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegration.ts @@ -20,7 +20,7 @@ import { AuthorizeResult, PermissionCondition, PermissionCriteria, -} from '@backstage/permission-common'; +} from '@backstage/plugin-permission-common'; import { PermissionRule } from '../types'; import { conditionFor } from './conditionFor'; import { applyConditions, mapConditions } from './util'; diff --git a/plugins/permission-node/src/integration/util.test.ts b/plugins/permission-node/src/integration/util.test.ts index 44a000d395..93f51d8275 100644 --- a/plugins/permission-node/src/integration/util.test.ts +++ b/plugins/permission-node/src/integration/util.test.ts @@ -17,7 +17,7 @@ import { PermissionCondition, PermissionCriteria, -} from '@backstage/permission-common'; +} from '@backstage/plugin-permission-common'; import { applyConditions, mapConditions } from './util'; describe('integration utils', () => { diff --git a/plugins/permission-node/src/integration/util.ts b/plugins/permission-node/src/integration/util.ts index 355964d573..631aee269e 100644 --- a/plugins/permission-node/src/integration/util.ts +++ b/plugins/permission-node/src/integration/util.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { PermissionCriteria } from '@backstage/permission-common'; +import { PermissionCriteria } from '@backstage/plugin-permission-common'; const isAndCriteria = ( filter: PermissionCriteria, diff --git a/plugins/permission-node/src/policy/types.ts b/plugins/permission-node/src/policy/types.ts index e81757d59c..d594d3bdb7 100644 --- a/plugins/permission-node/src/policy/types.ts +++ b/plugins/permission-node/src/policy/types.ts @@ -19,7 +19,7 @@ import { AuthorizeResult, PermissionCondition, PermissionCriteria, -} from '@backstage/permission-common'; +} from '@backstage/plugin-permission-common'; import { BackstageIdentity } from '@backstage/plugin-auth-backend'; /** diff --git a/plugins/permission-node/src/types.ts b/plugins/permission-node/src/types.ts index 308eafe481..d3379c40a6 100644 --- a/plugins/permission-node/src/types.ts +++ b/plugins/permission-node/src/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { PermissionCriteria } from '@backstage/permission-common'; +import type { PermissionCriteria } from '@backstage/plugin-permission-common'; /** * A conditional rule that can be provided in an {@link AuthorizeResult} response to an From 75dd00c8b627b4d80e1b1c0ca714897ff6a9af5e Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Fri, 12 Nov 2021 12:37:23 +0000 Subject: [PATCH 22/51] authz: add missing fields to permission-node package.json Signed-off-by: Mike Lewis --- plugins/permission-node/package.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index eaf9efdbba..4e17d7ecfb 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -10,6 +10,16 @@ "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", From 5c5a1d16e41ee5e1baddba44bcaa7d95e5fcaa07 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Fri, 12 Nov 2021 13:43:59 +0000 Subject: [PATCH 23/51] authz: remove superfluous union type PermissionCriteria is a union type which already includes TQuery itself, so we don't need a second union here. Signed-off-by: Mike Lewis --- plugins/permission-node/api-report.md | 2 +- plugins/permission-node/src/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 0044807857..4ee8953d70 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -99,7 +99,7 @@ export type PermissionRule< name: string; description: string; apply(resource: TResource, ...params: TParams): boolean; - toQuery(...params: TParams): TQuery | PermissionCriteria; + toQuery(...params: TParams): PermissionCriteria; }; // @public diff --git a/plugins/permission-node/src/types.ts b/plugins/permission-node/src/types.ts index d3379c40a6..f983c80ea5 100644 --- a/plugins/permission-node/src/types.ts +++ b/plugins/permission-node/src/types.ts @@ -49,5 +49,5 @@ export type PermissionRule< * can be used for loading a collection of resources efficiently with conditional criteria already * applied. */ - toQuery(...params: TParams): TQuery | PermissionCriteria; + toQuery(...params: TParams): PermissionCriteria; }; From 27d5f5fed3e4343f2f2389fa5ba57ebf9e496550 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Wed, 17 Nov 2021 11:17:50 -0700 Subject: [PATCH 24/51] Improve api-report Signed-off-by: Tim Hansen --- plugins/permission-node/api-report.md | 44 ++++++++++------ plugins/permission-node/package.json | 1 + .../src/integration/conditionFor.ts | 13 +++++ .../createPermissionIntegration.ts | 50 +++++++++++++++++-- plugins/permission-node/src/policy/index.ts | 1 + plugins/permission-node/src/policy/types.ts | 10 ++-- plugins/permission-node/src/types.ts | 4 +- 7 files changed, 97 insertions(+), 26 deletions(-) diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 4ee8953d70..a292f6acd2 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -17,8 +17,15 @@ export type ApplyConditionsRequest = { conditions: PermissionCriteria; }; -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-permission-node" does not have an export "AuthorizeResult" -// +// @public +export type Condition = TRule extends PermissionRule< + any, + any, + infer TParams +> + ? (...params: TParams) => PermissionCondition + : never; + // @public export type ConditionalPolicyResult = { result: AuthorizeResult.CONDITIONAL; @@ -29,9 +36,7 @@ export type ConditionalPolicyResult = { }; }; -// Warning: (ae-missing-release-tag) "conditionFor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export const conditionFor: ( rule: PermissionRule, ) => (...params: TParams) => { @@ -39,9 +44,14 @@ export const conditionFor: ( params: TParams; }; -// Warning: (ae-missing-release-tag) "createPermissionIntegration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public +export type Conditions< + TRules extends Record>, +> = { + [Name in keyof TRules]: Condition; +}; + +// @public export const createPermissionIntegration: < TResource, TRules extends { @@ -79,8 +89,6 @@ export const createPermissionIntegration: < // @public export interface PermissionPolicy { - // Warning: (ae-forgotten-export) The symbol "PolicyAuthorizeRequest" needs to be exported by the entry point index.d.ts - // // (undocumented) handle( request: PolicyAuthorizeRequest, @@ -88,8 +96,6 @@ export interface PermissionPolicy { ): Promise; } -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-permission-node" does not have an export "AuthorizeResult" -// // @public export type PermissionRule< TResource, @@ -102,6 +108,9 @@ export type PermissionRule< toQuery(...params: TParams): PermissionCriteria; }; +// @public +export type PolicyAuthorizeRequest = Omit; + // @public export type PolicyResult = | { @@ -109,8 +118,11 @@ export type PolicyResult = } | ConditionalPolicyResult; -// Warnings were encountered during analysis: -// -// src/integration/createPermissionIntegration.d.ts:28:5 - (ae-forgotten-export) The symbol "QueryType" needs to be exported by the entry point index.d.ts -// src/integration/createPermissionIntegration.d.ts:29:5 - (ae-forgotten-export) The symbol "Conditions" needs to be exported by the entry point index.d.ts +// @public +export type QueryType = TRules extends Record< + string, + PermissionRule +> + ? TQuery + : never; ``` diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index 4e17d7ecfb..830c945158 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -8,6 +8,7 @@ "publishConfig": { "access": "public", "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", "types": "dist/index.d.ts" }, "homepage": "https://backstage.io", diff --git a/plugins/permission-node/src/integration/conditionFor.ts b/plugins/permission-node/src/integration/conditionFor.ts index 3a13a3afe3..7451b35bca 100644 --- a/plugins/permission-node/src/integration/conditionFor.ts +++ b/plugins/permission-node/src/integration/conditionFor.ts @@ -16,6 +16,19 @@ import { PermissionRule } from '../types'; +/** + * Creates a condition function for a given authorization rule and parameter type. + * + * 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 createPermissionIntegration} helper, which creates + * conditions for the rules supplied. However, a different plugin can also add rules to this + * integration (using the returned `registerPermissionRule` from this function), and create the + * condition to be used in an {@link PermissionPolicy} using this method directly. + * @public + */ export const conditionFor = (rule: PermissionRule) => (...params: TParams) => ({ diff --git a/plugins/permission-node/src/integration/createPermissionIntegration.ts b/plugins/permission-node/src/integration/createPermissionIntegration.ts index 7a7ae0ea4b..72118572e5 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegration.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegration.ts @@ -56,21 +56,65 @@ export type ApplyConditionsRequest = { conditions: PermissionCriteria; }; -type Condition = TRule extends PermissionRule +/** + * An authorization condition, which is a function to evaluate a given {@link PermissionRule} with + * a specific set of parameters. + * @see createPermissionIntegration + * @public + */ +export type Condition = TRule extends PermissionRule< + any, + any, + infer TParams +> ? (...params: TParams) => PermissionCondition : never; -type Conditions>> = { +/** + * A collection of keyed {@link Condition} functions produced from {@link PermissionRule}. + * @see createPermissionIntegration + * @public + */ +export type Conditions< + TRules extends Record>, +> = { [Name in keyof TRules]: Condition; }; -type QueryType = TRules extends Record< +/** + * The plugin-specific type which authorization conditions are translated into, for plugin use in + * filtering resources. + * @public + */ +export type QueryType = TRules extends Record< string, PermissionRule > ? TQuery : never; +/** + * Create a permission and authorization integration for a plugin that supports _conditional + * authorization_ for resources. + * + * 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` are a map of {@link PermissionRule} 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 createPermissionIntegration = < TResource, TRules extends { [key: string]: PermissionRule }, diff --git a/plugins/permission-node/src/policy/index.ts b/plugins/permission-node/src/policy/index.ts index 226d50df21..f04c2a094c 100644 --- a/plugins/permission-node/src/policy/index.ts +++ b/plugins/permission-node/src/policy/index.ts @@ -17,5 +17,6 @@ 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 index d594d3bdb7..36186d5e57 100644 --- a/plugins/permission-node/src/policy/types.ts +++ b/plugins/permission-node/src/policy/types.ts @@ -25,9 +25,9 @@ import { BackstageIdentity } from '@backstage/plugin-auth-backend'; /** * An authorization request to be evaluated by the {@link PermissionPolicy}. * - * This differs from {@link 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. + * 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; @@ -39,8 +39,8 @@ export type PolicyAuthorizeRequest = Omit; * conditions hold when evaluated. The conditions will be evaluated by the corresponding plugin * which knows about the referenced permission rules. * - * Similar to {@link AuthorizeResult}, but with the plugin and resource identifiers needed to - * evaluate the returned conditions. + * Similar to {@link @backstage/permission-common#AuthorizeResult}, but with the plugin and resource + * identifiers needed to evaluate the returned conditions. * @public */ export type ConditionalPolicyResult = { diff --git a/plugins/permission-node/src/types.ts b/plugins/permission-node/src/types.ts index f983c80ea5..a4e95fbd61 100644 --- a/plugins/permission-node/src/types.ts +++ b/plugins/permission-node/src/types.ts @@ -17,8 +17,8 @@ import type { PermissionCriteria } from '@backstage/plugin-permission-common'; /** - * A conditional rule that can be provided in an {@link AuthorizeResult} response to an - * authorization request. + * A conditional rule that can be provided in an + * {@link @backstage/permission-common#AuthorizeResult} response to an authorization request. * * 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 From 0eb339c3a482cab5ac323d9cd89e0db7d3bf8809 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Thu, 18 Nov 2021 13:54:28 +0000 Subject: [PATCH 25/51] permission-node: more visible condition transform in mapCriteria test Signed-off-by: Mike Lewis --- .../src/integration/util.test.ts | 43 ++++++------------- 1 file changed, 14 insertions(+), 29 deletions(-) diff --git a/plugins/permission-node/src/integration/util.test.ts b/plugins/permission-node/src/integration/util.test.ts index 93f51d8275..72e87b2a7b 100644 --- a/plugins/permission-node/src/integration/util.test.ts +++ b/plugins/permission-node/src/integration/util.test.ts @@ -25,21 +25,21 @@ describe('integration utils', () => { criteria: PermissionCriteria; expectedResult: { applyConditions: boolean; - mapConditions: PermissionCriteria<{ query: string; params: unknown[] }>; + mapConditions: PermissionCriteria; }; }[] = [ { criteria: { rule: 'test-rule-1', params: [] }, expectedResult: { applyConditions: true, - mapConditions: { query: 'test-rule-1', params: [] }, + mapConditions: 'test-rule-1', }, }, { criteria: { rule: 'test-rule-2', params: [] }, expectedResult: { applyConditions: false, - mapConditions: { query: 'test-rule-2', params: [] }, + mapConditions: 'test-rule-2', }, }, { @@ -52,10 +52,7 @@ describe('integration utils', () => { expectedResult: { applyConditions: true, mapConditions: { - anyOf: [ - { query: 'test-rule-1', params: ['a', 'b'] }, - { query: 'test-rule-2', params: ['c', 'd'] }, - ], + anyOf: ['test-rule-1:a,b', 'test-rule-2:c,d'], }, }, }, @@ -69,10 +66,7 @@ describe('integration utils', () => { expectedResult: { applyConditions: false, mapConditions: { - allOf: [ - { query: 'test-rule-1', params: ['e', 'f'] }, - { query: 'test-rule-2', params: ['g', 'h'] }, - ], + allOf: ['test-rule-1:e,f', 'test-rule-2:g,h'], }, }, }, @@ -83,7 +77,7 @@ describe('integration utils', () => { expectedResult: { applyConditions: true, mapConditions: { - not: { query: 'test-rule-2', params: ['i'] }, + not: 'test-rule-2:i', }, }, }, @@ -111,17 +105,11 @@ describe('integration utils', () => { mapConditions: { allOf: [ { - anyOf: [ - { query: 'test-rule-1', params: ['j'] }, - { query: 'test-rule-2', params: ['k'] }, - ], + anyOf: ['test-rule-1:j', 'test-rule-2:k'], }, { not: { - allOf: [ - { query: 'test-rule-1', params: ['l'] }, - { query: 'test-rule-2', params: ['m'] }, - ], + allOf: ['test-rule-1:l', 'test-rule-2:m'], }, }, ], @@ -152,17 +140,11 @@ describe('integration utils', () => { mapConditions: { allOf: [ { - anyOf: [ - { query: 'test-rule-1', params: ['j'] }, - { query: 'test-rule-2', params: ['k'] }, - ], + anyOf: ['test-rule-1:j', 'test-rule-2:k'], }, { not: { - allOf: [ - { query: 'test-rule-1', params: ['l'] }, - { not: { query: 'test-rule-2', params: ['m'] } }, - ], + allOf: ['test-rule-1:l', { not: 'test-rule-2:m' }], }, }, ], @@ -194,7 +176,10 @@ describe('integration utils', () => { }); describe('mapConditions', () => { - const mapFn = jest.fn(({ rule, params }) => ({ query: rule, params })); + const mapFn = jest.fn( + ({ rule, params }) => + `${rule}${params.length ? `:${params.join(',')}` : ''}`, + ); it('invokes mapFn to transform conditions', () => { mapConditions({ rule: 'test-rule-1', params: ['foo', 'bar'] }, mapFn); From 70ce7c91e269d8ed84405daae49eed3096da6ff6 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Fri, 19 Nov 2021 18:40:41 +0000 Subject: [PATCH 26/51] permission-node: bump @backstage/cli version Signed-off-by: Mike Lewis --- plugins/permission-node/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index 830c945158..79923eae97 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -37,7 +37,7 @@ "zod": "^3.11.6" }, "devDependencies": { - "@backstage/cli": "^0.8.1", + "@backstage/cli": "^0.9.0", "@types/supertest": "^2.0.8", "supertest": "^6.1.3" }, From 3254303a96937f9b346ae9259107a823c6d78b9e Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Fri, 19 Nov 2021 13:43:59 +0000 Subject: [PATCH 27/51] permission-node: refactor and split createPermissionIntegration This refactor makes the createPermissionIntegration system much more flexible by splitting it up into a few different helpers with different responsibilities. This frees up plugin authors to connect together the different parts of the permission integration in whatever way is convenient for them, and makes the process of registering additional permission rules a bit more explicit, by requiring them to be passed in when constructing the systems for transforming or applying conditions. Signed-off-by: Mike Lewis Co-authored-by: Joon Park Co-authored-by: Tim Hansen --- plugins/permission-node/api-report.md | 67 ++-- plugins/permission-node/package.json | 3 +- .../createConditionExports.test.ts | 79 +++++ .../src/integration/createConditionExports.ts | 94 ++++++ ...test.ts => createConditionFactory.test.ts} | 8 +- ...ditionFor.ts => createConditionFactory.ts} | 13 +- .../createConditionTransformer.test.ts | 158 ++++++++++ .../integration/createConditionTransformer.ts | 78 +++++ .../createPermissionIntegration.test.ts | 293 ------------------ .../createPermissionIntegration.ts | 242 --------------- .../createPermissionIntegrationRouter.test.ts | 210 +++++++++++++ .../createPermissionIntegrationRouter.ts | 165 ++++++++++ .../permission-node/src/integration/index.ts | 6 +- .../src/integration/util.test.ts | 230 ++++---------- .../permission-node/src/integration/util.ts | 53 +--- 15 files changed, 908 insertions(+), 791 deletions(-) create mode 100644 plugins/permission-node/src/integration/createConditionExports.test.ts create mode 100644 plugins/permission-node/src/integration/createConditionExports.ts rename plugins/permission-node/src/integration/{conditionFor.test.ts => createConditionFactory.test.ts} (80%) rename plugins/permission-node/src/integration/{conditionFor.ts => createConditionFactory.ts} (68%) create mode 100644 plugins/permission-node/src/integration/createConditionTransformer.test.ts create mode 100644 plugins/permission-node/src/integration/createConditionTransformer.ts delete mode 100644 plugins/permission-node/src/integration/createPermissionIntegration.test.ts delete mode 100644 plugins/permission-node/src/integration/createPermissionIntegration.ts create mode 100644 plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts create mode 100644 plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index a292f6acd2..1aab9e1f47 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -36,14 +36,6 @@ export type ConditionalPolicyResult = { }; }; -// @public -export const conditionFor: ( - rule: PermissionRule, -) => (...params: TParams) => { - rule: string; - params: TParams; -}; - // @public export type Conditions< TRules extends Record>, @@ -52,41 +44,58 @@ export type Conditions< }; // @public -export const createPermissionIntegration: < +export type ConditionTransformer = ( + conditions: PermissionCriteria, +) => PermissionCriteria; + +// @public +export const createConditionExports: < TResource, - TRules extends { - [key: string]: PermissionRule; - }, - TGetResourceParams extends any[] = [], + TRules extends Record>, >({ pluginId, resourceType, rules, - getResource, }: { pluginId: string; resourceType: string; rules: TRules; - getResource: ( - resourceRef: string, - ...params: TGetResourceParams - ) => Promise; }) => { - createPermissionIntegrationRouter: (...params: TGetResourceParams) => Router; - toQuery: ( - conditions: PermissionCriteria, - ) => PermissionCriteria>; conditions: Conditions; createConditions: (conditions: PermissionCriteria) => { pluginId: string; resourceType: string; conditions: PermissionCriteria; }; - registerPermissionRule: ( - rule: PermissionRule, unknown[]>, - ) => void; }; +// @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) @@ -117,12 +126,4 @@ export type PolicyResult = result: AuthorizeResult.ALLOW | AuthorizeResult.DENY; } | ConditionalPolicyResult; - -// @public -export type QueryType = TRules extends Record< - string, - PermissionRule -> - ? TQuery - : never; ``` diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index 79923eae97..4a9c837eb6 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -8,7 +8,6 @@ "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "module": "dist/index.esm.js", "types": "dist/index.d.ts" }, "homepage": "https://backstage.io", @@ -32,7 +31,7 @@ "dependencies": { "@backstage/plugin-auth-backend": "^0.4.6", "@backstage/plugin-permission-common": "^0.1.0", - "@types/express": "*", + "@types/express": "^4.17.6", "express": "^4.17.1", "zod": "^3.11.6" }, 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..d7f8c675d2 --- /dev/null +++ b/plugins/permission-node/src/integration/createConditionExports.ts @@ -0,0 +1,94 @@ +/* + * 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. It 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>, +>({ + pluginId, + resourceType, + rules, +}: { + pluginId: string; + resourceType: string; + rules: TRules; +}): { + conditions: Conditions; + createConditions: (conditions: PermissionCriteria) => { + pluginId: string; + resourceType: string; + conditions: PermissionCriteria; + }; +} => ({ + 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/conditionFor.test.ts b/plugins/permission-node/src/integration/createConditionFactory.test.ts similarity index 80% rename from plugins/permission-node/src/integration/conditionFor.test.ts rename to plugins/permission-node/src/integration/createConditionFactory.test.ts index d1099e7555..8dd43f5da6 100644 --- a/plugins/permission-node/src/integration/conditionFor.test.ts +++ b/plugins/permission-node/src/integration/createConditionFactory.test.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import { conditionFor } from './conditionFor'; +import { createConditionFactory } from './createConditionFactory'; -describe('conditionFor', () => { +describe('createConditionFactory', () => { const testRule = { name: 'test-rule', description: 'test-description', @@ -25,12 +25,12 @@ describe('conditionFor', () => { }; it('returns a function', () => { - expect(conditionFor(testRule)).toEqual(expect.any(Function)); + expect(createConditionFactory(testRule)).toEqual(expect.any(Function)); }); describe('return value', () => { it('constructs a condition with the rule name and supplied params', () => { - const conditionFactory = conditionFor(testRule); + 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/conditionFor.ts b/plugins/permission-node/src/integration/createConditionFactory.ts similarity index 68% rename from plugins/permission-node/src/integration/conditionFor.ts rename to plugins/permission-node/src/integration/createConditionFactory.ts index 7451b35bca..066233bf41 100644 --- a/plugins/permission-node/src/integration/conditionFor.ts +++ b/plugins/permission-node/src/integration/createConditionFactory.ts @@ -17,19 +17,20 @@ import { PermissionRule } from '../types'; /** - * Creates a condition function for a given authorization rule and parameter type. + * Creates a condition factory function for a given authorization rule and parameter types. * * 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 createPermissionIntegration} helper, which creates - * conditions for the rules supplied. However, a different plugin can also add rules to this - * integration (using the returned `registerPermissionRule` from this function), and create the - * condition to be used in an {@link PermissionPolicy} using this method directly. + * 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 with. + * * @public */ -export const conditionFor = +export const createConditionFactory = (rule: PermissionRule) => (...params: TParams) => ({ rule: rule.name, 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/createPermissionIntegration.test.ts b/plugins/permission-node/src/integration/createPermissionIntegration.test.ts deleted file mode 100644 index 1ef310954f..0000000000 --- a/plugins/permission-node/src/integration/createPermissionIntegration.test.ts +++ /dev/null @@ -1,293 +0,0 @@ -/* - * 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 { createPermissionIntegration } from './createPermissionIntegration'; - -const mockGetResource: jest.MockedFunction< - (resourceRef: string) => Promise -> = jest.fn((resourceRef: string) => - Promise.resolve({ - resourceRef, - }), -); - -const testIntegration = () => - createPermissionIntegration({ - pluginId: 'test-plugin', - resourceType: 'test-resource', - getResource: mockGetResource, - 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('createPermissionIntegration', () => { - describe('createPermissionIntegrationRouter', () => { - let app: Express; - let router: Router; - - beforeEach(() => { - const { createPermissionIntegrationRouter } = testIntegration(); - - router = createPermissionIntegrationRouter(); - app = express().use(router); - }); - - it('works', async () => { - expect(router).toBeDefined(); - }); - - describe('POST /permissions/apply-conditions', () => { - it('returns 200/ALLOW when criteria match', async () => { - const response = await request(app) - .post('/permissions/apply-conditions') - .send({ - resourceRef: 'default:test/resource', - resourceType: 'test-resource', - conditions: { - rule: 'testRule1', - params: ['a', 1], - }, - }); - - expect(response.status).toEqual(200); - expect(response.body).toEqual({ result: AuthorizeResult.ALLOW }); - }); - - it('returns 200/DENY when criteria do not match', async () => { - const response = await request(app) - .post('/permissions/apply-conditions') - .send({ - resourceRef: 'default:test/resource', - resourceType: 'test-resource', - conditions: { - anyOf: [], - }, - }); - - 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, - ); - }); - }); - }); - - describe('toQuery', () => { - it('converts conditions to plugin-specific queries using rule toQuery methods', () => { - const { toQuery } = testIntegration(); - - expect( - toQuery({ - anyOf: [ - { - allOf: [ - { rule: 'testRule1', params: ['a', 1] }, - { rule: 'testRule2', params: [{ foo: 'bar' }] }, - ], - }, - { - not: { rule: 'testRule1', params: ['b', 2] }, - }, - ], - }), - ).toEqual({ - anyOf: [ - { - allOf: [ - { query: 'testRule1', params: ['a', 1] }, - { query: 'testRule2', params: [{ foo: 'bar' }] }, - ], - }, - { - not: { query: 'testRule1', params: ['b', 2] }, - }, - ], - }); - }); - }); - - 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] }], - }, - }); - }); - }); - - describe('registerPermissionRule', () => { - it('adds support for the new rule in toQuery', () => { - const { registerPermissionRule, toQuery } = testIntegration(); - - registerPermissionRule({ - name: 'testRule3', - description: 'Test rule 3', - apply: jest.fn((_resource: any, _firstParam: string) => false), - toQuery: jest.fn((firstParam: string) => ({ - query: 'testRule3', - params: [firstParam], - })), - }); - - expect( - toQuery({ - rule: 'testRule3', - params: ['abc'], - }), - ).toEqual({ - query: 'testRule3', - params: ['abc'], - }); - }); - - it('adds support for the new rule in the apply-conditions endpoint', async () => { - const { registerPermissionRule, createPermissionIntegrationRouter } = - testIntegration(); - - const app = express().use(createPermissionIntegrationRouter()); - - registerPermissionRule({ - name: 'testRule3', - description: 'Test rule 3', - apply: jest.fn((_resource: any, _firstParam: string) => false), - toQuery: jest.fn((firstParam: string) => ({ - query: 'testRule3', - params: [firstParam], - })), - }); - - const response = await request(app) - .post('/permissions/apply-conditions') - .send({ - resourceRef: 'default:test/resource', - resourceType: 'test-resource', - conditions: { - not: { - rule: 'testRule3', - params: ['a'], - }, - }, - }); - - expect(response.status).toEqual(200); - expect(response.body).toEqual({ result: AuthorizeResult.ALLOW }); - }); - }); -}); diff --git a/plugins/permission-node/src/integration/createPermissionIntegration.ts b/plugins/permission-node/src/integration/createPermissionIntegration.ts deleted file mode 100644 index 72118572e5..0000000000 --- a/plugins/permission-node/src/integration/createPermissionIntegration.ts +++ /dev/null @@ -1,242 +0,0 @@ -/* - * 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 { conditionFor } from './conditionFor'; -import { applyConditions, mapConditions } 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, to finalize a conditional - * authorization response. - * @public - */ -export type ApplyConditionsRequest = { - resourceRef: string; - resourceType: string; - conditions: PermissionCriteria; -}; - -/** - * An authorization condition, which is a function to evaluate a given {@link PermissionRule} with - * a specific set of parameters. - * @see createPermissionIntegration - * @public - */ -export type Condition = TRule extends PermissionRule< - any, - any, - infer TParams -> - ? (...params: TParams) => PermissionCondition - : never; - -/** - * A collection of keyed {@link Condition} functions produced from {@link PermissionRule}. - * @see createPermissionIntegration - * @public - */ -export type Conditions< - TRules extends Record>, -> = { - [Name in keyof TRules]: Condition; -}; - -/** - * The plugin-specific type which authorization conditions are translated into, for plugin use in - * filtering resources. - * @public - */ -export type QueryType = TRules extends Record< - string, - PermissionRule -> - ? TQuery - : never; - -/** - * Create a permission and authorization integration for a plugin that supports _conditional - * authorization_ for resources. - * - * 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` are a map of {@link PermissionRule} 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 createPermissionIntegration = < - TResource, - TRules extends { [key: string]: PermissionRule }, - TGetResourceParams extends any[] = [], ->({ - pluginId, - resourceType, - rules, - getResource, -}: { - pluginId: string; - resourceType: string; - rules: TRules; - getResource: ( - resourceRef: string, - ...params: TGetResourceParams - ) => Promise; -}): { - createPermissionIntegrationRouter: (...params: TGetResourceParams) => Router; - toQuery: ( - conditions: PermissionCriteria, - ) => PermissionCriteria>; - conditions: Conditions; - createConditions: (conditions: PermissionCriteria) => { - pluginId: string; - resourceType: string; - conditions: PermissionCriteria; - }; - registerPermissionRule: ( - rule: PermissionRule>, - ) => void; -} => { - const rulesMap = new Map(Object.values(rules).map(rule => [rule.name, rule])); - - const getRule = (name: string) => { - const rule = rulesMap.get(name); - - if (!rule) { - throw new Error(`Unexpected permission rule: ${name}`); - } - - return rule; - }; - - return { - createPermissionIntegrationRouter: ( - ...getResourceParams: TGetResourceParams - ) => { - const router = Router(); - - 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, - ...getResourceParams, - ); - - if (!resource) { - return res - .status(400) - .send(`Resource for ref ${body.resourceRef} not found.`); - } - - return res.status(200).json({ - result: applyConditions(body.conditions, ({ rule, params }) => - getRule(rule).apply(resource, ...params), - ) - ? AuthorizeResult.ALLOW - : AuthorizeResult.DENY, - }); - }, - ); - - return router; - }, - toQuery: ( - conditions: PermissionCriteria, - ): PermissionCriteria> => { - return mapConditions(conditions, ({ rule, params }) => - getRule(rule).toQuery(...params), - ); - }, - conditions: Object.entries(rules).reduce( - (acc, [key, rule]) => ({ - ...acc, - [key]: conditionFor(rule), - }), - {} as Conditions, - ), - createConditions: ( - conditions: PermissionCriteria, - ) => ({ - pluginId, - resourceType, - conditions, - }), - registerPermissionRule: rule => { - rulesMap.set(rule.name, rule); - }, - }; -}; 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..6694b893b7 --- /dev/null +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -0,0 +1,165 @@ +/* + * 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; +}; + +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. + * + * 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 index 697d25f65d..f070d57c8f 100644 --- a/plugins/permission-node/src/integration/index.ts +++ b/plugins/permission-node/src/integration/index.ts @@ -14,5 +14,7 @@ * limitations under the License. */ -export * from './conditionFor'; -export * from './createPermissionIntegration'; +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 index 72e87b2a7b..d27c0a4243 100644 --- a/plugins/permission-node/src/integration/util.test.ts +++ b/plugins/permission-node/src/integration/util.test.ts @@ -15,188 +15,72 @@ */ import { - PermissionCondition, - PermissionCriteria, -} from '@backstage/plugin-permission-common'; -import { applyConditions, mapConditions } from './util'; + createGetRule, + isAndCriteria, + isNotCriteria, + isOrCriteria, +} from './util'; -describe('integration utils', () => { - const testCases: { - criteria: PermissionCriteria; - expectedResult: { - applyConditions: boolean; - mapConditions: PermissionCriteria; +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(), }; - }[] = [ - { - criteria: { rule: 'test-rule-1', params: [] }, - expectedResult: { - applyConditions: true, - mapConditions: 'test-rule-1', - }, - }, - { - criteria: { rule: 'test-rule-2', params: [] }, - expectedResult: { - applyConditions: false, - mapConditions: 'test-rule-2', - }, - }, - { - criteria: { - anyOf: [ - { rule: 'test-rule-1', params: ['a', 'b'] }, - { rule: 'test-rule-2', params: ['c', 'd'] }, - ], - }, - expectedResult: { - applyConditions: true, - mapConditions: { - anyOf: ['test-rule-1:a,b', 'test-rule-2:c,d'], - }, - }, - }, - { - criteria: { - allOf: [ - { rule: 'test-rule-1', params: ['e', 'f'] }, - { rule: 'test-rule-2', params: ['g', 'h'] }, - ], - }, - expectedResult: { - applyConditions: false, - mapConditions: { - allOf: ['test-rule-1:e,f', 'test-rule-2:g,h'], - }, - }, - }, - { - criteria: { - not: { rule: 'test-rule-2', params: ['i'] }, - }, - expectedResult: { - applyConditions: true, - mapConditions: { - not: 'test-rule-2:i', - }, - }, - }, - { - criteria: { - allOf: [ - { - anyOf: [ - { rule: 'test-rule-1', params: ['j'] }, - { rule: 'test-rule-2', params: ['k'] }, - ], - }, - { - not: { - allOf: [ - { rule: 'test-rule-1', params: ['l'] }, - { rule: 'test-rule-2', params: ['m'] }, - ], - }, - }, - ], - }, - expectedResult: { - applyConditions: true, - mapConditions: { - allOf: [ - { - anyOf: ['test-rule-1:j', 'test-rule-2:k'], - }, - { - not: { - allOf: ['test-rule-1:l', 'test-rule-2:m'], - }, - }, - ], - }, - }, - }, - { - criteria: { - allOf: [ - { - anyOf: [ - { rule: 'test-rule-1', params: ['j'] }, - { rule: 'test-rule-2', params: ['k'] }, - ], - }, - { - not: { - allOf: [ - { rule: 'test-rule-1', params: ['l'] }, - { not: { rule: 'test-rule-2', params: ['m'] } }, - ], - }, - }, - ], - }, - expectedResult: { - applyConditions: false, - mapConditions: { - allOf: [ - { - anyOf: ['test-rule-1:j', 'test-rule-2:k'], - }, - { - not: { - allOf: ['test-rule-1:l', { not: 'test-rule-2:m' }], - }, - }, - ], - }, - }, - }, - ]; - describe('applyConditions', () => { - const applyFn = jest.fn(condition => condition.rule === 'test-rule-1'); + const testRule2 = { + name: 'test-rule-2', + description: 'Test rule 2', + apply: jest.fn(), + toQuery: jest.fn(), + }; - it('invokes applyFn with rules to determine result', () => { - applyConditions({ rule: 'test-rule-1', params: ['foo', 'bar'] }, applyFn); - - expect(applyFn).toHaveBeenCalledWith({ - rule: 'test-rule-1', - params: ['foo', 'bar'], - }); + beforeEach(() => { + getRule = createGetRule([testRule1, testRule2]); }); - it.each(testCases)( - 'works with criteria %#', - ({ criteria, expectedResult }) => { - expect(applyConditions(criteria, applyFn)).toEqual( - expectedResult.applyConditions, - ); - }, - ); + 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('mapConditions', () => { - const mapFn = jest.fn( - ({ rule, params }) => - `${rule}${params.length ? `:${params.join(',')}` : ''}`, - ); - - it('invokes mapFn to transform conditions', () => { - mapConditions({ rule: 'test-rule-1', params: ['foo', 'bar'] }, mapFn); - - expect(mapFn).toHaveBeenCalledWith({ - rule: 'test-rule-1', - params: ['foo', 'bar'], - }); + describe('isOrCriteria', () => { + it('returns true if input has a top-level "anyOf" property', () => { + expect(isOrCriteria({ anyOf: { not: { allOf: [] } } })).toEqual(true); }); - it.each(testCases)( - 'works with criteria %#', - ({ criteria, expectedResult }) => { - expect(mapConditions(criteria, mapFn)).toEqual( - expectedResult.mapConditions, - ); - }, - ); + 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 index 631aee269e..d9457cfefc 100644 --- a/plugins/permission-node/src/integration/util.ts +++ b/plugins/permission-node/src/integration/util.ts @@ -15,54 +15,35 @@ */ import { PermissionCriteria } from '@backstage/plugin-permission-common'; +import { PermissionRule } from '../types'; -const isAndCriteria = ( +export const isAndCriteria = ( filter: PermissionCriteria, ): filter is { allOf: PermissionCriteria[] } => Object.prototype.hasOwnProperty.call(filter, 'allOf'); -const isOrCriteria = ( +export const isOrCriteria = ( filter: PermissionCriteria, ): filter is { anyOf: PermissionCriteria[] } => Object.prototype.hasOwnProperty.call(filter, 'anyOf'); -const isNotCriteria = ( +export const isNotCriteria = ( filter: PermissionCriteria, ): filter is { not: PermissionCriteria } => Object.prototype.hasOwnProperty.call(filter, 'not'); -export const mapConditions = ( - criteria: PermissionCriteria, - mapFn: (condition: TQueryIn) => TQueryOut, -): PermissionCriteria => { - if (isAndCriteria(criteria)) { - return { - allOf: criteria.allOf.map(child => mapConditions(child, mapFn)), - }; - } else if (isOrCriteria(criteria)) { - return { - anyOf: criteria.anyOf.map(child => mapConditions(child, mapFn)), - }; - } else if (isNotCriteria(criteria)) { - return { - not: mapConditions(criteria.not, mapFn), - }; - } +export const createGetRule = ( + rules: PermissionRule[], +) => { + const rulesMap = new Map(Object.values(rules).map(rule => [rule.name, rule])); - return mapFn(criteria); -}; - -export const applyConditions = ( - criteria: PermissionCriteria, - applyFn: (condition: TQuery) => boolean, -): boolean => { - if (isAndCriteria(criteria)) { - return criteria.allOf.every(child => applyConditions(child, applyFn)); - } else if (isOrCriteria(criteria)) { - return criteria.anyOf.some(child => applyConditions(child, applyFn)); - } else if (isNotCriteria(criteria)) { - return !applyConditions(criteria.not, applyFn); - } - - return applyFn(criteria); + return (name: string): PermissionRule => { + const rule = rulesMap.get(name); + + if (!rule) { + throw new Error(`Unexpected permission rule: ${name}`); + } + + return rule; + }; }; From 5bff67aac43eb68ceece4704fa788959b9e07c73 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Mon, 22 Nov 2021 13:06:39 +0000 Subject: [PATCH 28/51] permission-node: expose ApplyConditionsResponse type This type will be shared with the backend. Signed-off-by: Mike Lewis --- plugins/permission-node/api-report.md | 5 +++++ .../integration/createPermissionIntegrationRouter.ts | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 1aab9e1f47..95e1eb0711 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -17,6 +17,11 @@ export type ApplyConditionsRequest = { conditions: PermissionCriteria; }; +// @public +export type ApplyConditionsResponse = { + result: AuthorizeResult.ALLOW | AuthorizeResult.DENY; +}; + // @public export type Condition = TRule extends PermissionRule< any, diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index 6694b893b7..7757678128 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -61,6 +61,16 @@ export type ApplyConditionsRequest = { 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, From 740f9582905654276c7582bd0aa66e7117f4b47c Mon Sep 17 00:00:00 2001 From: Joon Park Date: Mon, 22 Nov 2021 21:01:51 +0000 Subject: [PATCH 29/51] Handle empty values array in EntityFilter While an empty values array is not really a sensible input, if an empty array is provided, technically nothing should be returned as a matching value, thus the existing behavior was technically a bug. Signed-off-by: Joon Park --- .changeset/many-waves-visit.md | 5 +++ .../src/service/NextEntitiesCatalog.test.ts | 32 +++++++++++++++++++ .../src/service/NextEntitiesCatalog.ts | 2 +- 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 .changeset/many-waves-visit.md 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/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', From b922d3be1b9b07de70f8ed0e5789e24365fc2b51 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 23 Nov 2021 12:10:37 +0100 Subject: [PATCH 30/51] yarn.lock: bump rollup-plugin-postcss Signed-off-by: Patrik Oldsberg --- yarn.lock | 681 ++++++++++++++++++++++-------------------------------- 1 file changed, 277 insertions(+), 404 deletions(-) diff --git a/yarn.lock b/yarn.lock index 07a5febcb8..ed2be71ddb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6742,6 +6742,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" @@ -8938,7 +8943,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= @@ -10388,7 +10393,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 +10675,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 +11318,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 +11334,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 +11342,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 +11861,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 +12057,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 +12149,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 +12204,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 +12261,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" @@ -16055,7 +16019,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 +16143,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 +16215,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 +16588,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 +16857,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 +16980,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 +17034,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 +17347,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 +18925,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" @@ -21361,7 +21290,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 +21300,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 +22754,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 +22825,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 +22952,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 +23053,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== @@ -25072,11 +24958,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 +25069,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 +25136,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 +26628,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 +26766,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 +26785,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 +27955,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 +28494,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" From a3c5b2f70b038ebccc0c31d61b9e52856fe098f1 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 23 Nov 2021 16:33:29 +0100 Subject: [PATCH 31/51] core-plugin-api: Deprecate Plugin register and output Co-authored-by: blam Co-authored-by: Patrik Oldsberg Signed-off-by: Johan Haals --- packages/core-app-api/src/app/AppManager.tsx | 29 ++++++++++++------- .../core-plugin-api/src/plugin/Plugin.tsx | 23 ++++++++++++--- packages/core-plugin-api/src/plugin/types.ts | 23 ++++++++++++++- plugins/cost-insights/src/plugin.ts | 4 +-- 4 files changed, 61 insertions(+), 18 deletions(-) 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/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/types.ts b/packages/core-plugin-api/src/plugin/types.ts index aeb7037c51..c258b6f9fc 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 * @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/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, From 950b36393cc6bf94ab7d568e8149ddebdce3ecb7 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 23 Nov 2021 16:49:33 +0100 Subject: [PATCH 32/51] core-plugin-api: Add tests and changesets Co-authored-by: blam Co-authored-by: Patrik Oldsberg Signed-off-by: Johan Haals --- .changeset/slow-olives-confess.md | 7 ++ .changeset/young-hats-shop.md | 5 ++ .../core-app-api/src/app/AppManager.test.tsx | 72 +++++++++++++++ .../src/plugin/Plugin.test.tsx | 89 +++++++++++++++++++ 4 files changed, 173 insertions(+) create mode 100644 .changeset/slow-olives-confess.md create mode 100644 .changeset/young-hats-shop.md create mode 100644 packages/core-plugin-api/src/plugin/Plugin.test.tsx 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/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/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-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([]); + }); +}); From 6de3b8fb24abadeb678e376fae3c7144cf3da954 Mon Sep 17 00:00:00 2001 From: jodybro Date: Tue, 23 Nov 2021 13:30:01 -0500 Subject: [PATCH 33/51] support v1 api for ingress object Signed-off-by: jodybro --- contrib/chart/backstage/templates/ingress.yaml | 6 ++++++ 1 file changed, 6 insertions(+) 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 From f1ba6dfb1d7fa1dec651d9330a1ea2c17f1b5be1 Mon Sep 17 00:00:00 2001 From: goenning Date: Tue, 23 Nov 2021 19:03:46 +0000 Subject: [PATCH 34/51] fix comments, support onpremise and paging Signed-off-by: goenning --- .changeset/strong-seahorses-scream.md | 2 +- microsite/sidebars.json | 6 +- mkdocs.yml | 1 + .../AzureDevOpsDiscoveryProcessor.ts | 4 +- .../ingestion/processors/azure/azure.test.ts | 92 +++++++++++++++++++ .../src/ingestion/processors/azure/azure.ts | 55 ++++++----- 6 files changed, 136 insertions(+), 24 deletions(-) diff --git a/.changeset/strong-seahorses-scream.md b/.changeset/strong-seahorses-scream.md index 445ed65c81..bbd1d44378 100644 --- a/.changeset/strong-seahorses-scream.md +++ b/.changeset/strong-seahorses-scream.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-catalog-backend': minor +'@backstage/plugin-catalog-backend': patch --- Added Azure DevOps discovery processor diff --git a/microsite/sidebars.json b/microsite/sidebars.json index ec5c5d8d68..bc026ec196 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -124,7 +124,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 8c7114abbc..08107f255d 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/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts index b86e3f9c82..58ee2155e9 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts @@ -85,7 +85,9 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { catalogPath, ); - this.logger.info(`Found ${files.length} files in Azure DevOps.`); + this.logger.debug( + `Found ${files.length} files in Azure DevOps from ${location.target}.`, + ); for (const file of files) { emit( diff --git a/plugins/catalog-backend/src/ingestion/processors/azure/azure.test.ts b/plugins/catalog-backend/src/ingestion/processors/azure/azure.test.ts index d9dfe5d1f4..67cc120bab 100644 --- a/plugins/catalog-backend/src/ingestion/processors/azure/azure.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/azure/azure.test.ts @@ -34,6 +34,7 @@ describe('azure', () => { 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)); @@ -81,6 +82,7 @@ describe('azure', () => { 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)); @@ -120,6 +122,7 @@ describe('azure', () => { 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)); @@ -137,4 +140,93 @@ describe('azure', () => { ), ).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 index ec1b9ba0fb..0c7b17483a 100644 --- a/plugins/catalog-backend/src/ingestion/processors/azure/azure.ts +++ b/plugins/catalog-backend/src/ingestion/processors/azure/azure.ts @@ -33,6 +33,9 @@ export interface CodeSearchResultItem { }; } +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, @@ -41,27 +44,37 @@ export async function codeSearch( repo: string, path: string, ): Promise { - // TODO: What's the search URL for onpremises DevOps? - const searchUrl = `https://almsearch.dev.azure.com/${org}/${project}/_apis/search/codesearchresults?api-version=6.0-preview.1`; - const opts = getAzureRequestOptions(azureConfig); - const response = await fetch(searchUrl, { - method: 'POST', - headers: { - ...opts.headers, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - searchText: `path:${path} repo:${repo || '*'}`, - $top: 1000, - }), - }); + 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`; - if (response.status !== 200) { - throw new Error( - `Azure DevOps search failed with response status ${response.status}`, - ); - } + let items: CodeSearchResultItem[] = []; + let hasMorePages = true; - const responseBody: CodeSearchResponse = await response.json(); - return responseBody.results; + 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; } From f803ac931e3cd6d0e29b7de32e9bd5eb81b40dc0 Mon Sep 17 00:00:00 2001 From: Lorenzo Orsatti <49567430+lorsatti@users.noreply.github.com> Date: Tue, 23 Nov 2021 21:41:06 +0100 Subject: [PATCH 35/51] add missing styles to Swagger (dark mode) Signed-off-by: Lorenzo Orsatti <49567430+lorsatti@users.noreply.github.com> --- .../OpenApiDefinitionWidget/OpenApiDefinition.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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, From c982fc12cbf0798caefdebd7c36e8b35b809598e Mon Sep 17 00:00:00 2001 From: Lorenzo Orsatti <49567430+lorsatti@users.noreply.github.com> Date: Tue, 23 Nov 2021 22:03:03 +0100 Subject: [PATCH 36/51] add changeset Signed-off-by: Lorenzo Orsatti <49567430+lorsatti@users.noreply.github.com> --- .changeset/yellow-apes-brush.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/yellow-apes-brush.md 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. From fb90b70de501da7e481deb9749e4ec560140c65d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Nov 2021 04:11:52 +0000 Subject: [PATCH 37/51] chore(deps-dev): bump @testing-library/cypress from 7.0.6 to 8.0.2 Bumps [@testing-library/cypress](https://github.com/kentcdodds/cypress-testing-library) from 7.0.6 to 8.0.2. - [Release notes](https://github.com/kentcdodds/cypress-testing-library/releases) - [Changelog](https://github.com/testing-library/cypress-testing-library/blob/main/CHANGELOG.md) - [Commits](https://github.com/kentcdodds/cypress-testing-library/compare/v7.0.6...v8.0.2) --- updated-dependencies: - dependency-name: "@testing-library/cypress" dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- packages/app/package.json | 2 +- yarn.lock | 85 ++++++++++++++++++++++++++++++++------- 2 files changed, 72 insertions(+), 15 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 5b9e97d317..09141f7cf7 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -66,7 +66,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/yarn.lock b/yarn.lock index ed2be71ddb..51bf649f1f 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" @@ -3654,6 +3654,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" @@ -6651,13 +6662,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 +6684,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" @@ -8322,6 +8347,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" @@ -9016,7 +9048,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== @@ -9046,6 +9078,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" @@ -9367,6 +9404,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" @@ -13126,6 +13168,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" @@ -23233,6 +23280,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" @@ -29142,9 +29199,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" From b8f430eee03b66dd3d50bd3e745abf56a54022e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Nov 2021 04:13:08 +0000 Subject: [PATCH 38/51] chore(deps-dev): bump @storybook/addon-actions from 6.3.7 to 6.3.12 Bumps [@storybook/addon-actions](https://github.com/storybookjs/storybook/tree/HEAD/addons/actions) from 6.3.7 to 6.3.12. - [Release notes](https://github.com/storybookjs/storybook/releases) - [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md) - [Commits](https://github.com/storybookjs/storybook/commits/v6.3.12/addons/actions) --- updated-dependencies: - dependency-name: "@storybook/addon-actions" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 61 +++++++++++-------------------------------------------- 1 file changed, 12 insertions(+), 49 deletions(-) diff --git a/yarn.lock b/yarn.lock index ed2be71ddb..86998a31d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5556,16 +5556,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 +5841,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 +5916,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" @@ -29142,9 +29105,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" From 88f5378397dfe3b626ee492d34b78a0f6d93ea4d Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 24 Nov 2021 09:36:40 +0100 Subject: [PATCH 39/51] Add export, update API report Signed-off-by: Johan Haals --- packages/core-plugin-api/api-report.md | 11 +++++++++-- packages/core-plugin-api/src/plugin/index.ts | 1 + packages/core-plugin-api/src/plugin/types.ts | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) 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/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 c258b6f9fc..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 + * @deprecated will be removed * @public */ export type FeatureFlagOutput = { From f658a341783ba4507b215c9dd77ffc3b75e90a02 Mon Sep 17 00:00:00 2001 From: Nicolas Arnold Date: Wed, 24 Nov 2021 09:31:12 +0000 Subject: [PATCH 40/51] Using stringEntityRef for comparision Signed-off-by: Nicolas Arnold --- .../src/service/DefaultLocationService.test.ts | 2 +- .../src/service/DefaultLocationService.ts | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-backend/src/service/DefaultLocationService.test.ts b/plugins/catalog-backend/src/service/DefaultLocationService.test.ts index e2b82ad2c0..562bae4867 100644 --- a/plugins/catalog-backend/src/service/DefaultLocationService.test.ts +++ b/plugins/catalog-backend/src/service/DefaultLocationService.test.ts @@ -196,7 +196,7 @@ describe('DefaultLocationServiceTest', () => { { type: 'url', target: 'https://backstage.io/catalog-info.yaml' }, true, ), - ).rejects.toThrowError('Duplicate nested entity: foo'); + ).rejects.toThrowError('Duplicate nested entity: location:default/foo'); }); it('should return exists false when the location does not exist beforehand', async () => { diff --git a/plugins/catalog-backend/src/service/DefaultLocationService.ts b/plugins/catalog-backend/src/service/DefaultLocationService.ts index d28112eda3..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, @@ -69,13 +70,18 @@ export class DefaultLocationService implements LocationService { }); if (processed.ok) { - const { metadata, kind } = processed.completedEntity; if ( entities.some( - e => e.metadata.name === metadata.name && e.kind === kind, + e => + stringifyEntityRef(e) === + stringifyEntityRef(processed.completedEntity), ) ) { - throw new Error(`Duplicate nested entity: ${metadata.name}`); + throw new Error( + `Duplicate nested entity: ${stringifyEntityRef( + processed.completedEntity, + )}`, + ); } unprocessedEntities.push(...processed.deferredEntities); entities.push(processed.completedEntity); From 92405f32bb0cf4acd0d860b354fca411281f62ee Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 24 Nov 2021 13:55:28 +0100 Subject: [PATCH 41/51] bug: should check the children of the first parse, not the root. Signed-off-by: blam --- ...WorkflowRunner.test.ts => NunjucksWorkflowRunner.test.ts} | 0 .../src/scaffolder/tasks/NunjucksWorkflowRunner.ts | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) rename plugins/scaffolder-backend/src/scaffolder/tasks/{DefaultWorkflowRunner.test.ts => NunjucksWorkflowRunner.test.ts} (100%) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts similarity index 100% rename from plugins/scaffolder-backend/src/scaffolder/tasks/DefaultWorkflowRunner.test.ts rename to plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index c66d967103..1139ce41c5 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) ); } @@ -147,7 +148,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { return JSON.parse(templated); } } catch (ex) { - this.options.logger.error( + console.error( `Failed to parse template string: ${value} with error ${ex.message}`, ); } From 5fd0e077bc3e38cf98dfb172690b4fcdbab4a1d1 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 24 Nov 2021 14:01:26 +0100 Subject: [PATCH 42/51] chore: adding test for this bug Signed-off-by: blam --- .../tasks/NunjucksWorkflowRunner.test.ts | 25 +++++++++++++++++++ .../tasks/NunjucksWorkflowRunner.ts | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts index 1f7877791c..b2e2e85a6c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.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 1139ce41c5..5b42eef459 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -148,7 +148,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { return JSON.parse(templated); } } catch (ex) { - console.error( + this.options.logger.error( `Failed to parse template string: ${value} with error ${ex.message}`, ); } From 4be343d2dd464fd8e21fe2ea7dc9f45e4261ced4 Mon Sep 17 00:00:00 2001 From: Lorenzo Orsatti <49567430+lorsatti@users.noreply.github.com> Date: Wed, 24 Nov 2021 14:03:00 +0100 Subject: [PATCH 43/51] add LogMeIn as an adopter Signed-off-by: Lorenzo Orsatti <49567430+lorsatti@users.noreply.github.com> --- ADOPTERS.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ADOPTERS.md b/ADOPTERS.md index 67aa8ec709..c4c84d11be 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -66,8 +66,9 @@ | [Palo Alto Networks](https://www.paloaltonetworks.com) | [Jeremy Guarini](https://github.com/jeremyguarini), [Brian Lomeland](https://github.com/bbbmmmlll), [Palo Alto Networks](https://github.com/PaloAltoNetworks) | Developer portal, service catalog, documentation and tooling | | [Signal Iduna Group](https://www.signal-iduna.de/) | [Jonas Thomsen](https://github.com/JoThomsen) | Developer Portal, documentation, monitoring, service catalog for our insurance ecosystem | | [Tradeshift](https://www.tradeshift.com/) | [Soren Mathiasen](https://github.com/sorenmat) | Developer Portal: documentation, monitoring, service templates, service catalog for our micro services | -| [Unity](https://unity.com) | [Ted Cordery](https://github.com/TeddyBallGame) | A centralized service catalog with documentation for our service engineers. | -| [PicPay](https://www.picpay.com) | [Luis Baroni](https://github.com/lcsbaroni), [Renata Poluceno](https://github.com/renatapoluceno), [PicPay](https://github.com/picpay) | Developer portal for building services throught templates, service catalog with ownership of services, documentation and metrics providing autonomy and visibility for all. | +| [Unity](https://unity.com) | [Ted Cordery](https://github.com/TeddyBallGame) | A centralized service catalog with documentation for our service engineers. | +| [PicPay](https://www.picpay.com) | [Luis Baroni](https://github.com/lcsbaroni), [Renata Poluceno](https://github.com/renatapoluceno), [PicPay](https://github.com/picpay) | Developer portal for building services throught templates, service catalog with ownership of services, documentation and metrics providing autonomy and visibility for all. | | [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 🚀 | \ No newline at end of file +| [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. | From e634a47ce50236b65a73140f0c4b0c7fbd559c1f Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 24 Nov 2021 14:03:22 +0100 Subject: [PATCH 44/51] chore: added changeset Signed-off-by: blam --- .changeset/shiny-insects-remember.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shiny-insects-remember.md 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. From e99db76f6ad95174c7b4d75ed556b7cae88de24b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Oct 2021 14:48:41 +0000 Subject: [PATCH 45/51] build(deps): bump @gitbeaker/node from 30.3.0 to 34.4.1 Bumps [@gitbeaker/node](https://github.com/jdalrymple/gitbeaker) from 30.3.0 to 34.4.1. - [Release notes](https://github.com/jdalrymple/gitbeaker/releases) - [Changelog](https://github.com/jdalrymple/gitbeaker/blob/master/CHANGELOG.md) - [Commits](https://github.com/jdalrymple/gitbeaker/compare/30.3.0...34.4.1) --- updated-dependencies: - dependency-name: "@gitbeaker/node" dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- packages/backend/package.json | 2 +- plugins/scaffolder-backend/package.json | 2 +- yarn.lock | 47 +++++++++++++++++++++---- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 4fb692f040..794f5ac1a6 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.4.1", "@octokit/rest": "^18.5.3", "azure-devops-node-api": "^11.0.1", "dockerode": "^3.3.1", diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 374cfeffd6..01d0b793bf 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -41,7 +41,7 @@ "@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/node": "^34.4.1", "@octokit/rest": "^18.5.3", "@octokit/webhooks": "^9.14.1", "@types/express": "^4.17.6", diff --git a/yarn.lock b/yarn.lock index 420fd4add8..ce88d83aa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2879,7 +2879,7 @@ 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": +"@gitbeaker/core@^30.2.0": version "30.3.0" resolved "https://registry.npmjs.org/@gitbeaker/core/-/core-30.3.0.tgz#d005891d47cfacb41d4a3cc8bf3ee8c68c53d378" integrity sha512-j7GHsFo6AOuRmLaK4F4Kx967jTy6LFZh5vjmRxjlRvX0qlfl3NJKiQIl30fZz1rPXWdQLAq+kwl1i0BKrWhOpA== @@ -2890,13 +2890,25 @@ 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/core@^34.4.1": + version "34.4.1" + resolved "https://registry.npmjs.org/@gitbeaker/core/-/core-34.4.1.tgz#4458c7a607d5d3bd28202e897fab758fae78f747" + integrity sha512-M109vfwG9qaKH9oPouMRq8oM1ohJ8btCNmuxlfbj4OYbP2q8JhB5yO4xJeGiA0FdG/BWtrqEASQvJS3H4RNM3Q== dependencies: - "@gitbeaker/core" "^30.3.0" - "@gitbeaker/requester-utils" "^30.3.0" + "@gitbeaker/requester-utils" "^34.4.1" + form-data "^4.0.0" + li "^1.3.0" + mime-types "^2.1.32" + query-string "^7.0.0" + xcase "^2.0.1" + +"@gitbeaker/node@^34.4.1": + version "34.4.1" + resolved "https://registry.npmjs.org/@gitbeaker/node/-/node-34.4.1.tgz#872ebd0cae16343008a1f0ed937bd1c159f42007" + integrity sha512-0hXpZF3WU9WJgSD6rmj1oCwzQVf5cEw6HiXjwlkwQXv2hwRlZETL4M8JALInRD1ngMOoREe33eTNwyoRWmS2lA== + dependencies: + "@gitbeaker/core" "^34.4.1" + "@gitbeaker/requester-utils" "^34.4.1" delay "^5.0.0" got "^11.8.2" xcase "^2.0.1" @@ -2910,6 +2922,15 @@ query-string "^7.0.0" xcase "^2.0.1" +"@gitbeaker/requester-utils@^34.4.1": + version "34.4.1" + resolved "https://registry.npmjs.org/@gitbeaker/requester-utils/-/requester-utils-34.4.1.tgz#e5f1e9397de1cb5219640b3cf96d8cb7f41e3f14" + integrity sha512-YqRNzV5lwhLrIltgjfaYFCsdp/jHthbc4IEmKO1yZw5aAE7aJrtR/iXMxPy2JvnGODNgIhBSxHaXlCdLJWYIkA== + dependencies: + form-data "^4.0.0" + qs "^6.10.1" + xcase "^2.0.1" + "@google-cloud/common@^3.7.0": version "3.7.0" resolved "https://registry.npmjs.org/@google-cloud/common/-/common-3.7.0.tgz#ee3fba75aeaa614978aebf8740380670026592aa" @@ -20452,6 +20473,11 @@ mime-db@1.49.0: resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== +mime-db@1.50.0: + version "1.50.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz#abd4ac94e98d3c0e185016c67ab45d5fde40c11f" + integrity sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A== + "mime-db@>= 1.43.0 < 2": version "1.48.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" @@ -20476,6 +20502,13 @@ mime-types@^2.0.8, mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, m dependencies: mime-db "1.49.0" +mime-types@^2.1.32: + version "2.1.33" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz#1fa12a904472fafd068e48d9e8401f74d3f70edb" + integrity sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g== + dependencies: + mime-db "1.50.0" + mime@1.6.0, mime@^1.4.1: version "1.6.0" resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" From 42ebbc18c0dc476baf1efa6322ce3b026a5a37c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 24 Nov 2021 14:21:45 +0100 Subject: [PATCH 46/51] get both gitbeaker packages bumped, and add changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/thirty-tigers-refuse.md | 6 ++ packages/backend/package.json | 2 +- .../packages/backend/package.json.hbs | 2 +- plugins/scaffolder-backend/package.json | 4 +- yarn.lock | 69 ++++++------------- 5 files changed, 31 insertions(+), 52 deletions(-) create mode 100644 .changeset/thirty-tigers-refuse.md 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/packages/backend/package.json b/packages/backend/package.json index 794f5ac1a6..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": "^34.4.1", + "@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/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/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 01d0b793bf..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": "^34.4.1", + "@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/yarn.lock b/yarn.lock index ce88d83aa3..1e4d402072 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2879,53 +2879,33 @@ resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210" integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw== -"@gitbeaker/core@^30.2.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/core@^34.4.1": - version "34.4.1" - resolved "https://registry.npmjs.org/@gitbeaker/core/-/core-34.4.1.tgz#4458c7a607d5d3bd28202e897fab758fae78f747" - integrity sha512-M109vfwG9qaKH9oPouMRq8oM1ohJ8btCNmuxlfbj4OYbP2q8JhB5yO4xJeGiA0FdG/BWtrqEASQvJS3H4RNM3Q== +"@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/requester-utils" "^34.4.1" - form-data "^4.0.0" - li "^1.3.0" - mime-types "^2.1.32" - query-string "^7.0.0" - xcase "^2.0.1" - -"@gitbeaker/node@^34.4.1": - version "34.4.1" - resolved "https://registry.npmjs.org/@gitbeaker/node/-/node-34.4.1.tgz#872ebd0cae16343008a1f0ed937bd1c159f42007" - integrity sha512-0hXpZF3WU9WJgSD6rmj1oCwzQVf5cEw6HiXjwlkwQXv2hwRlZETL4M8JALInRD1ngMOoREe33eTNwyoRWmS2lA== - dependencies: - "@gitbeaker/core" "^34.4.1" - "@gitbeaker/requester-utils" "^34.4.1" + "@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== - dependencies: - form-data "^4.0.0" - query-string "^7.0.0" - xcase "^2.0.1" - -"@gitbeaker/requester-utils@^34.4.1": - version "34.4.1" - resolved "https://registry.npmjs.org/@gitbeaker/requester-utils/-/requester-utils-34.4.1.tgz#e5f1e9397de1cb5219640b3cf96d8cb7f41e3f14" - integrity sha512-YqRNzV5lwhLrIltgjfaYFCsdp/jHthbc4IEmKO1yZw5aAE7aJrtR/iXMxPy2JvnGODNgIhBSxHaXlCdLJWYIkA== +"@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" qs "^6.10.1" @@ -20473,11 +20453,6 @@ mime-db@1.49.0: resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== -mime-db@1.50.0: - version "1.50.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz#abd4ac94e98d3c0e185016c67ab45d5fde40c11f" - integrity sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A== - "mime-db@>= 1.43.0 < 2": version "1.48.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz#e35b31045dd7eada3aaad537ed88a33afbef2d1d" @@ -20502,13 +20477,6 @@ mime-types@^2.0.8, mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, m dependencies: mime-db "1.49.0" -mime-types@^2.1.32: - version "2.1.33" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz#1fa12a904472fafd068e48d9e8401f74d3f70edb" - integrity sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g== - dependencies: - mime-db "1.50.0" - mime@1.6.0, mime@^1.4.1: version "1.6.0" resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -20519,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" From b08dbb103552ad16064c0c84bad975311152320d Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Wed, 24 Nov 2021 14:18:11 +0000 Subject: [PATCH 47/51] permission-node: destructure options inside function to simplify api-report Signed-off-by: Mike Lewis --- plugins/permission-node/api-report.md | 6 +-- .../src/integration/createConditionExports.ts | 38 ++++++++++--------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md index 95e1eb0711..e5fe9a7087 100644 --- a/plugins/permission-node/api-report.md +++ b/plugins/permission-node/api-report.md @@ -57,11 +57,7 @@ export type ConditionTransformer = ( export const createConditionExports: < TResource, TRules extends Record>, ->({ - pluginId, - resourceType, - rules, -}: { +>(options: { pluginId: string; resourceType: string; rules: TRules; diff --git a/plugins/permission-node/src/integration/createConditionExports.ts b/plugins/permission-node/src/integration/createConditionExports.ts index d7f8c675d2..d69064a56c 100644 --- a/plugins/permission-node/src/integration/createConditionExports.ts +++ b/plugins/permission-node/src/integration/createConditionExports.ts @@ -63,11 +63,7 @@ export type Conditions< export const createConditionExports = < TResource, TRules extends Record>, ->({ - pluginId, - resourceType, - rules, -}: { +>(options: { pluginId: string; resourceType: string; rules: TRules; @@ -78,17 +74,23 @@ export const createConditionExports = < resourceType: string; conditions: PermissionCriteria; }; -} => ({ - conditions: Object.entries(rules).reduce( - (acc, [key, rule]) => ({ - ...acc, - [key]: createConditionFactory(rule), +} => { + 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, }), - {} as Conditions, - ), - createConditions: (conditions: PermissionCriteria) => ({ - pluginId, - resourceType, - conditions, - }), -}); + }; +}; From f5a25ec8046dd40c05f258ae5f58a430b37a9039 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Wed, 24 Nov 2021 14:19:32 +0000 Subject: [PATCH 48/51] permission-node: fix typo in doc comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Mike Lewis --- .../permission-node/src/integration/createConditionFactory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/permission-node/src/integration/createConditionFactory.ts b/plugins/permission-node/src/integration/createConditionFactory.ts index 066233bf41..8ee94091e1 100644 --- a/plugins/permission-node/src/integration/createConditionFactory.ts +++ b/plugins/permission-node/src/integration/createConditionFactory.ts @@ -26,7 +26,7 @@ import { PermissionRule } from '../types'; * 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 with. + * they're intended to integrate. * * @public */ From aded9bbc22444e1da75c0d60947096e49d7e8154 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Wed, 24 Nov 2021 14:20:56 +0000 Subject: [PATCH 49/51] permission-node: set version to 0.0.0 before initial release Signed-off-by: Mike Lewis --- plugins/permission-node/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index 4a9c837eb6..e22e2d2de4 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-permission-node", "description": "Common permission and authorization utilities for backend plugins", - "version": "0.1.0", + "version": "0.0.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", From 44b46644d996c82aa1aa611e539db3844661fe5d Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Wed, 24 Nov 2021 14:22:32 +0000 Subject: [PATCH 50/51] permission-node: add changeset for initial release Signed-off-by: Mike Lewis --- .changeset/heavy-tools-hang.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/heavy-tools-hang.md 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). From 370da15e7b225de270c8fc90f6cb8b817a02c859 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Wed, 24 Nov 2021 14:30:21 +0000 Subject: [PATCH 51/51] permission-node: separate doc comment summaries with @remarks tag Signed-off-by: Mike Lewis --- plugins/permission-node/src/index.ts | 1 - .../src/integration/createConditionExports.ts | 6 +++++- .../src/integration/createConditionFactory.ts | 2 ++ .../src/integration/createPermissionIntegrationRouter.ts | 2 ++ plugins/permission-node/src/policy/types.ts | 9 +++++++++ plugins/permission-node/src/types.ts | 3 +++ 6 files changed, 21 insertions(+), 2 deletions(-) diff --git a/plugins/permission-node/src/index.ts b/plugins/permission-node/src/index.ts index 697c02ab22..39527bac71 100644 --- a/plugins/permission-node/src/index.ts +++ b/plugins/permission-node/src/index.ts @@ -19,7 +19,6 @@ * * @packageDocumentation */ - export * from './integration'; export * from './policy'; export * from './types'; diff --git a/plugins/permission-node/src/integration/createConditionExports.ts b/plugins/permission-node/src/integration/createConditionExports.ts index d69064a56c..30cff6628a 100644 --- a/plugins/permission-node/src/integration/createConditionExports.ts +++ b/plugins/permission-node/src/integration/createConditionExports.ts @@ -49,7 +49,11 @@ export type Conditions< /** * Creates the recommended condition-related exports for a given plugin based on the built-in - * {@link PermissionRule}s it supports. It returns a `conditions` object containing a + * {@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. diff --git a/plugins/permission-node/src/integration/createConditionFactory.ts b/plugins/permission-node/src/integration/createConditionFactory.ts index 8ee94091e1..84a8dc86a6 100644 --- a/plugins/permission-node/src/integration/createConditionFactory.ts +++ b/plugins/permission-node/src/integration/createConditionFactory.ts @@ -19,6 +19,8 @@ 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. diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts index 7757678128..99e282b144 100644 --- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts +++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts @@ -97,6 +97,8 @@ const applyConditions = ( * 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 diff --git a/plugins/permission-node/src/policy/types.ts b/plugins/permission-node/src/policy/types.ts index 36186d5e57..44380bad26 100644 --- a/plugins/permission-node/src/policy/types.ts +++ b/plugins/permission-node/src/policy/types.ts @@ -25,9 +25,12 @@ 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; @@ -35,6 +38,8 @@ 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. @@ -54,6 +59,7 @@ export type ConditionalPolicyResult = { /** * The result of evaluating an authorization request with a {@link PermissionPolicy}. + * * @public */ export type PolicyResult = @@ -63,6 +69,8 @@ export type PolicyResult = /** * 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 @@ -71,6 +79,7 @@ export type PolicyResult = * 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 { diff --git a/plugins/permission-node/src/types.ts b/plugins/permission-node/src/types.ts index a4e95fbd61..678befc99c 100644 --- a/plugins/permission-node/src/types.ts +++ b/plugins/permission-node/src/types.ts @@ -20,6 +20,8 @@ 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. @@ -27,6 +29,7 @@ import type { PermissionCriteria } from '@backstage/plugin-permission-common'; * 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<