diff --git a/.changeset/clever-rats-rush.md b/.changeset/clever-rats-rush.md new file mode 100644 index 0000000000..c34427f8f5 --- /dev/null +++ b/.changeset/clever-rats-rush.md @@ -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. diff --git a/.changeset/real-rockets-divide.md b/.changeset/real-rockets-divide.md new file mode 100644 index 0000000000..2506a89f22 --- /dev/null +++ b/.changeset/real-rockets-divide.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Minor doc string changes diff --git a/.changeset/slow-walls-report.md b/.changeset/slow-walls-report.md new file mode 100644 index 0000000000..e5ab380006 --- /dev/null +++ b/.changeset/slow-walls-report.md @@ -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); +} +``` diff --git a/.changeset/thin-chairs-ring.md b/.changeset/thin-chairs-ring.md new file mode 100644 index 0000000000..104c78a96d --- /dev/null +++ b/.changeset/thin-chairs-ring.md @@ -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. diff --git a/.github/workflows/api-breaking-changes-comment.yml b/.github/workflows/api-breaking-changes-comment.yml index e28f147706..038061adac 100644 --- a/.github/workflows/api-breaking-changes-comment.yml +++ b/.github/workflows/api-breaking-changes-comment.yml @@ -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 diff --git a/.github/workflows/api-breaking-changes.yml b/.github/workflows/api-breaking-changes.yml index 1c2596bad6..62195b155e 100644 --- a/.github/workflows/api-breaking-changes.yml +++ b/.github/workflows/api-breaking-changes.yml @@ -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: | diff --git a/.github/workflows/automate_changeset_feedback.yml b/.github/workflows/automate_changeset_feedback.yml index 1f07a848fb..a0c0e44902 100644 --- a/.github/workflows/automate_changeset_feedback.yml +++ b/.github/workflows/automate_changeset_feedback.yml @@ -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 diff --git a/.github/workflows/automate_merge_message.yml b/.github/workflows/automate_merge_message.yml index 4ab75b816f..4b388d7850 100644 --- a/.github/workflows/automate_merge_message.yml +++ b/.github/workflows/automate_merge_message.yml @@ -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 }}' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af727736f2..a50be35023 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.github/workflows/deploy_docker-image.yml b/.github/workflows/deploy_docker-image.yml index a45c9c35bd..d5934e8ad8 100644 --- a/.github/workflows/deploy_docker-image.yml +++ b/.github/workflows/deploy_docker-image.yml @@ -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 }} diff --git a/.github/workflows/deploy_microsite.yml b/.github/workflows/deploy_microsite.yml index 7dad0d3b20..92c5f28498 100644 --- a/.github/workflows/deploy_microsite.yml +++ b/.github/workflows/deploy_microsite.yml @@ -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 diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index 8bafa2fe43..bd00c2055d 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -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 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index b6f72a66a6..23799a9a6b 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -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 diff --git a/.github/workflows/sync_code-formatting.yml b/.github/workflows/sync_code-formatting.yml index f59eb048e0..2aa84bb208 100644 --- a/.github/workflows/sync_code-formatting.yml +++ b/.github/workflows/sync_code-formatting.yml @@ -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 diff --git a/.github/workflows/sync_dependabot-changesets.yml b/.github/workflows/sync_dependabot-changesets.yml index adf39be8df..a89ab7629a 100644 --- a/.github/workflows/sync_dependabot-changesets.yml +++ b/.github/workflows/sync_dependabot-changesets.yml @@ -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 }} diff --git a/.github/workflows/sync_release-manifest.yml b/.github/workflows/sync_release-manifest.yml index 87a8837c48..4b209739c7 100644 --- a/.github/workflows/sync_release-manifest.yml +++ b/.github/workflows/sync_release-manifest.yml @@ -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 diff --git a/.github/workflows/sync_renovate-changesets.yml b/.github/workflows/sync_renovate-changesets.yml index 200bc6eaaa..1c9b4a6186 100644 --- a/.github/workflows/sync_renovate-changesets.yml +++ b/.github/workflows/sync_renovate-changesets.yml @@ -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 }} diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml index 9c709508b3..1390b4ec61 100644 --- a/.github/workflows/sync_snyk-github-issues.yml +++ b/.github/workflows/sync_snyk-github-issues.yml @@ -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 diff --git a/.github/workflows/sync_snyk-monitor.yml b/.github/workflows/sync_snyk-monitor.yml index 1b48930410..f746a8c339 100644 --- a/.github/workflows/sync_snyk-monitor.yml +++ b/.github/workflows/sync_snyk-monitor.yml @@ -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: diff --git a/.github/workflows/sync_version-packages.yml b/.github/workflows/sync_version-packages.yml index aac606ae90..77fb5f951e 100644 --- a/.github/workflows/sync_version-packages.yml +++ b/.github/workflows/sync_version-packages.yml @@ -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 diff --git a/.github/workflows/verify_accessibility.yml b/.github/workflows/verify_accessibility.yml index cfeb8ba51f..29ff740f68 100644 --- a/.github/workflows/verify_accessibility.yml +++ b/.github/workflows/verify_accessibility.yml @@ -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: diff --git a/.github/workflows/verify_codeql.yml b/.github/workflows/verify_codeql.yml index 63ba85906e..c77b65929b 100644 --- a/.github/workflows/verify_codeql.yml +++ b/.github/workflows/verify_codeql.yml @@ -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. diff --git a/.github/workflows/verify_docs-quality.yml b/.github/workflows/verify_docs-quality.yml index 8e2ae20596..176136f821 100644 --- a/.github/workflows/verify_docs-quality.yml +++ b/.github/workflows/verify_docs-quality.yml @@ -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 diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index 69a92abb9a..0c487429e9 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -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: | diff --git a/.github/workflows/verify_e2e-techdocs.yml b/.github/workflows/verify_e2e-techdocs.yml index 15bb9b3479..f6b300af7e 100644 --- a/.github/workflows/verify_e2e-techdocs.yml +++ b/.github/workflows/verify_e2e-techdocs.yml @@ -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' diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index 911698867c..b6bfef51e5 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -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: | diff --git a/.github/workflows/verify_fossa.yml b/.github/workflows/verify_fossa.yml index 8b3bfc9b13..5deaeafeaf 100644 --- a/.github/workflows/verify_fossa.yml +++ b/.github/workflows/verify_fossa.yml @@ -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" diff --git a/.github/workflows/verify_microsite.yml b/.github/workflows/verify_microsite.yml index 5527953767..1d6aac21ca 100644 --- a/.github/workflows/verify_microsite.yml +++ b/.github/workflows/verify_microsite.yml @@ -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 diff --git a/.github/workflows/verify_microsite_accessibility.yml b/.github/workflows/verify_microsite_accessibility.yml index 350c46309e..6626f4def9 100644 --- a/.github/workflows/verify_microsite_accessibility.yml +++ b/.github/workflows/verify_microsite_accessibility.yml @@ -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 diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_storybook.yml index 77a01f9ffd..ae3470b079 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_storybook.yml @@ -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 diff --git a/.github/workflows/verify_windows.yml b/.github/workflows/verify_windows.yml index 4cb2cab220..d184bdca08 100644 --- a/.github/workflows/verify_windows.yml +++ b/.github/workflows/verify_windows.yml @@ -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 diff --git a/docs/backend-system/core-services/root-http-router.md b/docs/backend-system/core-services/root-http-router.md index 88a10455f5..a2316b957b 100644 --- a/docs/backend-system/core-services/root-http-router.md +++ b/docs/backend-system/core-services/root-http-router.md @@ -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()); diff --git a/docs/tooling/cli/02-build-system.md b/docs/tooling/cli/02-build-system.md index dfd95ae6eb..f1fb5f6c9e 100644 --- a/docs/tooling/cli/02-build-system.md +++ b/docs/tooling/cli/02-build-system.md @@ -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 diff --git a/docs/tooling/local-dev/profiling.md b/docs/tooling/local-dev/profiling.md index bbc263c3d5..b1c904d6d0 100644 --- a/docs/tooling/local-dev/profiling.md +++ b/docs/tooling/local-dev/profiling.md @@ -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. diff --git a/packages/app/src/components/catalog/EntityPage.test.tsx b/packages/app/src/components/catalog/EntityPage.test.tsx index 5d2b23b007..0943a82efb 100644 --- a/packages/app/src/components/catalog/EntityPage.test.tsx +++ b/packages/app/src/components/catalog/EntityPage.test.tsx @@ -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', () => { diff --git a/packages/backend-defaults/src/entrypoints/auth/helpers.test.ts b/packages/backend-defaults/src/entrypoints/auth/helpers.test.ts new file mode 100644 index 0000000000..8454aa3ecc --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/auth/helpers.test.ts @@ -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/); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/auth/helpers.ts b/packages/backend-defaults/src/entrypoints/auth/helpers.ts index eebe45eb76..dad36670b9 100644 --- a/packages/backend-defaults/src/entrypoints/auth/helpers.ts +++ b/packages/backend-defaults/src/entrypoints/auth/helpers.ts @@ -28,16 +28,23 @@ export function createCredentialsWithServicePrincipal( token?: string, accessRestrictions?: BackstagePrincipalAccessRestrictions, ): InternalBackstageCredentials { - 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 { - 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 { diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts index a18ddbe096..59811c15d4 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts @@ -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()); diff --git a/packages/backend-test-utils/report.api.md b/packages/backend-test-utils/report.api.md index 1235cd7cb3..d9c1c4c721 100644 --- a/packages/backend-test-utils/report.api.md +++ b/packages/backend-test-utils/report.api.md @@ -150,7 +150,7 @@ export function mockErrorHandler(): ErrorRequestHandler< Record >; -// @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". diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 8b0f196399..789b75de9d 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -128,7 +128,46 @@ function simpleMock( } /** + * 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 { diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts index 1e29f2548a..50f93444ed 100644 --- a/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts +++ b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts @@ -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, diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts index 3b4b7136bb..bd82d58736 100644 --- a/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts +++ b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts @@ -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', diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts index 1335808280..c1359ee6b2 100644 --- a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts @@ -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']], diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts index 5631a9a886..39f1d0dd68 100644 --- a/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts @@ -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, diff --git a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts index fc77754f6f..8a7bb9e671 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts @@ -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({ diff --git a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts index 6dde0c0334..0818bbb253 100644 --- a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts @@ -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, diff --git a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.test.ts index 7897eef42e..a740b3089b 100644 --- a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.test.ts @@ -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, diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts index 8ca818ebd8..a34589d746 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts @@ -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 () => { diff --git a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.test.ts index 5e7370a947..621172f8cc 100644 --- a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.test.ts @@ -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(), diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index 01277a09c7..d995edffdf 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -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, diff --git a/packages/core-app-api/src/app/AppRouter.test.tsx b/packages/core-app-api/src/app/AppRouter.test.tsx index 9f3ab2ad89..b595f91794 100644 --- a/packages/core-app-api/src/app/AppRouter.test.tsx +++ b/packages/core-app-api/src/app/AppRouter.test.tsx @@ -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; }; diff --git a/packages/core-app-api/src/app/resolveRouteBindings.test.ts b/packages/core-app-api/src/app/resolveRouteBindings.test.ts index fdc1a9e456..e3dbbba0af 100644 --- a/packages/core-app-api/src/app/resolveRouteBindings.test.ts +++ b/packages/core-app-api/src/app/resolveRouteBindings.test.ts @@ -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], ); diff --git a/packages/core-app-api/src/routing/RouteTracker.test.tsx b/packages/core-app-api/src/routing/RouteTracker.test.tsx index 25d5cfbcfb..ecbd44315d 100644 --- a/packages/core-app-api/src/routing/RouteTracker.test.tsx +++ b/packages/core-app-api/src/routing/RouteTracker.test.tsx @@ -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 = { - captureEvent: jest.fn(), - }; + const mockedAnalytics = mockApis.analytics(); beforeEach(() => { jest.clearAllMocks(); diff --git a/packages/core-components/src/components/AutoLogout/Autologout.test.tsx b/packages/core-components/src/components/AutoLogout/Autologout.test.tsx index 164ad716d2..5a555cd215 100644 --- a/packages/core-components/src/components/AutoLogout/Autologout.test.tsx +++ b/packages/core-components/src/components/AutoLogout/Autologout.test.tsx @@ -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', () => { diff --git a/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx b/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx index 8d93186ccc..6f6da1231f 100644 --- a/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx +++ b/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx @@ -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('', () => { - 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( diff --git a/packages/core-components/src/components/Link/Link.test.tsx b/packages/core-components/src/components/Link/Link.test.tsx index fe616299c3..0a86c06d43 100644 --- a/packages/core-components/src/components/Link/Link.test.tsx +++ b/packages/core-components/src/components/Link/Link.test.tsx @@ -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('', () => { 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('', () => { // 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('', () => { 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('', () => { expect(customOnClick).toHaveBeenCalled(); // But there should be no analytics event. - expect(analyticsApi.getEvents()).toHaveLength(0); + expect(analyticsApi.captureEvent).not.toHaveBeenCalled(); }); }); diff --git a/packages/core-components/src/components/SupportButton/SupportButton.test.tsx b/packages/core-components/src/components/SupportButton/SupportButton.test.tsx index 6b0e239aa5..9ed8fb8401 100644 --- a/packages/core-components/src/components/SupportButton/SupportButton.test.tsx +++ b/packages/core-components/src/components/SupportButton/SupportButton.test.tsx @@ -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' }], + }, + ], + }, }, }, }); diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx index e4e65b613f..ad7e650d24 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx +++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx @@ -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' }), ], ]} > diff --git a/packages/core-components/src/layout/Sidebar/Items.test.tsx b/packages/core-components/src/layout/Sidebar/Items.test.tsx index 6c959bfd0d..ee0f0ea401 100644 --- a/packages/core-components/src/layout/Sidebar/Items.test.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.test.tsx @@ -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( diff --git a/packages/frontend-app-api/src/routing/RouteTracker.test.tsx b/packages/frontend-app-api/src/routing/RouteTracker.test.tsx index aeec03992f..372afea4c3 100644 --- a/packages/frontend-app-api/src/routing/RouteTracker.test.tsx +++ b/packages/frontend-app-api/src/routing/RouteTracker.test.tsx @@ -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'; diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts index 7ffe64d6d7..eac6e9d2aa 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts @@ -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']), }), ); diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx index 72594cefd1..6c5331a972 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx @@ -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', diff --git a/packages/frontend-defaults/src/createApp.test.tsx b/packages/frontend-defaults/src/createApp.test.tsx index 2890700653..06872a5691 100644 --- a/packages/frontend-defaults/src/createApp.test.tsx +++ b/packages/frontend-defaults/src/createApp.test.tsx @@ -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', diff --git a/packages/frontend-defaults/src/createPublicSignInApp.test.tsx b/packages/frontend-defaults/src/createPublicSignInApp.test.tsx index ac1adf9b0f..560abc602f 100644 --- a/packages/frontend-defaults/src/createPublicSignInApp.test.tsx +++ b/packages/frontend-defaults/src/createPublicSignInApp.test.tsx @@ -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
; }, diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx index 3e260f0c9b..14aaebb778 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx @@ -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' }), - ]); + ); }); }); diff --git a/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.test.ts index 65daad0586..678eff0b4c 100644 --- a/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.test.ts @@ -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().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(); } diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index fe93b05b57..6285a8b479 100644 --- a/packages/frontend-test-utils/package.json +++ b/packages/frontend-test-utils/package.json @@ -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", diff --git a/packages/frontend-test-utils/report.api.md b/packages/frontend-test-utils/report.api.md index 3229da9898..dc37ec4fb4 100644 --- a/packages/frontend-test-utils/report.api.md +++ b/packages/frontend-test-utils/report.api.md @@ -3,13 +3,12 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// /// 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 = { - factory: ApiFactory; -} & { - [Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return - ? TApi[Key] & jest.MockInstance - : TApi[Key]; -}; +export { ApiMock }; // @public (undocumented) export function createExtensionTester( @@ -103,6 +96,8 @@ export class MockAnalyticsApi implements AnalyticsApi { getEvents(): AnalyticsEvent[]; } +export { mockApis }; + export { MockConfigApi }; export { MockErrorApi }; diff --git a/packages/frontend-test-utils/src/apis/index.ts b/packages/frontend-test-utils/src/apis/index.ts index 564c327aef..e5a0786cd0 100644 --- a/packages/frontend-test-utils/src/apis/index.ts +++ b/packages/frontend-test-utils/src/apis/index.ts @@ -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'; diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 8a1ea4f083..5fa0a19adb 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -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 } diff --git a/packages/test-utils/report-alpha.api.md b/packages/test-utils/report-alpha.api.md index ba098135fb..19a3786fc9 100644 --- a/packages/test-utils/report-alpha.api.md +++ b/packages/test-utils/report-alpha.api.md @@ -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) ``` diff --git a/packages/test-utils/report.api.md b/packages/test-utils/report.api.md index 4a1f297c4e..11657ce534 100644 --- a/packages/test-utils/report.api.md +++ b/packages/test-utils/report.api.md @@ -3,8 +3,11 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + 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 = { + factory: ApiFactory; +} & { + [Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return + ? TApi[Key] & jest.MockInstance + : TApi[Key]; +}; // @public export type AsyncLogCollector = () => Promise; @@ -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; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; + } + export function config(options?: { data?: JsonObject }): ConfigApi; + export namespace config { + const factory: ( + options?: + | { + data?: JsonObject | undefined; + } + | undefined, + ) => ApiFactory; + const mock: (partialImpl?: Partial | undefined) => ApiMock; + } + export function discovery(options?: { baseUrl?: string }): DiscoveryApi; + export namespace discovery { + const // (undocumented) + factory: ( + options?: + | { + baseUrl?: string | undefined; + } + | undefined, + ) => ApiFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; + } + 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; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; + } + 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; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; + } + export function storage(options?: { data?: JsonObject }): StorageApi; + export namespace storage { + const // (undocumented) + factory: ( + options?: + | { + data?: JsonObject | undefined; + } + | undefined, + ) => ApiFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; + } + export function translation(): TranslationApi; + export namespace translation { + const // (undocumented) + factory: () => ApiFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; + } +} + // @public @deprecated export function mockBreakpoint(options: { matches: boolean }): void; -// @public +// @public @deprecated export class MockConfigApi implements ConfigApi { constructor(data: JsonObject); get(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; } -// @public +// @public @deprecated export class MockStorageApi implements StorageApi { // (undocumented) static create(data?: MockStorageBucket): MockStorageApi; @@ -177,7 +308,7 @@ export class MockStorageApi implements StorageApi { snapshot(key: string): StorageValueSnapshot; } -// @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". ``` diff --git a/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts b/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts index 6da225df1c..788d2ca0d8 100644 --- a/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts +++ b/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts @@ -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[] = []; diff --git a/packages/frontend-test-utils/src/apis/ApiMock.ts b/packages/test-utils/src/testUtils/apis/ApiMock.ts similarity index 94% rename from packages/frontend-test-utils/src/apis/ApiMock.ts rename to packages/test-utils/src/testUtils/apis/ApiMock.ts index d11689f98a..fe66f35458 100644 --- a/packages/frontend-test-utils/src/apis/ApiMock.ts +++ b/packages/test-utils/src/testUtils/apis/ApiMock.ts @@ -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 diff --git a/packages/test-utils/src/testUtils/apis/ConfigApi/MockConfigApi.ts b/packages/test-utils/src/testUtils/apis/ConfigApi/MockConfigApi.ts index 372c295c02..3c5329962a 100644 --- a/packages/test-utils/src/testUtils/apis/ConfigApi/MockConfigApi.ts +++ b/packages/test-utils/src/testUtils/apis/ConfigApi/MockConfigApi.ts @@ -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({ diff --git a/packages/test-utils/src/testUtils/apis/PermissionApi/MockPermissionApi.ts b/packages/test-utils/src/testUtils/apis/PermissionApi/MockPermissionApi.ts index 163af5dd53..6bd3c5d4c5 100644 --- a/packages/test-utils/src/testUtils/apis/PermissionApi/MockPermissionApi.ts +++ b/packages/test-utils/src/testUtils/apis/PermissionApi/MockPermissionApi.ts @@ -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 { diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts index 63aa4235e1..cdae96d5e4 100644 --- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts @@ -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'; diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts index 30d523aed2..2fb70834ef 100644 --- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts @@ -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 { diff --git a/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts b/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts index 608b634cc4..46c5bfd5d3 100644 --- a/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts +++ b/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts @@ -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({ diff --git a/packages/test-utils/src/testUtils/apis/index.ts b/packages/test-utils/src/testUtils/apis/index.ts index e122a4b432..94738a598e 100644 --- a/packages/test-utils/src/testUtils/apis/index.ts +++ b/packages/test-utils/src/testUtils/apis/index.ts @@ -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'; diff --git a/packages/test-utils/src/testUtils/apis/mockApis.test.tsx b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx new file mode 100644 index 0000000000..31cb03e117 --- /dev/null +++ b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx @@ -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(resolve => { + storage.observe$('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(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('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(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('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(k: string): StorageValueSnapshot { + 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: '
' })).toBe('Foo
'); + expect( + snapshot.t('foo', { + foo: '
', + interpolation: { escapeValue: true }, + }), + ).toBe('Foo <div>'); + }); + + 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); + }); + }); +}); diff --git a/packages/test-utils/src/testUtils/apis/mockApis.ts b/packages/test-utils/src/testUtils/apis/mockApis.ts new file mode 100644 index 0000000000..00473679d2 --- /dev/null +++ b/packages/test-utils/src/testUtils/apis/mockApis.ts @@ -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( + ref: ApiRef, + factory: (...args: TArgs) => TApi, +): (...args: TArgs) => ApiFactory { + return (...args) => + createApiFactory({ + api: ref, + deps: {}, + factory: () => factory(...args), + }); +} + +/** @internal */ +function simpleMock( + ref: ApiRef, + mockFactory: () => jest.Mocked, +): (partialImpl?: Partial) => ApiMock { + 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; + }; +} + +/** + * 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 => ({ + 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( + * + * + * , + * ); + * ``` + */ + 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/`. + * + * @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 => ({ + 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(), + })); + } +} diff --git a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx index b8d8b07436..34706e8136 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx @@ -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} diff --git a/plugins/auth-react/src/components/CookieAuthRedirect/CookieAuthRedirect.test.tsx b/plugins/auth-react/src/components/CookieAuthRedirect/CookieAuthRedirect.test.tsx index 6c0141a9d1..18c6408407 100644 --- a/plugins/auth-react/src/components/CookieAuthRedirect/CookieAuthRedirect.test.tsx +++ b/plugins/auth-react/src/components/CookieAuthRedirect/CookieAuthRedirect.test.tsx @@ -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(); diff --git a/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx b/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx index bba876e6be..8b07abb163 100644 --- a/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx +++ b/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx @@ -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', () => { @@ -76,7 +71,7 @@ describe('CookieAuthRefreshProvider', () => { @@ -107,7 +102,7 @@ describe('CookieAuthRefreshProvider', () => { @@ -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', () => { diff --git a/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.test.tsx b/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.test.tsx index 422f392e93..6b59dbcb9e 100644 --- a/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.test.tsx +++ b/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.test.tsx @@ -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' }, ), ); diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx index 916653f600..22eff03843 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx @@ -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('', () => { ], })); - const analyticsSpy = new MockAnalyticsApi(); + const analyticsApi = mockApis.analytics(); await renderInTestApp( - + {wrapper} , { @@ -228,12 +228,14 @@ describe('', () => { 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}', + }, + }), + ); }); }); diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx index 9a081747ad..054986e595 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx @@ -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('', () => { }), ); - const analyticsSpy = new MockAnalyticsApi(); + const analyticsApi = mockApis.analytics(); await renderInTestApp( - + {wrapper} , { @@ -243,10 +243,12 @@ describe.skip('', () => { 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('', () => { }), ); - const analyticsSpy = new MockAnalyticsApi(); + const analyticsApi = mockApis.analytics(); await renderInTestApp( - + {wrapper} , { @@ -274,12 +276,14 @@ describe.skip('', () => { 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', + }, + }), + ); }); }); diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx index 1ed8026a2b..f832c842a5 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx @@ -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('', () => { @@ -122,10 +122,12 @@ describe('', () => { apis={[ [ configApiRef, - new MockConfigApi({ - catalog: { - import: { - entityFilename: 'anvil.yaml', + mockApis.config({ + data: { + catalog: { + import: { + entityFilename: 'anvil.yaml', + }, }, }, }), diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx index 6e81b27c55..acc2715fec 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx @@ -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('', () => { post: jest.fn(), }; - const configApi = new MockConfigApi({}); + const configApi = mockApis.config(); const Wrapper = ({ children }: { children?: React.ReactNode }) => ( ', () => { it('should add to favorites', async () => { await renderInTestApp( @@ -79,7 +77,7 @@ describe('', () => { await renderInTestApp( diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index 888940f30b..3ef5c242a1 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -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; +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>; +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('', () => { const mockQueryEntitiesImplementation: CatalogApi['queryEntities'] = async request => { @@ -133,19 +135,13 @@ describe('', () => { 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 () => { diff --git a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx index 4c620060fe..8ea9f63472 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx @@ -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) => { 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({ diff --git a/plugins/catalog-react/src/hooks/useEntity.test.tsx b/plugins/catalog-react/src/hooks/useEntity.test.tsx index 2923ed828d..022584bd4c 100644 --- a/plugins/catalog-react/src/hooks/useEntity.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntity.test.tsx @@ -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'); }); }); diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index 0bda2d19ec..2ed944d339 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -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; - const ownershipEntityRefs = ['user:default/guest']; -const mockIdentityApi: Partial = { - 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 = diff --git a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx index 5516a249ea..684580792d 100644 --- a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx @@ -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>; - - 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 }) => ( @@ -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, }); diff --git a/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts b/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts index b696e44e0a..5e9e6657af 100644 --- a/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts +++ b/plugins/catalog/src/apis/StarredEntitiesApi/DefaultStarredEntitiesApi.test.ts @@ -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 }); diff --git a/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts b/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts index c9083969ee..9f99c6e2dd 100644 --- a/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts +++ b/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts @@ -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 () => { diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx index b175593b68..87f35bbc3e 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx @@ -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('', () => { const catalogApi = catalogApiMock.mock(); @@ -411,10 +411,6 @@ describe('', () => { }, }; - mockAuthorize.mockImplementation(async () => ({ - result: AuthorizeResult.ALLOW, - })); - await renderInTestApp( ', () => { ScmIntegrationsApi.fromConfig(new ConfigReader({})), ], [catalogApiRef, catalogApi], - [permissionApiRef, mockPermissionApi], + [permissionApiRef, mockApis.permission()], ]} > @@ -466,10 +462,6 @@ describe('', () => { }, }; - mockAuthorize.mockImplementation(async () => ({ - result: AuthorizeResult.DENY, - })); - await renderInTestApp( ', () => { ScmIntegrationsApi.fromConfig(new ConfigReader({})), ], [catalogApiRef, catalogApi], - [permissionApiRef, mockPermissionApi], + [ + permissionApiRef, + mockApis.permission({ authorize: AuthorizeResult.DENY }), + ], ]} > @@ -766,9 +761,6 @@ describe('', () => { namespace: 'default', }, }; - mockAuthorize.mockImplementation(async () => ({ - result: AuthorizeResult.ALLOW, - })); await renderInTestApp( ', () => { ), ], [catalogApiRef, catalogApi], - [permissionApiRef, mockPermissionApi], + [permissionApiRef, mockApis.permission()], ]} > @@ -819,9 +811,6 @@ describe('', () => { namespace: 'default', }, }; - mockAuthorize.mockImplementation(async () => ({ - result: AuthorizeResult.DENY, - })); await renderInTestApp( new Map() }}> ', () => { ), ], [catalogApiRef, catalogApi], - [permissionApiRef, mockPermissionApi], + [ + permissionApiRef, + mockApis.permission({ authorize: AuthorizeResult.DENY }), + ], ]} > diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx index f5a16e3af3..07cf98359e 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx @@ -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 = { + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + ownershipEntityRefs: ['user:default/guest', 'group:default/tools'], displayName: 'Display Name', - }; - const identityApi: Partial = { - 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} diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx index f7220cb1ea..4725f35c86 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx @@ -17,7 +17,7 @@ import { EntityProvider } from '@backstage/plugin-catalog-react'; import { permissionApiRef } from '@backstage/plugin-permission-react'; import { - MockPermissionApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -26,11 +26,9 @@ import { fireEvent, screen } from '@testing-library/react'; import * as React from 'react'; import { EntityContextMenu } from './EntityContextMenu'; -const mockPermissionApi = new MockPermissionApi(); - function render(children: React.ReactNode) { return renderInTestApp( - + + { }, } as Entity; - const mockApis = TestApiRegistry.from( + const apis = TestApiRegistry.from( [catalogApiRef, catalogApiMock()], [alertApiRef, {} as AlertApi], [starredEntitiesApiRef, new MockStarredEntitiesApi()], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ); it('renders simplest case', async () => { await renderInTestApp( - + @@ -93,7 +93,7 @@ describe('EntityLayout', () => { } as Entity; await renderInTestApp( - + @@ -117,7 +117,7 @@ describe('EntityLayout', () => { it('renders default error message when entity is not found', async () => { await renderInTestApp( - + @@ -142,7 +142,7 @@ describe('EntityLayout', () => { it('renders custom message when entity is not found', async () => { await renderInTestApp( - + Oppps.. Your entity was not found
} @@ -171,7 +171,7 @@ describe('EntityLayout', () => { it('navigates when user clicks different tab', async () => { await renderInTestApp( - + @@ -211,7 +211,7 @@ describe('EntityLayout', () => { const shouldNotRenderTab = (e: Entity) => e.metadata.name === 'some-entity'; await renderInTestApp( - + @@ -254,7 +254,7 @@ describe('EntityLayout', () => { relations: [{ type: 'ownedBy', targetRef: mockTargetRef }], }; await renderInTestApp( - + @@ -327,7 +327,7 @@ describe('EntityLayout - CleanUpAfterRemoval', () => { [catalogApiRef, catalogApi], [alertApiRef, alertApi], [starredEntitiesApiRef, new MockStarredEntitiesApi()], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -378,7 +378,7 @@ describe('EntityLayout - CleanUpAfterRemoval', () => { [catalogApiRef, catalogApi], [alertApiRef, alertApi], [starredEntitiesApiRef, new MockStarredEntitiesApi()], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > diff --git a/plugins/home/src/api/VisitsStorageApi.test.ts b/plugins/home/src/api/VisitsStorageApi.test.ts index 5e8bc2dbfc..55cf623149 100644 --- a/plugins/home/src/api/VisitsStorageApi.test.ts +++ b/plugins/home/src/api/VisitsStorageApi.test.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { BackstageUserIdentity, IdentityApi } from '@backstage/core-plugin-api'; import { VisitsStorageApi } from './VisitsStorageApi'; -import { MockStorageApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; import { Visit, VisitsApi } from './VisitsApi'; describe('VisitsStorageApi.create', () => { @@ -26,13 +25,9 @@ describe('VisitsStorageApi.create', () => { () => Math.floor(Math.random() * 16).toString(16), // 0x0 to 0xf ) as `${string}-${string}-${string}-${string}-${string}`; - const mockIdentityApi: IdentityApi = { - signOut: jest.fn(), - getProfileInfo: jest.fn(), - getBackstageIdentity: async () => - ({ userEntityRef: 'user:default/guest' } as BackstageUserIdentity), - getCredentials: jest.fn(), - }; + const mockIdentityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + }); beforeEach(() => { window.crypto.randomUUID = mockRandomUUID; @@ -40,14 +35,14 @@ describe('VisitsStorageApi.create', () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); jest.useRealTimers(); window.localStorage.clear(); }); it('instantiates', () => { const api = VisitsStorageApi.create({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), identityApi: mockIdentityApi, }); expect(api).toBeTruthy(); @@ -56,7 +51,7 @@ describe('VisitsStorageApi.create', () => { describe('.save()', () => { it('saves a visit', async () => { const api = VisitsStorageApi.create({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), identityApi: mockIdentityApi, }); const visit = { @@ -73,7 +68,7 @@ describe('VisitsStorageApi.create', () => { it('can control the number of stored entities', async () => { const api = VisitsStorageApi.create({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), identityApi: mockIdentityApi, limit: 2, }); @@ -107,7 +102,7 @@ describe('VisitsStorageApi.create', () => { it('correctly bumps the hits from a previous visit', async () => { const api = VisitsStorageApi.create({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), identityApi: mockIdentityApi, }); const visit = { @@ -149,7 +144,7 @@ describe('VisitsStorageApi.create', () => { beforeEach(() => { api = VisitsStorageApi.create({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), identityApi: mockIdentityApi, }); diff --git a/plugins/home/src/api/VisitsWebStorageApi.test.ts b/plugins/home/src/api/VisitsWebStorageApi.test.ts index 1cf1fa3129..8d8669e99e 100644 --- a/plugins/home/src/api/VisitsWebStorageApi.test.ts +++ b/plugins/home/src/api/VisitsWebStorageApi.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { BackstageUserIdentity, IdentityApi } from '@backstage/core-plugin-api'; +import { mockApis } from '@backstage/test-utils'; import { VisitsWebStorageApi } from './VisitsWebStorageApi'; describe('VisitsWebStorageApi.create()', () => { @@ -24,13 +24,9 @@ describe('VisitsWebStorageApi.create()', () => { () => Math.floor(Math.random() * 16).toString(16), // 0x0 to 0xf ) as `${string}-${string}-${string}-${string}-${string}`; - const mockIdentityApi: IdentityApi = { - signOut: jest.fn(), - getProfileInfo: jest.fn(), - getBackstageIdentity: async () => - ({ userEntityRef: 'user:default/guest' } as BackstageUserIdentity), - getCredentials: jest.fn(), - }; + const mockIdentityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + }); const mockErrorApi = { post: jest.fn(), error$: jest.fn() }; @@ -40,7 +36,7 @@ describe('VisitsWebStorageApi.create()', () => { afterEach(() => { window.localStorage.clear(); - jest.resetAllMocks(); + jest.clearAllMocks(); }); it('instantiates with only identitiyApi', () => { diff --git a/plugins/home/src/api/config.test.ts b/plugins/home/src/api/config.test.ts index c21d856038..8ff472fd6b 100644 --- a/plugins/home/src/api/config.test.ts +++ b/plugins/home/src/api/config.test.ts @@ -15,15 +15,17 @@ */ import { readFilterConfig, createFilterByQueryParamFromConfig } from './config'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; describe('config', () => { describe('readFilterConfig', () => { it('returns filter data', async () => { - const mockConfig = new MockConfigApi({ - field: 'pathname', - operator: '==', - value: '/home', + const mockConfig = mockApis.config({ + data: { + field: 'pathname', + operator: '==', + value: '/home', + }, }); const res = readFilterConfig(mockConfig); expect(res).toEqual({ @@ -34,10 +36,12 @@ describe('config', () => { }); it('returns undefined for invalid filter', async () => { - const mockInvalidConfig = new MockConfigApi({ - myField: 'pathname', - operator: '==', - value: '3', + const mockInvalidConfig = mockApis.config({ + data: { + myField: 'pathname', + operator: '==', + value: '3', + }, }); const res = readFilterConfig(mockInvalidConfig); expect(res).toEqual(undefined); @@ -46,15 +50,19 @@ describe('config', () => { describe('createFilterByQueryParamFromConfig', () => { it('returns filter data', async () => { - const mockConfig1 = new MockConfigApi({ - field: 'id', - operator: '==', - value: '3', + const mockConfig1 = mockApis.config({ + data: { + field: 'id', + operator: '==', + value: '3', + }, }); - const mockConfig2 = new MockConfigApi({ - field: 'pathname', - operator: '==', - value: 'path', + const mockConfig2 = mockApis.config({ + data: { + field: 'pathname', + operator: '==', + value: 'path', + }, }); const res = createFilterByQueryParamFromConfig([ mockConfig1, @@ -67,15 +75,19 @@ describe('config', () => { }); it('returns only valid filters', async () => { - const mockValidConfig = new MockConfigApi({ - field: 'id', - operator: '==', - value: 3, + const mockValidConfig = mockApis.config({ + data: { + field: 'id', + operator: '==', + value: 3, + }, }); - const mockInvalidConfig = new MockConfigApi({ - myField: 'pathname', - operator: '==', - value: 'path', + const mockInvalidConfig = mockApis.config({ + data: { + myField: 'pathname', + operator: '==', + value: 'path', + }, }); const res = createFilterByQueryParamFromConfig([ mockValidConfig, diff --git a/plugins/home/src/homePageComponents/VisitedByType/Content.test.tsx b/plugins/home/src/homePageComponents/VisitedByType/Content.test.tsx index 055ebf998b..ec2d9eb068 100644 --- a/plugins/home/src/homePageComponents/VisitedByType/Content.test.tsx +++ b/plugins/home/src/homePageComponents/VisitedByType/Content.test.tsx @@ -19,7 +19,7 @@ import { Content } from './Content'; import { TestApiProvider, renderInTestApp, - MockConfigApi, + mockApis, } from '@backstage/test-utils'; import { visitsApiRef } from '../../api'; import { ContextProvider } from './Context'; @@ -138,16 +138,18 @@ describe('', () => { }); it('allows recent items to be filtered using config', async () => { - const configApiMock = new MockConfigApi({ - home: { - recentVisits: { - filterBy: [ - { - field: 'pathname', - operator: '==', - value: '/tech-radar', - }, - ], + const configApiMock = mockApis.config({ + data: { + home: { + recentVisits: { + filterBy: [ + { + field: 'pathname', + operator: '==', + value: '/tech-radar', + }, + ], + }, }, }, }); @@ -206,20 +208,22 @@ describe('', () => { }); it('allows recent items to have no filter if the filter config is not valid', async () => { - const configApiMock = new MockConfigApi({ - home: { - recentVisits: { - filterBy: [ - { - operator: '==', - value: '/tech-radar', - }, - { - field: 'pathname', - operator: '==', - value: '/explore', - }, - ], + const configApiMock = mockApis.config({ + data: { + home: { + recentVisits: { + filterBy: [ + { + operator: '==', + value: '/tech-radar', + }, + { + field: 'pathname', + operator: '==', + value: '/explore', + }, + ], + }, }, }, }); @@ -283,16 +287,18 @@ describe('', () => { }); it('allows top items to be filtered using config', async () => { - const configApiMock = new MockConfigApi({ - home: { - topVisits: { - filterBy: [ - { - field: 'pathname', - operator: '==', - value: '/explore', - }, - ], + const configApiMock = mockApis.config({ + data: { + home: { + topVisits: { + filterBy: [ + { + field: 'pathname', + operator: '==', + value: '/explore', + }, + ], + }, }, }, }); diff --git a/plugins/kubernetes-react/src/components/PodExecTerminal/PodExecTerminal.test.tsx b/plugins/kubernetes-react/src/components/PodExecTerminal/PodExecTerminal.test.tsx index 4aa6112436..0e56c9599a 100644 --- a/plugins/kubernetes-react/src/components/PodExecTerminal/PodExecTerminal.test.tsx +++ b/plugins/kubernetes-react/src/components/PodExecTerminal/PodExecTerminal.test.tsx @@ -14,8 +14,9 @@ * limitations under the License. */ -import { DiscoveryApi, discoveryApiRef } from '@backstage/core-plugin-api'; +import { discoveryApiRef } from '@backstage/core-plugin-api'; import { + mockApis, renderInTestApp, TestApiProvider, textContentMatcher, @@ -37,9 +38,7 @@ describe('PodExecTerminal', () => { const podName = 'pod1'; const podNamespace = 'podNamespace'; - const mockDiscoveryApi: Partial = { - getBaseUrl: () => Promise.resolve('http://localhost'), - }; + const mockDiscoveryApi = mockApis.discovery(); it('Should render an XTerm web terminal', async () => { await renderInTestApp( @@ -62,7 +61,7 @@ describe('PodExecTerminal', () => { it('Should connect to WebSocket server & render response', async () => { const server = new WS( - 'ws://localhost/proxy/api/v1/namespaces/podNamespace/pods/pod1/exec?container=container2&stdin=true&stdout=true&stderr=true&tty=true&command=%2Fbin%2Fsh', + 'ws://example.com/api/kubernetes/proxy/api/v1/namespaces/podNamespace/pods/pod1/exec?container=container2&stdin=true&stdout=true&stderr=true&tty=true&command=%2Fbin%2Fsh', ); await renderInTestApp( diff --git a/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.test.tsx b/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.test.tsx index 066fa0f874..4718f8f29f 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.test.tsx +++ b/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.test.tsx @@ -17,19 +17,21 @@ import React from 'react'; import { screen } from '@testing-library/react'; -import { TestApiProvider, renderInTestApp } from '@backstage/test-utils'; +import { + TestApiProvider, + mockApis, + renderInTestApp, +} from '@backstage/test-utils'; import '@testing-library/jest-dom'; import { PodDrawer } from './PodDrawer'; -import { DiscoveryApi, discoveryApiRef } from '@backstage/core-plugin-api'; +import { discoveryApiRef } from '@backstage/core-plugin-api'; jest.mock('../../../hooks/useIsPodExecTerminalSupported'); describe('PodDrawer', () => { it('Should show title and container names', async () => { - const mockDiscoveryApi: Partial = { - getBaseUrl: () => Promise.resolve('http://localhost'), - }; + const mockDiscoveryApi = mockApis.discovery(); await renderInTestApp( diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx index 6b8261e015..04de321c50 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx @@ -22,7 +22,11 @@ import { StarredEntitiesApi, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + mockApis, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; import React from 'react'; import { MembersListCard } from './MembersListCard'; import { @@ -184,7 +188,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -212,7 +216,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -238,7 +242,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -273,7 +277,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -308,7 +312,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -366,7 +370,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -406,7 +410,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > diff --git a/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.test.tsx b/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.test.tsx index 9eb9b3f70f..de46be98be 100644 --- a/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.test.tsx +++ b/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.test.tsx @@ -14,11 +14,15 @@ * limitations under the License. */ -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + renderInTestApp, + TestApiProvider, + mockApis, +} from '@backstage/test-utils'; import React from 'react'; import { MyGroupsSidebarItem } from './MyGroupsSidebarItem'; import GroupIcon from '@material-ui/icons/People'; -import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; +import { identityApiRef } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; @@ -26,13 +30,10 @@ import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; describe('MyGroupsSidebarItem Test', () => { describe('For guests or users with no groups', () => { it('MyGroupsSidebarItem should be empty', async () => { - const identityApi: Partial = { - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/guest', - ownershipEntityRefs: ['user:default/guest'], - }), - }; + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + ownershipEntityRefs: ['user:default/guest'], + }); const catalogApi = catalogApiMock(); const rendered = await renderInTestApp( { describe('For users that are members of a single group', () => { it('MyGroupsSidebarItem should display a single item that links to their group', async () => { - const identityApi: Partial = { - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/nigel.manning', - ownershipEntityRefs: ['user:default/nigel.manning'], - }), - }; + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/nigel.manning', + ownershipEntityRefs: ['user:default/nigel.manning'], + }); const catalogApi = catalogApiMock.mock({ getEntities: async () => ({ items: [ @@ -114,13 +112,10 @@ describe('MyGroupsSidebarItem Test', () => { describe('For users that are members of multiple groups', () => { it('MyGroupsSidebarItem should display a sub-menu with all their groups and a link to each group', async () => { - const identityApi: Partial = { - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/nigel.manning', - ownershipEntityRefs: ['user:default/nigel.manning'], - }), - }; + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/nigel.manning', + ownershipEntityRefs: ['user:default/nigel.manning'], + }); const catalogApi = catalogApiMock.mock({ getEntities: async () => ({ items: [ @@ -192,13 +187,10 @@ describe('MyGroupsSidebarItem Test', () => { describe('When an additional filter is not provided', () => { it('catalogApi.getEntities() should be called with the default filter', async () => { - const identityApi: Partial = { - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/guest', - ownershipEntityRefs: ['user:default/guest'], - }), - }; + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + ownershipEntityRefs: ['user:default/guest'], + }); const mockCatalogApi = catalogApiMock.mock(); await renderInTestApp( { describe('When an additional filter is provided', () => { it('catalogApi.getEntities() should be called with an additional filter item', async () => { - const identityApi: Partial = { - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/guest', - ownershipEntityRefs: ['user:default/guest'], - }), - }; + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + ownershipEntityRefs: ['user:default/guest'], + }); const mockCatalogApi = catalogApiMock.mock(); await renderInTestApp( { - const mockPermissionApi = { authorize: jest.fn() }; - it('Returns loading when permissionApi has not yet responded.', () => { - mockPermissionApi.authorize.mockReturnValueOnce(new Promise(() => {})); + const permissionApi = mockApis.permission.mock({ + authorize: async () => new Promise(() => {}), + }); - const { getByText } = renderComponent(mockPermissionApi); + const { getByText } = renderComponent(permissionApi); - expect(mockPermissionApi.authorize).toHaveBeenCalledWith({ permission }); + expect(permissionApi.authorize).toHaveBeenCalledWith({ permission }); expect(getByText('loading')).toBeTruthy(); }); it('Returns allowed when permissionApi allows authorization.', async () => { - mockPermissionApi.authorize.mockResolvedValueOnce({ - result: AuthorizeResult.ALLOW, + const permissionApi = mockApis.permission({ + authorize: AuthorizeResult.ALLOW, }); + jest.spyOn(permissionApi, 'authorize'); - const { findByText } = renderComponent(mockPermissionApi); + const { findByText } = renderComponent(permissionApi); - expect(mockPermissionApi.authorize).toHaveBeenCalledWith({ permission }); + expect(permissionApi.authorize).toHaveBeenCalledWith({ permission }); expect(await findByText('content')).toBeTruthy(); }); it('Returns not allowed when permissionApi denies authorization.', async () => { - mockPermissionApi.authorize.mockResolvedValueOnce({ - result: AuthorizeResult.DENY, + const permissionApi = mockApis.permission({ + authorize: AuthorizeResult.DENY, }); + jest.spyOn(permissionApi, 'authorize'); - const { findByText } = renderComponent(mockPermissionApi); + const { findByText } = renderComponent(permissionApi); - expect(mockPermissionApi.authorize).toHaveBeenCalledWith({ permission }); + expect(permissionApi.authorize).toHaveBeenCalledWith({ permission }); await expect(findByText('content')).rejects.toThrow(); }); }); diff --git a/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx index 58033b718c..9ae2a507dd 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { fireEvent } from '@testing-library/react'; import { CardHeader } from './CardHeader'; import { ThemeProvider } from '@material-ui/core/styles'; import { lightTheme } from '@backstage/theme'; import { - MockStorageApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -44,7 +45,7 @@ describe('CardHeader', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], ]} @@ -75,7 +76,7 @@ describe('CardHeader', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], ]} @@ -135,7 +136,7 @@ describe('CardHeader', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], ]} @@ -164,7 +165,7 @@ describe('CardHeader', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], ]} diff --git a/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx index febdc7c99e..673f1e4a9e 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; import { entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; import { - MockPermissionApi, - MockStorageApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -51,10 +51,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -81,10 +81,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -113,10 +113,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -143,10 +143,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -179,10 +179,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -219,10 +219,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -264,10 +264,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -313,10 +313,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -356,10 +356,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -396,10 +396,10 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -428,9 +428,6 @@ describe('TemplateCard', () => { }, }; const mockOnSelected = jest.fn(); - const mockAuthorize = jest - .fn() - .mockImplementation(async () => ({ result: AuthorizeResult.DENY })); // SWR used by the usePermission hook needs cache to be reset for each test const { queryByText } = await renderInTestApp( new Map() }}> @@ -439,10 +436,13 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, new MockPermissionApi(mockAuthorize)], + [ + permissionApiRef, + mockApis.permission({ authorize: AuthorizeResult.DENY }), + ], ]} > diff --git a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx index 49f5c1feb0..243b9b7e79 100644 --- a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx @@ -16,7 +16,7 @@ import { ApiProvider } from '@backstage/core-app-api'; import { - MockAnalyticsApi, + mockApis, renderInTestApp, TestApiRegistry, } from '@backstage/test-utils'; @@ -42,7 +42,7 @@ const scaffolderApiMock: jest.Mocked = { const catalogApi = catalogApiMock.mock(); -const analyticsMock = new MockAnalyticsApi(); +const analyticsMock = mockApis.analytics(); const apis = TestApiRegistry.from( [scaffolderApiRef, scaffolderApiMock], [catalogApiRef, catalogApi], diff --git a/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx b/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx index 6079ec2e1d..01274cf6a6 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx @@ -22,9 +22,9 @@ import { import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { permissionApiRef } from '@backstage/plugin-permission-react'; import { - MockStorageApi, renderInTestApp, TestApiProvider, + mockApis, } from '@backstage/test-utils'; import React from 'react'; import { rootRouteRef } from '../../../routes'; @@ -52,10 +52,10 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -74,10 +74,10 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -97,10 +97,10 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -119,10 +119,10 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -142,10 +142,10 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -164,10 +164,10 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -185,10 +185,10 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > = { }; const catalogApi = catalogApiMock.mock(); +const analyticsApi = mockApis.analytics(); -const analyticsMock = new MockAnalyticsApi(); const apis = TestApiRegistry.from( [scaffolderApiRef, scaffolderApiMock], [catalogApiRef, catalogApi], - [analyticsApiRef, analyticsMock], + [analyticsApiRef, analyticsApi], [catalogApiRef, catalogApi], ); @@ -81,6 +81,7 @@ const entityRefResponse = { }, }, }; + describe('TemplateWizardPage', () => { it('captures expected analytics events', async () => { scaffolderApiMock.scaffold.mockResolvedValue({ taskId: 'xyz' }); @@ -130,20 +131,29 @@ describe('TemplateWizardPage', () => { }); // The "Next Step" button should have fired an event - expect(analyticsMock.getEvents()[0]).toMatchObject({ - action: 'click', - subject: 'Next Step (1)', - context: { entityRef: 'template:default/test' }, - }); + expect(analyticsApi.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'click', + subject: 'Next Step (1)', + context: expect.objectContaining({ + entityRef: 'template:default/test', + }), + }), + ); // And the "Create" button should have fired an event - expect(analyticsMock.getEvents()[1]).toMatchObject({ - action: 'create', - subject: 'expected-name', - context: { entityRef: 'template:default/test' }, - value: 120, - }); + expect(analyticsApi.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'create', + subject: 'expected-name', + context: expect.objectContaining({ + entityRef: 'template:default/test', + }), + value: 120, + }), + ); }); + describe('scaffolder page context menu', () => { it('should render if editUrl is set to url', async () => { catalogApi.getEntityByRef.mockResolvedValue({ @@ -175,6 +185,7 @@ describe('TemplateWizardPage', () => { ); expect(queryByTestId('menu-button')).toBeInTheDocument(); }); + it('should not render if editUrl is undefined', async () => { catalogApi.getEntityByRef.mockResolvedValue({ apiVersion: 'v1', diff --git a/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx index 96374662a4..0a78e41966 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/ListTaskPage.test.tsx @@ -15,7 +15,11 @@ */ import { Entity } from '@backstage/catalog-model'; -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + renderInTestApp, + TestApiProvider, + mockApis, +} from '@backstage/test-utils'; import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import React from 'react'; @@ -31,12 +35,7 @@ import { rootRouteRef } from '../../routes'; describe('', () => { const catalogApi = catalogApiMock.mock(); - const identityApi = { - getBackstageIdentity: jest.fn(), - getProfileInfo: jest.fn(), - getCredentials: jest.fn(), - signOut: jest.fn(), - }; + const identityApi = mockApis.identity(); const scaffolderApiMock: jest.Mocked> = { scaffold: jest.fn(), diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx index 35eb9ce0b7..bd5676ba31 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx @@ -15,7 +15,11 @@ */ import { Entity } from '@backstage/catalog-model'; -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + renderInTestApp, + TestApiProvider, + mockApis, +} from '@backstage/test-utils'; import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import React from 'react'; @@ -24,13 +28,7 @@ import { identityApiRef } from '@backstage/core-plugin-api'; describe('', () => { const catalogApi = catalogApiMock.mock(); - - const identityApi = { - getBackstageIdentity: jest.fn(), - getProfileInfo: jest.fn(), - getCredentials: jest.fn(), - signOut: jest.fn(), - }; + const identityApi = mockApis.identity(); it('should render the column with the user', async () => { const props = { diff --git a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.test.tsx b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.test.tsx index 70d2193470..d62ef6f95e 100644 --- a/plugins/scaffolder/src/components/OngoingTask/OngoingTask.test.tsx +++ b/plugins/scaffolder/src/components/OngoingTask/OngoingTask.test.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { renderInTestApp, TestApiProvider, - MockPermissionApi, + mockApis, } from '@backstage/test-utils'; import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react'; import { act, fireEvent, waitFor, within } from '@testing-library/react'; @@ -70,7 +70,7 @@ describe('OngoingTask', () => { @@ -146,10 +146,9 @@ describe('OngoingTask', () => { }); it('should have cancel and start over buttons be disabled without the proper permissions', async () => { - const mockAuthorize = jest - .fn() - .mockImplementation(async () => ({ result: AuthorizeResult.DENY })); - const permissionApi: PermissionApi = { authorize: mockAuthorize }; + const permissionApi = mockApis.permission({ + authorize: AuthorizeResult.DENY, + }); const rendered = await render(permissionApi); const { getByTestId } = rendered; diff --git a/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx b/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx index d4a37d0752..57ce2d3023 100644 --- a/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx @@ -18,7 +18,11 @@ import React from 'react'; import { waitFor } from '@testing-library/react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { MyGroupsPicker } from './MyGroupsPicker'; -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + renderInTestApp, + TestApiProvider, + mockApis, +} from '@backstage/test-utils'; import { catalogApiRef, entityPresentationApiRef, @@ -26,7 +30,6 @@ import { import { Entity } from '@backstage/catalog-model'; import { ErrorApi, - IdentityApi, errorApiRef, identityApiRef, } from '@backstage/core-plugin-api'; @@ -34,25 +37,9 @@ import userEvent from '@testing-library/user-event'; import { ScaffolderRJSFFieldProps as FieldProps } from '@backstage/plugin-scaffolder-react'; import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog'; -// Create a mock IdentityApi -const mockIdentityApi: IdentityApi = { - getProfileInfo: () => - Promise.resolve({ - displayName: 'Bob', - email: 'bob@example.com', - picture: 'https://example.com/picture.jpg', - }), - getBackstageIdentity: () => - Promise.resolve({ - id: 'Bob', - idToken: 'token', - type: 'user', - userEntityRef: 'user:default/bob', - ownershipEntityRefs: ['group:default/group1', 'group:default/group2'], - }), - getCredentials: () => Promise.resolve({ token: 'token' }), - signOut: () => Promise.resolve(), -}; +const mockIdentityApi = mockApis.identity({ + userEntityRef: 'user:default/bob', +}); describe('', () => { let entities: Entity[]; @@ -96,7 +83,7 @@ describe('', () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); it('should only return the groups a user is part of and not the groups a user is not part of', async () => { diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx index 3f667a4ad6..0e4df4fb34 100644 --- a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx @@ -20,7 +20,7 @@ import userEvent from '@testing-library/user-event'; import { configApiRef } from '@backstage/core-plugin-api'; import { ConfigReader } from '@backstage/core-app-api'; import { - MockAnalyticsApi, + mockApis, TestApiProvider, renderInTestApp, } from '@backstage/test-utils'; @@ -273,7 +273,7 @@ describe('SearchBar', () => { }); it('Does not capture analytics event if not enabled in app', async () => { - const analyticsApiMock = new MockAnalyticsApi(); + const analyticsApiMock = mockApis.analytics(); await renderInTestApp( { await waitFor(() => expect(textbox).toHaveValue(value)); - expect(analyticsApiMock.getEvents()).toHaveLength(0); + expect(analyticsApiMock.captureEvent).not.toHaveBeenCalled(); }); it('Renders custom search icon', async () => { diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx index abf5e828f6..bbc6454613 100644 --- a/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import { screen, render, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; @@ -35,10 +35,12 @@ const SearchContextFilterSpy = ({ name }: { name: string }) => { }; describe('SearchFilter.Autocomplete', () => { - const configApiMock = new MockConfigApi({ - search: { - query: { - pageLimit: 100, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pageLimit: 100, + }, }, }, }); diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx index c51a4832e3..e155a093d5 100644 --- a/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx @@ -22,7 +22,7 @@ import { configApiRef } from '@backstage/core-plugin-api'; import { SearchContextProvider } from '../../context'; import { SearchFilter } from './SearchFilter'; -import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import { searchApiRef } from '../../api'; describe('SearchFilter', () => { @@ -37,10 +37,12 @@ describe('SearchFilter', () => { const values = ['value1', 'value2']; const filters = { unrelated: 'unrelated' }; - const configApiMock = new MockConfigApi({ - search: { - query: { - pagelimit: 10, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pagelimit: 10, + }, }, }, }); diff --git a/plugins/search-react/src/components/SearchFilter/hooks.test.tsx b/plugins/search-react/src/components/SearchFilter/hooks.test.tsx index 884512963f..28a6fbfd60 100644 --- a/plugins/search-react/src/components/SearchFilter/hooks.test.tsx +++ b/plugins/search-react/src/components/SearchFilter/hooks.test.tsx @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { ApiProvider } from '@backstage/core-app-api'; -import { MockConfigApi, TestApiRegistry } from '@backstage/test-utils'; +import { mockApis, TestApiRegistry } from '@backstage/test-utils'; import { act, renderHook, waitFor } from '@testing-library/react'; import { searchApiRef } from '../../api'; @@ -27,17 +28,19 @@ jest.useFakeTimers(); describe('SearchFilter.hooks', () => { describe('useDefaultFilterValue', () => { - const configApiMock = new MockConfigApi({ - search: { - query: { - pageLimit: 100, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pageLimit: 100, + }, }, }, }); const searchApiMock = { query: jest.fn().mockResolvedValue({ results: [] }), }; - const mockApis = TestApiRegistry.from( + const apis = TestApiRegistry.from( [searchApiRef, searchApiMock], [configApiRef, configApiMock], ); @@ -54,7 +57,7 @@ describe('SearchFilter.hooks', () => { filters: {}, }; return ( - + diff --git a/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx b/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx index 7c86e16e95..b0a64f0061 100644 --- a/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx +++ b/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx @@ -19,7 +19,7 @@ import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { - MockConfigApi, + mockApis, renderWithEffects, TestApiProvider, } from '@backstage/test-utils'; @@ -31,10 +31,12 @@ import { SearchPagination } from './SearchPagination'; import { configApiRef } from '@backstage/core-plugin-api'; describe('SearchPagination', () => { - const configApiMock = new MockConfigApi({ - search: { - query: { - pagelimit: 10, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pagelimit: 10, + }, }, }, }); diff --git a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx index 66513783d4..fe1e279dd3 100644 --- a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx +++ b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx @@ -24,7 +24,7 @@ import DocsIcon from '@material-ui/icons/InsertDriveFile'; import { renderInTestApp, TestApiProvider, - MockAnalyticsApi, + mockApis, } from '@backstage/test-utils'; import { createPlugin, analyticsApiRef } from '@backstage/core-plugin-api'; @@ -40,7 +40,7 @@ import { const query = jest.fn().mockResolvedValue({ results: [] }); const searchApiMock = { query }; -const analyticsApiMock = new MockAnalyticsApi(); +const analyticsApiMock = mockApis.analytics(); describe('SearchResultGroup', () => { const results = [ diff --git a/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx b/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx index 146e5822a4..86ce237135 100644 --- a/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx +++ b/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx @@ -20,7 +20,7 @@ import { screen, waitFor } from '@testing-library/react'; import { TestApiProvider, renderInTestApp, - MockAnalyticsApi, + mockApis, } from '@backstage/test-utils'; import { analyticsApiRef, createPlugin } from '@backstage/core-plugin-api'; @@ -31,7 +31,7 @@ import { SearchResultList } from './SearchResultList'; const query = jest.fn().mockResolvedValue({ results: [] }); const searchApiMock = { query }; -const analyticsApiMock = new MockAnalyticsApi(); +const analyticsApiMock = mockApis.analytics(); describe('SearchResultList', () => { const results = [ diff --git a/plugins/search-react/src/context/SearchContext.test.tsx b/plugins/search-react/src/context/SearchContext.test.tsx index 58fde35667..f858715adb 100644 --- a/plugins/search-react/src/context/SearchContext.test.tsx +++ b/plugins/search-react/src/context/SearchContext.test.tsx @@ -22,7 +22,7 @@ import { act, renderHook, } from '@testing-library/react'; -import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { SearchContextProvider, @@ -37,7 +37,7 @@ describe('SearchContext', () => { } satisfies typeof searchApiRef.T; const wrapper = ({ children, initialState, config = {} }: any) => { - const configApiMock = new MockConfigApi(config); + const configApiMock = mockApis.config({ data: config }); return ( { describe('analytics', () => { it('captures analytics events if enabled in app', async () => { - const analyticsApiMock = { - captureEvent: jest.fn(), - } satisfies typeof analyticsApiRef.T; + const analyticsApiMock = mockApis.analytics(); searchApiMock.query.mockResolvedValue({ results: [], @@ -433,7 +431,7 @@ describe('SearchContext', () => { const { result } = renderHook(() => useSearch(), { wrapper: ({ children }) => { - const configApiMock = new MockConfigApi({}); + const configApiMock = mockApis.config(); return ( { }); it('captures analytics events even if number of results does not exist', async () => { - const analyticsApiMock = { - captureEvent: jest.fn(), - } satisfies typeof analyticsApiRef.T; + const analyticsApiMock = mockApis.analytics(); searchApiMock.query.mockResolvedValue({ results: [], @@ -491,7 +487,7 @@ describe('SearchContext', () => { const { result } = renderHook(() => useSearch(), { wrapper: ({ children }) => { - const configApiMock = new MockConfigApi({}); + const configApiMock = mockApis.config(); return ( { screen.getByRole('link', { name: /Search Result 1/ }), ); - expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ action: 'discover', subject: 'Search Result 1', context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' }, @@ -141,7 +141,7 @@ describe('extensions', () => { await userEvent.click(screen.getByRole('listitem')); - expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ action: 'discover', subject: 'Search Result 1', context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' }, diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx index 453b766791..d184daa429 100644 --- a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import { act, render, waitFor } from '@testing-library/react'; import user from '@testing-library/user-event'; import { @@ -41,10 +41,12 @@ jest.mock('@backstage/plugin-search-react', () => ({ })); describe('SearchType.Accordion', () => { - const configApiMock = new MockConfigApi({ - search: { - query: { - pagelimit: 10, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pagelimit: 10, + }, }, }, }); diff --git a/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx b/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx index c0d8549253..d06106e1ae 100644 --- a/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import { act, render } from '@testing-library/react'; import user from '@testing-library/user-event'; import { @@ -40,10 +40,12 @@ jest.mock('@backstage/plugin-search-react', () => ({ describe('SearchType.Tabs', () => { const searchApiMock = { query: jest.fn().mockResolvedValue({ results: [] }) }; - const configApiMock = new MockConfigApi({ - search: { - query: { - pageLimit: 100, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pageLimit: 100, + }, }, }, }); diff --git a/plugins/search/src/components/SearchType/SearchType.test.tsx b/plugins/search/src/components/SearchType/SearchType.test.tsx index 353b45c0fb..6a393798de 100644 --- a/plugins/search/src/components/SearchType/SearchType.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.test.tsx @@ -23,7 +23,7 @@ import { searchApiRef, } from '@backstage/plugin-search-react'; import { SearchType } from './SearchType'; -import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; describe('SearchType', () => { const initialState = { @@ -36,10 +36,12 @@ describe('SearchType', () => { const values = ['value1', 'value2']; const typeValues = ['preselected']; - const configApiMock = new MockConfigApi({ - search: { - query: { - pagelimit: 10, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pagelimit: 10, + }, }, }, }); diff --git a/plugins/signals/src/api/SignalsClient.test.ts b/plugins/signals/src/api/SignalsClient.test.ts index 2e93cd69f1..b6944cfdc1 100644 --- a/plugins/signals/src/api/SignalsClient.test.ts +++ b/plugins/signals/src/api/SignalsClient.test.ts @@ -14,27 +14,21 @@ * limitations under the License. */ -import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; +import { mockApis } from '@backstage/test-utils'; import WS from 'jest-websocket-mock'; import { SignalClient } from './SignalClient'; describe('SignalsClient', () => { - const tokenFunction = jest.fn(); - const baseUrlFunction = jest.fn(); - const identity = { - getCredentials: tokenFunction, - } as unknown as IdentityApi; - const discoveryApi = { - getBaseUrl: baseUrlFunction, - } as unknown as DiscoveryApi; + const identity = mockApis.identity({ token: '12345' }); + const discoveryApi = mockApis.discovery({ baseUrl: 'http://localhost:1234' }); let server: WS; beforeEach(async () => { - jest.resetAllMocks(); - tokenFunction.mockResolvedValue({ token: '12345' }); - baseUrlFunction.mockResolvedValue('http://localhost:1234'); - server = new WS('ws://localhost:1234', { jsonProtocol: true }); + jest.clearAllMocks(); + server = new WS('ws://localhost:1234/api/signals', { + jsonProtocol: true, + }); }); afterEach(() => { diff --git a/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx index 85ffaa5782..2bc770cc31 100644 --- a/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx +++ b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx @@ -19,7 +19,7 @@ import { screen, fireEvent, waitFor } from '@testing-library/react'; import { analyticsApiRef } from '@backstage/core-plugin-api'; import { - MockAnalyticsApi, + mockApis, TestApiProvider, renderInTestApp, } from '@backstage/test-utils'; @@ -55,11 +55,11 @@ const defaultGitlabProps = { }; describe('FeedbackLink', () => { - const apiSpy = new MockAnalyticsApi(); + const analytics = mockApis.analytics(); it('Should open new Github issue tab', async () => { await renderInTestApp( - + , ); @@ -77,7 +77,7 @@ describe('FeedbackLink', () => { it('Should open new Gitlab issue tab', async () => { await renderInTestApp( - + , ); @@ -95,7 +95,7 @@ describe('FeedbackLink', () => { it('Should track click events', async () => { await renderInTestApp( - + , ); @@ -103,10 +103,12 @@ describe('FeedbackLink', () => { fireEvent.click(screen.getByText(/Open new Github issue/)); await waitFor(() => { - expect(apiSpy.getEvents()[0]).toMatchObject({ - action: 'click', - subject: 'Open new Github issue', - }); + expect(analytics.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'click', + subject: 'Open new Github issue', + }), + ); }); }); }); diff --git a/plugins/techdocs-react/src/context.test.tsx b/plugins/techdocs-react/src/context.test.tsx index a923b3c528..6605d9ceaf 100644 --- a/plugins/techdocs-react/src/context.test.tsx +++ b/plugins/techdocs-react/src/context.test.tsx @@ -13,17 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { renderHook, act, waitFor } from '@testing-library/react'; import { ThemeProvider } from '@material-ui/core/styles'; import { lightTheme } from '@backstage/theme'; -import { - MockAnalyticsApi, - MockConfigApi, - TestApiProvider, -} from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { analyticsApiRef, @@ -65,7 +62,7 @@ const techdocsApiMock = { getTechDocsMetadata: jest.fn().mockResolvedValue(mockTechDocsMetadata), }; -const analyticsApiMock = new MockAnalyticsApi(); +const analyticsApiMock = mockApis.analytics(); const wrapper = ({ entityRef = { @@ -84,7 +81,7 @@ const wrapper = ({ @@ -169,12 +166,12 @@ describe('useTechDocsReaderPage', () => { wrapper, }); await waitFor(() => { - expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ action: 'action', subject: 'subject', - context: { + context: expect.objectContaining({ entityRef: 'component:default/test', - }, + }), }); }); }); diff --git a/plugins/techdocs/src/client.test.ts b/plugins/techdocs/src/client.test.ts index 62d497d790..f66587a071 100644 --- a/plugins/techdocs/src/client.test.ts +++ b/plugins/techdocs/src/client.test.ts @@ -15,10 +15,9 @@ */ import { UrlPatternDiscovery } from '@backstage/core-app-api'; -import { IdentityApi } from '@backstage/core-plugin-api'; import { NotFoundError } from '@backstage/errors'; import { fetchEventSource } from '@microsoft/fetch-event-source'; -import { MockConfigApi, MockFetchApi } from '@backstage/test-utils'; +import { mockApis, MockFetchApi } from '@backstage/test-utils'; import { TechDocsStorageClient } from './client'; jest.mock('@microsoft/fetch-event-source'); @@ -34,16 +33,13 @@ const mockEntity = { describe('TechDocsStorageClient', () => { const mockBaseUrl = 'http://backstage:9191/api/techdocs'; - const configApi = new MockConfigApi({}); + const configApi = mockApis.config(); const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); - const identityApi: jest.Mocked = { - getCredentials: jest.fn(), - } as unknown as jest.Mocked; + const identityApi = mockApis.identity(); const fetchApi = new MockFetchApi({ injectIdentityAuth: { identityApi } }); beforeEach(() => { jest.resetAllMocks(); - identityApi.getCredentials.mockResolvedValue({ token: undefined }); }); it('should return correct base url based on defined storage', async () => { @@ -95,7 +91,6 @@ describe('TechDocsStorageClient', () => { await Promise.resolve(); onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' }); }); - identityApi.getCredentials.mockResolvedValue({}); await storageApi.syncEntityDocs(mockEntity); expect(mockFetchEventSource).toHaveBeenCalledWith( @@ -126,7 +121,6 @@ describe('TechDocsStorageClient', () => { onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' }); }); - identityApi.getCredentials.mockResolvedValue({}); await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual( 'cached', ); @@ -146,7 +140,6 @@ describe('TechDocsStorageClient', () => { onmessage?.({ id: '', event: 'finish', data: '{"updated": true}' }); }); - identityApi.getCredentials.mockResolvedValue({}); await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual( 'updated', ); @@ -169,7 +162,6 @@ describe('TechDocsStorageClient', () => { onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' }); }); - identityApi.getCredentials.mockResolvedValue({}); const logHandler = jest.fn(); await expect( storageApi.syncEntityDocs(mockEntity, logHandler), @@ -192,7 +184,6 @@ describe('TechDocsStorageClient', () => { }); // we await later after we emitted the error - identityApi.getCredentials.mockResolvedValue({}); const promise = storageApi.syncEntityDocs(mockEntity).then(); await expect(promise).rejects.toThrow(NotFoundError); @@ -207,7 +198,6 @@ describe('TechDocsStorageClient', () => { }); // we await later after we emitted the error - identityApi.getCredentials.mockResolvedValue({}); const promise = storageApi.syncEntityDocs(mockEntity).then(); mockFetchEventSource.mockImplementation(async (_url, options) => { diff --git a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx index 513a56265c..4c3be25325 100644 --- a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx +++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx @@ -14,22 +14,18 @@ * limitations under the License. */ -import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; -import { - ConfigApi, - configApiRef, - storageApiRef, -} from '@backstage/core-plugin-api'; +import { ApiProvider } from '@backstage/core-app-api'; +import { configApiRef, storageApiRef } from '@backstage/core-plugin-api'; import { + MockStarredEntitiesApi, catalogApiRef, starredEntitiesApiRef, - MockStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { - MockStorageApi, - renderInTestApp, TestApiRegistry, + mockApis, + renderInTestApp, } from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; @@ -50,18 +46,14 @@ const mockCatalogApi = catalogApiMock({ }); describe('TechDocs Home', () => { - const configApi: ConfigApi = new ConfigReader({ - organization: { - name: 'My Company', - }, + const configApi = mockApis.config({ + data: { organization: { name: 'My Company' } }, }); - const storageApi = MockStorageApi.create(); - const apiRegistry = TestApiRegistry.from( [catalogApiRef, mockCatalogApi], [configApiRef, configApi], - [storageApiRef, storageApi], + [storageApiRef, mockApis.storage()], [starredEntitiesApiRef, new MockStarredEntitiesApi()], ); diff --git a/plugins/techdocs/src/home/components/Grids/EntityListDocsGrid.test.tsx b/plugins/techdocs/src/home/components/Grids/EntityListDocsGrid.test.tsx index 947c81ae85..25471d0769 100644 --- a/plugins/techdocs/src/home/components/Grids/EntityListDocsGrid.test.tsx +++ b/plugins/techdocs/src/home/components/Grids/EntityListDocsGrid.test.tsx @@ -14,12 +14,8 @@ * limitations under the License. */ -import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; -import { - ConfigApi, - configApiRef, - storageApiRef, -} from '@backstage/core-plugin-api'; +import { ApiProvider } from '@backstage/core-app-api'; +import { configApiRef, storageApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef, starredEntitiesApiRef, @@ -30,9 +26,9 @@ import { catalogApiMock, } from '@backstage/plugin-catalog-react/testUtils'; import { - MockStorageApi, renderInTestApp, TestApiRegistry, + mockApis, } from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; @@ -68,21 +64,17 @@ const mockCatalogApi = catalogApiMock({ entities }); describe('Entity List Docs Grid', () => { beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); - const configApi: ConfigApi = new ConfigReader({ - organization: { - name: 'My Company', - }, + const configApi = mockApis.config({ + data: { organization: { name: 'My Company' } }, }); - const storageApi = MockStorageApi.create(); - const apiRegistry = TestApiRegistry.from( [catalogApiRef, mockCatalogApi], [configApiRef, configApi], - [storageApiRef, storageApi], + [storageApiRef, mockApis.storage()], [starredEntitiesApiRef, new MockStarredEntitiesApi()], ); diff --git a/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx index 59c388df69..84c7275228 100644 --- a/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx @@ -16,12 +16,11 @@ import { TechDocsNotFound } from './TechDocsNotFound'; import React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import { - MockAnalyticsApi, + mockApis, TestApiProvider, renderInTestApp, - wrapInTestApp, } from '@backstage/test-utils'; import { analyticsApiRef } from '@backstage/core-plugin-api'; @@ -58,18 +57,16 @@ describe('', () => { }); it('should trigger analytics event not-found', async () => { - const mockAnalyticsApi = new MockAnalyticsApi(); + const mockAnalyticsApi = mockApis.analytics(); - render( - wrapInTestApp( - - - , - ), + await renderInTestApp( + + + , ); await waitFor(() => { - expect(mockAnalyticsApi.getEvents()[0]).toMatchObject({ + expect(mockAnalyticsApi.captureEvent).toHaveBeenCalledWith({ action: 'not-found', subject: '/the/pathname?the=search#the-anchor', attributes: { @@ -77,6 +74,7 @@ describe('', () => { namespace: 'namespace', kind: 'kind', }, + context: expect.anything(), }); }); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx index 5e8cd7af27..afb57e22ba 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { - MockConfigApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -80,12 +81,6 @@ const techdocsStorageApiMock: jest.Mocked = { syncEntityDocs: jest.fn(), }; -const discoveryApiMock = { - getBaseUrl: jest - .fn() - .mockResolvedValue('https://localhost:7000/api/techdocs'), -}; - const fetchApiMock = { fetch: jest.fn().mockResolvedValue({ ok: true, @@ -106,10 +101,8 @@ jest.mock('@backstage/core-components', () => ({ Page: jest.fn(), })); -const configApi = new MockConfigApi({ - app: { - baseUrl: 'http://localhost:3000', - }, +const configApi = mockApis.config({ + data: { app: { baseUrl: 'http://localhost:3000' } }, }); const Wrapper = ({ children }: { children: React.ReactNode }) => { @@ -117,7 +110,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => { { const baseUrl = 'http://localhost:3000'; await renderInTestApp( , @@ -75,16 +67,7 @@ describe('useNavigateUrl', () => { const baseUrl = 'http://localhost:3000/instance'; await renderInTestApp( , @@ -95,16 +78,7 @@ describe('useNavigateUrl', () => { const baseUrl = 'http://localhost:3000'; await renderInTestApp( , diff --git a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts index 6bb0035b83..e54ac36624 100644 --- a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts +++ b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts @@ -18,10 +18,13 @@ import { DiscoveryApi, ErrorApi, FetchApi, - IdentityApi, StorageApi, } from '@backstage/core-plugin-api'; -import { MockFetchApi, registerMswTestHooks } from '@backstage/test-utils'; +import { + MockFetchApi, + mockApis, + registerMswTestHooks, +} from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { UserSettingsStorage } from './UserSettingsStorage'; @@ -35,13 +38,8 @@ describe('Persistent Storage API', () => { const mockDiscoveryApi = { getBaseUrl: async () => mockBaseUrl, }; - const mockIdentityApi: Partial = { - getCredentials: async () => ({ token: 'a-token' }), - }; - const mockIdentityApiFallback: Partial = { - // This API recreates the guest mode, where the WebStorage is used as fallback - getCredentials: async () => ({}), - }; + const mockIdentityApi = mockApis.identity({ token: 'a-token' }); + const mockIdentityApiFallback = mockApis.identity(); const createPersistentStorage = ( args?: Partial<{ @@ -55,7 +53,7 @@ describe('Persistent Storage API', () => { errorApi: mockErrorApi, fetchApi: new MockFetchApi(), discoveryApi: mockDiscoveryApi, - identityApi: mockIdentityApi as IdentityApi, + identityApi: mockIdentityApi, ...args, }); }; @@ -72,13 +70,13 @@ describe('Persistent Storage API', () => { errorApi: mockErrorApi, fetchApi: new MockFetchApi(), discoveryApi: mockDiscoveryApi, - identityApi: mockIdentityApiFallback as IdentityApi, + identityApi: mockIdentityApiFallback, ...args, }); }; afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); it('should return undefined for values which are unset', async () => { diff --git a/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx index 14b7b7b48b..8a2e882aa8 100644 --- a/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx +++ b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx @@ -20,16 +20,15 @@ import { DefaultSettingsPage } from './DefaultSettingsPage'; import { UserSettingsTab } from '../UserSettingsTab'; import { useOutlet } from 'react-router-dom'; import { SettingsLayout } from '../SettingsLayout'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useOutlet: jest.fn().mockReturnValue(undefined), })); -const catalogApiMock: jest.Mocked = { - getEntityByRef: jest.fn(), -} as any; +const catalogApi = catalogApiMock(); describe('', () => { beforeEach(() => { @@ -38,7 +37,7 @@ describe('', () => { it('should render the settings page with 3 tabs', async () => { const { container } = await renderInTestApp( - + , ); @@ -54,7 +53,7 @@ describe('', () => { ); const { container } = await renderInTestApp( - + , ); @@ -71,7 +70,7 @@ describe('', () => { ); const { container } = await renderInTestApp( - + , ); diff --git a/plugins/user-settings/src/components/General/UserSettingsIdentityCard.test.tsx b/plugins/user-settings/src/components/General/UserSettingsIdentityCard.test.tsx index b1a811d7fa..6fe0ee01d7 100644 --- a/plugins/user-settings/src/components/General/UserSettingsIdentityCard.test.tsx +++ b/plugins/user-settings/src/components/General/UserSettingsIdentityCard.test.tsx @@ -14,32 +14,28 @@ * limitations under the License. */ -import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; +import { + renderInTestApp, + TestApiRegistry, + mockApis, +} from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; import { UserSettingsIdentityCard } from './UserSettingsIdentityCard'; import { ApiProvider } from '@backstage/core-app-api'; import { identityApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; +import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; const apiRegistry = TestApiRegistry.from( [ identityApiRef, - { - getProfileInfo: jest.fn(async () => ({})), - getBackstageIdentity: jest.fn(async () => ({ - type: 'user' as const, - userEntityRef: 'foo:bar/foobar', - ownershipEntityRefs: ['user:default/test-ownership'], - })), - }, - ], - [ - catalogApiRef, - { - getEntityByRef: jest.fn(), - }, + mockApis.identity({ + userEntityRef: 'foo:bar/foobar', + ownershipEntityRefs: ['user:default/test-ownership'], + }), ], + [catalogApiRef, catalogApiMock.mock()], ); describe('', () => { diff --git a/plugins/user-settings/src/components/General/UserSettingsMenu.test.tsx b/plugins/user-settings/src/components/General/UserSettingsMenu.test.tsx index c3b0aae4a7..0af33fac00 100644 --- a/plugins/user-settings/src/components/General/UserSettingsMenu.test.tsx +++ b/plugins/user-settings/src/components/General/UserSettingsMenu.test.tsx @@ -18,6 +18,7 @@ import { MockErrorApi, TestApiProvider, renderInTestApp, + mockApis, } from '@backstage/test-utils'; import { errorApiRef, identityApiRef } from '@backstage/core-plugin-api'; import { fireEvent, waitFor, screen } from '@testing-library/react'; @@ -35,9 +36,9 @@ describe('', () => { }); it('handles errors that occur when signing out', async () => { - const failingIdentityApi = { - signOut: jest.fn().mockRejectedValue(new Error('Logout error')), - }; + const failingIdentityApi = mockApis.identity.mock({ + signOut: () => Promise.reject(new Error('Logout error')), + }); const mockErrorApi = new MockErrorApi({ collect: true }); await renderInTestApp( ({})), - getBackstageIdentity: jest.fn(async () => ({ - type: 'user' as const, - userEntityRef: 'foo:bar/foobar', - ownershipEntityRefs: ['user:default/test-ownership'], - })), - }, - ], + [identityApiRef, mockApis.identity()], [ catalogApiRef, - { - getEntityByRef: jest.fn(async () => { - return { + catalogApiMock({ + entities: [ + { apiVersion: 'backstage.io/v1beta1', kind: 'User', metadata: { - name: 'Guest', + name: 'test', annotations: {}, }, spec: { @@ -50,9 +45,9 @@ const apiRegistry = TestApiRegistry.from( picture: 'https://example.com/avatar.png', }, }, - }; - }), - }, + }, + ], + }), ], ); diff --git a/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx b/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx index acdb6c3f87..12431a44a2 100644 --- a/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx +++ b/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx @@ -22,20 +22,15 @@ import { useOutlet } from 'react-router-dom'; import { SettingsLayout } from '../SettingsLayout'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { - CatalogApi, - catalogApiRef, - entityRouteRef, -} from '@backstage/plugin-catalog-react'; +import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; +import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useOutlet: jest.fn().mockReturnValue(undefined), })); -const catalogApiMock: jest.Mocked = { - getEntityByRef: jest.fn(), -} as any; +const catalogApi = catalogApiMock(); describe('', () => { beforeEach(() => { @@ -44,7 +39,7 @@ describe('', () => { it('should render the default settings page with 3 tabs', async () => { const { container } = await renderInTestApp( - + , { @@ -64,7 +59,7 @@ describe('', () => { ); (useOutlet as jest.Mock).mockReturnValue(advancedTabRoute); const { container } = await renderInTestApp( - + , { @@ -85,7 +80,7 @@ describe('', () => { ); (useOutlet as jest.Mock).mockReturnValue(advancedTabRoute); const { container } = await renderInTestApp( - + , { @@ -115,7 +110,7 @@ describe('', () => { ); (useOutlet as jest.Mock).mockReturnValue(customLayout); const { container } = await renderInTestApp( - + , { diff --git a/yarn.lock b/yarn.lock index 6402cf8408..f8618cba94 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4712,7 +4712,6 @@ __metadata: zod: ^3.22.4 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 @@ -8593,6 +8592,7 @@ __metadata: "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@testing-library/jest-dom": ^6.0.0 + "@types/jest": "*" "@types/react": ^18.0.0 cross-fetch: ^4.0.0 i18next: ^22.4.15 @@ -8603,11 +8603,14 @@ __metadata: zen-observable: ^0.10.0 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 languageName: unknown @@ -13073,8 +13076,8 @@ __metadata: linkType: hard "@opentelemetry/auto-instrumentations-node@npm:^0.50.0": - version: 0.50.1 - resolution: "@opentelemetry/auto-instrumentations-node@npm:0.50.1" + version: 0.50.2 + resolution: "@opentelemetry/auto-instrumentations-node@npm:0.50.2" dependencies: "@opentelemetry/instrumentation": ^0.53.0 "@opentelemetry/instrumentation-amqplib": ^0.42.0 @@ -13106,7 +13109,7 @@ __metadata: "@opentelemetry/instrumentation-mysql2": ^0.41.0 "@opentelemetry/instrumentation-nestjs-core": ^0.40.0 "@opentelemetry/instrumentation-net": ^0.39.0 - "@opentelemetry/instrumentation-pg": ^0.45.0 + "@opentelemetry/instrumentation-pg": ^0.45.1 "@opentelemetry/instrumentation-pino": ^0.42.0 "@opentelemetry/instrumentation-redis": ^0.42.0 "@opentelemetry/instrumentation-redis-4": ^0.42.1 @@ -13116,16 +13119,16 @@ __metadata: "@opentelemetry/instrumentation-tedious": ^0.14.0 "@opentelemetry/instrumentation-undici": ^0.6.0 "@opentelemetry/instrumentation-winston": ^0.40.0 - "@opentelemetry/resource-detector-alibaba-cloud": ^0.29.2 + "@opentelemetry/resource-detector-alibaba-cloud": ^0.29.3 "@opentelemetry/resource-detector-aws": ^1.6.2 "@opentelemetry/resource-detector-azure": ^0.2.11 - "@opentelemetry/resource-detector-container": ^0.4.2 + "@opentelemetry/resource-detector-container": ^0.4.3 "@opentelemetry/resource-detector-gcp": ^0.29.12 "@opentelemetry/resources": ^1.24.0 "@opentelemetry/sdk-node": ^0.53.0 peerDependencies: "@opentelemetry/api": ^1.4.1 - checksum: 978c1abc6c61d0544fae8db30faa32d5400013edd3d57298031491e87d4bf6ec7bfea58d8020b76c01c87eac63496d3abd22f32f25e8cc00d20e31c72d81f4ee + checksum: b90b4962b90722c458a00cf9efeeb0fff53091230ffc15043b935dc81d862d9c3125b5cc71fda836cb8283a4fb51ecffa59fbd2f0f4a3359420384d5dd68e26b languageName: node linkType: hard @@ -13149,7 +13152,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/core@npm:1.26.0, @opentelemetry/core@npm:^1.0.0, @opentelemetry/core@npm:^1.1.0, @opentelemetry/core@npm:^1.25.0, @opentelemetry/core@npm:^1.25.1, @opentelemetry/core@npm:^1.8.0": +"@opentelemetry/core@npm:1.26.0, @opentelemetry/core@npm:^1.0.0, @opentelemetry/core@npm:^1.1.0, @opentelemetry/core@npm:^1.25.0, @opentelemetry/core@npm:^1.25.1, @opentelemetry/core@npm:^1.26.0, @opentelemetry/core@npm:^1.8.0": version: 1.26.0 resolution: "@opentelemetry/core@npm:1.26.0" dependencies: @@ -13645,18 +13648,19 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/instrumentation-pg@npm:^0.45.0": - version: 0.45.0 - resolution: "@opentelemetry/instrumentation-pg@npm:0.45.0" +"@opentelemetry/instrumentation-pg@npm:^0.45.1": + version: 0.45.1 + resolution: "@opentelemetry/instrumentation-pg@npm:0.45.1" dependencies: + "@opentelemetry/core": ^1.26.0 "@opentelemetry/instrumentation": ^0.53.0 - "@opentelemetry/semantic-conventions": ^1.27.0 + "@opentelemetry/semantic-conventions": 1.27.0 "@opentelemetry/sql-common": ^0.40.1 "@types/pg": 8.6.1 "@types/pg-pool": 2.0.6 peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 059f79eeca92e27e1eee06ab55659a4eeee389c004ee2d8542e6bcce2147faf7c0ea568a5f305b2ee8065df575c44f6de80effb5c76adc1d3f098cdb3c2e3bdf + checksum: 29e6641ca221fe1e7c1a659ac37f07573f86abfc7f2a6afafdb528d70677522c54a35ff4fd3243a965dc4a8fc27f2c17b422fbc32789c0ee8a27e67e1f47aff0 languageName: node linkType: hard @@ -13881,15 +13885,16 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/resource-detector-alibaba-cloud@npm:^0.29.2": - version: 0.29.2 - resolution: "@opentelemetry/resource-detector-alibaba-cloud@npm:0.29.2" +"@opentelemetry/resource-detector-alibaba-cloud@npm:^0.29.3": + version: 0.29.3 + resolution: "@opentelemetry/resource-detector-alibaba-cloud@npm:0.29.3" dependencies: + "@opentelemetry/core": ^1.26.0 "@opentelemetry/resources": ^1.10.0 "@opentelemetry/semantic-conventions": ^1.27.0 peerDependencies: "@opentelemetry/api": ^1.0.0 - checksum: 817a024d28c5f3c79a65377115193ccf2171626e8d79e47210baea9db75c55c0a48d8f50fd19d9bf2ece422c3ae7c75d5d64879d4941e4a15c1757a2877be77e + checksum: e3cb36aa8b4ce7b6fc60fe15f51139493728f4c8e7af11f70cb567b5667680c16147bc382aa1a330b8bca26b392e7dddd5f66b6c393ca50aa315329baa0bea14 languageName: node linkType: hard @@ -13919,15 +13924,16 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/resource-detector-container@npm:^0.4.2": - version: 0.4.2 - resolution: "@opentelemetry/resource-detector-container@npm:0.4.2" +"@opentelemetry/resource-detector-container@npm:^0.4.3": + version: 0.4.3 + resolution: "@opentelemetry/resource-detector-container@npm:0.4.3" dependencies: + "@opentelemetry/core": ^1.26.0 "@opentelemetry/resources": ^1.10.0 "@opentelemetry/semantic-conventions": ^1.27.0 peerDependencies: "@opentelemetry/api": ^1.0.0 - checksum: 69b7d105b8d58e92bd4d35b7eba7a8be97fa4c89ba8006a590c03d028f84150bc5360d176933b0aba03c81ee0613cf46c74213cb52e96c05dd1497e2b4512875 + checksum: 301a98524027442030cb6ea17c75988c29925586d86acde637d9129007d78e4dd27f7a3af30998000c475d2aae2a08d3269259e5d49973a7824f41ee144285e4 languageName: node linkType: hard @@ -18199,7 +18205,7 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:^29.5.11": +"@types/jest@npm:*, @types/jest@npm:^29.5.11": version: 29.5.13 resolution: "@types/jest@npm:29.5.13" dependencies: @@ -18776,11 +18782,11 @@ __metadata: linkType: hard "@types/react-dom@npm:^18": - version: 18.3.0 - resolution: "@types/react-dom@npm:18.3.0" + version: 18.3.1 + resolution: "@types/react-dom@npm:18.3.1" dependencies: "@types/react": "*" - checksum: a0cd9b1b815a6abd2a367a9eabdd8df8dd8f13f95897b2f9e1359ea3ac6619f957c1432ece004af7d95e2a7caddbba19faa045f831f32d6263483fc5404a7596 + checksum: ad28ecce3915d30dc76adc2a1373fda1745ba429cea290e16c6628df9a05fd80b6403c8e87d78b45e6c60e51df7a67add389ab62b90070fbfdc9bda8307d9953 languageName: node linkType: hard