Merge branch 'master' into scorecard-tech-insights

Signed-off-by: irma12 <irma@roadie.io>
This commit is contained in:
irma12
2021-11-24 22:53:10 +01:00
181 changed files with 4102 additions and 1640 deletions
+5
View File
@@ -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).
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Providing an empty values array in an EntityFilter will now return no matches.
+5
View File
@@ -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.
+7
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Added Azure DevOps discovery processor
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Detect a duplicate entities when adding locations through dry run
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/create-app': patch
'@backstage/plugin-scaffolder-backend': patch
---
Bump gitbeaker to the latest version
+5
View File
@@ -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.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-cost-insights': patch
---
Supply featureFlags using featureFlag config option.
+1
View File
@@ -71,3 +71,4 @@
| [Epic Games](https://www.epicgames.com) | [Brian Jung](https://github.com/brian-at-epic), [Jeff Goldian](https://github.com/jeffgoldian-Epic) | Developer Portal: Service Catalog, Documentation, Software Templates and more making our internal teams' lives easier! |
| [Globo](https://globo.com) | [Carlos Gusmão](https://github.com/caeugusmao), [Guilherme Vierno](https://github.com/vierno), [Denis Aoki](https://github.com/dnsaoki2), [Maycon Dionisio](https://github.com/MayconDionisio), | Reduce the friction of accessing the information engineers need about Globo's digital services through a coherent and centralized experience. |
| [QBE](https://www.qbe.com/) | [Daniel Steel](https://github.com/danielsteelqbe), [Pete Jespers](https://github.com/petejespersqbe) | Developer portal allowing our global teams to explore and create applications, documentation and cloud infrastructure easily and quickly 🚀 |
| [LogMeIn](https://www.logmein.com) | [Lorenzo Orsatti](https://github.com/lorsatti) | Improve onboarding experience of new developers. Discover faster and painlessly developer documentation, API definitions and team information. Provide useful dev metrics in a central place. Provide easy-to-use templates for new services. |
@@ -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
+49
View File
@@ -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)
+8 -5
View File
@@ -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(
<ApiProvider apis={ApiRegistry.from([[analyticsApiRef, apiSpy]])}>
<TestApiProvider apis={[[analyticsApiRef, apiSpy]]}>
<SomeComponentUnderTest />
</ApiProvider>,
</TestApiProvider>,
),
);
+5 -1
View File
@@ -125,7 +125,11 @@
{
"type": "subcategory",
"label": "Azure",
"ids": ["integrations/azure/locations", "integrations/azure/org"]
"ids": [
"integrations/azure/locations",
"integrations/azure/discovery",
"integrations/azure/org"
]
},
{
"type": "subcategory",
+1
View File
@@ -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'
+1 -1
View File
@@ -67,7 +67,7 @@
"devDependencies": {
"@backstage/test-utils": "^0.1.22",
"@rjsf/core": "^3.2.1",
"@testing-library/cypress": "^7.0.1",
"@testing-library/cypress": "^8.0.2",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^13.1.8",
@@ -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<typeof githubActionsApiRef.T>;
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(
<ApiProvider apis={apis}>
<TestApiProvider
apis={[
[githubActionsApiRef, mockedApi],
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
}),
],
]}
>
<EntityProvider entity={entity}>
<EntityLayout>
<EntityLayout.Route path="/ci-cd" title="CI-CD">
@@ -68,7 +71,7 @@ describe('EntityPage Test', () => {
</EntityLayout.Route>
</EntityLayout>
</EntityProvider>
</ApiProvider>,
</TestApiProvider>,
);
expect(rendered.getByText('ExampleComponent')).toBeInTheDocument();
+1 -1
View File
@@ -52,7 +52,7 @@
"@backstage/plugin-tech-insights-node": "^0.1.0",
"@backstage/plugin-tech-insights-backend-module-jsonfc": "^0.1.0",
"@backstage/plugin-todo-backend": "^0.1.13",
"@gitbeaker/node": "^30.2.0",
"@gitbeaker/node": "^34.6.0",
"@octokit/rest": "^18.5.3",
"azure-devops-node-api": "^11.0.1",
"dockerode": "^3.3.1",
@@ -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(
<Provider>
<Router>
<Routes>
<Route path="/" element={<ExposedComponent />} />
<Route path="/foo" element={<HiddenComponent />} />
</Routes>
</Router>
</Provider>,
);
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)];
+19 -10
View File
@@ -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;
}
}
}
+9 -2
View File
@@ -258,6 +258,7 @@ export type BackstagePlugin<
getId(): string;
output(): PluginOutput[];
getApis(): Iterable<AnyApiFactory>;
getFeatureFlags(): Iterable<PluginFeatureFlagConfig>;
provide<T>(extension: Extension<T>): 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
@@ -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([]);
});
});
+19 -4
View File
@@ -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<PluginFeatureFlagConfig> {
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<PluginOutput>();
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<PluginOutput>();
if (!this.config.register) {
return outputs;
}
this.config.register({
featureFlags: {
@@ -70,7 +86,6 @@ export class PluginImpl<
},
});
this.storedOutput = outputs;
return this.storedOutput;
}
@@ -25,4 +25,5 @@ export type {
PluginConfig,
PluginHooks,
PluginOutput,
PluginFeatureFlagConfig,
} from './types';
+22 -1
View File
@@ -19,7 +19,7 @@ import { AnyApiFactory } from '../apis/system';
/**
* Replace with using {@link RouteRef}s.
*
* @deprecated will be removed
* @public
*/
export type FeatureFlagOutput = {
@@ -31,6 +31,7 @@ export type FeatureFlagOutput = {
* {@link FeatureFlagOutput} type.
*
* @public
* @deprecated Use {@link BackstagePlugin.getFeatureFlags} instead.
*/
export type PluginOutput = FeatureFlagOutput;
@@ -71,13 +72,30 @@ export type BackstagePlugin<
ExternalRoutes extends AnyExternalRoutes = {},
> = {
getId(): string;
/**
* @deprecated use getFeatureFlags instead.
* */
output(): PluginOutput[];
getApis(): Iterable<AnyApiFactory>;
/**
* Returns all registered feature flags for this plugin.
*/
getFeatureFlags(): Iterable<PluginFeatureFlagConfig>;
provide<T>(extension: Extension<T>): 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<AnyApiFactory>;
/** @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 = {
@@ -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",
@@ -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<AppThemeApi>;
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(
<ApiProvider apis={apiRegistry}>
<TestApiProvider apis={[[appThemeApiRef, appThemeApi]]}>
<SidebarThemeSwitcher />
</ApiProvider>,
</TestApiProvider>,
);
const button = getByLabelText('Switch Theme');
@@ -76,9 +73,9 @@ describe('SidebarThemeSwitcher', () => {
it('should select different theme', async () => {
const { getByLabelText, getByRole, getByText } = await renderInTestApp(
<ApiProvider apis={apiRegistry}>
<TestApiProvider apis={[[appThemeApiRef, appThemeApi]]}>
<SidebarThemeSwitcher />
</ApiProvider>,
</TestApiProvider>,
);
const button = getByLabelText('Switch Theme');
+48 -70
View File
@@ -1,6 +1,5 @@
import {
AlertApiForwarder,
ApiRegistry,
ErrorAlerter,
ErrorApiForwarder,
GithubAuth,
@@ -27,78 +26,57 @@ import {
configApiRef,
} from '@backstage/core-plugin-api';
const builder = ApiRegistry.builder();
builder.add(configApiRef, new ConfigReader({}));
const alertApi = builder.add(alertApiRef, new AlertApiForwarder());
builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder()));
builder.add(identityApiRef, {
const configApi = new ConfigReader({});
const alertApi = new AlertApiForwarder();
const errorApi = new ErrorAlerter(alertApi, new ErrorApiForwarder());
const identityApi = {
getUserId: () => 'guest',
getProfile: () => ({ email: 'guest@example.com' }),
getIdToken: () => undefined,
signOut: async () => {},
};
const oauthRequestApi = new OAuthRequestManager();
const googleAuthApi = GoogleAuth.create({
apiOrigin: 'http://localhost:7007',
basePath: '/auth/',
oauthRequestApi,
});
const githubAuthApi = GithubAuth.create({
apiOrigin: 'http://localhost:7007',
basePath: '/auth/',
oauthRequestApi,
});
const gitlabAuthApi = GitlabAuth.create({
apiOrigin: 'http://localhost:7007',
basePath: '/auth/',
oauthRequestApi,
});
const oktaAuthApi = OktaAuth.create({
apiOrigin: 'http://localhost:7007',
basePath: '/auth/',
oauthRequestApi,
});
const auth0AuthApi = Auth0Auth.create({
apiOrigin: 'http://localhost:7007',
basePath: '/auth/',
oauthRequestApi,
});
const oauth2Api = OAuth2.create({
apiOrigin: 'http://localhost:7007',
basePath: '/auth/',
oauthRequestApi,
});
const oauthRequestApi = builder.add(
oauthRequestApiRef,
new OAuthRequestManager(),
);
builder.add(
googleAuthApiRef,
GoogleAuth.create({
apiOrigin: 'http://localhost:7007',
basePath: '/auth/',
oauthRequestApi,
}),
);
builder.add(
githubAuthApiRef,
GithubAuth.create({
apiOrigin: 'http://localhost:7007',
basePath: '/auth/',
oauthRequestApi,
}),
);
builder.add(
gitlabAuthApiRef,
GitlabAuth.create({
apiOrigin: 'http://localhost:7007',
basePath: '/auth/',
oauthRequestApi,
}),
);
builder.add(
oktaAuthApiRef,
OktaAuth.create({
apiOrigin: 'http://localhost:7007',
basePath: '/auth/',
oauthRequestApi,
}),
);
builder.add(
auth0AuthApiRef,
Auth0Auth.create({
apiOrigin: 'http://localhost:7007',
basePath: '/auth/',
oauthRequestApi,
}),
);
builder.add(
oauth2ApiRef,
OAuth2.create({
apiOrigin: 'http://localhost:7007',
basePath: '/auth/',
oauthRequestApi,
}),
);
export const apis = builder.build();
export const apis = [
[configApiRef, configApi],
[alertApiRef, alertApi],
[errorApiRef, errorApi],
[identityApiRef, identityApi],
[oauthRequestApiRef, oauthRequestApi],
[googleAuthApiRef, googleAuthApi],
[githubAuthApiRef, githubAuthApi],
[gitlabAuthApiRef, gitlabAuthApi],
[oktaAuthApiRef, oktaAuthApi],
[auth0AuthApiRef, auth0AuthApi],
[oauth2ApiRef, oauth2Api],
];
+3 -3
View File
@@ -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 => (
<ApiProvider apis={apis}>
<TestApiProvider apis={apis}>
<ThemeProvider theme={useDarkMode() ? darkTheme : lightTheme}>
<CssBaseline>
<AlertDisplay />
<Content>{story()}</Content>
</CssBaseline>
</ThemeProvider>
</ApiProvider>
</TestApiProvider>
));
addParameters({
+17
View File
@@ -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: {}
@@ -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(
<ApiProvider apis={ApiRegistry.with(errorApiRef, mockErrorApi)}>
<TestApiProvider apis={[[errorApiRef, mockErrorApi]]}>
<A />
</ApiProvider>,
</TestApiProvider>,
);
expect(rendered.getByText('foo')).toBeInTheDocument();
@@ -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('<ApiDefinitionCard />', () => {
const apiDocsConfig: jest.Mocked<ApiDocsConfig> = {
@@ -31,10 +30,10 @@ describe('<ApiDefinitionCard />', () => {
let Wrapper: React.ComponentType;
beforeEach(() => {
const apis = ApiRegistry.with(apiDocsConfigRef, apiDocsConfig);
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
<TestApiProvider apis={[[apiDocsConfigRef, apiDocsConfig]]}>
{children}
</TestApiProvider>
);
});
@@ -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('<ApiTypeTitle />', () => {
const apiDocsConfig: jest.Mocked<ApiDocsConfig> = {
@@ -28,10 +27,10 @@ describe('<ApiTypeTitle />', () => {
let Wrapper: React.ComponentType;
beforeEach(() => {
const apis = ApiRegistry.with(apiDocsConfigRef, apiDocsConfig);
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
<TestApiProvider apis={[[apiDocsConfigRef, apiDocsConfig]]}>
{children}
</TestApiProvider>
);
});
@@ -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(
<ApiProvider
apis={ApiRegistry.from([
<TestApiProvider
apis={[
[catalogApiRef, catalogApi],
[configApiRef, configApi],
[storageApiRef, storageApi],
@@ -98,10 +98,10 @@ describe('ApiCatalogPage', () => {
new DefaultStarredEntitiesApi({ storageApi }),
],
[apiDocsConfigRef, apiDocsConfig],
])}
]}
>
{children}
</ApiProvider>,
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
@@ -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('<ConsumedApisCard />', () => {
const apiDocsConfig: jest.Mocked<ApiDocsConfig> = {
@@ -43,13 +42,15 @@ describe('<ConsumedApisCard />', () => {
let Wrapper: React.ComponentType;
beforeEach(() => {
const apis = ApiRegistry.with(catalogApiRef, catalogApi).with(
apiDocsConfigRef,
apiDocsConfig,
);
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
<TestApiProvider
apis={[
[catalogApiRef, catalogApi],
[apiDocsConfigRef, apiDocsConfig],
]}
>
{children}
</TestApiProvider>
);
});
@@ -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('<HasApisCard />', () => {
const apiDocsConfig: jest.Mocked<ApiDocsConfig> = {
@@ -43,13 +42,15 @@ describe('<HasApisCard />', () => {
let Wrapper: React.ComponentType;
beforeEach(() => {
const apis = ApiRegistry.with(catalogApiRef, catalogApi).with(
apiDocsConfigRef,
apiDocsConfig,
);
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
<TestApiProvider
apis={[
[catalogApiRef, catalogApi],
[apiDocsConfigRef, apiDocsConfig],
]}
>
{children}
</TestApiProvider>
);
});
@@ -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('<ProvidedApisCard />', () => {
const apiDocsConfig: jest.Mocked<ApiDocsConfig> = {
@@ -43,13 +42,15 @@ describe('<ProvidedApisCard />', () => {
let Wrapper: React.ComponentType;
beforeEach(() => {
const apis = ApiRegistry.with(catalogApiRef, catalogApi).with(
apiDocsConfigRef,
apiDocsConfig,
);
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
<TestApiProvider
apis={[
[catalogApiRef, catalogApi],
[apiDocsConfigRef, apiDocsConfig],
]}
>
{children}
</TestApiProvider>
);
});
@@ -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('<ConsumingComponentsCard />', () => {
const catalogApi: jest.Mocked<CatalogApi> = {
@@ -39,10 +38,10 @@ describe('<ConsumingComponentsCard />', () => {
let Wrapper: React.ComponentType;
beforeEach(() => {
const apis = ApiRegistry.with(catalogApiRef, catalogApi);
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
{children}
</TestApiProvider>
);
});
@@ -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('<ProvidingComponentsCard />', () => {
const catalogApi: jest.Mocked<CatalogApi> = {
@@ -39,10 +38,10 @@ describe('<ProvidingComponentsCard />', () => {
let Wrapper: React.ComponentType;
beforeEach(() => {
const apis = ApiRegistry.with(catalogApiRef, catalogApi);
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
{children}
</TestApiProvider>
);
});
@@ -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,
@@ -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(
<ApiProvider
apis={ApiRegistry.with(badgesApiRef, mockApi).with(
errorApiRef,
{} as ErrorApi,
)}
<TestApiProvider
apis={[
[badgesApiRef, mockApi],
[errorApiRef, {} as ErrorApi],
]}
>
<EntityProvider entity={mockEntity}>
<EntityBadgesDialog open />
</EntityProvider>
</ApiProvider>,
</TestApiProvider>,
);
await expect(
@@ -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 () => {
+20
View File
@@ -174,6 +174,26 @@ export class AwsS3DiscoveryProcessor implements CatalogProcessor {
): Promise<boolean>;
}
// 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<boolean>;
}
// 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)
@@ -0,0 +1,277 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getVoidLogger } from '@backstage/backend-common';
import { LocationSpec } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import { codeSearch } from './azure';
import {
AzureDevOpsDiscoveryProcessor,
parseUrl,
} from './AzureDevOpsDiscoveryProcessor';
jest.mock('./azure');
const mockCodeSearch = codeSearch as jest.MockedFunction<typeof codeSearch>;
describe('AzureDevOpsDiscoveryProcessor', () => {
describe('parseUrl', () => {
it('parses well formed URLs', () => {
expect(parseUrl('https://dev.azure.com/my-org/my-proj')).toEqual({
baseUrl: 'https://dev.azure.com',
org: 'my-org',
project: 'my-proj',
repo: '',
catalogPath: '/catalog-info.yaml',
});
expect(
parseUrl(
'https://dev.azure.com/spotify/engineering/_git/backstage?path=/catalog.yaml',
),
).toEqual({
baseUrl: 'https://dev.azure.com',
org: 'spotify',
project: 'engineering',
repo: 'backstage',
catalogPath: '/catalog.yaml',
});
expect(
parseUrl(
'https://azuredevops.mycompany.com/spotify/engineering/_git/backstage?path=/src/*/catalog.yaml',
),
).toEqual({
baseUrl: 'https://azuredevops.mycompany.com',
org: 'spotify',
project: 'engineering',
repo: 'backstage',
catalogPath: '/src/*/catalog.yaml',
});
});
it('throws on incorrectly formed URLs', () => {
expect(() => parseUrl('https://dev.azure.com')).toThrow();
expect(() => parseUrl('https://dev.azure.com//')).toThrow();
expect(() => parseUrl('https://dev.azure.com//foo')).toThrow();
});
});
describe('reject unrelated entries', () => {
it('rejects unknown types', async () => {
const processor = AzureDevOpsDiscoveryProcessor.fromConfig(
new ConfigReader({
integrations: {
azure: [{ host: 'dev.azure.com', token: 'blob' }],
},
}),
{ logger: getVoidLogger() },
);
const location: LocationSpec = {
type: 'not-azure-discovery',
target: 'https://dev.azure.com',
};
await expect(
processor.readLocation(location, false, () => {}),
).resolves.toBeFalsy();
});
});
it('rejects unknown targets', async () => {
const processor = AzureDevOpsDiscoveryProcessor.fromConfig(
new ConfigReader({
integrations: {
github: [
{ host: 'dev.azure.com', token: 'blob' },
{ host: 'azure.myorg.com', token: 'blob' },
],
},
}),
{ logger: getVoidLogger() },
);
const location: LocationSpec = {
type: 'azure-discovery',
target: 'https://not.azure.com/org/project',
};
await expect(
processor.readLocation(location, false, () => {}),
).rejects.toThrow(
/There is no Azure integration that matches https:\/\/not.azure.com\/org\/project. Please add a configuration entry for it under integrations.azure/,
);
});
describe('handles repositories', () => {
const processor = AzureDevOpsDiscoveryProcessor.fromConfig(
new ConfigReader({
integrations: {
github: [{ host: 'dev.azure.com', token: 'blob' }],
},
}),
{ logger: getVoidLogger() },
);
beforeEach(() => {
mockCodeSearch.mockClear();
});
it('output all locations found on from code search', async () => {
const location: LocationSpec = {
type: 'azure-discovery',
target: 'https://dev.azure.com/shopify/engineering',
};
mockCodeSearch.mockResolvedValueOnce([
{
fileName: 'catalog-info.yaml',
path: '/catalog-info.yaml',
repository: {
name: 'backstage',
},
},
{
fileName: 'catalog-info.yaml',
path: '/src/catalog-info.yaml',
repository: {
name: 'ios-app',
},
},
]);
const emitter = jest.fn();
await processor.readLocation(location, false, emitter);
expect(mockCodeSearch).toHaveBeenCalledWith(
{ host: 'dev.azure.com' },
'shopify',
'engineering',
'',
'/catalog-info.yaml',
);
expect(emitter).toHaveBeenCalledTimes(2);
expect(emitter).toHaveBeenCalledWith({
type: 'location',
location: {
type: 'url',
target:
'https://dev.azure.com/shopify/engineering/_git/backstage?path=/catalog-info.yaml',
},
optional: true,
});
expect(emitter).toHaveBeenCalledWith({
type: 'location',
location: {
type: 'url',
target:
'https://dev.azure.com/shopify/engineering/_git/ios-app?path=/src/catalog-info.yaml',
},
optional: true,
});
});
it('output single locations from code search', async () => {
const location: LocationSpec = {
type: 'azure-discovery',
target: 'https://dev.azure.com/shopify/engineering/_git/backstage',
};
mockCodeSearch.mockResolvedValueOnce([
{
fileName: 'catalog-info.yaml',
path: '/catalog-info.yaml',
repository: {
name: 'backstage',
},
},
]);
const emitter = jest.fn();
await processor.readLocation(location, false, emitter);
expect(mockCodeSearch).toHaveBeenCalledWith(
{ host: 'dev.azure.com' },
'shopify',
'engineering',
'backstage',
'/catalog-info.yaml',
);
expect(emitter).toHaveBeenCalledTimes(1);
expect(emitter).toHaveBeenCalledWith({
type: 'location',
location: {
type: 'url',
target:
'https://dev.azure.com/shopify/engineering/_git/backstage?path=/catalog-info.yaml',
},
optional: true,
});
});
it('output single locations with different file name from code search', async () => {
const location: LocationSpec = {
type: 'azure-discovery',
target:
'https://dev.azure.com/shopify/engineering?path=/src/*/catalog.yaml',
};
mockCodeSearch.mockResolvedValueOnce([
{
fileName: 'catalog.yaml',
path: '/src/main/catalog.yaml',
repository: {
name: 'backstage',
},
},
]);
const emitter = jest.fn();
await processor.readLocation(location, false, emitter);
expect(mockCodeSearch).toHaveBeenCalledWith(
{ host: 'dev.azure.com' },
'shopify',
'engineering',
'',
'/src/*/catalog.yaml',
);
expect(emitter).toHaveBeenCalledTimes(1);
expect(emitter).toHaveBeenCalledWith({
type: 'location',
location: {
type: 'url',
target:
'https://dev.azure.com/shopify/engineering/_git/backstage?path=/src/main/catalog.yaml',
},
optional: true,
});
});
it('output nothing when code search does not find anything', async () => {
const location: LocationSpec = {
type: 'azure-discovery',
target: 'https://dev.azure.com/shopify/engineering/_git/backstage',
};
mockCodeSearch.mockResolvedValueOnce([]);
const emitter = jest.fn();
await processor.readLocation(location, false, emitter);
expect(mockCodeSearch).toHaveBeenCalledWith(
{ host: 'dev.azure.com' },
'shopify',
'engineering',
'backstage',
'/catalog-info.yaml',
);
expect(emitter).not.toHaveBeenCalled();
});
});
});
@@ -0,0 +1,151 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { LocationSpec } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
import { Logger } from 'winston';
import * as results from './results';
import { CatalogProcessor, CatalogProcessorEmit } from './types';
import { codeSearch } from './azure';
/**
* Extracts repositories out of an Azure DevOps org.
*
* The following will create locations for all projects which have a catalog-info.yaml
* on the default branch. The first is shorthand for the second.
*
* target: "https://dev.azure.com/org/project"
* or
* target: https://dev.azure.com/org/project?path=/catalog-info.yaml
*
* You may also explicitly specify a single repo:
*
* target: https://dev.azure.com/org/project/_git/repo
**/
export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor {
private readonly integrations: ScmIntegrations;
private readonly logger: Logger;
static fromConfig(config: Config, options: { logger: Logger }) {
const integrations = ScmIntegrations.fromConfig(config);
return new AzureDevOpsDiscoveryProcessor({
...options,
integrations,
});
}
constructor(options: { integrations: ScmIntegrations; logger: Logger }) {
this.integrations = options.integrations;
this.logger = options.logger;
}
async readLocation(
location: LocationSpec,
_optional: boolean,
emit: CatalogProcessorEmit,
): Promise<boolean> {
if (location.type !== 'azure-discovery') {
return false;
}
const azureConfig = this.integrations.azure.byUrl(location.target)?.config;
if (!azureConfig) {
throw new Error(
`There is no Azure integration that matches ${location.target}. Please add a configuration entry for it under integrations.azure`,
);
}
const { baseUrl, org, project, repo, catalogPath } = parseUrl(
location.target,
);
this.logger.info(
`Reading Azure DevOps repositories from ${location.target}`,
);
const files = await codeSearch(
azureConfig,
org,
project,
repo,
catalogPath,
);
this.logger.debug(
`Found ${files.length} files in Azure DevOps from ${location.target}.`,
);
for (const file of files) {
emit(
results.location(
{
type: 'url',
target: `${baseUrl}/${org}/${project}/_git/${file.repository.name}?path=${file.path}`,
},
// Not all locations may actually exist, since the user defined them as a wildcard pattern.
// Thus, we emit them as optional and let the downstream processor find them while not outputting
// an error if it couldn't.
true,
),
);
}
return true;
}
}
/**
* parseUrl extracts segments from the Azure DevOps URL.
**/
export function parseUrl(urlString: string): {
baseUrl: string;
org: string;
project: string;
repo: string;
catalogPath: string;
} {
const url = new URL(urlString);
const path = url.pathname.substr(1).split('/');
const catalogPath = url.searchParams.get('path') || '/catalog-info.yaml';
if (path.length === 2 && path[0].length && path[1].length) {
return {
baseUrl: url.origin,
org: decodeURIComponent(path[0]),
project: decodeURIComponent(path[1]),
repo: '',
catalogPath,
};
} else if (
path.length === 4 &&
path[0].length &&
path[1].length &&
path[2].length &&
path[3].length
) {
return {
baseUrl: url.origin,
org: decodeURIComponent(path[0]),
project: decodeURIComponent(path[1]),
repo: decodeURIComponent(path[3]),
catalogPath,
};
}
throw new Error(`Failed to parse ${urlString}`);
}
@@ -0,0 +1,232 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { setupServer } from 'msw/node';
import { setupRequestMockHandlers } from '@backstage/test-utils';
import { rest } from 'msw';
import { codeSearch, CodeSearchResponse } from './azure';
describe('azure', () => {
const server = setupServer();
setupRequestMockHandlers(server);
describe('codeSearch', () => {
it('returns empty when nothing is found', async () => {
const response: CodeSearchResponse = { count: 0, results: [] };
server.use(
rest.post(
`https://almsearch.dev.azure.com/shopify/engineering/_apis/search/codesearchresults`,
(req, res, ctx) => {
expect(req.headers.get('Authorization')).toBe('Basic OkFCQw==');
expect(req.body).toEqual({
searchText: 'path:/catalog-info.yaml repo:*',
$skip: 0,
$top: 1000,
});
return res(ctx.json(response));
},
),
);
await expect(
codeSearch(
{ host: 'dev.azure.com', token: 'ABC' },
'shopify',
'engineering',
'',
'/catalog-info.yaml',
),
).resolves.toEqual([]);
});
});
it('returns entries when request matches some files', async () => {
const response: CodeSearchResponse = {
count: 2,
results: [
{
fileName: 'catalog-info.yaml',
path: '/catalog-info.yaml',
repository: {
name: 'backstage',
},
},
{
fileName: 'catalog-info.yaml',
path: '/catalog-info.yaml',
repository: {
name: 'ios-app',
},
},
],
};
server.use(
rest.post(
`https://almsearch.dev.azure.com/shopify/engineering/_apis/search/codesearchresults`,
(req, res, ctx) => {
expect(req.headers.get('Authorization')).toBe('Basic OkFCQw==');
expect(req.body).toEqual({
searchText: 'path:/catalog-info.yaml repo:*',
$skip: 0,
$top: 1000,
});
return res(ctx.json(response));
},
),
);
await expect(
codeSearch(
{ host: 'dev.azure.com', token: 'ABC' },
'shopify',
'engineering',
'',
'/catalog-info.yaml',
),
).resolves.toEqual(response.results);
});
it('searches in specific repo if parameter is set', async () => {
const response: CodeSearchResponse = {
count: 1,
results: [
{
fileName: 'catalog-info.yaml',
path: '/catalog-info.yaml',
repository: {
name: 'backstage',
},
},
],
};
server.use(
rest.post(
`https://almsearch.dev.azure.com/shopify/engineering/_apis/search/codesearchresults`,
(req, res, ctx) => {
expect(req.headers.get('Authorization')).toBe('Basic OkFCQw==');
expect(req.body).toEqual({
searchText: 'path:/catalog-info.yaml repo:backstage',
$skip: 0,
$top: 1000,
});
return res(ctx.json(response));
},
),
);
await expect(
codeSearch(
{ host: 'dev.azure.com', token: 'ABC' },
'shopify',
'engineering',
'backstage',
'/catalog-info.yaml',
),
).resolves.toEqual(response.results);
});
it('can search using onpremise api', async () => {
const response: CodeSearchResponse = {
count: 1,
results: [
{
fileName: 'catalog-info.yaml',
path: '/catalog-info.yaml',
repository: {
name: 'backstage',
},
},
],
};
server.use(
rest.post(
`https://azuredevops.mycompany.com/shopify/engineering/_apis/search/codesearchresults`,
(req, res, ctx) => {
expect(req.headers.get('Authorization')).toBe('Basic OkFCQw==');
expect(req.body).toEqual({
searchText: 'path:/catalog-info.yaml repo:*',
$skip: 0,
$top: 1000,
});
return res(ctx.json(response));
},
),
);
await expect(
codeSearch(
{ host: 'azuredevops.mycompany.com', token: 'ABC' },
'shopify',
'engineering',
'',
'/catalog-info.yaml',
),
).resolves.toEqual(response.results);
});
it('searches multiple pages if response contains many items', async () => {
const totalCount = 2401;
const generateItems = (count: number) => {
return Array.from(Array(count).keys()).map(_ => ({
fileName: 'catalog-info.yaml',
path: '/catalog-info.yaml',
repository: {
name: 'backstage',
},
}));
};
server.use(
rest.post(
`https://almsearch.dev.azure.com/shopify/engineering/_apis/search/codesearchresults`,
(req, res, ctx) => {
expect(req.headers.get('Authorization')).toBe('Basic OkFCQw==');
expect(req.body).toMatchObject({
searchText: 'path:/catalog-info.yaml repo:backstage',
$top: 1000,
});
const body = req.body as { $skip: number; $top: number };
const countItemsToReturn =
body.$top + body.$skip > totalCount
? totalCount - body.$skip
: body.$top;
return res(
ctx.json({
count: totalCount,
results: generateItems(countItemsToReturn),
}),
);
},
),
);
await expect(
codeSearch(
{ host: 'dev.azure.com', token: 'ABC' },
'shopify',
'engineering',
'backstage',
'/catalog-info.yaml',
),
).resolves.toHaveLength(totalCount);
});
});
@@ -0,0 +1,80 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fetch from 'cross-fetch';
import {
AzureIntegrationConfig,
getAzureRequestOptions,
} from '@backstage/integration';
export interface CodeSearchResponse {
count: number;
results: CodeSearchResultItem[];
}
export interface CodeSearchResultItem {
fileName: string;
path: string;
repository: {
name: string;
};
}
const isCloud = (host: string) => host === 'dev.azure.com';
const PAGE_SIZE = 1000;
// codeSearch returns all files that matches the given search path.
export async function codeSearch(
azureConfig: AzureIntegrationConfig,
org: string,
project: string,
repo: string,
path: string,
): Promise<CodeSearchResultItem[]> {
const searchBaseUrl = isCloud(azureConfig.host)
? 'https://almsearch.dev.azure.com'
: `https://${azureConfig.host}`;
const searchUrl = `${searchBaseUrl}/${org}/${project}/_apis/search/codesearchresults?api-version=6.0-preview.1`;
let items: CodeSearchResultItem[] = [];
let hasMorePages = true;
do {
const response = await fetch(searchUrl, {
...getAzureRequestOptions(azureConfig, {
'Content-Type': 'application/json',
}),
method: 'POST',
body: JSON.stringify({
searchText: `path:${path} repo:${repo || '*'}`,
$skip: items.length,
$top: PAGE_SIZE,
}),
});
if (response.status !== 200) {
throw new Error(
`Azure DevOps search failed with response status ${response.status}`,
);
}
const body: CodeSearchResponse = await response.json();
items = [...items, ...body.results];
hasMorePages = body.count > items.length;
} while (hasMorePages);
return items;
}
@@ -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';
@@ -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';
@@ -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 }),
@@ -148,6 +148,57 @@ describe('DefaultLocationServiceTest', () => {
expect(result.exists).toBe(true);
});
it('should fail when there are duplicate entities using dry run', async () => {
store.listLocations.mockResolvedValueOnce([]);
orchestrator.process.mockResolvedValueOnce({
ok: true,
state: {},
completedEntity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Location',
metadata: {
name: 'foo',
},
},
deferredEntities: [
{
entity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Location',
metadata: {
name: 'foo',
},
},
locationKey: 'file:///tmp/mock.yaml',
},
],
relations: [],
errors: [],
});
orchestrator.process.mockResolvedValueOnce({
ok: true,
state: {},
completedEntity: {
apiVersion: 'backstage.io/v1alpha1',
kind: 'Location',
metadata: {
name: 'foo',
},
},
deferredEntities: [],
relations: [],
errors: [],
});
await expect(
locationService.createLocation(
{ type: 'url', target: 'https://backstage.io/catalog-info.yaml' },
true,
),
).rejects.toThrowError('Duplicate nested entity: location:default/foo');
});
it('should return exists false when the location does not exist beforehand', async () => {
orchestrator.process.mockResolvedValueOnce({
ok: true,
@@ -19,6 +19,7 @@ import {
LocationSpec,
LOCATION_ANNOTATION,
ORIGIN_LOCATION_ANNOTATION,
stringifyEntityRef,
} from '@backstage/catalog-model';
import {
CatalogProcessingOrchestrator,
@@ -54,6 +55,43 @@ export class DefaultLocationService implements LocationService {
return this.store.deleteLocation(id);
}
private async processEntities(
unprocessedEntities: DeferredEntity[],
): Promise<Entity[]> {
const entities: Entity[] = [];
while (unprocessedEntities.length) {
const currentEntity = unprocessedEntities.pop();
if (!currentEntity) {
continue;
}
const processed = await this.orchestrator.process({
entity: currentEntity.entity,
state: {}, // we process without the existing cache
});
if (processed.ok) {
if (
entities.some(
e =>
stringifyEntityRef(e) ===
stringifyEntityRef(processed.completedEntity),
)
) {
throw new Error(
`Duplicate nested entity: ${stringifyEntityRef(
processed.completedEntity,
)}`,
);
}
unprocessedEntities.push(...processed.deferredEntities);
entities.push(processed.completedEntity);
} else {
throw Error(processed.errors.map(String).join(', '));
}
}
return entities;
}
private async dryRunCreateLocation(
spec: LocationSpec,
): Promise<{ location: Location; entities: Entity[]; exists?: boolean }> {
@@ -86,24 +124,7 @@ export class DefaultLocationService implements LocationService {
const unprocessedEntities: DeferredEntity[] = [
{ entity, locationKey: `${spec.type}:${spec.target}` },
];
const entities: Entity[] = [];
while (unprocessedEntities.length) {
const currentEntity = unprocessedEntities.pop();
if (!currentEntity) {
continue;
}
const processed = await this.orchestrator.process({
entity: currentEntity.entity,
state: {}, // we process without the existing cache
});
if (processed.ok) {
unprocessedEntities.push(...processed.deferredEntities);
entities.push(processed.completedEntity);
} else {
throw Error(processed.errors.map(String).join(', '));
}
}
const entities: Entity[] = await this.processEntities(unprocessedEntities);
return {
exists: await existsPromise,
@@ -44,6 +44,7 @@ import {
CatalogProcessorParser,
CodeOwnersProcessor,
FileReaderProcessor,
AzureDevOpsDiscoveryProcessor,
GithubDiscoveryProcessor,
GithubOrgReaderProcessor,
GitLabDiscoveryProcessor,
@@ -292,6 +293,7 @@ export class NextCatalogBuilder {
return [
new FileReaderProcessor(),
BitbucketDiscoveryProcessor.fromConfig(config, { logger }),
AzureDevOpsDiscoveryProcessor.fromConfig(config, { logger }),
GithubDiscoveryProcessor.fromConfig(config, { logger }),
GithubOrgReaderProcessor.fromConfig(config, { logger }),
GitLabDiscoveryProcessor.fromConfig(config, { logger }),
@@ -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);
},
);
});
});
@@ -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',
@@ -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('<CatalogGraphCard/>', () => {
let entity: Entity;
let wrapper: JSX.Element;
let catalog: jest.Mocked<CatalogApi>;
let apis: ApiRegistry;
let apis: TestApiRegistry;
beforeAll(() => {
Object.defineProperty(window.SVGElement.prototype, 'getBBox', {
@@ -61,7 +66,7 @@ describe('<CatalogGraphCard/>', () => {
refreshEntity: jest.fn(),
getEntityAncestors: jest.fn(),
};
apis = ApiRegistry.with(catalogApiRef, catalog);
apis = TestApiRegistry.from([catalogApiRef, catalog]);
wrapper = (
<ApiProvider apis={apis}>
@@ -123,9 +128,9 @@ describe('<CatalogGraphCard/>', () => {
test('captures analytics event on click', async () => {
const analyticsSpy = new MockAnalyticsApi();
const { findByText } = await renderInTestApp(
<ApiProvider apis={ApiRegistry.from([[analyticsApiRef, analyticsSpy]])}>
<TestApiProvider apis={[[analyticsApiRef, analyticsSpy]]}>
{wrapper}
</ApiProvider>,
</TestApiProvider>,
{
mountedRoutes: {
'/entity/{kind}/{namespace}/{name}': catalogEntityRouteRef,
@@ -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('<CatalogGraphPage/>', () => {
refreshEntity: jest.fn(),
getEntityAncestors: jest.fn(),
};
const apis = ApiRegistry.with(catalogApiRef, catalog);
wrapper = (
<ApiProvider apis={apis}>
<TestApiProvider apis={[[catalogApiRef, catalog]]}>
<CatalogGraphPage
initialState={{
showFilters: false,
@@ -101,7 +103,7 @@ describe('<CatalogGraphPage/>', () => {
selectedKinds: ['b'],
}}
/>
</ApiProvider>
</TestApiProvider>
);
});
@@ -172,9 +174,9 @@ describe('<CatalogGraphPage/>', () => {
test('should capture analytics event when selecting other entity', async () => {
const analyticsSpy = new MockAnalyticsApi();
const { getByText, findAllByTestId } = await renderInTestApp(
<ApiProvider apis={ApiRegistry.from([[analyticsApiRef, analyticsSpy]])}>
<TestApiProvider apis={[[analyticsApiRef, analyticsSpy]]}>
{wrapper}
</ApiProvider>,
</TestApiProvider>,
{
mountedRoutes: {
'/entity/{kind}/{namespace}/{name}': catalogEntityRouteRef,
@@ -195,9 +197,9 @@ describe('<CatalogGraphPage/>', () => {
test('should capture analytics event when navigating to entity', async () => {
const analyticsSpy = new MockAnalyticsApi();
const { getByText, findAllByTestId } = await renderInTestApp(
<ApiProvider apis={ApiRegistry.from([[analyticsApiRef, analyticsSpy]])}>
<TestApiProvider apis={[[analyticsApiRef, analyticsSpy]]}>
{wrapper}
</ApiProvider>,
</TestApiProvider>,
{
mountedRoutes: {
'/entity/{kind}/{namespace}/{name}': catalogEntityRouteRef,
@@ -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('<EntityRelationsGraph/>', () => {
refreshEntity: jest.fn(),
getEntityAncestors: jest.fn(),
};
const apis = ApiRegistry.with(catalogApiRef, catalog);
Wrapper = ({ children }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
<TestApiProvider apis={[[catalogApiRef, catalog]]}>
{children}
</TestApiProvider>
);
});
@@ -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('<DefaultImportPage />', () => {
},
};
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('<DefaultImportPage />', () => {
catalogApi: {} as any,
configApi: {} as any,
}),
);
],
);
});
it('renders without exploding', async () => {
@@ -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('<ImportInfoCard />', () => {
let apis: ApiRegistry;
let apis: TestApiRegistry;
let catalogImportApi: jest.Mocked<CatalogImportApi>;
beforeEach(() => {
@@ -35,26 +35,29 @@ describe('<ImportInfoCard />', () => {
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(
<ApiProvider apis={apis}>
<TestApiProvider
apis={[
[configApiRef, new ConfigReader({ integrations: {} })],
[catalogImportApiRef, catalogImportApi],
]}
>
<ImportInfoCard />
</ApiProvider>,
</TestApiProvider>,
);
expect(getByText('Register an existing component')).toBeInTheDocument();
@@ -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('<ImportPage />', () => {
},
};
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('<ImportPage />', () => {
catalogApi: {} as any,
configApi: new ConfigReader({}),
}),
);
],
);
});
afterEach(() => jest.resetAllMocks());
@@ -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('<StepInitAnalyzeUrl />', () => {
};
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider
apis={ApiRegistry.with(catalogImportApiRef, catalogImportApi).with(
errorApiRef,
errorApi,
)}
<TestApiProvider
apis={[
[catalogImportApiRef, catalogImportApi],
[errorApiRef, errorApi],
]}
>
{children}
</ApiProvider>
</TestApiProvider>
);
const location = {
@@ -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('<StepPrepareCreatePullRequest />', () => {
};
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider
apis={ApiRegistry.with(catalogImportApiRef, catalogImportApi)
.with(catalogApiRef, catalogApi)
.with(errorApiRef, errorApi)}
<TestApiProvider
apis={[
[catalogImportApiRef, catalogImportApi],
[catalogApiRef, catalogApi],
[errorApiRef, errorApi],
]}
>
{children}
</ApiProvider>
</TestApiProvider>
);
const onPrepareFn = jest.fn();
@@ -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('<EntityTypePicker/>', () => {
it('renders available entity types', async () => {
@@ -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 }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
<TestApiProvider
apis={[
[catalogApiRef, new CatalogClient({ discoveryApi })],
[alertApiRef, alertApi],
]}
>
{children}
</TestApiProvider>
);
const stateSpy = jest.spyOn(state, 'useUnregisterEntityDialogState');
@@ -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<T>(): { promise: Promise<T>; resolve: (value: T) => void } {
let resolve: (value: T) => void = () => {};
@@ -51,9 +51,9 @@ describe('useUnregisterEntityDialogState', () => {
const catalogApi = catalogApiMock as Partial<CatalogApi> as CatalogApi;
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={ApiRegistry.with(catalogApiRef, catalogApi)}>
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
{children}
</ApiProvider>
</TestApiProvider>
);
let entity: Entity;
@@ -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<IdentityApi>;
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';
@@ -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<CatalogApi> = {
const wrapper = ({ children }: PropsWithChildren<{}>) => {
return (
<ApiProvider apis={ApiRegistry.with(catalogApiRef, mockCatalogApi)}>
<TestApiProvider apis={[[catalogApiRef, mockCatalogApi]]}>
{children}
</ApiProvider>
</TestApiProvider>
);
};
@@ -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<CatalogApi> = {
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 (
<ApiProvider apis={apis}>
<TestApiProvider
apis={[
[configApiRef, mockConfigApi],
[catalogApiRef, mockCatalogApi],
[identityApiRef, mockIdentityApi],
[storageApiRef, MockStorageApi.create()],
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
}),
],
]}
>
<EntityListProvider>
<EntityKindPicker initialFilter="component" hidden />
<UserListPicker initialFilter={userFilter} />
{children}
</EntityListProvider>
</ApiProvider>
</TestApiProvider>
);
};
@@ -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 }) => (
<ApiProvider
apis={ApiRegistry.with(identityApiRef, identityApi).with(
catalogApiRef,
catalogApi,
)}
<TestApiProvider
apis={[
[identityApiRef, identityApi],
[catalogApiRef, catalogApi],
]}
>
{children}
</ApiProvider>
</TestApiProvider>
);
const ownedEntity: ComponentEntity = {
@@ -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<{}>) => (
<ApiProvider
apis={ApiRegistry.with(
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({ storageApi: mockStorage }),
)}
<TestApiProvider
apis={[
[
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({ storageApi: mockStorage }),
],
]}
>
{children}
</ApiProvider>
</TestApiProvider>
);
});
@@ -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<{}>) => (
<ApiProvider
apis={ApiRegistry.with(starredEntitiesApiRef, mockStarredEntitiesApi)}
>
<TestApiProvider apis={[[starredEntitiesApiRef, mockStarredEntitiesApi]]}>
{children}
</ApiProvider>
</TestApiProvider>
);
});
@@ -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('<AboutCard />', () => {
},
],
};
const apis = ApiRegistry.with(
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {},
}),
),
).with(catalogApiRef, catalogApi);
const { getByText } = await renderInTestApp(
<ApiProvider apis={apis}>
<TestApiProvider
apis={[
[
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {},
}),
),
],
[catalogApiRef, catalogApi],
]}
>
<EntityProvider entity={entity}>
<AboutCard />
</EntityProvider>
</ApiProvider>,
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
@@ -116,28 +116,32 @@ describe('<AboutCard />', () => {
lifecycle: 'production',
},
};
const apis = ApiRegistry.with(
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {
github: [
{
host: 'github.com',
token: '...',
},
],
},
}),
),
).with(catalogApiRef, catalogApi);
const { getByText } = await renderInTestApp(
<ApiProvider apis={apis}>
<TestApiProvider
apis={[
[
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {
github: [
{
host: 'github.com',
token: '...',
},
],
},
}),
),
],
[catalogApiRef, catalogApi],
]}
>
<EntityProvider entity={entity}>
<AboutCard />
</EntityProvider>
</ApiProvider>,
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
@@ -167,28 +171,32 @@ describe('<AboutCard />', () => {
lifecycle: 'production',
},
};
const apis = ApiRegistry.with(
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {
github: [
{
host: 'github.com',
token: '...',
},
],
},
}),
),
).with(catalogApiRef, catalogApi);
const { getByTitle } = await renderInTestApp(
<ApiProvider apis={apis}>
<TestApiProvider
apis={[
[
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {
github: [
{
host: 'github.com',
token: '...',
},
],
},
}),
),
],
[catalogApiRef, catalogApi],
]}
>
<EntityProvider entity={entity}>
<AboutCard />
</EntityProvider>
</ApiProvider>,
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
@@ -216,17 +224,21 @@ describe('<AboutCard />', () => {
lifecycle: 'production',
},
};
const apis = ApiRegistry.with(
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(new ConfigReader({})),
).with(catalogApiRef, catalogApi);
const { getByText } = await renderInTestApp(
<ApiProvider apis={apis}>
<TestApiProvider
apis={[
[
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(new ConfigReader({})),
],
[catalogApiRef, catalogApi],
]}
>
<EntityProvider entity={entity}>
<AboutCard />
</EntityProvider>
</ApiProvider>,
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
@@ -253,17 +265,21 @@ describe('<AboutCard />', () => {
lifecycle: 'production',
},
};
const apis = ApiRegistry.with(
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(new ConfigReader({})),
).with(catalogApiRef, catalogApi);
const { getByTitle } = await renderInTestApp(
<ApiProvider apis={apis}>
<TestApiProvider
apis={[
[
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(new ConfigReader({})),
],
[catalogApiRef, catalogApi],
]}
>
<EntityProvider entity={entity}>
<AboutCard />
</EntityProvider>
</ApiProvider>,
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
@@ -295,17 +311,21 @@ describe('<AboutCard />', () => {
lifecycle: 'production',
},
};
const apis = ApiRegistry.with(
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(new ConfigReader({})),
).with(catalogApiRef, catalogApi);
const { queryByTitle } = await renderInTestApp(
<ApiProvider apis={apis}>
<TestApiProvider
apis={[
[
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(new ConfigReader({})),
],
[catalogApiRef, catalogApi],
]}
>
<EntityProvider entity={entity}>
<AboutCard />
</EntityProvider>
</ApiProvider>,
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
@@ -332,28 +352,32 @@ describe('<AboutCard />', () => {
lifecycle: 'production',
},
};
const apis = ApiRegistry.with(
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {
github: [
{
host: 'github.com',
token: '...',
},
],
},
}),
),
).with(catalogApiRef, catalogApi);
const { getByText } = await renderInTestApp(
<ApiProvider apis={apis}>
<TestApiProvider
apis={[
[
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {
github: [
{
host: 'github.com',
token: '...',
},
],
},
}),
),
],
[catalogApiRef, catalogApi],
]}
>
<EntityProvider entity={entity}>
<AboutCard />
</EntityProvider>
</ApiProvider>,
</TestApiProvider>,
{
mountedRoutes: {
'/docs/:namespace/:kind/:name': viewTechDocRouteRef,
@@ -381,28 +405,32 @@ describe('<AboutCard />', () => {
lifecycle: 'production',
},
};
const apis = ApiRegistry.with(
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {
github: [
{
host: 'github.com',
token: '...',
},
],
},
}),
),
).with(catalogApiRef, catalogApi);
const { getByText } = await renderInTestApp(
<ApiProvider apis={apis}>
<TestApiProvider
apis={[
[
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {
github: [
{
host: 'github.com',
token: '...',
},
],
},
}),
),
],
[catalogApiRef, catalogApi],
]}
>
<EntityProvider entity={entity}>
<AboutCard />
</EntityProvider>
</ApiProvider>,
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
@@ -429,28 +457,32 @@ describe('<AboutCard />', () => {
lifecycle: 'production',
},
};
const apis = ApiRegistry.with(
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {
github: [
{
host: 'github.com',
token: '...',
},
],
},
}),
),
).with(catalogApiRef, catalogApi);
const { getByText } = await renderInTestApp(
<ApiProvider apis={apis}>
<TestApiProvider
apis={[
[
scmIntegrationsApiRef,
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {
github: [
{
host: 'github.com',
token: '...',
},
],
},
}),
),
],
[catalogApiRef, catalogApi],
]}
>
<EntityProvider entity={entity}>
<AboutCard />
</EntityProvider>
</ApiProvider>,
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
@@ -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<CatalogApi>);
const apis = TestApiRegistry.from([
catalogApiRef,
{
getEntities: jest.fn().mockResolvedValue({ items: entities }),
},
]);
describe('<CatalogKindHeader />', () => {
it('renders available kinds', async () => {
@@ -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(
<ApiProvider
apis={ApiRegistry.from([
<TestApiProvider
apis={[
[catalogApiRef, catalogApi],
[identityApiRef, identityApi],
[storageApiRef, storageApi],
@@ -138,10 +138,10 @@ describe('CatalogPage', () => {
starredEntitiesApiRef,
new DefaultStarredEntitiesApi({ storageApi }),
],
])}
]}
>
{children}
</ApiProvider>,
</TestApiProvider>,
{
mountedRoutes: {
'/create': createComponentRouteRef,
@@ -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();
@@ -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('<DependencyOfComponentsCard />', () => {
const catalogApi: jest.Mocked<CatalogApi> = {
getLocationById: jest.fn(),
getEntityByName: jest.fn(),
getEntities: jest.fn(),
addLocation: jest.fn(),
getLocationByEntity: jest.fn(),
removeEntityByUid: jest.fn(),
} as any;
const getEntities: jest.MockedFunction<CatalogApi['getEntities']> = jest.fn();
let Wrapper: React.ComponentType;
beforeEach(() => {
const apis = ApiRegistry.with(catalogApiRef, catalogApi);
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
<TestApiProvider apis={[[catalogApiRef, { getEntities }]]}>
{children}
</TestApiProvider>
);
});
@@ -97,7 +89,7 @@ describe('<DependencyOfComponentsCard />', () => {
},
],
};
catalogApi.getEntities.mockResolvedValue({
getEntities.mockResolvedValue({
items: [
{
apiVersion: 'v1',
@@ -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('<DependsOnComponentsCard />', () => {
const catalogApi: jest.Mocked<CatalogApi> = {
getLocationById: jest.fn(),
getEntityByName: jest.fn(),
getEntities: jest.fn(),
addLocation: jest.fn(),
getLocationByEntity: jest.fn(),
removeEntityByUid: jest.fn(),
} as any;
const getEntities: jest.MockedFunction<CatalogApi['getEntities']> = jest.fn();
let Wrapper: React.ComponentType;
beforeEach(() => {
const apis = ApiRegistry.with(catalogApiRef, catalogApi);
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
<TestApiProvider apis={[[catalogApiRef, { getEntities }]]}>
{children}
</TestApiProvider>
);
});
@@ -97,7 +89,7 @@ describe('<DependsOnComponentsCard />', () => {
},
],
};
catalogApi.getEntities.mockResolvedValue({
getEntities.mockResolvedValue({
items: [
{
apiVersion: 'v1',
@@ -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('<DependsOnResourcesCard />', () => {
const catalogApi: jest.Mocked<CatalogApi> = {
getLocationById: jest.fn(),
getEntityByName: jest.fn(),
getEntities: jest.fn(),
addLocation: jest.fn(),
getLocationByEntity: jest.fn(),
removeEntityByUid: jest.fn(),
} as any;
const getEntities: jest.MockedFunction<CatalogApi['getEntities']> = jest.fn();
let Wrapper: React.ComponentType;
beforeEach(() => {
const apis = ApiRegistry.with(catalogApiRef, catalogApi);
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
<TestApiProvider apis={[[catalogApiRef, { getEntities }]]}>
{children}
</TestApiProvider>
);
});
@@ -97,7 +89,7 @@ describe('<DependsOnResourcesCard />', () => {
},
],
};
catalogApi.getEntities.mockResolvedValue({
getEntities.mockResolvedValue({
items: [
{
apiVersion: 'v1',
@@ -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 () => {
@@ -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<AlertApi> = {
@@ -34,10 +33,6 @@ describe('DeleteEntityDialog', () => {
const catalogClient: jest.Mocked<CatalogApi> = {
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 }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
<TestApiProvider
apis={[
[catalogApiRef, catalogClient],
[alertApiRef, alertApi],
]}
>
{children}
</TestApiProvider>
);
afterEach(() => {
@@ -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('<EntityOrphanWarning />', () => {
const catalogClient: jest.Mocked<CatalogApi> = {
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('<EntityOrphanWarning />', () => {
};
const { getByText } = await renderInTestApp(
<ApiProvider apis={apis}>
<TestApiProvider
apis={[
[
catalogApiRef,
{
removeEntityByUid: jest.fn(),
},
],
]}
>
<EntityProvider entity={entity}>
<EntityOrphanWarning />
</EntityProvider>
</ApiProvider>,
</TestApiProvider>,
{
mountedRoutes: {
'/create': catalogRouteRef,
@@ -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('<EntityProcessErrors />', () => {
const catalogClient: jest.Mocked<CatalogApi> = {
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('<EntityProcessErrors />', () => {
},
};
catalogClient.getEntityAncestors.mockResolvedValue({
getEntityAncestors.mockResolvedValue({
root: getEntityName(entity),
items: [{ entity, parents: [] }],
});
@@ -198,7 +198,7 @@ describe('<EntityProcessErrors />', () => {
],
},
};
catalogClient.getEntityAncestors.mockResolvedValue({
getEntityAncestors.mockResolvedValue({
root: getEntityName(entity),
items: [
{ entity, parents: [getEntityName(parent)] },
@@ -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 }) => (
<ApiProvider apis={ApiRegistry.with(featureFlagsApiRef, mockFeatureFlagsApi)}>
<TestApiProvider apis={[[featureFlagsApiRef, mockFeatureFlagsApi]]}>
{children}
</ApiProvider>
</TestApiProvider>
);
describe('EntitySwitch', () => {
@@ -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('<HasComponentsCard />', () => {
const catalogApi: jest.Mocked<CatalogApi> = {
getLocationById: jest.fn(),
getEntityByName: jest.fn(),
getEntities: jest.fn(),
addLocation: jest.fn(),
getLocationByEntity: jest.fn(),
removeEntityByUid: jest.fn(),
} as any;
const getEntities: jest.MockedFunction<CatalogApi['getEntities']> = jest.fn();
let Wrapper: React.ComponentType;
beforeEach(() => {
const apis = ApiRegistry.with(catalogApiRef, catalogApi);
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
<TestApiProvider apis={[[catalogApiRef, { getEntities }]]}>
{children}
</TestApiProvider>
);
});
@@ -97,7 +89,7 @@ describe('<HasComponentsCard />', () => {
},
],
};
catalogApi.getEntities.mockResolvedValue({
getEntities.mockResolvedValue({
items: [
{
apiVersion: 'v1',
@@ -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('<HasResourcesCard />', () => {
const catalogApi: jest.Mocked<CatalogApi> = {
getLocationById: jest.fn(),
getEntityByName: jest.fn(),
getEntities: jest.fn(),
addLocation: jest.fn(),
getLocationByEntity: jest.fn(),
removeEntityByUid: jest.fn(),
} as any;
const getEntities: jest.MockedFunction<CatalogApi['getEntities']> = jest.fn();
let Wrapper: React.ComponentType;
beforeEach(() => {
const apis = ApiRegistry.with(catalogApiRef, catalogApi);
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
<TestApiProvider apis={[[catalogApiRef, { getEntities }]]}>
{children}
</TestApiProvider>
);
});
@@ -92,7 +84,7 @@ describe('<HasResourcesCard />', () => {
},
],
};
catalogApi.getEntities.mockResolvedValue({
getEntities.mockResolvedValue({
items: [
{
apiVersion: 'v1',
@@ -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('<HasSubcomponentsCard />', () => {
const catalogApi: jest.Mocked<CatalogApi> = {
getLocationById: jest.fn(),
getEntityByName: jest.fn(),
getEntities: jest.fn(),
addLocation: jest.fn(),
getLocationByEntity: jest.fn(),
removeEntityByUid: jest.fn(),
} as any;
const getEntities: jest.MockedFunction<CatalogApi['getEntities']> = jest.fn();
let Wrapper: React.ComponentType;
beforeEach(() => {
const apis = ApiRegistry.with(catalogApiRef, catalogApi);
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
<TestApiProvider apis={[[catalogApiRef, { getEntities }]]}>
{children}
</TestApiProvider>
);
});
@@ -97,7 +89,7 @@ describe('<HasSubcomponentsCard />', () => {
},
],
};
catalogApi.getEntities.mockResolvedValue({
getEntities.mockResolvedValue({
items: [
{
apiVersion: 'v1',
@@ -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('<HasSystemsCard />', () => {
const catalogApi: jest.Mocked<CatalogApi> = {
getLocationById: jest.fn(),
getEntityByName: jest.fn(),
getEntities: jest.fn(),
addLocation: jest.fn(),
getLocationByEntity: jest.fn(),
removeEntityByUid: jest.fn(),
} as any;
const getEntities: jest.MockedFunction<CatalogApi['getEntities']> = jest.fn();
let Wrapper: React.ComponentType;
beforeEach(() => {
const apis = ApiRegistry.with(catalogApiRef, catalogApi);
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
<TestApiProvider apis={[[catalogApiRef, { getEntities }]]}>
{children}
</TestApiProvider>
);
});
@@ -95,7 +87,7 @@ describe('<HasSystemsCard />', () => {
},
],
};
catalogApi.getEntities.mockResolvedValue({
getEntities.mockResolvedValue({
items: [
{
apiVersion: 'v1',
@@ -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('<SystemDiagramCard />', () => {
beforeAll(() => {
@@ -55,11 +54,11 @@ describe('<SystemDiagramCard />', () => {
};
const { queryByText } = await renderInTestApp(
<ApiProvider apis={ApiRegistry.from([[catalogApiRef, catalogApi]])}>
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<EntityProvider entity={entity}>
<SystemDiagramCard />
</EntityProvider>
</ApiProvider>,
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
@@ -114,11 +113,11 @@ describe('<SystemDiagramCard />', () => {
};
const { getByText } = await renderInTestApp(
<ApiProvider apis={ApiRegistry.from([[catalogApiRef, catalogApi]])}>
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<EntityProvider entity={entity}>
<SystemDiagramCard />
</EntityProvider>
</ApiProvider>,
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
@@ -173,11 +172,11 @@ describe('<SystemDiagramCard />', () => {
};
const { getByText } = await renderInTestApp(
<ApiProvider apis={ApiRegistry.from([[catalogApiRef, catalogApi]])}>
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<EntityProvider entity={entity}>
<SystemDiagramCard />
</EntityProvider>
</ApiProvider>,
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
@@ -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('<CostInsightsHeader/>', () => {
@@ -29,7 +29,7 @@ describe('<CostInsightsHeader/>', () => {
}),
};
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(
+1 -3
View File
@@ -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,
@@ -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<T> = PropsWithChildren<Partial<T>>;
@@ -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 <ApiProvider apis={defaultContext}>{children}</ApiProvider>;
return (
<TestApiProvider
apis={[
[identityApiRef, { ...defaultIdentityApi, ...context.identityApi }],
[
costInsightsApiRef,
{ ...defaultCostInsightsApi, ...context.costInsightsApi },
],
]}
>
{children}
</TestApiProvider>
);
};
@@ -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('<DefaultExplorePage />', () => {
const catalogApi: jest.Mocked<typeof catalogApiRef.T> = {
@@ -36,9 +35,9 @@ describe('<DefaultExplorePage />', () => {
};
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={ApiRegistry.with(catalogApiRef, catalogApi)}>
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
{children}
</ApiProvider>
</TestApiProvider>
);
beforeEach(() => {
@@ -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('<DomainExplorerContent />', () => {
const catalogApi: jest.Mocked<typeof catalogApiRef.T> = {
@@ -38,9 +37,9 @@ describe('<DomainExplorerContent />', () => {
};
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={ApiRegistry.with(catalogApiRef, catalogApi)}>
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
{children}
</ApiProvider>
</TestApiProvider>
);
const mountedRoutes = {
@@ -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<FeatureFlagsApi> = {
registerFlag: jest.fn(),
};
const mockApis = ApiRegistry.with(featureFlagsApiRef, featureFlagsApi);
describe('<ExploreLayout />', () => {
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={mockApis}>{children}</ApiProvider>
<TestApiProvider apis={[[featureFlagsApiRef, featureFlagsApi]]}>
{children}
</TestApiProvider>
);
afterEach(() => {
@@ -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('<GroupsDiagram />', () => {
beforeAll(() => {
@@ -57,9 +56,9 @@ describe('<GroupsDiagram />', () => {
};
const { getByText } = await renderInTestApp(
<ApiProvider apis={ApiRegistry.from([[catalogApiRef, catalogApi]])}>
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
<GroupsDiagram />
</ApiProvider>,
</TestApiProvider>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
@@ -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('<GroupsExplorerContent />', () => {
const catalogApi: jest.Mocked<typeof catalogApiRef.T> = {
@@ -37,9 +36,9 @@ describe('<GroupsExplorerContent />', () => {
};
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={ApiRegistry.with(catalogApiRef, catalogApi)}>
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
{children}
</ApiProvider>
</TestApiProvider>
);
const mountedRoutes = {
@@ -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('<ToolExplorerContent />', () => {
const exploreToolsConfigApi: jest.Mocked<typeof exploreToolsConfigRef.T> = {
@@ -33,11 +32,9 @@ describe('<ToolExplorerContent />', () => {
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ThemeProvider theme={lightTheme}>
<ApiProvider
apis={ApiRegistry.with(exploreToolsConfigRef, exploreToolsConfigApi)}
>
<TestApiProvider apis={[[exploreToolsConfigRef, exploreToolsConfigApi]]}>
{children}
</ApiProvider>
</TestApiProvider>
</ThemeProvider>
);
@@ -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: () => {
@@ -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('<FossaCard />', () => {
const fossaApi: jest.Mocked<FossaApi> = {
@@ -30,10 +29,10 @@ describe('<FossaCard />', () => {
let Wrapper: React.ComponentType;
beforeEach(() => {
const apis = ApiRegistry.with(fossaApiRef, fossaApi);
Wrapper = ({ children }: { children?: React.ReactNode }) => (
<ApiProvider apis={apis}>{children}</ApiProvider>
<TestApiProvider apis={[[fossaApiRef, fossaApi]]}>
{children}
</TestApiProvider>
);
});

Some files were not shown because too many files have changed in this diff Show More