Merge branch 'master' into feat/rspack

This commit is contained in:
JounQin
2024-10-12 13:09:10 +08:00
committed by GitHub
149 changed files with 2479 additions and 1128 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-defaults': patch
---
Sensitive internal fields on `BackstageCredentials` objects are now defined as read-only properties in order to minimize risk of leakage.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-test-utils': patch
---
Minor doc string changes
+13
View File
@@ -0,0 +1,13 @@
---
'@backstage/backend-defaults': patch
---
The default root HTTP service implementation will now pretty-print JSON responses in development.
If you are overriding the `rootHttpRouterServiceFactory` with a `configure` function that doesn't call `applyDefaults`, you can introduce this functionality by adding the following snippet inside `configure`:
```ts
if (process.env.NODE_ENV === 'development') {
app.set('json spaces', 2);
}
```
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/test-utils': minor
'@backstage/frontend-test-utils': patch
---
Added a `mockApis` export, which will replace the `MockX` API implementation classes and their related types. This is analogous with the backend's `mockServices`.
**DEPRECATED** several old helpers:
- Deprecated `MockAnalyticsApi`, please use `mockApis.analytics` instead.
- Deprecated `MockConfigApi`, please use `mockApis.config` instead.
- Deprecated `MockPermissionApi`, please use `mockApis.permission` instead.
- Deprecated `MockStorageApi`, please use `mockApis.storage` instead.
- Deprecated `MockTranslationApi`, please use `mockApis.translation` instead.
@@ -90,7 +90,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4
# Identify comment to be updated
- name: Find comment for API Changes
+2 -2
View File
@@ -18,7 +18,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
# Fetch the commit that's merged into the base rather than the target ref
# This will let us diff only the contents of the PR, without fetching more history
@@ -46,7 +46,7 @@ jobs:
cat ${{ github.event_path }} > event.json
- name: Upload Artifacts
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
with:
name: preview-spec
path: |
@@ -27,7 +27,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
# Fetch the commit that's merged into the base rather than the target ref
# This will let us diff only the contents of the PR, without fetching more history
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
ref: '${{ github.event.pull_request.merge_commit_sha }}'
+3 -3
View File
@@ -32,7 +32,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
@@ -68,7 +68,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
@@ -209,7 +209,7 @@ jobs:
INTEGRATION_TEST_AZURE_TOKEN: ${{ secrets.INTEGRATION_TEST_AZURE_TOKEN }}
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: fetch master branch
run: git fetch origin master
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
egress-policy: audit
- name: checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
path: backstage
ref: ${{ github.event.client_payload.version && env.RELEASE_VERSION || github.ref }}
+7 -7
View File
@@ -54,7 +54,7 @@ jobs:
result-encoding: string
- name: checkout latest release
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
ref: refs/tags/${{ steps.find-release.outputs.result }}
@@ -73,7 +73,7 @@ jobs:
run: yarn build:api-docs
- name: upload API reference
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
with:
name: stable-reference
path: docs/reference/
@@ -96,7 +96,7 @@ jobs:
egress-policy: audit
- name: checkout master
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: use node.js 18.x
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
@@ -113,7 +113,7 @@ jobs:
run: yarn build:api-docs
- name: upload API reference
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
with:
name: next-reference
path: docs/reference/
@@ -130,7 +130,7 @@ jobs:
working-directory: storybook
- name: storybook upload
uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
with:
name: storybook
path: storybook/dist/
@@ -170,7 +170,7 @@ jobs:
# Stable docs
- name: checkout latest release
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
ref: refs/tags/${{ needs.stable.outputs.release }}
@@ -198,7 +198,7 @@ jobs:
# Next docs
- name: checkout master
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
clean: false
+1 -1
View File
@@ -68,7 +68,7 @@ jobs:
INTEGRATION_TEST_AZURE_TOKEN: ${{ secrets.INTEGRATION_TEST_AZURE_TOKEN }}
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
egress-policy: audit
- name: 'Checkout code'
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
persist-credentials: false
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
# Fetch changes to previous commit - required for 'only_changed' in Prettier action
fetch-depth: 0
@@ -16,7 +16,7 @@ jobs:
egress-policy: audit
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
fetch-depth: 2
ref: ${{ github.head_ref }}
+2 -2
View File
@@ -13,7 +13,7 @@ jobs:
egress-policy: audit
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
# 'v' prefix is added here for the tag, we keep it out of the manifest logic
ref: v${{ github.event.client_payload.version }}
@@ -35,7 +35,7 @@ jobs:
# Checkout backstage/versions into /backstage/versions, which is where store the output
- name: Checkout versions
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
repository: backstage/versions
path: versions
@@ -16,7 +16,7 @@ jobs:
egress-policy: audit
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
fetch-depth: 2
ref: ${{ github.head_ref }}
@@ -16,7 +16,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: use node.js 18.x
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Monitor and Synchronize Snyk Policies
uses: snyk/actions/node@cdb760004ba9ea4d525f2e043745dfe85bb9077e # master
with:
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
fetch-depth: 20000
fetch-tags: true
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Use Node.js 18.x
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
with:
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
# Vale does not support file excludes, so we use the script to generate a list of files instead
# The action also does not allow args or a local config file to be passed in, so the files array
+1 -1
View File
@@ -45,7 +45,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Configure Git
run: |
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0
with:
python-version: '3.9'
+1 -1
View File
@@ -42,7 +42,7 @@ jobs:
git config --global core.autocrlf false
git config --global core.eol lf
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Configure Git
run: |
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
egress-policy: audit
- name: Checkout
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Install Fossa
run: "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash"
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: use node.js 18.x
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
@@ -19,7 +19,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Use Node.js 18.x
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
fetch-depth: 0 # Required to retrieve git history
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
@@ -59,7 +59,7 @@ const backend = createBackend();
backend.add(
rootHttpRouterServiceFactory({
configure: ({ app, middleware, routes, config, logger, lifecycle }) => {
configure: ({ app, middleware, routes, config, logger, healthRouter }) => {
// Refer to https://expressjs.com/en/guide/writing-middleware.html on how to write express middleware
const customMiddleware = {
logging(): RequestHandler {
@@ -88,11 +88,18 @@ backend.add(
},
};
// The default implementation pretty-prints JSON responses in development
if (process.env.NODE_ENV === 'development') {
app.set('json spaces', 2);
}
// the built in middleware is provided through an option in the configure function
app.use(middleware.helmet());
app.use(middleware.cors());
app.use(middleware.compression());
app.use(healthRouter);
// you can add you your own middleware in here
app.use(customMiddleware.logging());
+12 -15
View File
@@ -364,22 +364,19 @@ load performance.
### Backend Development
The backend development bundling is also based on Webpack, but rather than
starting up a web server, the backend is started up using the
[`RunScriptWebpackPlugin`](https://www.npmjs.com/package/run-script-webpack-plugin).
The reason for using Webpack for development of the backend is both that it is a
convenient way to handle transpilation of a large set of packages, as well us
allowing us to use hot module replacement and maintaining state while reloading
individual backend modules. This is particularly useful when running the backend
with in-memory SQLite as the database choice.
The backend development setup does not use any bundling process. It runs a
Node.js process directly, with on-the-fly transpilation from TypeScript to
JavaScript. The transpilation is done with a custom transform based on
[SWC](https://swc.rs/).
Except for executing in Node.js rather than a web server, the backend
development bundling configuration is quite similar to the frontend one. It
shares most of the Webpack configuration, including the transpilation setup.
Some differences are that it does not inject any environment variables or node
module fallbacks, and it uses
[`webpack-node-externals`](https://www.npmjs.com/package/webpack-node-externals)
to avoid bundling in dependency modules.
During development the backend Node.js process will restart whenever there is a
change to the source code. This means that any in-memory data will be lost. In
order to store data between restarts, the backend process has an IPC channel
available to store and restore data from the parent CLI process. The primary
purpose of this, which is already built-in, is to restore the contents of
databases when using SQLite for development. You can also use it for your own
purposes, with the `DevDataStore` utility exported from the
`@backstage/backend-dev-utils` package.
If you want to inspect the running Node.js process, the `--inspect` and
`--inspect-brk` flags can be used, as they will be passed through as options to
+3 -3
View File
@@ -7,7 +7,7 @@ description: Finding performance bottlenecks in your Backstage application
Profiling can help you find performance bottlenecks in your code. This guide will show you how to profile
both the backend and frontend of your Backstage application.
# Backend
## Backend
To profile the backend, start the backend with the `--inspect` flag:
@@ -27,7 +27,7 @@ You can also use the `Memory` tab to profile the backend's memory usage and find
It's recommended to start profiling with short periods of time to avoid too much data being collected.
## Stress testing
### Stress testing
To get more out of profiling, you might want to introduce additional load to your application with some tooling.
One such tool is called [AutoCannon](https://www.npmjs.com/package/autocannon) which can be used to stress test the
@@ -55,7 +55,7 @@ autocannon -H "Authorization=Bearer autocannon12345" http://localhost:7007/api/c
See more command options in the AutoCannon documentation.
# Frontend
## Frontend
Profiling the frontend can be done by using the `React DevTools` extension for Chrome or Firefox.
The extension is available for download from the Chrome Web Store or the Firefox Add-ons website.
@@ -22,7 +22,7 @@ import {
} from '@backstage/plugin-catalog-react';
import { permissionApiRef } from '@backstage/plugin-permission-react';
import {
MockPermissionApi,
mockApis,
renderInTestApp,
TestApiProvider,
} from '@backstage/test-utils';
@@ -46,7 +46,6 @@ describe('EntityPage Test', () => {
},
};
const mockPermissionApi = new MockPermissionApi();
const rootRouteRef = catalogPlugin.routes.catalogIndex;
describe('cicdContent', () => {
@@ -55,7 +54,7 @@ describe('EntityPage Test', () => {
<TestApiProvider
apis={[
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
[permissionApiRef, mockPermissionApi],
[permissionApiRef, mockApis.permission()],
]}
>
<EntityProvider entity={entity}>
@@ -0,0 +1,67 @@
/*
* Copyright 2024 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 {
createCredentialsWithNonePrincipal,
createCredentialsWithServicePrincipal,
createCredentialsWithUserPrincipal,
} from './helpers';
describe('credentials', () => {
it('should be created', () => {
expect(createCredentialsWithServicePrincipal('my-service')).toEqual({
$$type: '@backstage/BackstageCredentials',
version: 'v1',
principal: {
type: 'service',
subject: 'my-service',
},
});
expect(
createCredentialsWithUserPrincipal('user:default/mock', 'my-token'),
).toEqual({
$$type: '@backstage/BackstageCredentials',
version: 'v1',
principal: {
type: 'user',
userEntityRef: 'user:default/mock',
},
});
expect(createCredentialsWithNonePrincipal()).toEqual({
$$type: '@backstage/BackstageCredentials',
version: 'v1',
principal: {
type: 'none',
},
});
});
it('should not include tokens when serialized', () => {
expect(
JSON.stringify(
createCredentialsWithServicePrincipal('my-service', 'my-token'),
),
).not.toMatch(/my-token/);
expect(
JSON.stringify(
createCredentialsWithUserPrincipal('user:default/mock', 'my-token'),
),
).not.toMatch(/my-token/);
});
});
@@ -28,16 +28,23 @@ export function createCredentialsWithServicePrincipal(
token?: string,
accessRestrictions?: BackstagePrincipalAccessRestrictions,
): InternalBackstageCredentials<BackstageServicePrincipal> {
return {
$$type: '@backstage/BackstageCredentials',
version: 'v1',
token,
principal: {
type: 'service',
subject: sub,
accessRestrictions,
return Object.defineProperty(
{
$$type: '@backstage/BackstageCredentials',
version: 'v1',
principal: {
type: 'service',
subject: sub,
accessRestrictions,
},
},
};
'token',
{
enumerable: false,
configurable: true,
value: token,
},
);
}
export function createCredentialsWithUserPrincipal(
@@ -45,16 +52,23 @@ export function createCredentialsWithUserPrincipal(
token: string,
expiresAt?: Date,
): InternalBackstageCredentials<BackstageUserPrincipal> {
return {
$$type: '@backstage/BackstageCredentials',
version: 'v1',
token,
expiresAt,
principal: {
type: 'user',
userEntityRef: sub,
return Object.defineProperty(
{
$$type: '@backstage/BackstageCredentials',
version: 'v1',
expiresAt,
principal: {
type: 'user',
userEntityRef: sub,
},
},
};
'token',
{
enumerable: false,
configurable: true,
value: token,
},
);
}
export function createCredentialsWithNonePrincipal(): InternalBackstageCredentials<BackstageNonePrincipal> {
@@ -106,6 +106,9 @@ const rootHttpRouterServiceFactoryWithOptions = (
lifecycle,
healthRouter,
applyDefaults() {
if (process.env.NODE_ENV === 'development') {
app.set('json spaces', 2);
}
app.use(middleware.helmet());
app.use(middleware.cors());
app.use(middleware.compression());
+59 -60
View File
@@ -150,7 +150,7 @@ export function mockErrorHandler(): ErrorRequestHandler<
Record<string, any>
>;
// @public (undocumented)
// @public
export namespace mockServices {
// (undocumented)
export function auth(options?: {
@@ -491,65 +491,64 @@ export class TestDatabases {
// src/next/services/mockCredentials.d.ts:116:9 - (ae-undocumented) Missing documentation for "invalidToken".
// src/next/services/mockCredentials.d.ts:117:9 - (ae-undocumented) Missing documentation for "invalidHeader".
// src/next/services/mockServices.d.ts:5:1 - (ae-undocumented) Missing documentation for "ServiceMock".
// src/next/services/mockServices.d.ts:13:1 - (ae-undocumented) Missing documentation for "mockServices".
// src/next/services/mockServices.d.ts:14:5 - (ae-undocumented) Missing documentation for "rootConfig".
// src/next/services/mockServices.d.ts:15:5 - (ae-undocumented) Missing documentation for "rootConfig".
// src/next/services/mockServices.d.ts:16:9 - (ae-undocumented) Missing documentation for "Options".
// src/next/services/mockServices.d.ts:19:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:20:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:22:5 - (ae-undocumented) Missing documentation for "rootLogger".
// src/next/services/mockServices.d.ts:23:5 - (ae-undocumented) Missing documentation for "rootLogger".
// src/next/services/mockServices.d.ts:24:9 - (ae-undocumented) Missing documentation for "Options".
// src/next/services/mockServices.d.ts:27:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:28:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:30:5 - (ae-undocumented) Missing documentation for "auth".
// src/next/services/mockServices.d.ts:34:5 - (ae-undocumented) Missing documentation for "auth".
// src/next/services/mockServices.d.ts:35:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:36:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:38:5 - (ae-undocumented) Missing documentation for "discovery".
// src/next/services/mockServices.d.ts:39:5 - (ae-undocumented) Missing documentation for "discovery".
// src/next/services/mockServices.d.ts:40:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:41:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:61:5 - (ae-undocumented) Missing documentation for "httpAuth".
// src/next/services/mockServices.d.ts:72:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:82:5 - (ae-undocumented) Missing documentation for "userInfo".
// src/next/services/mockServices.d.ts:90:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:92:5 - (ae-undocumented) Missing documentation for "cache".
// src/next/services/mockServices.d.ts:93:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:94:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:96:5 - (ae-undocumented) Missing documentation for "database".
// src/next/services/mockServices.d.ts:97:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:98:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:100:5 - (ae-undocumented) Missing documentation for "rootHealth".
// src/next/services/mockServices.d.ts:101:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:102:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:104:5 - (ae-undocumented) Missing documentation for "httpRouter".
// src/next/services/mockServices.d.ts:105:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:106:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:108:5 - (ae-undocumented) Missing documentation for "rootHttpRouter".
// src/next/services/mockServices.d.ts:109:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:110:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:112:5 - (ae-undocumented) Missing documentation for "lifecycle".
// src/next/services/mockServices.d.ts:113:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:114:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:116:5 - (ae-undocumented) Missing documentation for "logger".
// src/next/services/mockServices.d.ts:117:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:118:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:120:5 - (ae-undocumented) Missing documentation for "permissions".
// src/next/services/mockServices.d.ts:121:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:122:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:124:5 - (ae-undocumented) Missing documentation for "rootLifecycle".
// src/next/services/mockServices.d.ts:125:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:126:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:128:5 - (ae-undocumented) Missing documentation for "scheduler".
// src/next/services/mockServices.d.ts:129:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:130:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:132:5 - (ae-undocumented) Missing documentation for "urlReader".
// src/next/services/mockServices.d.ts:133:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:134:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:136:5 - (ae-undocumented) Missing documentation for "events".
// src/next/services/mockServices.d.ts:137:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:138:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:53:5 - (ae-undocumented) Missing documentation for "rootConfig".
// src/next/services/mockServices.d.ts:54:5 - (ae-undocumented) Missing documentation for "rootConfig".
// src/next/services/mockServices.d.ts:55:9 - (ae-undocumented) Missing documentation for "Options".
// src/next/services/mockServices.d.ts:58:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:59:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:61:5 - (ae-undocumented) Missing documentation for "rootLogger".
// src/next/services/mockServices.d.ts:62:5 - (ae-undocumented) Missing documentation for "rootLogger".
// src/next/services/mockServices.d.ts:63:9 - (ae-undocumented) Missing documentation for "Options".
// src/next/services/mockServices.d.ts:66:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:67:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:69:5 - (ae-undocumented) Missing documentation for "auth".
// src/next/services/mockServices.d.ts:73:5 - (ae-undocumented) Missing documentation for "auth".
// src/next/services/mockServices.d.ts:74:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:75:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:77:5 - (ae-undocumented) Missing documentation for "discovery".
// src/next/services/mockServices.d.ts:78:5 - (ae-undocumented) Missing documentation for "discovery".
// src/next/services/mockServices.d.ts:79:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:80:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:100:5 - (ae-undocumented) Missing documentation for "httpAuth".
// src/next/services/mockServices.d.ts:111:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:121:5 - (ae-undocumented) Missing documentation for "userInfo".
// src/next/services/mockServices.d.ts:129:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:131:5 - (ae-undocumented) Missing documentation for "cache".
// src/next/services/mockServices.d.ts:132:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:133:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:135:5 - (ae-undocumented) Missing documentation for "database".
// src/next/services/mockServices.d.ts:136:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:137:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:139:5 - (ae-undocumented) Missing documentation for "rootHealth".
// src/next/services/mockServices.d.ts:140:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:141:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:143:5 - (ae-undocumented) Missing documentation for "httpRouter".
// src/next/services/mockServices.d.ts:144:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:145:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:147:5 - (ae-undocumented) Missing documentation for "rootHttpRouter".
// src/next/services/mockServices.d.ts:148:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:149:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:151:5 - (ae-undocumented) Missing documentation for "lifecycle".
// src/next/services/mockServices.d.ts:152:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:153:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:155:5 - (ae-undocumented) Missing documentation for "logger".
// src/next/services/mockServices.d.ts:156:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:157:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:159:5 - (ae-undocumented) Missing documentation for "permissions".
// src/next/services/mockServices.d.ts:160:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:161:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:163:5 - (ae-undocumented) Missing documentation for "rootLifecycle".
// src/next/services/mockServices.d.ts:164:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:165:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:167:5 - (ae-undocumented) Missing documentation for "scheduler".
// src/next/services/mockServices.d.ts:168:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:169:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:171:5 - (ae-undocumented) Missing documentation for "urlReader".
// src/next/services/mockServices.d.ts:172:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:173:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/services/mockServices.d.ts:175:5 - (ae-undocumented) Missing documentation for "events".
// src/next/services/mockServices.d.ts:176:15 - (ae-undocumented) Missing documentation for "factory".
// src/next/services/mockServices.d.ts:177:15 - (ae-undocumented) Missing documentation for "mock".
// src/next/wiring/TestBackend.d.ts:5:1 - (ae-undocumented) Missing documentation for "TestBackendOptions".
// src/next/wiring/TestBackend.d.ts:6:5 - (ae-undocumented) Missing documentation for "extensionPoints".
// src/next/wiring/TestBackend.d.ts:14:5 - (ae-undocumented) Missing documentation for "features".
@@ -128,7 +128,46 @@ function simpleMock<TService>(
}
/**
* Mock implementations of the core services, to be used in tests.
*
* @public
* @remarks
*
* There are some variations among the services depending on what needs tests
* might have, but overall there are three main usage patterns:
*
* 1. Creating an actual fake service instance, often with a simplified version
* of functionality, by calling the mock service itself as a function.
*
* ```ts
* // The function often accepts parameters that control its behavior
* const foo = mockServices.foo();
* ```
*
* 2. Creating a mock service, where all methods are replaced with jest mocks, by
* calling the service's `mock` function.
*
* ```ts
* // You can optionally supply a subset of its methods to implement
* const foo = mockServices.foo.mock({
* someMethod: () => 'mocked result',
* });
* // After exercising your test, you can make assertions on the mock:
* expect(foo.someMethod).toHaveBeenCalledTimes(2);
* expect(foo.otherMethod).toHaveBeenCalledWith(testData);
* ```
*
* 3. Creating a service factory that behaves similarly to the mock as per above.
*
* ```ts
* await startTestBackend({
* features: [
* mockServices.foo.factory({
* someMethod: () => 'mocked result',
* })
* ],
* });
* ```
*/
export namespace mockServices {
export function rootConfig(options?: rootConfig.Options): RootConfigService {
@@ -15,8 +15,8 @@
*/
import { ConfigReader } from '@backstage/config';
import { IdentityApi } from '@backstage/core-plugin-api';
import { IdentityAuthInjectorFetchMiddleware } from './IdentityAuthInjectorFetchMiddleware';
import { mockApis } from '@backstage/test-utils';
describe('IdentityAuthInjectorFetchMiddleware', () => {
it('creates using defaults', async () => {
@@ -58,10 +58,7 @@ describe('IdentityAuthInjectorFetchMiddleware', () => {
});
it('injects the header only when a token is available', async () => {
const tokenFunction = jest.fn();
const identityApi = {
getCredentials: tokenFunction,
} as unknown as IdentityApi;
const identityApi = mockApis.identity.mock();
const middleware = new IdentityAuthInjectorFetchMiddleware(
identityApi,
@@ -73,27 +70,25 @@ describe('IdentityAuthInjectorFetchMiddleware', () => {
const outer = middleware.apply(inner);
// No token available
tokenFunction.mockResolvedValueOnce({ token: undefined });
identityApi.getCredentials.mockResolvedValueOnce({ token: undefined });
await outer(new Request('https://example.com'));
expect([...inner.mock.calls[0][0].headers.entries()]).toEqual([]);
// Supply a token, header gets added
tokenFunction.mockResolvedValueOnce({ token: 'token' });
identityApi.getCredentials.mockResolvedValueOnce({ token: 'token' });
await outer(new Request('https://example.com'));
expect([...inner.mock.calls[1][0].headers.entries()]).toEqual([
['authorization', 'Bearer token'],
]);
// Token no longer available
tokenFunction.mockResolvedValueOnce({ token: undefined });
identityApi.getCredentials.mockResolvedValueOnce({ token: undefined });
await outer(new Request('https://example.com'));
expect([...inner.mock.calls[2][0].headers.entries()]).toEqual([]);
});
it('does not overwrite an existing header with the same name', async () => {
const identityApi = {
getCredentials: () => ({ token: 'token' }),
} as unknown as IdentityApi;
const identityApi = mockApis.identity({ token: 'token' });
const middleware = new IdentityAuthInjectorFetchMiddleware(
identityApi,
@@ -14,15 +14,14 @@
* limitations under the License.
*/
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { mockApis } from '@backstage/test-utils';
import { PluginProtocolResolverFetchMiddleware } from './PluginProtocolResolverFetchMiddleware';
describe('PluginProtocolResolverFetchMiddleware', () => {
it.each([['https://passthrough.com/a']])(
'passes through regular URLs, %p',
async url => {
const resolve = jest.fn();
const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi;
const discoveryApi = mockApis.discovery.mock();
const middleware = new PluginProtocolResolverFetchMiddleware(
discoveryApi,
);
@@ -31,7 +30,7 @@ describe('PluginProtocolResolverFetchMiddleware', () => {
await outer(url);
expect(inner.mock.calls[0][0]).toBe(url);
expect(resolve).not.toHaveBeenCalled();
expect(discoveryApi.getBaseUrl).not.toHaveBeenCalled();
},
);
@@ -76,30 +75,29 @@ describe('PluginProtocolResolverFetchMiddleware', () => {
])(
'resolves backstage URLs, %p',
async (original, host, resolved, result) => {
const resolve = jest.fn();
const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi;
const discoveryApi = mockApis.discovery.mock({
getBaseUrl: async () => resolved,
});
const middleware = new PluginProtocolResolverFetchMiddleware(
discoveryApi,
);
const inner = jest.fn();
const outer = middleware.apply(inner);
resolve.mockResolvedValueOnce(resolved);
await outer(original);
expect(inner.mock.calls[0][0]).toBe(result);
expect(resolve).toHaveBeenLastCalledWith(host);
expect(discoveryApi.getBaseUrl).toHaveBeenLastCalledWith(host);
},
);
it('properly supports transferring request bodies too', async () => {
const resolve = jest.fn();
const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi;
const discoveryApi = mockApis.discovery.mock({
getBaseUrl: async () => 'https://elsewhere.com',
});
const middleware = new PluginProtocolResolverFetchMiddleware(discoveryApi);
const inner = jest.fn();
const outer = middleware.apply(inner);
resolve.mockResolvedValue('https://elsewhere.com');
await outer('plugin://a', {
method: 'POST',
body: '123',
@@ -17,7 +17,7 @@
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import { UrlPatternDiscovery } from '../../DiscoveryApi';
import BitbucketAuth from './BitbucketAuth';
import { MockConfigApi } from '@backstage/test-utils';
import { mockApis } from '@backstage/test-utils';
const getSession = jest.fn();
@@ -33,7 +33,7 @@ describe('BitbucketAuth', () => {
jest.resetAllMocks();
});
const configApi = new MockConfigApi({});
const configApi = mockApis.config();
it.each([
['team api write_repository', ['team', 'api', 'write_repository']],
@@ -17,7 +17,7 @@
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import { UrlPatternDiscovery } from '../../DiscoveryApi';
import BitbucketServerAuth from './BitbucketServerAuth';
import { MockConfigApi } from '@backstage/test-utils';
import { mockApis } from '@backstage/test-utils';
const getSession = jest.fn();
@@ -41,7 +41,7 @@ describe('BitbucketServerAuth', () => {
['PROJECT_ADMIN', 'REPO_READ', 'ACCOUNT_WRITE'],
],
])(`should normalize scopes correctly - %p`, (scope, scopes) => {
const configApi = new MockConfigApi({});
const configApi = mockApis.config();
const bitbucketServerAuth = BitbucketServerAuth.create({
configApi: configApi,
@@ -17,7 +17,7 @@
import { UrlPatternDiscovery } from '../../DiscoveryApi';
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import GithubAuth from './GithubAuth';
import { MockConfigApi } from '@backstage/test-utils';
import { mockApis } from '@backstage/test-utils';
const getSession = jest.fn();
@@ -33,7 +33,7 @@ describe('GithubAuth', () => {
jest.resetAllMocks();
});
const configApi = new MockConfigApi({});
const configApi = mockApis.config();
it('should forward access token request to session manager', async () => {
const githubAuth = GithubAuth.create({
@@ -17,7 +17,7 @@
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import { UrlPatternDiscovery } from '../../DiscoveryApi';
import GitlabAuth from './GitlabAuth';
import { MockConfigApi } from '@backstage/test-utils';
import { mockApis } from '@backstage/test-utils';
const getSession = jest.fn();
@@ -40,7 +40,7 @@ describe('GitlabAuth', () => {
],
['read_repository sudo', ['read_repository', 'sudo']],
])(`should normalize scopes correctly - %p`, (scope, scopes) => {
const configApi = new MockConfigApi({});
const configApi = mockApis.config();
const gitlabAuth = GitlabAuth.create({
configApi: configApi,
@@ -17,7 +17,7 @@
import GoogleAuth from './GoogleAuth';
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import { UrlPatternDiscovery } from '../../DiscoveryApi';
import { MockConfigApi } from '@backstage/test-utils';
import { mockApis } from '@backstage/test-utils';
const PREFIX = 'https://www.googleapis.com/auth/';
@@ -59,7 +59,7 @@ describe('GoogleAuth', () => {
[`${PREFIX}profile`, [`${PREFIX}profile`]],
[`${PREFIX}openid`, [`${PREFIX}openid`]],
])(`should normalize scopes correctly - %p`, (scope, scopes) => {
const configApi = new MockConfigApi({});
const configApi = mockApis.config();
const googleAuth = GoogleAuth.create({
configApi: configApi,
@@ -17,7 +17,7 @@
import OAuth2 from './OAuth2';
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import { UrlPatternDiscovery } from '../../DiscoveryApi';
import { MockConfigApi } from '@backstage/test-utils';
import { mockApis } from '@backstage/test-utils';
const theFuture = new Date(Date.now() + 3600000);
const thePast = new Date(Date.now() - 10);
@@ -35,7 +35,7 @@ jest.mock('../../../../lib/AuthSessionManager', () => ({
},
}));
const configApi = new MockConfigApi({});
const configApi = mockApis.config();
describe('OAuth2', () => {
it('should get refreshed access token', async () => {
@@ -17,7 +17,7 @@
import OktaAuth from './OktaAuth';
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import { UrlPatternDiscovery } from '../../DiscoveryApi';
import { MockConfigApi } from '@backstage/test-utils';
import { mockApis } from '@backstage/test-utils';
const PREFIX = 'okta.';
@@ -51,7 +51,7 @@ describe('OktaAuth', () => {
[`${PREFIX}profile`, [`${PREFIX}profile`]],
[`${PREFIX}openid`, [`${PREFIX}openid`]],
])(`should normalize scopes correctly - %p`, (scope, scopes) => {
const configApi = new MockConfigApi({});
const configApi = mockApis.config();
const auth = OktaAuth.create({
configApi: configApi,
oauthRequestApi: new MockOAuthApi(),
@@ -14,9 +14,9 @@
* limitations under the License.
*/
import { LocalStorageFeatureFlags, NoOpAnalyticsApi } from '../apis';
import { LocalStorageFeatureFlags } from '../apis';
import {
MockAnalyticsApi,
mockApis,
renderWithEffects,
withLogCollector,
registerMswTestHooks,
@@ -59,7 +59,7 @@ describe('Integration Test', () => {
const noOpAnalyticsApi = createApiFactory(
analyticsApiRef,
new NoOpAnalyticsApi(),
mockApis.analytics(),
);
const noopErrorApi = createApiFactory(errorApiRef, {
error$() {
@@ -575,7 +575,7 @@ describe('Integration Test', () => {
});
it('should track route changes via analytics api', async () => {
const mockAnalyticsApi = new MockAnalyticsApi();
const mockAnalyticsApi = mockApis.analytics();
const apis = [createApiFactory(analyticsApiRef, mockAnalyticsApi)];
const app = new AppManager({
apis,
@@ -608,26 +608,27 @@ describe('Integration Test', () => {
);
// Capture initial and subsequent navigation events with expected context.
const capturedEvents = mockAnalyticsApi.getEvents();
expect(capturedEvents[0]).toMatchObject({
expect(mockAnalyticsApi.captureEvent).toHaveBeenCalledTimes(2);
expect(mockAnalyticsApi.captureEvent).toHaveBeenNthCalledWith(1, {
action: 'navigate',
subject: '/',
attributes: {},
context: {
extension: 'App',
pluginId: 'blob',
routeRef: 'ref-1-2',
},
});
expect(capturedEvents[1]).toMatchObject({
expect(mockAnalyticsApi.captureEvent).toHaveBeenNthCalledWith(2, {
action: 'navigate',
subject: '/foo',
attributes: {},
context: {
extension: 'App',
pluginId: 'plugin2',
routeRef: 'ref-2',
},
});
expect(capturedEvents).toHaveLength(2);
});
it('should throw some error when the route has duplicate params', async () => {
@@ -877,9 +878,9 @@ describe('Integration Test', () => {
}),
}),
};
const discoveryApiMock = {
getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7007/app'),
};
const discoveryApiMock = mockApis.discovery.mock({
getBaseUrl: async () => 'http://localhost:7007/app',
});
const app = new AppManager({
icons,
@@ -18,7 +18,6 @@ import React from 'react';
import {
AppComponents,
configApiRef,
IdentityApi,
identityApiRef,
SignInPageProps,
useApi,
@@ -30,7 +29,7 @@ import { render, screen } from '@testing-library/react';
import { AppRouter } from './AppRouter';
import useAsync from 'react-use/esm/useAsync';
import { AppContextProvider } from './AppContext';
import { TestApiProvider } from '@backstage/test-utils';
import { mockApis, TestApiProvider } from '@backstage/test-utils';
import { ConfigReader } from '@backstage/config';
function UserRefDisplay() {
@@ -78,13 +77,7 @@ describe('AppRouter', () => {
const appIdentityProxy = new AppIdentityProxy();
const SignInPage = (props: SignInPageProps) => {
props.onSignInSuccess({
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/test',
ownershipEntityRefs: ['user:default/test'],
}),
} as IdentityApi);
props.onSignInSuccess(mockApis.identity());
return null;
};
@@ -20,7 +20,7 @@ import {
createRouteRef,
} from '@backstage/core-plugin-api';
import { collectRouteIds, resolveRouteBindings } from './resolveRouteBindings';
import { MockConfigApi } from '@backstage/test-utils';
import { mockApis } from '@backstage/test-utils';
describe('resolveRouteBindings', () => {
it('runs happy path', () => {
@@ -30,7 +30,7 @@ describe('resolveRouteBindings', () => {
({ bind }) => {
bind(external, { myRoute: ref });
},
new MockConfigApi({}),
mockApis.config(),
[],
);
@@ -45,7 +45,7 @@ describe('resolveRouteBindings', () => {
({ bind }) => {
bind(external, { someOtherRoute: ref } as any);
},
new MockConfigApi({}),
mockApis.config(),
[],
),
).toThrow('Key someOtherRoute is not an existing external route');
@@ -56,8 +56,10 @@ describe('resolveRouteBindings', () => {
const myTarget = createRouteRef({ id: 'test' });
const result = resolveRouteBindings(
() => {},
new MockConfigApi({
app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } },
mockApis.config({
data: {
app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } },
},
}),
[
createPlugin({
@@ -84,8 +86,10 @@ describe('resolveRouteBindings', () => {
({ bind }) => {
bind({ mySource }, { mySource: false });
},
new MockConfigApi({
app: { routes: { bindings: { 'test.mySource': 'myTarget' } } },
mockApis.config({
data: {
app: { routes: { bindings: { 'test.mySource': 'myTarget' } } },
},
}),
[
createPlugin({
@@ -106,8 +110,8 @@ describe('resolveRouteBindings', () => {
({ bind }) => {
bind({ mySource }, { mySource: myTarget });
},
new MockConfigApi({
app: { routes: { bindings: { 'test.mySource': false } } },
mockApis.config({
data: { app: { routes: { bindings: { 'test.mySource': false } } } },
}),
[
createPlugin({
@@ -128,7 +132,7 @@ describe('resolveRouteBindings', () => {
expect(() =>
resolveRouteBindings(
() => {},
new MockConfigApi({ app: { routes: { bindings: 'derp' } } }),
mockApis.config({ data: { app: { routes: { bindings: 'derp' } } } }),
[],
),
).toThrow(
@@ -138,8 +142,8 @@ describe('resolveRouteBindings', () => {
expect(() =>
resolveRouteBindings(
() => {},
new MockConfigApi({
app: { routes: { bindings: { 'test.mySource': true } } },
mockApis.config({
data: { app: { routes: { bindings: { 'test.mySource': true } } } },
}),
[],
),
@@ -150,8 +154,10 @@ describe('resolveRouteBindings', () => {
expect(() =>
resolveRouteBindings(
() => {},
new MockConfigApi({
app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } },
mockApis.config({
data: {
app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } },
},
}),
[],
),
@@ -162,8 +168,10 @@ describe('resolveRouteBindings', () => {
expect(() =>
resolveRouteBindings(
() => {},
new MockConfigApi({
app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } },
mockApis.config({
data: {
app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } },
},
}),
[
createPlugin({
@@ -198,17 +206,17 @@ describe('resolveRouteBindings', () => {
});
// defaultTarget wins only if no bind or config matches
let result = resolveRouteBindings(() => {}, new MockConfigApi({}), [
plugin,
]);
let result = resolveRouteBindings(() => {}, mockApis.config(), [plugin]);
expect(result.get(source)).toBe(target1);
// config wins over defaultTarget
result = resolveRouteBindings(
() => {},
new MockConfigApi({
app: { routes: { bindings: { 'test.source': 'test.target2' } } },
mockApis.config({
data: {
app: { routes: { bindings: { 'test.source': 'test.target2' } } },
},
}),
[plugin],
);
@@ -220,7 +228,7 @@ describe('resolveRouteBindings', () => {
({ bind }) => {
bind(plugin.externalRoutes, { source: plugin.routes.target2 });
},
new MockConfigApi({}),
mockApis.config(),
[plugin],
);
@@ -257,17 +265,15 @@ describe('collectRouteIds', () => {
});
// resolves normally with no config
let result = resolveRouteBindings(() => {}, new MockConfigApi({}), [
plugin,
]);
let result = resolveRouteBindings(() => {}, mockApis.config(), [plugin]);
expect(result.get(source)).toBe(target1);
// can be disabled
result = resolveRouteBindings(
() => {},
new MockConfigApi({
app: { routes: { bindings: { 'test.source': false } } },
mockApis.config({
data: { app: { routes: { bindings: { 'test.source': false } } } },
}),
[plugin],
);
@@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TestApiProvider } from '@backstage/test-utils';
import { TestApiProvider, mockApis } from '@backstage/test-utils';
import React from 'react';
import { BackstageRouteObject } from './types';
import { fireEvent, render } from '@testing-library/react';
import { RouteTracker } from './RouteTracker';
import { Link, MemoryRouter, Route, Routes } from 'react-router-dom';
import {
AnalyticsApi,
analyticsApiRef,
createPlugin,
createRouteRef,
@@ -68,9 +68,7 @@ describe('RouteTracker', () => {
},
];
const mockedAnalytics: jest.Mocked<AnalyticsApi> = {
captureEvent: jest.fn(),
};
const mockedAnalytics = mockApis.analytics();
beforeEach(() => {
jest.clearAllMocks();
@@ -13,23 +13,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createMocks } from 'react-idle-timer';
// eslint-disable-next-line no-restricted-imports
import { MessageChannel } from 'worker_threads';
import { ApiProvider } from '@backstage/core-app-api';
import { identityApiRef } from '@backstage/core-plugin-api';
import { TestApiRegistry, renderInTestApp } from '@backstage/test-utils';
import {
TestApiRegistry,
renderInTestApp,
mockApis,
} from '@backstage/test-utils';
import React from 'react';
import { AutoLogout } from './AutoLogout';
import { cleanup } from '@testing-library/react';
// Mock the signOut function of identityApiRef
const mockSignOut = jest.fn();
const mockIdentityApi = {
signOut: mockSignOut,
getCredentials: jest.fn().mockReturnValue({ token: 'xxx' }),
};
const mockIdentityApi = mockApis.identity({ token: 'xxx' });
const apis = TestApiRegistry.from([identityApiRef, mockIdentityApi]);
describe('AutoLogout', () => {
@@ -16,24 +16,18 @@
import React from 'react';
import { fireEvent } from '@testing-library/react';
import { TestApiRegistry, renderInTestApp } from '@backstage/test-utils';
import {
TestApiRegistry,
renderInTestApp,
mockApis,
} from '@backstage/test-utils';
import { DismissableBanner } from './DismissableBanner';
import { ApiProvider, WebStorage } from '@backstage/core-app-api';
import { storageApiRef, StorageApi } from '@backstage/core-plugin-api';
import { ApiProvider } from '@backstage/core-app-api';
import { storageApiRef } from '@backstage/core-plugin-api';
import { screen } from '@testing-library/react';
describe('<DismissableBanner />', () => {
let apis: TestApiRegistry;
const mockErrorApi = { post: jest.fn(), error$: jest.fn() };
const createWebStorage = (): StorageApi => {
return WebStorage.create({
errorApi: mockErrorApi,
});
};
beforeEach(() => {
apis = TestApiRegistry.from([storageApiRef, createWebStorage()]);
});
const apis = TestApiRegistry.from([storageApiRef, mockApis.storage()]);
it('renders the message and the popover', async () => {
await renderInTestApp(
@@ -17,7 +17,7 @@
import React, { ComponentType } from 'react';
import { fireEvent, waitFor, screen, renderHook } from '@testing-library/react';
import {
MockAnalyticsApi,
mockApis,
TestApiProvider,
renderInTestApp,
} from '@backstage/test-utils';
@@ -71,7 +71,7 @@ describe('<Link />', () => {
it('captures click using analytics api', async () => {
const linkText = 'Navigate!';
const analyticsApi = new MockAnalyticsApi();
const analyticsApi = mockApis.analytics();
const customOnClick = jest.fn();
await renderInTestApp(
@@ -86,13 +86,15 @@ describe('<Link />', () => {
// Analytics event should have been fired.
await waitFor(() => {
expect(analyticsApi.getEvents()[0]).toMatchObject({
action: 'click',
subject: linkText,
attributes: {
to: '/test',
},
});
expect(analyticsApi.captureEvent).toHaveBeenCalledWith(
expect.objectContaining({
action: 'click',
subject: linkText,
attributes: {
to: '/test',
},
}),
);
// Custom onClick handler should have still been fired too.
expect(customOnClick).toHaveBeenCalled();
@@ -101,7 +103,7 @@ describe('<Link />', () => {
it('does not capture click when noTrack is set', async () => {
const linkText = 'Navigate!';
const analyticsApi = new MockAnalyticsApi();
const analyticsApi = mockApis.analytics();
const customOnClick = jest.fn();
await renderInTestApp(
@@ -120,7 +122,7 @@ describe('<Link />', () => {
expect(customOnClick).toHaveBeenCalled();
// But there should be no analytics event.
expect(analyticsApi.getEvents()).toHaveLength(0);
expect(analyticsApi.captureEvent).not.toHaveBeenCalled();
});
});
@@ -16,7 +16,7 @@
import { configApiRef } from '@backstage/core-plugin-api';
import {
MockConfigApi,
mockApis,
renderInTestApp,
TestApiProvider,
} from '@backstage/test-utils';
@@ -24,17 +24,19 @@ import { act, fireEvent, screen } from '@testing-library/react';
import React from 'react';
import { SupportButton } from './SupportButton';
const configApi = new MockConfigApi({
app: {
support: {
url: 'https://github.com',
items: [
{
title: 'Github',
icon: 'github',
links: [{ title: 'Github Issues', url: '/issues' }],
},
],
const configApi = mockApis.config({
data: {
app: {
support: {
url: 'https://github.com',
items: [
{
title: 'Github',
icon: 'github',
links: [{ title: 'Github Issues', url: '/issues' }],
},
],
},
},
},
});
@@ -20,6 +20,7 @@ import { rest } from 'msw';
import { setupServer } from 'msw/node';
import {
TestApiProvider,
mockApis,
registerMswTestHooks,
wrapInTestApp,
} from '@backstage/test-utils';
@@ -37,9 +38,7 @@ describe('ProxiedSignInPage', () => {
apis={[
[
discoveryApiRef,
{
getBaseUrl: async () => 'http://example.com/api/auth',
},
mockApis.discovery({ baseUrl: 'http://example.com' }),
],
]}
>
@@ -16,7 +16,7 @@
import React from 'react';
import {
MockAnalyticsApi,
mockApis,
TestApiProvider,
renderInTestApp,
} from '@backstage/test-utils';
@@ -36,9 +36,8 @@ const useStyles = makeStyles({
},
});
let analyticsApiMock: MockAnalyticsApi;
const handleSidebarItemClick = jest.fn();
const analyticsApiMock = mockApis.analytics();
async function renderSidebar() {
const { result } = renderHook(() => useStyles());
@@ -79,7 +78,6 @@ async function renderSidebar() {
describe('Items', () => {
beforeEach(async () => {
jest.clearAllMocks();
analyticsApiMock = new MockAnalyticsApi();
await renderSidebar();
});
@@ -107,8 +105,7 @@ describe('Items', () => {
await screen.findByRole('button', { name: /create/i }),
);
expect(handleSidebarItemClick).toHaveBeenCalledTimes(1);
expect(analyticsApiMock.getEvents()).toHaveLength(1);
expect(analyticsApiMock.getEvents()[0]).toMatchObject({
expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({
action: 'click',
subject: 'Create...',
context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' },
@@ -119,8 +116,7 @@ describe('Items', () => {
it('should send link clicks to analytics', async () => {
await userEvent.click(await screen.findByRole('link', { name: /docs/i }));
expect(handleSidebarItemClick).toHaveBeenCalledTimes(1);
expect(analyticsApiMock.getEvents()).toHaveLength(1);
expect(analyticsApiMock.getEvents()[0]).toMatchObject({
expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({
action: 'click',
subject: 'Docs',
context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' },
@@ -132,10 +128,10 @@ describe('Items', () => {
await userEvent.click(
await screen.findByRole('link', { name: /explore/i }),
);
expect(handleSidebarItemClick).toHaveBeenCalledTimes(1);
expect(analyticsApiMock.getEvents()).toHaveLength(0);
expect(analyticsApiMock.captureEvent).not.toHaveBeenCalled();
});
});
describe('SidebarSearchField', () => {
it('should be defaultPrevented when enter is pressed', async () => {
const searchEvent = createEvent.keyDown(
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TestApiProvider } from '@backstage/test-utils';
import React, { useEffect } from 'react';
import { BackstageRouteObject } from './types';
@@ -28,7 +28,7 @@ import {
createFrontendPlugin,
createRouteRef,
} from '@backstage/frontend-plugin-api';
import { MockConfigApi, TestApiRegistry } from '@backstage/test-utils';
import { mockApis, TestApiRegistry } from '@backstage/test-utils';
import appPlugin from '@backstage/plugin-app';
import { readAppExtensionsConfig } from '../tree/readAppExtensionsConfig';
@@ -92,7 +92,7 @@ function routeInfoFromExtensions(extensions: ExtensionDefinition[]) {
builtinExtensions: [
resolveExtensionDefinition(Root, { namespace: 'root' }),
],
parameters: readAppExtensionsConfig(new MockConfigApi({})),
parameters: readAppExtensionsConfig(mockApis.config()),
forbidden: new Set(['root']),
}),
);
@@ -29,7 +29,7 @@ import {
} from '@backstage/frontend-plugin-api';
import { screen, render } from '@testing-library/react';
import { createSpecializedApp } from './createSpecializedApp';
import { MockConfigApi } from '@backstage/test-utils';
import { mockApis } from '@backstage/test-utils';
import React from 'react';
import {
configApiRef,
@@ -98,7 +98,7 @@ describe('createSpecializedApp', () => {
it('should forward config', () => {
const app = createSpecializedApp({
config: new MockConfigApi({ test: 'foo' }),
config: mockApis.config({ data: { test: 'foo' } }),
features: [
createFrontendPlugin({
id: 'test',
@@ -26,7 +26,7 @@ import {
} from '@backstage/frontend-plugin-api';
import { screen, waitFor } from '@testing-library/react';
import { CreateAppFeatureLoader, createApp } from './createApp';
import { MockConfigApi, renderWithEffects } from '@backstage/test-utils';
import { mockApis, renderWithEffects } from '@backstage/test-utils';
import React from 'react';
import { featureFlagsApiRef, useApi } from '@backstage/core-plugin-api';
import appPlugin from '@backstage/plugin-app';
@@ -35,12 +35,14 @@ describe('createApp', () => {
it('should allow themes to be installed', async () => {
const app = createApp({
configLoader: async () => ({
config: new MockConfigApi({
app: {
extensions: [
{ 'theme:app/light': false },
{ 'theme:app/dark': false },
],
config: mockApis.config({
data: {
app: {
extensions: [
{ 'theme:app/light': false },
{ 'theme:app/dark': false },
],
},
},
}),
}),
@@ -72,7 +74,7 @@ describe('createApp', () => {
it('should deduplicate features keeping the last received one', async () => {
const duplicatedFeatureId = 'test';
const app = createApp({
configLoader: async () => ({ config: new MockConfigApi({}) }),
configLoader: async () => ({ config: mockApis.config() }),
features: [
createFrontendPlugin({
id: duplicatedFeatureId,
@@ -135,7 +137,7 @@ describe('createApp', () => {
const app = createApp({
configLoader: async () => ({
config: new MockConfigApi({ key: 'config-value' }),
config: mockApis.config({ data: { key: 'config-value' } }),
}),
features: [appPlugin, loader],
});
@@ -159,7 +161,7 @@ describe('createApp', () => {
const app = createApp({
configLoader: async () => ({
config: new MockConfigApi({}),
config: mockApis.config(),
}),
features: [loader],
});
@@ -173,7 +175,7 @@ describe('createApp', () => {
it('should register feature flags', async () => {
const app = createApp({
configLoader: async () => ({ config: new MockConfigApi({}) }),
configLoader: async () => ({ config: mockApis.config() }),
features: [
appPlugin.withOverrides({
extensions: [
@@ -227,7 +229,7 @@ describe('createApp', () => {
let appTreeApi: AppTreeApi | undefined = undefined;
const app = createApp({
configLoader: async () => ({ config: new MockConfigApi({}) }),
configLoader: async () => ({ config: mockApis.config() }),
features: [
createFrontendPlugin({
id: 'my-plugin',
@@ -15,14 +15,13 @@
*/
import {
IdentityApi,
SignInPageBlueprint,
createFrontendModule,
} from '@backstage/frontend-plugin-api';
import { render, screen, waitFor } from '@testing-library/react';
import React, { useEffect } from 'react';
import { createPublicSignInApp } from './createPublicSignInApp';
import { MockConfigApi } from '@backstage/test-utils';
import { mockApis } from '@backstage/test-utils';
describe('createPublicSignInApp', () => {
beforeEach(() => {
@@ -31,7 +30,7 @@ describe('createPublicSignInApp', () => {
it('should render a sign-in page', async () => {
const app = createPublicSignInApp({
configLoader: async () => ({ config: new MockConfigApi({}) }),
configLoader: async () => ({ config: mockApis.config() }),
features: [
createFrontendModule({
pluginId: 'app',
@@ -59,7 +58,7 @@ describe('createPublicSignInApp', () => {
.mockReturnValue();
const app = createPublicSignInApp({
configLoader: async () => ({ config: new MockConfigApi({}) }),
configLoader: async () => ({ config: mockApis.config() }),
features: [
createFrontendModule({
pluginId: 'app',
@@ -70,9 +69,9 @@ describe('createPublicSignInApp', () => {
async () =>
({ onSignInSuccess }) => {
useEffect(() => {
onSignInSuccess({
getCredentials: async () => ({ token: 'mock-token' }),
} as IdentityApi);
onSignInSuccess(
mockApis.identity({ token: 'mock-token' }),
);
}, [onSignInSuccess]);
return <div />;
},
@@ -17,7 +17,7 @@
import React, { useEffect } from 'react';
import { act, screen, waitFor } from '@testing-library/react';
import {
MockAnalyticsApi,
mockApis,
TestApiProvider,
withLogCollector,
} from '@backstage/test-utils';
@@ -93,7 +93,7 @@ describe('ExtensionBoundary', () => {
it('should wrap children with analytics context', async () => {
const action = 'render';
const subject = 'analytics';
const analyticsApiMock = new MockAnalyticsApi();
const analyticsApiMock = mockApis.analytics();
const AnalyticsComponent = () => {
const analytics = useAnalytics();
@@ -112,17 +112,15 @@ describe('ExtensionBoundary', () => {
);
await waitFor(() => {
const event = analyticsApiMock
.getEvents()
.find(e => e.subject === subject);
expect(event).toMatchObject({
action,
subject,
context: {
extensionId: 'test',
},
});
expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith(
expect.objectContaining({
action,
subject,
context: expect.objectContaining({
extensionId: 'test',
}),
}),
);
});
});
@@ -136,7 +134,7 @@ describe('ExtensionBoundary', () => {
});
return null;
};
const analyticsApiMock = new MockAnalyticsApi();
const analyticsApiMock = mockApis.analytics();
await act(async () => {
renderInTestApp(
@@ -147,7 +145,7 @@ describe('ExtensionBoundary', () => {
);
});
expect(analyticsApiMock.getEvents()).toEqual([
expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith(
expect.objectContaining({
action: 'navigate',
subject: '/',
@@ -156,7 +154,9 @@ describe('ExtensionBoundary', () => {
extensionId: 'test',
}),
}),
);
expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith(
expect.objectContaining({ action: 'dummy' }),
]);
);
});
});
@@ -23,7 +23,7 @@ import { JsonObject } from '@backstage/types';
import { createExtension } from './createExtension';
import { createExtensionDataRef } from './createExtensionDataRef';
import { coreExtensionData } from './coreExtensionData';
import { MockConfigApi, renderWithEffects } from '@backstage/test-utils';
import { mockApis, renderWithEffects } from '@backstage/test-utils';
import { createExtensionInput } from './createExtensionInput';
const nameExtensionDataRef = createExtensionDataRef<string>().with({
@@ -128,7 +128,7 @@ function createTestAppRoot({
}) {
return createApp({
features: [...features],
configLoader: async () => ({ config: new MockConfigApi(config) }),
configLoader: async () => ({ config: mockApis.config({ data: config }) }),
}).createRoot();
}
@@ -50,7 +50,6 @@
},
"peerDependencies": {
"@testing-library/react": "^16.0.0",
"@types/jest": "*",
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",
+5 -10
View File
@@ -3,13 +3,12 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="jest" />
/// <reference types="react" />
import { AnalyticsApi } from '@backstage/frontend-plugin-api';
import { AnalyticsEvent } from '@backstage/frontend-plugin-api';
import { AnyExtensionDataRef } from '@backstage/frontend-plugin-api';
import { ApiFactory } from '@backstage/frontend-plugin-api';
import { ApiMock } from '@backstage/test-utils';
import { AppNode } from '@backstage/frontend-plugin-api';
import { AppNodeInstance } from '@backstage/frontend-plugin-api';
import { ErrorWithContext } from '@backstage/test-utils';
@@ -18,6 +17,7 @@ import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { ExtensionDefinitionParameters } from '@backstage/frontend-plugin-api';
import { FrontendFeature } from '@backstage/frontend-app-api';
import { JsonObject } from '@backstage/types';
import { mockApis } from '@backstage/test-utils';
import { MockConfigApi } from '@backstage/test-utils';
import { MockErrorApi } from '@backstage/test-utils';
import { MockErrorApiOptions } from '@backstage/test-utils';
@@ -34,14 +34,7 @@ import { TestApiProviderProps } from '@backstage/test-utils';
import { TestApiRegistry } from '@backstage/test-utils';
import { withLogCollector } from '@backstage/test-utils';
// @public
export type ApiMock<TApi> = {
factory: ApiFactory<TApi, TApi, {}>;
} & {
[Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return
? TApi[Key] & jest.MockInstance<Return, Args>
: TApi[Key];
};
export { ApiMock };
// @public (undocumented)
export function createExtensionTester<T extends ExtensionDefinitionParameters>(
@@ -103,6 +96,8 @@ export class MockAnalyticsApi implements AnalyticsApi {
getEvents(): AnalyticsEvent[];
}
export { mockApis };
export { MockConfigApi };
export { MockErrorApi };
@@ -24,7 +24,8 @@ export {
MockPermissionApi,
MockStorageApi,
type MockStorageBucket,
mockApis,
type ApiMock,
} from '@backstage/test-utils';
export { type ApiMock } from './ApiMock';
export { MockAnalyticsApi } from './AnalyticsApi/MockAnalyticsApi';
+5
View File
@@ -65,6 +65,7 @@
"devDependencies": {
"@backstage/cli": "workspace:^",
"@testing-library/jest-dom": "^6.0.0",
"@types/jest": "*",
"@types/react": "^18.0.0",
"msw": "^1.0.0",
"react": "^18.0.2",
@@ -73,12 +74,16 @@
},
"peerDependencies": {
"@testing-library/react": "^16.0.0",
"@types/jest": "*",
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"peerDependenciesMeta": {
"@types/jest": {
"optional": true
},
"@types/react": {
"optional": true
}
+5 -5
View File
@@ -8,7 +8,7 @@ import { TranslationApi } from '@backstage/core-plugin-api/alpha';
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
import { TranslationSnapshot } from '@backstage/core-plugin-api/alpha';
// @alpha (undocumented)
// @alpha @deprecated (undocumented)
export class MockTranslationApi implements TranslationApi {
// (undocumented)
static create(): MockTranslationApi;
@@ -30,10 +30,10 @@ export class MockTranslationApi implements TranslationApi {
// Warnings were encountered during analysis:
//
// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:4:1 - (ae-undocumented) Missing documentation for "MockTranslationApi".
// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:6:5 - (ae-undocumented) Missing documentation for "create".
// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:8:5 - (ae-undocumented) Missing documentation for "getTranslation".
// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:11:5 - (ae-undocumented) Missing documentation for "translation$".
// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:7:1 - (ae-undocumented) Missing documentation for "MockTranslationApi".
// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:9:5 - (ae-undocumented) Missing documentation for "create".
// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:11:5 - (ae-undocumented) Missing documentation for "getTranslation".
// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:14:5 - (ae-undocumented) Missing documentation for "translation$".
// (No @packageDocumentation comment for this package)
```
+157 -14
View File
@@ -3,8 +3,11 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="jest" />
import { AnalyticsApi } from '@backstage/core-plugin-api';
import { AnalyticsEvent } from '@backstage/core-plugin-api';
import { ApiFactory } from '@backstage/core-plugin-api';
import { ApiHolder } from '@backstage/core-plugin-api';
import { ApiRef } from '@backstage/core-plugin-api';
import { AppComponents } from '@backstage/core-plugin-api';
@@ -38,6 +41,16 @@ import { RenderResult } from '@testing-library/react';
import { RouteRef } from '@backstage/core-plugin-api';
import { StorageApi } from '@backstage/core-plugin-api';
import { StorageValueSnapshot } from '@backstage/core-plugin-api';
import { TranslationApi } from '@backstage/core-plugin-api/alpha';
// @public
export type ApiMock<TApi> = {
factory: ApiFactory<TApi, TApi, {}>;
} & {
[Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return
? TApi[Key] & jest.MockInstance<Return, Args>
: TApi[Key];
};
// @public
export type AsyncLogCollector = () => Promise<void>;
@@ -69,7 +82,7 @@ export type LogCollector = AsyncLogCollector | SyncLogCollector;
// @public
export type LogFuncs = 'log' | 'warn' | 'error';
// @public
// @public @deprecated
export class MockAnalyticsApi implements AnalyticsApi {
// (undocumented)
captureEvent(event: AnalyticsEvent): void;
@@ -77,10 +90,128 @@ export class MockAnalyticsApi implements AnalyticsApi {
getEvents(): AnalyticsEvent[];
}
// @public
export namespace mockApis {
export function analytics(): AnalyticsApi;
export namespace analytics {
const // (undocumented)
factory: () => ApiFactory<AnalyticsApi, AnalyticsApi, {}>;
const // (undocumented)
mock: (
partialImpl?: Partial<AnalyticsApi> | undefined,
) => ApiMock<AnalyticsApi>;
}
export function config(options?: { data?: JsonObject }): ConfigApi;
export namespace config {
const factory: (
options?:
| {
data?: JsonObject | undefined;
}
| undefined,
) => ApiFactory<Config, Config, {}>;
const mock: (partialImpl?: Partial<Config> | undefined) => ApiMock<Config>;
}
export function discovery(options?: { baseUrl?: string }): DiscoveryApi;
export namespace discovery {
const // (undocumented)
factory: (
options?:
| {
baseUrl?: string | undefined;
}
| undefined,
) => ApiFactory<DiscoveryApi, DiscoveryApi, {}>;
const // (undocumented)
mock: (
partialImpl?: Partial<DiscoveryApi> | undefined,
) => ApiMock<DiscoveryApi>;
}
export function identity(options?: {
userEntityRef?: string;
ownershipEntityRefs?: string[];
token?: string;
email?: string;
displayName?: string;
picture?: string;
}): IdentityApi;
export namespace identity {
const // (undocumented)
factory: (
options?:
| {
userEntityRef?: string | undefined;
ownershipEntityRefs?: string[] | undefined;
token?: string | undefined;
email?: string | undefined;
displayName?: string | undefined;
picture?: string | undefined;
}
| undefined,
) => ApiFactory<IdentityApi, IdentityApi, {}>;
const // (undocumented)
mock: (
partialImpl?: Partial<IdentityApi> | undefined,
) => ApiMock<IdentityApi>;
}
export function permission(options?: {
authorize?:
| AuthorizeResult.ALLOW
| AuthorizeResult.DENY
| ((
request: EvaluatePermissionRequest,
) => AuthorizeResult.ALLOW | AuthorizeResult.DENY);
}): PermissionApi;
export namespace permission {
const // (undocumented)
factory: (
options?:
| {
authorize?:
| AuthorizeResult.DENY
| AuthorizeResult.ALLOW
| ((
request: EvaluatePermissionRequest,
) => AuthorizeResult.DENY | AuthorizeResult.ALLOW)
| undefined;
}
| undefined,
) => ApiFactory<PermissionApi, PermissionApi, {}>;
const // (undocumented)
mock: (
partialImpl?: Partial<PermissionApi> | undefined,
) => ApiMock<PermissionApi>;
}
export function storage(options?: { data?: JsonObject }): StorageApi;
export namespace storage {
const // (undocumented)
factory: (
options?:
| {
data?: JsonObject | undefined;
}
| undefined,
) => ApiFactory<StorageApi, StorageApi, {}>;
const // (undocumented)
mock: (
partialImpl?: Partial<StorageApi> | undefined,
) => ApiMock<StorageApi>;
}
export function translation(): TranslationApi;
export namespace translation {
const // (undocumented)
factory: () => ApiFactory<TranslationApi, TranslationApi, {}>;
const // (undocumented)
mock: (
partialImpl?: Partial<TranslationApi> | undefined,
) => ApiMock<TranslationApi>;
}
}
// @public @deprecated
export function mockBreakpoint(options: { matches: boolean }): void;
// @public
// @public @deprecated
export class MockConfigApi implements ConfigApi {
constructor(data: JsonObject);
get<T = JsonValue>(key?: string): T;
@@ -146,7 +277,7 @@ export interface MockFetchApiOptions {
};
}
// @public
// @public @deprecated
export class MockPermissionApi implements PermissionApi {
constructor(
requestHandler?: (
@@ -159,7 +290,7 @@ export class MockPermissionApi implements PermissionApi {
): Promise<EvaluatePermissionResponse>;
}
// @public
// @public @deprecated
export class MockStorageApi implements StorageApi {
// (undocumented)
static create(data?: MockStorageBucket): MockStorageApi;
@@ -177,7 +308,7 @@ export class MockStorageApi implements StorageApi {
snapshot<T extends JsonValue>(key: string): StorageValueSnapshot<T>;
}
// @public
// @public @deprecated
export type MockStorageBucket = {
[key: string]: any;
};
@@ -276,17 +407,29 @@ export function wrapInTestApp(
// Warnings were encountered during analysis:
//
// src/deprecated.d.ts:5:1 - (ae-undocumented) Missing documentation for "setupRequestMockHandlers".
// src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.d.ts:10:5 - (ae-undocumented) Missing documentation for "captureEvent".
// src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.d.ts:11:5 - (ae-undocumented) Missing documentation for "getEvents".
// src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.d.ts:11:5 - (ae-undocumented) Missing documentation for "captureEvent".
// src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.d.ts:12:5 - (ae-undocumented) Missing documentation for "getEvents".
// src/testUtils/apis/ErrorApi/MockErrorApi.d.ts:28:5 - (ae-undocumented) Missing documentation for "post".
// src/testUtils/apis/ErrorApi/MockErrorApi.d.ts:29:5 - (ae-undocumented) Missing documentation for "error$".
// src/testUtils/apis/ErrorApi/MockErrorApi.d.ts:33:5 - (ae-undocumented) Missing documentation for "getErrors".
// src/testUtils/apis/ErrorApi/MockErrorApi.d.ts:34:5 - (ae-undocumented) Missing documentation for "waitForError".
// src/testUtils/apis/PermissionApi/MockPermissionApi.d.ts:13:5 - (ae-undocumented) Missing documentation for "authorize".
// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:19:5 - (ae-undocumented) Missing documentation for "create".
// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:20:5 - (ae-undocumented) Missing documentation for "forBucket".
// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:21:5 - (ae-undocumented) Missing documentation for "snapshot".
// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:22:5 - (ae-undocumented) Missing documentation for "set".
// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:23:5 - (ae-undocumented) Missing documentation for "remove".
// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:24:5 - (ae-undocumented) Missing documentation for "observe$".
// src/testUtils/apis/PermissionApi/MockPermissionApi.d.ts:14:5 - (ae-undocumented) Missing documentation for "authorize".
// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:21:5 - (ae-undocumented) Missing documentation for "create".
// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:22:5 - (ae-undocumented) Missing documentation for "forBucket".
// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:23:5 - (ae-undocumented) Missing documentation for "snapshot".
// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:24:5 - (ae-undocumented) Missing documentation for "set".
// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:25:5 - (ae-undocumented) Missing documentation for "remove".
// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:26:5 - (ae-undocumented) Missing documentation for "observe$".
// src/testUtils/apis/mockApis.d.ts:58:15 - (ae-undocumented) Missing documentation for "factory".
// src/testUtils/apis/mockApis.d.ts:59:15 - (ae-undocumented) Missing documentation for "mock".
// src/testUtils/apis/mockApis.d.ts:125:15 - (ae-undocumented) Missing documentation for "factory".
// src/testUtils/apis/mockApis.d.ts:128:15 - (ae-undocumented) Missing documentation for "mock".
// src/testUtils/apis/mockApis.d.ts:150:15 - (ae-undocumented) Missing documentation for "factory".
// src/testUtils/apis/mockApis.d.ts:158:15 - (ae-undocumented) Missing documentation for "mock".
// src/testUtils/apis/mockApis.d.ts:177:15 - (ae-undocumented) Missing documentation for "factory".
// src/testUtils/apis/mockApis.d.ts:180:15 - (ae-undocumented) Missing documentation for "mock".
// src/testUtils/apis/mockApis.d.ts:197:15 - (ae-undocumented) Missing documentation for "factory".
// src/testUtils/apis/mockApis.d.ts:200:15 - (ae-undocumented) Missing documentation for "mock".
// src/testUtils/apis/mockApis.d.ts:215:15 - (ae-undocumented) Missing documentation for "factory".
// src/testUtils/apis/mockApis.d.ts:216:15 - (ae-undocumented) Missing documentation for "mock".
```
@@ -21,6 +21,7 @@ import { AnalyticsApi, AnalyticsEvent } from '@backstage/core-plugin-api';
* Use getEvents in tests to verify captured events.
*
* @public
* @deprecated Use {@link @backstage/test-utils#mockApis.(analytics:namespace)} instead
*/
export class MockAnalyticsApi implements AnalyticsApi {
private events: AnalyticsEvent[] = [];
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { ApiFactory } from '@backstage/frontend-plugin-api';
import { ApiFactory } from '@backstage/core-plugin-api';
/**
* Represents a mocked version of an API, where you automatically have access to
@@ -23,6 +23,7 @@ import { ConfigApi } from '@backstage/core-plugin-api';
* that can be used to mock configuration using a plain object.
*
* @public
* @deprecated Use {@link mockApis.(config:namespace)} instead
* @example
* ```tsx
* const mockConfig = new MockConfigApi({
@@ -26,6 +26,7 @@ import {
* {@link @backstage/plugin-permission-react#PermissionApi}. Supply a
* requestHandler function to override the mock result returned for a given
* request.
* @deprecated Use {@link @backstage/test-utils#mockApis.(permission:namespace)} instead
* @public
*/
export class MockPermissionApi implements PermissionApi {
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { StorageApi } from '@backstage/core-plugin-api';
import { MockStorageApi } from './MockStorageApi';
@@ -20,12 +20,14 @@ import ObservableImpl from 'zen-observable';
/**
* Type for map holding data in {@link MockStorageApi}
* @deprecated Use {@link @backstage/test-utils#mockApis.(storage:namespace)} instead
* @public
*/
export type MockStorageBucket = { [key: string]: any };
/**
* Mock implementation of the {@link core-plugin-api#StorageApi} to be used in tests
* @deprecated Use {@link @backstage/test-utils#mockApis.(storage:namespace)} instead
* @public
*/
export class MockStorageApi implements StorageApi {
@@ -44,7 +46,21 @@ export class MockStorageApi implements StorageApi {
}
static create(data?: MockStorageBucket) {
return new MockStorageApi('', new Map(), data);
// Translate a nested data object structure into a flat object with keys
// like `/a/b` with their corresponding leaf values
const keyValues: { [key: string]: any } = {};
function put(value: { [key: string]: any }, namespace: string) {
for (const [key, val] of Object.entries(value)) {
if (typeof val === 'object' && val !== null) {
put(val, `${namespace}/${key}`);
} else {
const namespacedKey = `${namespace}/${key.replace(/^\//, '')}`;
keyValues[namespacedKey] = val;
}
}
}
put(data ?? {}, '');
return new MockStorageApi('', new Map(), keyValues);
}
forBucket(name: string): StorageApi {
@@ -30,7 +30,10 @@ import { toInternalTranslationRef } from '../../../../../core-plugin-api/src/tra
const DEFAULT_LANGUAGE = 'en';
/** @alpha */
/**
* @alpha
* @deprecated Use `mockApis` from `@backstage/test-utils` instead
*/
export class MockTranslationApi implements TranslationApi {
static create() {
const i18n = createI18n({
@@ -20,3 +20,5 @@ export * from './ErrorApi';
export * from './FetchApi';
export * from './PermissionApi';
export * from './StorageApi';
export { type ApiMock } from './ApiMock';
export { mockApis } from './mockApis';
@@ -0,0 +1,773 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
AuthorizeResult,
createPermission,
} from '@backstage/plugin-permission-common';
import { mockApis } from './mockApis';
import { JsonValue } from '@backstage/types';
import { StorageValueSnapshot } from '@backstage/core-plugin-api';
import { createTranslationRef } from '@backstage/core-plugin-api/alpha';
describe('mockApis', () => {
describe('analytics', () => {
it('can create an instance', () => {
const analytics = mockApis.analytics();
expect(
analytics.captureEvent({
action: 'a',
subject: 'b',
context: { pluginId: 'c', extension: 'd', routeRef: 'e' },
}),
).toBeUndefined();
});
it('can create a mock and make assertions on it', async () => {
expect.assertions(3);
const analytics = mockApis.analytics.mock({
captureEvent: event => {
expect(event).toEqual({
action: 'a',
subject: 'b',
context: { pluginId: 'c', extension: 'd', routeRef: 'e' },
});
},
});
expect(
analytics.captureEvent({
action: 'a',
subject: 'b',
context: { pluginId: 'c', extension: 'd', routeRef: 'e' },
}),
).toBeUndefined();
expect(analytics.captureEvent).toHaveBeenCalledTimes(1);
});
});
describe('config', () => {
const data = { backend: { baseUrl: 'http://test.com' } };
it('can create an instance', () => {
const empty = mockApis.config();
expect(empty.getOptional('backend.baseUrl')).toBeUndefined();
const notEmpty = mockApis.config({ data });
expect(notEmpty.getOptional('backend.baseUrl')).toEqual(
'http://test.com',
);
});
it('can create a mock and make assertions on it', async () => {
const mock = mockApis.config.mock({ getString: () => 'replaced' });
expect(mock.getString('a')).toEqual('replaced');
expect(mock.getString).toHaveBeenCalledTimes(1);
});
});
describe('discovery', () => {
it('can create an instance', async () => {
const empty = mockApis.discovery();
await expect(empty.getBaseUrl('catalog')).resolves.toBe(
'http://example.com/api/catalog',
);
const notEmpty = mockApis.discovery({ baseUrl: 'https://other.net' });
await expect(notEmpty.getBaseUrl('catalog')).resolves.toBe(
'https://other.net/api/catalog',
);
});
it('can create a mock and make assertions on it', async () => {
const empty = mockApis.discovery.mock();
expect(empty.getBaseUrl('catalog')).toBeUndefined();
expect(empty.getBaseUrl).toHaveBeenCalledTimes(1);
const notEmpty = mockApis.discovery.mock({
getBaseUrl: async () => 'replaced',
});
await expect(notEmpty.getBaseUrl('catalog')).resolves.toBe('replaced');
expect(notEmpty.getBaseUrl).toHaveBeenCalledTimes(1);
});
});
describe('identity', () => {
it('can create an instance', async () => {
const empty = mockApis.identity();
await expect(empty.getBackstageIdentity()).resolves.toEqual({
type: 'user',
userEntityRef: 'user:default/test',
ownershipEntityRefs: ['user:default/test'],
});
await expect(empty.getCredentials()).resolves.toEqual({});
await expect(empty.getProfileInfo()).resolves.toEqual({});
await expect(empty.signOut()).resolves.toBeUndefined();
const notEmpty = mockApis.identity({
userEntityRef: 'a',
ownershipEntityRefs: ['b'],
token: 'c',
email: 'd',
displayName: 'e',
picture: 'f',
});
await expect(notEmpty.getBackstageIdentity()).resolves.toEqual({
type: 'user',
userEntityRef: 'a',
ownershipEntityRefs: ['b'],
});
await expect(notEmpty.getCredentials()).resolves.toEqual({ token: 'c' });
await expect(notEmpty.getProfileInfo()).resolves.toEqual({
email: 'd',
displayName: 'e',
picture: 'f',
});
await expect(notEmpty.signOut()).resolves.toBeUndefined();
});
it('can create a mock and make assertions on it', async () => {
const empty = mockApis.identity.mock();
expect(empty.getBackstageIdentity()).toBeUndefined();
expect(empty.getCredentials()).toBeUndefined();
expect(empty.getProfileInfo()).toBeUndefined();
expect(empty.signOut()).toBeUndefined();
expect(empty.getBackstageIdentity).toHaveBeenCalledTimes(1);
expect(empty.getCredentials).toHaveBeenCalledTimes(1);
expect(empty.getProfileInfo).toHaveBeenCalledTimes(1);
expect(empty.signOut).toHaveBeenCalledTimes(1);
const notEmpty = mockApis.identity.mock({
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'a',
ownershipEntityRefs: ['b'],
}),
getCredentials: async () => ({ token: 'c' }),
getProfileInfo: async () => ({
email: 'd',
displayName: 'e',
picture: 'f',
}),
signOut: async () => undefined,
});
await expect(notEmpty.getBackstageIdentity()).resolves.toEqual({
type: 'user',
userEntityRef: 'a',
ownershipEntityRefs: ['b'],
});
await expect(notEmpty.getCredentials()).resolves.toEqual({ token: 'c' });
await expect(notEmpty.getProfileInfo()).resolves.toEqual({
email: 'd',
displayName: 'e',
picture: 'f',
});
await expect(notEmpty.signOut()).resolves.toBeUndefined();
expect(notEmpty.getBackstageIdentity).toHaveBeenCalledTimes(1);
expect(notEmpty.getCredentials).toHaveBeenCalledTimes(1);
expect(notEmpty.getProfileInfo).toHaveBeenCalledTimes(1);
expect(notEmpty.signOut).toHaveBeenCalledTimes(1);
});
});
describe('permission', () => {
it('can create an instance', async () => {
// default allow
const permission1 = mockApis.permission();
await expect(
permission1.authorize({
permission: createPermission({
name: 'permission.1',
attributes: {},
}),
}),
).resolves.toEqual({ result: AuthorizeResult.ALLOW });
// static value
const permission2 = mockApis.permission({
authorize: AuthorizeResult.DENY,
});
await expect(
permission2.authorize({
permission: createPermission({
name: 'permission.1',
attributes: {},
}),
}),
).resolves.toEqual({ result: AuthorizeResult.DENY });
// callback form
const permission3 = mockApis.permission({
authorize: req =>
req.permission.name === 'permission.1'
? AuthorizeResult.ALLOW
: AuthorizeResult.DENY,
});
await expect(
permission3.authorize({
permission: createPermission({
name: 'permission.1',
attributes: {},
}),
}),
).resolves.toEqual({ result: AuthorizeResult.ALLOW });
await expect(
permission3.authorize({
permission: createPermission({
name: 'permission.2',
attributes: {},
}),
}),
).resolves.toEqual({ result: AuthorizeResult.DENY });
});
it('can create a mock and make assertions on it', async () => {
const empty = mockApis.permission.mock();
expect(
empty.authorize({
permission: createPermission({
name: 'permission.1',
attributes: {},
}),
}),
).toBeUndefined();
expect(empty.authorize).toHaveBeenCalledTimes(1);
const notEmpty = mockApis.permission.mock({
authorize: async req => ({
result:
req.permission.name === 'permission.1'
? AuthorizeResult.ALLOW
: AuthorizeResult.DENY,
}),
});
await expect(
notEmpty.authorize({
permission: createPermission({
name: 'permission.1',
attributes: {},
}),
}),
).resolves.toEqual({ result: AuthorizeResult.ALLOW });
await expect(
notEmpty.authorize({
permission: createPermission({
name: 'permission.2',
attributes: {},
}),
}),
).resolves.toEqual({ result: AuthorizeResult.DENY });
expect(notEmpty.authorize).toHaveBeenCalledTimes(2);
});
});
describe('storage', () => {
describe('instance deep tests', () => {
it('should return undefined for values which are unset', async () => {
const storage = mockApis.storage();
expect(storage.snapshot('myfakekey').value).toBeUndefined();
expect(storage.snapshot('myfakekey')).toEqual({
key: 'myfakekey',
presence: 'absent',
value: undefined,
newValue: undefined,
});
});
it('should allow the setting and snapshotting of the simple data structures', async () => {
const storage = mockApis.storage();
await storage.set('myfakekey', 'helloimastring');
await storage.set('mysecondfakekey', 1234);
await storage.set('mythirdfakekey', true);
expect(storage.snapshot('myfakekey').value).toBe('helloimastring');
expect(storage.snapshot('mysecondfakekey').value).toBe(1234);
expect(storage.snapshot('mythirdfakekey').value).toBe(true);
expect(storage.snapshot('myfakekey')).toEqual({
key: 'myfakekey',
presence: 'present',
value: 'helloimastring',
});
expect(storage.snapshot('mysecondfakekey')).toEqual({
key: 'mysecondfakekey',
presence: 'present',
value: 1234,
});
expect(storage.snapshot('mythirdfakekey')).toEqual({
key: 'mythirdfakekey',
presence: 'present',
value: true,
});
});
it('should allow setting of complex datastructures', async () => {
const storage = mockApis.storage();
const mockData = {
something: 'here',
is: [{ super: { complex: [{ but: 'something', why: true }] } }],
};
await storage.set('myfakekey', mockData);
expect(storage.snapshot('myfakekey').value).toEqual(mockData);
expect(storage.snapshot('myfakekey')).toEqual({
key: 'myfakekey',
presence: 'present',
value: mockData,
});
});
it('should subscribe to key changes when setting a new value', async () => {
const storage = mockApis.storage();
const wrongKeyNextHandler = jest.fn();
const selectedKeyNextHandler = jest.fn();
const mockData = { hello: 'im a great new value' };
await new Promise<void>(resolve => {
storage.observe$<typeof mockData>('correctKey').subscribe({
next: (...args) => {
selectedKeyNextHandler(...args);
resolve();
},
});
storage.observe$('wrongKey').subscribe({ next: wrongKeyNextHandler });
storage.set('correctKey', mockData);
});
expect(wrongKeyNextHandler).not.toHaveBeenCalled();
expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1);
expect(selectedKeyNextHandler).toHaveBeenCalledWith({
key: 'correctKey',
presence: 'present',
value: mockData,
});
});
it('should subscribe to key changes when deleting a value', async () => {
const storage = mockApis.storage();
const wrongKeyNextHandler = jest.fn();
const selectedKeyNextHandler = jest.fn();
const mockData = { hello: 'im a great new value' };
storage.set('correctKey', mockData);
await new Promise<void>(resolve => {
storage.observe$('correctKey').subscribe({
next: (...args) => {
selectedKeyNextHandler(...args);
resolve();
},
});
storage.observe$('wrongKey').subscribe({ next: wrongKeyNextHandler });
storage.remove('correctKey');
});
expect(wrongKeyNextHandler).not.toHaveBeenCalled();
expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1);
expect(selectedKeyNextHandler).toHaveBeenCalledWith({
key: 'correctKey',
presence: 'absent',
value: undefined,
newValue: undefined,
});
});
it('should be able to create different buckets for different uses', async () => {
const rootStorage = mockApis.storage();
const firstStorage = rootStorage.forBucket('userSettings');
const secondStorage = rootStorage.forBucket('profileSettings');
const keyName = 'blobby';
await firstStorage.set(keyName, 'boop');
await secondStorage.set(keyName, 'deerp');
expect(firstStorage.snapshot(keyName)).not.toBe(
secondStorage.snapshot(keyName),
);
expect(firstStorage.snapshot(keyName).value).toBe('boop');
expect(secondStorage.snapshot(keyName).value).toBe('deerp');
expect(firstStorage.snapshot(keyName)).not.toEqual(
secondStorage.snapshot(keyName),
);
expect(firstStorage.snapshot(keyName)).toEqual({
key: keyName,
presence: 'present',
value: 'boop',
});
expect(secondStorage.snapshot(keyName)).toEqual({
key: keyName,
presence: 'present',
value: 'deerp',
});
});
it('should not clash with other namespaces when creating buckets', async () => {
const rootStorage = mockApis.storage();
// when getting key test2 it will translate to /profile/something/deep/test2
const firstStorage = rootStorage
.forBucket('profile')
.forBucket('something')
.forBucket('deep');
// when getting key deep/test2 it will translate to /profile/something/deep/test2
const secondStorage = rootStorage.forBucket('profile/something');
await firstStorage.set('test2', { error: true });
expect(secondStorage.snapshot('deep/test2').value).toBe(undefined);
expect(secondStorage.snapshot('deep/test2')).toMatchObject({
presence: 'absent',
});
});
it('should not reuse storage instances between different rootStorages', async () => {
const rootStorage1 = mockApis.storage();
const rootStorage2 = mockApis.storage();
const firstStorage = rootStorage1.forBucket('something');
const secondStorage = rootStorage2.forBucket('something');
await firstStorage.set('test2', true);
expect(firstStorage.snapshot('test2').value).toBe(true);
expect(secondStorage.snapshot('test2').value).toBe(undefined);
expect(firstStorage.snapshot('test2')).toEqual({
key: 'test2',
presence: 'present',
value: true,
});
expect(secondStorage.snapshot('test2')).toEqual({
key: 'test2',
presence: 'absent',
value: undefined,
});
});
it('should freeze the snapshot value', async () => {
const storage = mockApis.storage();
const data = { foo: 'bar', baz: [{ foo: 'bar' }] };
storage.set('foo', data);
const snapshot = storage.snapshot<typeof data>('foo');
expect(snapshot.value).not.toBe(data);
if (snapshot.presence !== 'present') {
throw new Error('Invalid presence');
}
expect(() => {
snapshot.value.foo = 'buzz';
}).toThrow(/Cannot assign to read only property/);
expect(() => {
snapshot.value.baz[0].foo = 'buzz';
}).toThrow(/Cannot assign to read only property/);
expect(() => {
snapshot.value.baz.push({ foo: 'buzz' });
}).toThrow(/Cannot add property 1, object is not extensible/);
});
it('should freeze observed values', async () => {
const storage = mockApis.storage();
const snapshotPromise = new Promise<any>(resolve => {
storage.observe$('test').subscribe({
next: resolve,
});
});
storage.set('test', {
foo: {
bar: 'baz',
},
});
const snapshot = await snapshotPromise;
expect(snapshot.presence).toBe('present');
expect(() => {
snapshot.value!.foo.bar = 'qux';
}).toThrow(/Cannot assign to read only property 'bar' of object/);
});
it('should JSON serialize stored values', async () => {
const storage = mockApis.storage();
storage.set<any>('test', {
foo: {
toJSON() {
return {
bar: 'baz',
};
},
},
});
expect(storage.snapshot('test')).toMatchObject({
presence: 'present',
value: {
foo: {
bar: 'baz',
},
},
});
});
});
it('can create an instance', () => {
const empty = mockApis.storage();
expect(empty.snapshot('a')).toEqual({ key: 'a', presence: 'absent' });
const notEmpty = mockApis.storage({ data: { a: 1, b: { c: 2 } } });
expect(notEmpty.snapshot('a')).toEqual({
key: 'a',
presence: 'present',
value: 1,
});
expect(notEmpty.forBucket('b').snapshot('c')).toEqual({
key: 'c',
presence: 'present',
value: 2,
});
});
it('can create a mock and make assertions on it', () => {
const empty = mockApis.storage.mock();
expect(empty.snapshot('a')).toBeUndefined();
expect(empty.snapshot).toHaveBeenCalledTimes(1);
const notEmpty = mockApis.storage.mock({
snapshot<T extends JsonValue>(k: string): StorageValueSnapshot<T> {
return { key: k, presence: 'present', value: 'v' as T };
},
});
expect(notEmpty.snapshot('a')).toEqual({
key: 'a',
presence: 'present',
value: 'v',
});
expect(notEmpty.snapshot).toHaveBeenCalledTimes(1);
});
});
describe('translation', () => {
describe('instance deep tests', () => {
function snapshotWithMessages<
const TMessages extends { [key in string]: string },
>(messages: TMessages) {
const translationApi = mockApis.translation();
const ref = createTranslationRef({
id: 'test',
messages,
});
const snapshot = translationApi.getTranslation(ref);
if (!snapshot.ready) {
throw new Error('Translation snapshot is not ready');
}
return snapshot;
}
it('should format plain messages', () => {
const snapshot = snapshotWithMessages({
foo: 'Foo',
bar: 'Bar',
baz: 'Baz',
});
expect(snapshot.t('foo')).toBe('Foo');
expect(snapshot.t('bar')).toBe('Bar');
expect(snapshot.t('baz')).toBe('Baz');
});
it('should support interpolation', () => {
const snapshot = snapshotWithMessages({
shallow: 'Foo {{ bar }}',
multiple: 'Foo {{ bar }} {{ baz }}',
deep: 'Foo {{ bar.baz }}',
});
// @ts-expect-error
expect(snapshot.t('shallow')).toBe('Foo {{ bar }}');
expect(snapshot.t('shallow', { bar: 'Bar' })).toBe('Foo Bar');
// @ts-expect-error
expect(snapshot.t('multiple')).toBe('Foo {{ bar }} {{ baz }}');
// @ts-expect-error
expect(snapshot.t('multiple', { bar: 'Bar' })).toBe(
'Foo Bar {{ baz }}',
);
expect(snapshot.t('multiple', { bar: 'Bar', baz: 'Baz' })).toBe(
'Foo Bar Baz',
);
// @ts-expect-error
expect(snapshot.t('deep')).toBe('Foo {{ bar.baz }}');
expect(snapshot.t('deep', { bar: { baz: 'Baz' } })).toBe('Foo Baz');
});
// Escaping isn't as useful in React, since we don't need to escape HTML in strings
it('should not escape by default', () => {
const snapshot = snapshotWithMessages({
foo: 'Foo {{ foo }}',
});
expect(snapshot.t('foo', { foo: '<div>' })).toBe('Foo <div>');
expect(
snapshot.t('foo', {
foo: '<div>',
interpolation: { escapeValue: true },
}),
).toBe('Foo &lt;div&gt;');
});
it('should support nesting', () => {
const snapshot = snapshotWithMessages({
foo: 'Foo $t(bar) $t(baz)',
bar: 'Nested',
baz: 'Baz {{ qux }}',
});
expect(snapshot.t('foo', { qux: 'Deep' })).toBe('Foo Nested Baz Deep');
});
it('should support formatting', () => {
const snapshot = snapshotWithMessages({
plain: '= {{ x }}',
number: '= {{ x, number }}',
numberFixed: '= {{ x, number(minimumFractionDigits: 2) }}',
relativeTime: '= {{ x, relativeTime }}',
relativeSeconds: '= {{ x, relativeTime(second) }}',
relativeSecondsShort:
'= {{ x, relativeTime(range: second; style: short) }}',
list: '= {{ x, list }}',
});
expect(snapshot.t('plain', { x: '5' })).toBe('= 5');
expect(snapshot.t('number', { x: 5 })).toBe('= 5');
expect(
snapshot.t('number', {
x: 5,
formatParams: { x: { minimumFractionDigits: 1 } },
}),
).toBe('= 5.0');
expect(snapshot.t('numberFixed', { x: 5 })).toBe('= 5.00');
expect(
snapshot.t('numberFixed', {
x: 5,
formatParams: { x: { minimumFractionDigits: 3 } },
}),
).toBe('= 5.000');
expect(snapshot.t('relativeTime', { x: 3 })).toBe('= in 3 days');
expect(snapshot.t('relativeTime', { x: -3 })).toBe('= 3 days ago');
expect(
snapshot.t('relativeTime', {
x: 15,
formatParams: { x: { range: 'weeks' } },
}),
).toBe('= in 15 weeks');
expect(
snapshot.t('relativeTime', {
x: 15,
formatParams: { x: { range: 'weeks', style: 'short' } },
}),
).toBe('= in 15 wk.');
expect(snapshot.t('relativeSeconds', { x: 1 })).toBe('= in 1 second');
expect(snapshot.t('relativeSeconds', { x: 2 })).toBe('= in 2 seconds');
expect(snapshot.t('relativeSeconds', { x: -3 })).toBe(
'= 3 seconds ago',
);
expect(snapshot.t('relativeSeconds', { x: 0 })).toBe('= in 0 seconds');
expect(snapshot.t('relativeSecondsShort', { x: 1 })).toBe(
'= in 1 sec.',
);
expect(snapshot.t('relativeSecondsShort', { x: 2 })).toBe(
'= in 2 sec.',
);
expect(snapshot.t('relativeSecondsShort', { x: -3 })).toBe(
'= 3 sec. ago',
);
expect(snapshot.t('relativeSecondsShort', { x: 0 })).toBe(
'= in 0 sec.',
);
expect(snapshot.t('list', { x: ['a'] })).toBe('= a');
expect(snapshot.t('list', { x: ['a', 'b'] })).toBe('= a and b');
expect(snapshot.t('list', { x: ['a', 'b', 'c'] })).toBe(
'= a, b, and c',
);
});
it('should support plurals', () => {
const snapshot = snapshotWithMessages({
derp_one: 'derp',
derp_other: 'derps',
derpWithCount_one: '{{ count }} derp',
derpWithCount_other: '{{ count }} derps',
});
expect(snapshot.t('derp', { count: 1 })).toBe('derp');
expect(snapshot.t('derp', { count: 2 })).toBe('derps');
expect(snapshot.t('derp', { count: 0 })).toBe('derps');
expect(snapshot.t('derpWithCount', { count: 1 })).toBe('1 derp');
expect(snapshot.t('derpWithCount', { count: 2 })).toBe('2 derps');
expect(snapshot.t('derpWithCount', { count: 0 })).toBe('0 derps');
});
});
it('can create an instance', () => {
const translation = mockApis.translation();
const ref = createTranslationRef({
id: 'test',
messages: { a: 'b' },
});
const result = translation.getTranslation(ref);
if (!result.ready) {
throw new Error('not ready');
}
expect(result.t('a')).toEqual('b');
});
it('can create a mock and make assertions on it', () => {
const ref = createTranslationRef({
id: 'test',
messages: { a: 'b' },
});
const empty = mockApis.translation.mock();
expect(empty.getTranslation(ref)).toBeUndefined();
const notEmpty = mockApis.translation.mock({
getTranslation: () =>
({
ready: true,
t: () => 'b',
} as any),
});
const result = notEmpty.getTranslation(ref);
if (!result.ready) {
throw new Error('not ready');
}
expect(result.t('a')).toEqual('b');
expect(notEmpty.getTranslation).toHaveBeenCalledTimes(1);
});
});
});
@@ -0,0 +1,381 @@
/*
* Copyright 2024 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 { ConfigReader } from '@backstage/config';
import {
AnalyticsApi,
ApiFactory,
ApiRef,
ConfigApi,
DiscoveryApi,
IdentityApi,
StorageApi,
analyticsApiRef,
configApiRef,
createApiFactory,
discoveryApiRef,
identityApiRef,
storageApiRef,
} from '@backstage/core-plugin-api';
import {
TranslationApi,
translationApiRef,
} from '@backstage/core-plugin-api/alpha';
import {
AuthorizeResult,
EvaluatePermissionRequest,
} from '@backstage/plugin-permission-common';
import {
PermissionApi,
permissionApiRef,
} from '@backstage/plugin-permission-react';
import { JsonObject } from '@backstage/types';
import { ApiMock } from './ApiMock';
import { MockPermissionApi } from './PermissionApi';
import { MockStorageApi } from './StorageApi';
import { MockTranslationApi } from './TranslationApi';
/** @internal */
function simpleFactory<TApi, TArgs extends unknown[]>(
ref: ApiRef<TApi>,
factory: (...args: TArgs) => TApi,
): (...args: TArgs) => ApiFactory<TApi, TApi, {}> {
return (...args) =>
createApiFactory({
api: ref,
deps: {},
factory: () => factory(...args),
});
}
/** @internal */
function simpleMock<TApi>(
ref: ApiRef<TApi>,
mockFactory: () => jest.Mocked<TApi>,
): (partialImpl?: Partial<TApi>) => ApiMock<TApi> {
return partialImpl => {
const mock = mockFactory();
if (partialImpl) {
for (const [key, impl] of Object.entries(partialImpl)) {
if (typeof impl === 'function') {
(mock as any)[key].mockImplementation(impl);
} else {
(mock as any)[key] = impl;
}
}
}
return Object.assign(mock, {
factory: createApiFactory({
api: ref,
deps: {},
factory: () => mock,
}),
}) as ApiMock<TApi>;
};
}
/**
* Mock implementations of the core utility APIs, to be used in tests.
*
* @public
* @remarks
*
* There are some variations among the APIs depending on what needs tests
* might have, but overall there are three main usage patterns:
*
* 1: Creating an actual fake API instance, often with a simplified version
* of functionality, by calling the mock API itself as a function.
*
* ```ts
* // The function often accepts parameters that control its behavior
* const foo = mockApis.foo();
* ```
*
* 2: Creating a mock API, where all methods are replaced with jest mocks, by
* calling the API's `mock` function.
*
* ```ts
* // You can optionally supply a subset of its methods to implement
* const foo = mockApis.foo.mock({
* someMethod: () => 'mocked result',
* });
* // After exercising your test, you can make assertions on the mock:
* expect(foo.someMethod).toHaveBeenCalledTimes(2);
* expect(foo.otherMethod).toHaveBeenCalledWith(testData);
* ```
*
* 3: Creating an API factory that behaves similarly to the mock as per above.
*
* ```ts
* const factory = mockApis.foo.factory({
* someMethod: () => 'mocked result',
* });
* ```
*/
export namespace mockApis {
const analyticsMockSkeleton = (): jest.Mocked<AnalyticsApi> => ({
captureEvent: jest.fn(),
});
/**
* Mock implementation of {@link @backstage/core-plugin-api#AnalyticsApi}.
*
* @public
*/
export function analytics(): AnalyticsApi {
return analyticsMockSkeleton();
}
/**
* Mock implementations of {@link @backstage/core-plugin-api#AnalyticsApi}.
*
* @public
*/
export namespace analytics {
export const factory = simpleFactory(analyticsApiRef, analytics);
export const mock = simpleMock(analyticsApiRef, analyticsMockSkeleton);
}
/**
* Fake implementation of {@link @backstage/core-plugin-api#ConfigApi}
* with optional data supplied.
*
* @public
* @example
*
* ```tsx
* const config = mockApis.config({
* data: { app: { baseUrl: 'https://example.com' } },
* });
*
* await renderInTestApp(
* <TestApiProvider apis={[[configApiRef, config]]}>
* <MyTestedComponent />
* </TestApiProvider>,
* );
* ```
*/
export function config(options?: { data?: JsonObject }): ConfigApi {
return new ConfigReader(options?.data, 'mock-config');
}
/**
* Mock helpers for {@link @backstage/core-plugin-api#ConfigApi}.
*
* @see {@link @backstage/core-plugin-api#mockApis.config}
* @public
*/
export namespace config {
/**
* Creates a factory for a fake implementation of
* {@link @backstage/core-plugin-api#ConfigApi} with optional
* configuration data supplied.
*
* @public
*/
export const factory = simpleFactory(configApiRef, config);
/**
* Creates a mock implementation of
* {@link @backstage/core-plugin-api#ConfigApi}. All methods are
* replaced with jest mock functions, and you can optionally pass in a
* subset of methods with an explicit implementation.
*
* @public
*/
export const mock = simpleMock(configApiRef, () => ({
has: jest.fn(),
keys: jest.fn(),
get: jest.fn(),
getOptional: jest.fn(),
getConfig: jest.fn(),
getOptionalConfig: jest.fn(),
getConfigArray: jest.fn(),
getOptionalConfigArray: jest.fn(),
getNumber: jest.fn(),
getOptionalNumber: jest.fn(),
getBoolean: jest.fn(),
getOptionalBoolean: jest.fn(),
getString: jest.fn(),
getOptionalString: jest.fn(),
getStringArray: jest.fn(),
getOptionalStringArray: jest.fn(),
}));
}
/**
* Fake implementation of {@link @backstage/core-plugin-api#DiscoveryApi}. By
* default returns URLs on the form `http://example.com/api/<pluginIs>`.
*
* @public
*/
export function discovery(options?: { baseUrl?: string }): DiscoveryApi {
const baseUrl = options?.baseUrl ?? 'http://example.com';
return {
async getBaseUrl(pluginId: string) {
return `${baseUrl}/api/${pluginId}`;
},
};
}
/**
* Mock implementations of {@link @backstage/core-plugin-api#DiscoveryApi}.
*
* @public
*/
export namespace discovery {
export const factory = simpleFactory(discoveryApiRef, discovery);
export const mock = simpleMock(discoveryApiRef, () => ({
getBaseUrl: jest.fn(),
}));
}
/**
* Fake implementation of {@link @backstage/core-plugin-api#IdentityApi}. By
* default returns no token or profile info, and the user `user:default/test`.
*
* @public
*/
export function identity(options?: {
userEntityRef?: string;
ownershipEntityRefs?: string[];
token?: string;
email?: string;
displayName?: string;
picture?: string;
}): IdentityApi {
const {
userEntityRef = 'user:default/test',
ownershipEntityRefs = ['user:default/test'],
token,
email,
displayName,
picture,
} = options ?? {};
return {
async getBackstageIdentity() {
return { type: 'user', ownershipEntityRefs, userEntityRef };
},
async getCredentials() {
return { token };
},
async getProfileInfo() {
return { email, displayName, picture };
},
async signOut() {},
};
}
/**
* Mock implementations of {@link @backstage/core-plugin-api#IdentityApi}.
*
* @public
*/
export namespace identity {
export const factory = simpleFactory(identityApiRef, identity);
export const mock = simpleMock(
identityApiRef,
(): jest.Mocked<IdentityApi> => ({
getBackstageIdentity: jest.fn(),
getCredentials: jest.fn(),
getProfileInfo: jest.fn(),
signOut: jest.fn(),
}),
);
}
/**
* Fake implementation of
* {@link @backstage/plugin-permission-react#PermissionApi}. By default allows
* all actions.
*
* @public
*/
export function permission(options?: {
authorize?:
| AuthorizeResult.ALLOW
| AuthorizeResult.DENY
| ((
request: EvaluatePermissionRequest,
) => AuthorizeResult.ALLOW | AuthorizeResult.DENY);
}): PermissionApi {
const authorizeInput = options?.authorize;
let authorize: (
request: EvaluatePermissionRequest,
) => AuthorizeResult.ALLOW | AuthorizeResult.DENY;
if (authorizeInput === undefined) {
authorize = () => AuthorizeResult.ALLOW;
} else if (typeof authorizeInput === 'function') {
authorize = authorizeInput;
} else {
authorize = () => authorizeInput;
}
return new MockPermissionApi(authorize);
}
/**
* Mock implementation of
* {@link @backstage/plugin-permission-react#PermissionApi}.
*
* @public
*/
export namespace permission {
export const factory = simpleFactory(permissionApiRef, permission);
export const mock = simpleMock(permissionApiRef, () => ({
authorize: jest.fn(),
}));
}
/**
* Fake implementation of {@link @backstage/core-plugin-api#StorageApi}.
* Stores data temporarily in memory.
*
* @public
*/
export function storage(options?: { data?: JsonObject }): StorageApi {
return MockStorageApi.create(options?.data);
}
/**
* Mock implementations of {@link @backstage/core-plugin-api#StorageApi}.
*
* @public
*/
export namespace storage {
export const factory = simpleFactory(storageApiRef, storage);
export const mock = simpleMock(storageApiRef, () => ({
forBucket: jest.fn(),
set: jest.fn(),
remove: jest.fn(),
observe$: jest.fn(),
snapshot: jest.fn(),
}));
}
/**
* Fake implementation of {@link @backstage/core-plugin-api/alpha#TranslationApi}.
* By default returns the default translation.
*
* @public
*/
export function translation(): TranslationApi {
return MockTranslationApi.create();
}
/**
* Mock implementations of {@link @backstage/core-plugin-api/alpha#TranslationApi}.
*
* @public
*/
export namespace translation {
export const factory = simpleFactory(translationApiRef, translation);
export const mock = simpleMock(translationApiRef, () => ({
getTranslation: jest.fn(),
translation$: jest.fn(),
}));
}
}
@@ -14,7 +14,6 @@
* limitations under the License.
*/
import { ConfigReader } from '@backstage/core-app-api';
import { TableColumn, TableProps } from '@backstage/core-components';
import { configApiRef, storageApiRef } from '@backstage/core-plugin-api';
import {
@@ -28,8 +27,7 @@ import {
} from '@backstage/plugin-catalog-react';
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
import {
MockPermissionApi,
MockStorageApi,
mockApis,
TestApiProvider,
renderInTestApp,
} from '@backstage/test-utils';
@@ -72,17 +70,15 @@ describe('DefaultApiExplorerPage', () => {
}),
});
const configApi = new ConfigReader({
organization: {
name: 'My Company',
},
const configApi = mockApis.config({
data: { organization: { name: 'My Company' } },
});
const apiDocsConfig = {
getApiDefinitionWidget: () => undefined,
};
const storageApi = MockStorageApi.create();
const storageApi = mockApis.storage();
const renderWrapped = (children: React.ReactNode) =>
renderInTestApp(
@@ -96,7 +92,7 @@ describe('DefaultApiExplorerPage', () => {
new DefaultStarredEntitiesApi({ storageApi }),
],
[apiDocsConfigRef, apiDocsConfig],
[permissionApiRef, new MockPermissionApi()],
[permissionApiRef, mockApis.permission()],
]}
>
{children}
@@ -16,12 +16,16 @@
import React from 'react';
import { screen, waitFor } from '@testing-library/react';
import { TestApiProvider, renderInTestApp } from '@backstage/test-utils';
import {
TestApiProvider,
renderInTestApp,
mockApis,
} from '@backstage/test-utils';
import { identityApiRef } from '@backstage/core-plugin-api';
import { CookieAuthRedirect } from './CookieAuthRedirect';
describe('CookieAuthRedirect', () => {
const identityApiMock = { getCredentials: jest.fn() };
const identityApiMock = mockApis.identity.mock();
beforeEach(() => {
jest.clearAllMocks();
@@ -19,7 +19,7 @@ import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CookieAuthRefreshProvider } from './CookieAuthRefreshProvider';
import {
MockStorageApi,
mockApis,
TestApiProvider,
renderInTestApp,
} from '@backstage/test-utils';
@@ -30,12 +30,7 @@ import {
} from '@backstage/core-plugin-api';
describe('CookieAuthRefreshProvider', () => {
const storageApiMock = MockStorageApi.create();
const discoveryApiMock = {
getBaseUrl: jest
.fn()
.mockResolvedValue('http://localhost:7000/api/techdocs'),
};
const discoveryApiMock = mockApis.discovery();
function getExpiresAtInFuture() {
const tenMinutesInMilliseconds = 10 * 60 * 1000;
@@ -51,7 +46,7 @@ describe('CookieAuthRefreshProvider', () => {
<TestApiProvider
apis={[
[fetchApiRef, fetchApiMock],
[storageApiRef, storageApiMock],
[storageApiRef, mockApis.storage()],
[discoveryApiRef, discoveryApiMock],
]}
>
@@ -76,7 +71,7 @@ describe('CookieAuthRefreshProvider', () => {
<TestApiProvider
apis={[
[fetchApiRef, fetchApiMock],
[storageApiRef, storageApiMock],
[storageApiRef, mockApis.storage()],
[discoveryApiRef, discoveryApiMock],
]}
>
@@ -107,7 +102,7 @@ describe('CookieAuthRefreshProvider', () => {
<TestApiProvider
apis={[
[fetchApiRef, fetchApiMock],
[storageApiRef, storageApiMock],
[storageApiRef, mockApis.storage()],
[discoveryApiRef, discoveryApiMock],
]}
>
@@ -119,7 +114,7 @@ describe('CookieAuthRefreshProvider', () => {
await waitFor(() =>
expect(fetchApiMock.fetch).toHaveBeenCalledWith(
'http://localhost:7000/api/techdocs/.backstage/auth/v1/cookie',
'http://example.com/api/techdocs/.backstage/auth/v1/cookie',
{ credentials: 'include' },
),
);
@@ -153,7 +148,7 @@ describe('CookieAuthRefreshProvider', () => {
<TestApiProvider
apis={[
[fetchApiRef, fetchApiMock],
[storageApiRef, storageApiMock],
[storageApiRef, mockApis.storage()],
[discoveryApiRef, discoveryApiMock],
]}
>
@@ -17,15 +17,11 @@
import React from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import { fetchApiRef, discoveryApiRef } from '@backstage/core-plugin-api';
import { TestApiProvider } from '@backstage/test-utils';
import { TestApiProvider, mockApis } from '@backstage/test-utils';
import { useCookieAuthRefresh } from './useCookieAuthRefresh';
describe('useCookieAuthRefresh', () => {
const discoveryApiMock = {
getBaseUrl: jest
.fn()
.mockResolvedValue('http://localhost:7000/api/techdocs'),
};
const discoveryApiMock = mockApis.discovery();
const now = 1710316886171;
const tenMinutesInMilliseconds = 10 * 60 * 1000;
@@ -269,7 +265,7 @@ describe('useCookieAuthRefresh', () => {
await waitFor(() =>
expect(fetchApiMock.fetch).toHaveBeenCalledWith(
'http://localhost:7000/api/techdocs/.backstage/auth/v1/cookie',
'http://example.com/api/techdocs/.backstage/auth/v1/cookie',
{ credentials: 'include' },
),
);
@@ -24,7 +24,7 @@ import {
} from '@backstage/plugin-catalog-react';
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
import {
MockAnalyticsApi,
mockApis,
renderInTestApp,
TestApiProvider,
TestApiRegistry,
@@ -212,9 +212,9 @@ describe('<CatalogGraphCard/>', () => {
],
}));
const analyticsSpy = new MockAnalyticsApi();
const analyticsApi = mockApis.analytics();
await renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, analyticsSpy]]}>
<TestApiProvider apis={[[analyticsApiRef, analyticsApi]]}>
{wrapper}
</TestApiProvider>,
{
@@ -228,12 +228,14 @@ describe('<CatalogGraphCard/>', () => {
expect(await screen.findByText('b:d/c')).toBeInTheDocument();
await userEvent.click(await screen.findByText('b:d/c'));
expect(analyticsSpy.getEvents()[0]).toMatchObject({
action: 'click',
subject: 'b:d/c',
attributes: {
to: '/entity/{kind}/{namespace}/{name}',
},
});
expect(analyticsApi.captureEvent).toHaveBeenCalledWith(
expect.objectContaining({
action: 'click',
subject: 'b:d/c',
attributes: {
to: '/entity/{kind}/{namespace}/{name}',
},
}),
);
});
});
@@ -23,7 +23,7 @@ import { analyticsApiRef } from '@backstage/core-plugin-api';
import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react';
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
import {
MockAnalyticsApi,
mockApis,
renderInTestApp,
TestApiProvider,
} from '@backstage/test-utils';
@@ -227,9 +227,9 @@ describe.skip('<CatalogGraphPage/>', () => {
}),
);
const analyticsSpy = new MockAnalyticsApi();
const analyticsApi = mockApis.analytics();
await renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, analyticsSpy]]}>
<TestApiProvider apis={[[analyticsApiRef, analyticsApi]]}>
{wrapper}
</TestApiProvider>,
{
@@ -243,10 +243,12 @@ describe.skip('<CatalogGraphPage/>', () => {
await userEvent.click(screen.getByText('b:d/e'));
expect(analyticsSpy.getEvents()[0]).toMatchObject({
action: 'click',
subject: 'b:d/e',
});
expect(analyticsApi.captureEvent).toHaveBeenCalledWith(
expect.objectContaining({
action: 'click',
subject: 'b:d/e',
}),
);
});
test('should capture analytics event when navigating to entity', async () => {
@@ -256,9 +258,9 @@ describe.skip('<CatalogGraphPage/>', () => {
}),
);
const analyticsSpy = new MockAnalyticsApi();
const analyticsApi = mockApis.analytics();
await renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, analyticsSpy]]}>
<TestApiProvider apis={[[analyticsApiRef, analyticsApi]]}>
{wrapper}
</TestApiProvider>,
{
@@ -274,12 +276,14 @@ describe.skip('<CatalogGraphPage/>', () => {
await user.keyboard('{Shift>}');
await user.click(screen.getByText('b:d/e'));
expect(analyticsSpy.getEvents()[0]).toMatchObject({
action: 'click',
subject: 'b:d/e',
attributes: {
to: '/entity/b/d/e',
},
});
expect(analyticsApi.captureEvent).toHaveBeenCalledWith(
expect.objectContaining({
action: 'click',
subject: 'b:d/e',
attributes: {
to: '/entity/b/d/e',
},
}),
);
});
});
@@ -16,7 +16,7 @@
import { Entity } from '@backstage/catalog-model';
import { configApiRef } from '@backstage/core-plugin-api';
import { MockConfigApi, TestApiProvider } from '@backstage/test-utils';
import { mockApis, TestApiProvider } from '@backstage/test-utils';
import { makeStyles } from '@material-ui/core/styles';
import { render, screen } from '@testing-library/react';
import { renderHook } from '@testing-library/react';
@@ -46,7 +46,7 @@ const entities: Entity[] = [
},
];
const mockConfigApi = new MockConfigApi({});
const mockConfigApi = mockApis.config();
const apis = [[configApiRef, mockConfigApi]] as const;
describe('<PreviewCatalogInfoComponent />', () => {
@@ -122,10 +122,12 @@ describe('<PreviewCatalogInfoComponent />', () => {
apis={[
[
configApiRef,
new MockConfigApi({
catalog: {
import: {
entityFilename: 'anvil.yaml',
mockApis.config({
data: {
catalog: {
import: {
entityFilename: 'anvil.yaml',
},
},
},
}),
@@ -16,7 +16,7 @@
import { configApiRef, errorApiRef } from '@backstage/core-plugin-api';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { TestApiProvider, MockConfigApi } from '@backstage/test-utils';
import { TestApiProvider, mockApis } from '@backstage/test-utils';
import TextField from '@material-ui/core/TextField';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
@@ -43,7 +43,7 @@ describe('<StepPrepareCreatePullRequest />', () => {
post: jest.fn(),
};
const configApi = new MockConfigApi({});
const configApi = mockApis.config();
const Wrapper = ({ children }: { children?: React.ReactNode }) => (
<TestApiProvider
@@ -21,7 +21,7 @@ import { MockStarredEntitiesApi, starredEntitiesApiRef } from '../../apis';
import { FavoriteEntity } from './FavoriteEntity';
import { ComponentEntity } from '@backstage/catalog-model';
import {
MockStorageApi,
mockApis,
renderInTestApp,
TestApiProvider,
} from '@backstage/test-utils';
@@ -41,14 +41,12 @@ const entity: ComponentEntity = {
},
};
const mockStorage = MockStorageApi.create();
describe('<FavoriteEntity/>', () => {
it('should add to favorites', async () => {
await renderInTestApp(
<TestApiProvider
apis={[
[storageApiRef, mockStorage],
[storageApiRef, mockApis.storage()],
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
]}
>
@@ -79,7 +77,7 @@ describe('<FavoriteEntity/>', () => {
await renderInTestApp(
<TestApiProvider
apis={[
[storageApiRef, mockStorage],
[storageApiRef, mockApis.storage()],
[starredEntitiesApiRef, starredEntities],
]}
>
@@ -34,15 +34,13 @@ import {
} from '@backstage/catalog-client';
import { catalogApiRef } from '../../api';
import {
MockStorageApi,
TestApiRegistry,
mockApis,
renderInTestApp,
} from '@backstage/test-utils';
import { ApiProvider } from '@backstage/core-app-api';
import {
ConfigApi,
configApiRef,
IdentityApi,
identityApiRef,
storageApiRef,
} from '@backstage/core-plugin-api';
@@ -61,15 +59,20 @@ const mockUser: UserEntity = {
},
};
const mockConfigApi = {
getOptionalString: () => 'Test Company',
} as Partial<ConfigApi>;
const ownershipEntityRefs = ['user:default/testuser'];
const mockConfigApi = mockApis.config({
data: { organization: { name: 'Test Company' } },
});
const mockCatalogApi = catalogApiMock.mock();
jest.spyOn(mockCatalogApi, 'queryEntities');
const mockIdentityApi = {
getBackstageIdentity: jest.fn(),
} as Partial<jest.Mocked<IdentityApi>>;
const mockIdentityApi = mockApis.identity({
userEntityRef: ownershipEntityRefs[0],
ownershipEntityRefs,
});
jest.spyOn(mockIdentityApi, 'getBackstageIdentity');
const mockStarredEntitiesApi = new MockStarredEntitiesApi();
@@ -77,11 +80,10 @@ const apis = TestApiRegistry.from(
[configApiRef, mockConfigApi],
[catalogApiRef, mockCatalogApi],
[identityApiRef, mockIdentityApi],
[storageApiRef, MockStorageApi.create()],
[storageApiRef, mockApis.storage()],
[starredEntitiesApiRef, mockStarredEntitiesApi],
);
const ownershipEntityRefs = ['user:default/testuser'];
describe('<UserListPicker />', () => {
const mockQueryEntitiesImplementation: CatalogApi['queryEntities'] =
async request => {
@@ -133,19 +135,13 @@ describe('<UserListPicker />', () => {
beforeEach(() => {
mockCatalogApi.getEntityByRef?.mockResolvedValue(mockUser);
mockIdentityApi.getBackstageIdentity?.mockResolvedValue({
ownershipEntityRefs,
type: 'user',
userEntityRef: 'user:default/testuser',
});
mockCatalogApi.queryEntities?.mockImplementation(
mockQueryEntitiesImplementation,
);
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});
it('renders filter groups', async () => {
@@ -23,11 +23,7 @@ import {
useEntityList,
} from '../../hooks';
import { catalogApiRef } from '../../api';
import {
ApiRef,
IdentityApi,
identityApiRef,
} from '@backstage/core-plugin-api';
import { ApiRef, identityApiRef } from '@backstage/core-plugin-api';
import { MemoryRouter } from 'react-router-dom';
import { useOwnedEntitiesCount } from './useOwnedEntitiesCount';
import {
@@ -36,12 +32,14 @@ import {
EntityUserFilter,
} from '../../filters';
import { useMountEffect } from '@react-hookz/web';
import { mockApis } from '@backstage/test-utils';
const mockCatalogApi = catalogApiMock.mock();
const mockGetBackstageIdentity: jest.MockedFn<
IdentityApi['getBackstageIdentity']
> = jest.fn();
const mockIdentityApi = mockApis.identity({
ownershipEntityRefs: ['user:default/spiderman', 'user:group/a-group'],
userEntityRef: 'user:default/spiderman',
});
jest.spyOn(mockIdentityApi, 'getBackstageIdentity');
jest.mock('@backstage/core-plugin-api', () => {
const actual = jest.requireActual('@backstage/core-plugin-api');
@@ -50,13 +48,9 @@ jest.mock('@backstage/core-plugin-api', () => {
useApi: (ref: ApiRef<any>) => {
if (ref === catalogApiRef) {
return mockCatalogApi;
} else if (ref === identityApiRef) {
return mockIdentityApi;
}
if (ref === identityApiRef) {
return {
getBackstageIdentity: mockGetBackstageIdentity,
};
}
return actual.useApi(ref);
},
};
@@ -65,12 +59,6 @@ jest.mock('@backstage/core-plugin-api', () => {
describe('useOwnedEntitiesCount', () => {
beforeEach(() => {
jest.clearAllMocks();
mockGetBackstageIdentity.mockResolvedValue({
ownershipEntityRefs: ['user:default/spiderman', 'user:group/a-group'],
userEntityRef: 'user:default/spiderman',
type: 'user',
});
});
it(`shouldn't invoke queryEntities when filters are loading`, async () => {
@@ -84,7 +72,9 @@ describe('useOwnedEntitiesCount', () => {
wrapper: createWrapperWithInitialFilters({}),
});
await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled());
await waitFor(() =>
expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(),
);
await expect(
waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalled()),
@@ -114,7 +104,9 @@ describe('useOwnedEntitiesCount', () => {
}),
});
await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled());
await waitFor(() =>
expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(),
);
await waitFor(() =>
expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({
@@ -151,7 +143,9 @@ describe('useOwnedEntitiesCount', () => {
}),
});
await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled());
await waitFor(() =>
expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(),
);
await expect(
waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalled()),
@@ -185,7 +179,9 @@ describe('useOwnedEntitiesCount', () => {
}),
});
await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled());
await waitFor(() =>
expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(),
);
await waitFor(() =>
expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({
@@ -25,7 +25,7 @@ import {
import { Entity } from '@backstage/catalog-model';
import { analyticsApiRef, useAnalytics } from '@backstage/core-plugin-api';
import {
MockAnalyticsApi,
mockApis,
TestApiRegistry,
withLogCollector,
} from '@backstage/test-utils';
@@ -57,7 +57,7 @@ describe('useEntity', () => {
});
it('should provide entityRef analytics context', () => {
const analyticsSpy = new MockAnalyticsApi();
const analyticsSpy = mockApis.analytics();
const apis = TestApiRegistry.from([analyticsApiRef, analyticsSpy]);
const { result } = renderHook(() => useAnalytics(), {
wrapper: ({ children }: React.PropsWithChildren<{}>) => (
@@ -69,9 +69,13 @@ describe('useEntity', () => {
result.current.captureEvent('test', 'value');
expect(analyticsSpy.getEvents()[0]).toMatchObject({
context: { entityRef: 'mykind:default/my-entity' },
});
expect(analyticsSpy.captureEvent).toHaveBeenCalledWith(
expect.objectContaining({
context: expect.objectContaining({
entityRef: 'mykind:default/my-entity',
}),
}),
);
});
});
@@ -127,7 +131,7 @@ describe('useAsyncEntity', () => {
});
it('should provide entityRef analytics context', () => {
const analyticsSpy = new MockAnalyticsApi();
const analyticsSpy = mockApis.analytics.mock();
const apis = TestApiRegistry.from([analyticsApiRef, analyticsSpy]);
const { result } = renderHook(() => useAnalytics(), {
wrapper: ({ children }: React.PropsWithChildren<{}>) => (
@@ -144,13 +148,13 @@ describe('useAsyncEntity', () => {
result.current.captureEvent('test', 'value');
expect(analyticsSpy.getEvents()[0]).toMatchObject({
context: { entityRef: 'mykind:default/my-entity' },
});
expect(analyticsSpy.captureEvent.mock.calls[0][0].context.entityRef).toBe(
'mykind:default/my-entity',
);
});
it('should omit entityRef analytics context', () => {
const analyticsSpy = new MockAnalyticsApi();
const analyticsSpy = mockApis.analytics.mock();
const apis = TestApiRegistry.from([analyticsApiRef, analyticsSpy]);
const { result } = renderHook(() => useAnalytics(), {
wrapper: ({ children }: PropsWithChildren<{}>) => (
@@ -162,6 +166,8 @@ describe('useAsyncEntity', () => {
result.current.captureEvent('test', 'value');
expect(analyticsSpy.getEvents()[0].context).not.toHaveProperty('entityRef');
expect(
analyticsSpy.captureEvent.mock.calls[0][0].context,
).not.toHaveProperty('entityRef');
});
});
@@ -18,14 +18,12 @@ import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
import { Entity } from '@backstage/catalog-model';
import {
alertApiRef,
ConfigApi,
configApiRef,
errorApiRef,
IdentityApi,
identityApiRef,
storageApiRef,
} from '@backstage/core-plugin-api';
import { MockStorageApi, TestApiProvider } from '@backstage/test-utils';
import { TestApiProvider, mockApis } from '@backstage/test-utils';
import { act, renderHook, waitFor } from '@testing-library/react';
import qs from 'qs';
import React, { PropsWithChildren } from 'react';
@@ -41,7 +39,6 @@ import {
import { EntityListProvider, useEntityList } from './useEntityListProvider';
import { useMountEffect } from '@react-hookz/web';
import { translationApiRef } from '@backstage/core-plugin-api/alpha';
import { MockTranslationApi } from '@backstage/test-utils/alpha';
import { EntityListPagination } from '../types';
const entities: Entity[] = [
@@ -67,20 +64,12 @@ const entities: Entity[] = [
},
];
const mockConfigApi = {
getOptionalString: () => '',
} as Partial<ConfigApi>;
const ownershipEntityRefs = ['user:default/guest'];
const mockIdentityApi: Partial<IdentityApi> = {
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs,
}),
getCredentials: async () => ({ token: undefined }),
};
const mockIdentityApi = mockApis.identity({
userEntityRef: 'user:default/guest',
ownershipEntityRefs,
});
const mockCatalogApi = catalogApiMock.mock({
getEntities: jest.fn().mockResolvedValue({ items: entities }),
queryEntities: jest.fn().mockResolvedValue({
@@ -108,13 +97,13 @@ const createWrapper =
<MemoryRouter initialEntries={[options.location ?? '']}>
<TestApiProvider
apis={[
[configApiRef, mockConfigApi],
[configApiRef, mockApis.config()],
[catalogApiRef, mockCatalogApi],
[identityApiRef, mockIdentityApi],
[storageApiRef, MockStorageApi.create()],
[storageApiRef, mockApis.storage()],
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
[alertApiRef, { post: jest.fn() }],
[translationApiRef, MockTranslationApi.create()],
[translationApiRef, mockApis.translation()],
[errorApiRef, { error$: jest.fn(), post: jest.fn() }],
]}
>
@@ -15,20 +15,17 @@
*/
import { ComponentEntity, RELATION_OWNED_BY } from '@backstage/catalog-model';
import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api';
import { TestApiProvider } from '@backstage/test-utils';
import { identityApiRef } from '@backstage/core-plugin-api';
import { TestApiProvider, mockApis } from '@backstage/test-utils';
import { renderHook, waitFor } from '@testing-library/react';
import React from 'react';
import { useEntityOwnership } from './useEntityOwnership';
describe('useEntityOwnership', () => {
type MockIdentityApi = jest.Mocked<Pick<IdentityApi, 'getBackstageIdentity'>>;
const mockIdentityApi: MockIdentityApi = {
getBackstageIdentity: jest.fn(),
};
const identityApi = mockIdentityApi as unknown as IdentityApi;
const identityApi = mockApis.identity({
userEntityRef: 'user:default/user1',
ownershipEntityRefs: ['user:default/user1', 'group:default/group1'],
});
const Wrapper = (props: { children?: React.ReactNode }) => (
<TestApiProvider apis={[[identityApiRef, identityApi]]}>
@@ -64,12 +61,6 @@ describe('useEntityOwnership', () => {
describe('useEntityOwnership', () => {
it('matches ownership via ownership entity refs', async () => {
mockIdentityApi.getBackstageIdentity.mockResolvedValue({
type: 'user',
userEntityRef: 'user:default/user1',
ownershipEntityRefs: ['user:default/user1', 'group:default/group1'],
});
const { result } = renderHook(() => useEntityOwnership(), {
wrapper: Wrapper,
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { MockStorageApi } from '@backstage/test-utils';
import { mockApis } from '@backstage/test-utils';
import { DefaultStarredEntitiesApi } from './DefaultStarredEntitiesApi';
import { performMigrationToTheNewBucket } from './migration';
@@ -44,7 +44,7 @@ describe('DefaultStarredEntitiesApi', () => {
describe('constructor', () => {
it('should call migration', () => {
const api = new DefaultStarredEntitiesApi({
storageApi: MockStorageApi.create(),
storageApi: mockApis.storage(),
});
expect(performMigrationToTheNewBucket).toHaveBeenCalledTimes(1);
expect(api).toBeDefined();
@@ -54,7 +54,7 @@ describe('DefaultStarredEntitiesApi', () => {
it('should notify and toggle starred entities', async () => {
const entityRef = 'component:default/mock';
const storageApi = MockStorageApi.create();
const storageApi = mockApis.storage();
const storageBucket = storageApi.forBucket('starredEntities');
const api = new DefaultStarredEntitiesApi({ storageApi });
@@ -85,7 +85,7 @@ describe('DefaultStarredEntitiesApi', () => {
it('should read starred entities from storage', async () => {
const entityRef = 'component:default/mock';
const storageApi = MockStorageApi.create();
const storageApi = mockApis.storage();
const storageBucket = storageApi.forBucket('starredEntities');
storageBucket.set('entityRefs', [entityRef]);
const api = new DefaultStarredEntitiesApi({ storageApi });
@@ -15,18 +15,18 @@
*/
import { StorageApi } from '@backstage/core-plugin-api';
import { MockStorageApi } from '@backstage/test-utils';
import { mockApis } from '@backstage/test-utils';
import { performMigrationToTheNewBucket } from './migration';
describe('performMigrationToTheNewBucket', () => {
let mockStorage: StorageApi;
beforeEach(() => {
mockStorage = MockStorageApi.create();
mockStorage = mockApis.storage();
});
afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});
it('should migrate', async () => {
@@ -24,7 +24,11 @@ import {
ScmIntegrationsApi,
scmIntegrationsApiRef,
} from '@backstage/integration-react';
import { TestApiProvider, renderInTestApp } from '@backstage/test-utils';
import {
TestApiProvider,
mockApis,
renderInTestApp,
} from '@backstage/test-utils';
import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes';
import { AboutCard } from './AboutCard';
@@ -37,10 +41,6 @@ import { permissionApiRef } from '@backstage/plugin-permission-react';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import { SWRConfig } from 'swr';
const mockAuthorize = jest.fn();
const mockPermissionApi = { authorize: mockAuthorize };
describe('<AboutCard />', () => {
const catalogApi = catalogApiMock.mock();
@@ -411,10 +411,6 @@ describe('<AboutCard />', () => {
},
};
mockAuthorize.mockImplementation(async () => ({
result: AuthorizeResult.ALLOW,
}));
await renderInTestApp(
<TestApiProvider
apis={[
@@ -423,7 +419,7 @@ describe('<AboutCard />', () => {
ScmIntegrationsApi.fromConfig(new ConfigReader({})),
],
[catalogApiRef, catalogApi],
[permissionApiRef, mockPermissionApi],
[permissionApiRef, mockApis.permission()],
]}
>
<EntityProvider entity={entity}>
@@ -466,10 +462,6 @@ describe('<AboutCard />', () => {
},
};
mockAuthorize.mockImplementation(async () => ({
result: AuthorizeResult.DENY,
}));
await renderInTestApp(
<TestApiProvider
apis={[
@@ -478,7 +470,10 @@ describe('<AboutCard />', () => {
ScmIntegrationsApi.fromConfig(new ConfigReader({})),
],
[catalogApiRef, catalogApi],
[permissionApiRef, mockPermissionApi],
[
permissionApiRef,
mockApis.permission({ authorize: AuthorizeResult.DENY }),
],
]}
>
<EntityProvider entity={entity}>
@@ -766,9 +761,6 @@ describe('<AboutCard />', () => {
namespace: 'default',
},
};
mockAuthorize.mockImplementation(async () => ({
result: AuthorizeResult.ALLOW,
}));
await renderInTestApp(
<TestApiProvider
apis={[
@@ -788,7 +780,7 @@ describe('<AboutCard />', () => {
),
],
[catalogApiRef, catalogApi],
[permissionApiRef, mockPermissionApi],
[permissionApiRef, mockApis.permission()],
]}
>
<EntityProvider entity={entity}>
@@ -819,9 +811,6 @@ describe('<AboutCard />', () => {
namespace: 'default',
},
};
mockAuthorize.mockImplementation(async () => ({
result: AuthorizeResult.DENY,
}));
await renderInTestApp(
<SWRConfig value={{ provider: () => new Map() }}>
<TestApiProvider
@@ -842,7 +831,10 @@ describe('<AboutCard />', () => {
),
],
[catalogApiRef, catalogApi],
[permissionApiRef, mockPermissionApi],
[
permissionApiRef,
mockApis.permission({ authorize: AuthorizeResult.DENY }),
],
]}
>
<EntityProvider entity={entity}>
@@ -17,12 +17,7 @@
import { QueryEntitiesInitialRequest } from '@backstage/catalog-client';
import { RELATION_OWNED_BY } from '@backstage/catalog-model';
import { TableColumn, TableProps } from '@backstage/core-components';
import {
IdentityApi,
identityApiRef,
ProfileInfo,
storageApiRef,
} from '@backstage/core-plugin-api';
import { identityApiRef, storageApiRef } from '@backstage/core-plugin-api';
import {
catalogApiRef,
entityRouteRef,
@@ -31,9 +26,8 @@ import {
} from '@backstage/plugin-catalog-react';
import { mockBreakpoint } from '@backstage/core-components/testUtils';
import {
MockPermissionApi,
MockStorageApi,
TestApiProvider,
mockApis,
renderInTestApp,
} from '@backstage/test-utils';
import DashboardIcon from '@material-ui/icons/Dashboard';
@@ -54,7 +48,6 @@ describe('DefaultCatalogPage', () => {
});
afterEach(() => {
window.history.replaceState = origReplaceState;
jest.clearAllMocks();
});
@@ -166,19 +159,11 @@ describe('DefaultCatalogPage', () => {
}),
});
const testProfile: Partial<ProfileInfo> = {
const identityApi = mockApis.identity({
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest', 'group:default/tools'],
displayName: 'Display Name',
};
const identityApi: Partial<IdentityApi> = {
getBackstageIdentity: async () => ({
type: 'user',
userEntityRef: 'user:default/guest',
ownershipEntityRefs: ['user:default/guest', 'group:default/tools'],
}),
getCredentials: async () => ({ token: undefined }),
getProfileInfo: async () => testProfile,
};
const storageApi = MockStorageApi.create();
});
const renderWrapped = (children: React.ReactNode) =>
renderInTestApp(
@@ -186,9 +171,9 @@ describe('DefaultCatalogPage', () => {
apis={[
[catalogApiRef, catalogApi],
[identityApiRef, identityApi],
[storageApiRef, storageApi],
[storageApiRef, mockApis.storage()],
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
[permissionApiRef, new MockPermissionApi()],
[permissionApiRef, mockApis.permission()],
]}
>
{children}

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