From 6bf7826258ee4e1f475b8728db207e882d5a7ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 6 Jan 2022 19:46:33 +0100 Subject: [PATCH] add and use MockFetchApi MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/modern-knives-roll.md | 2 +- .changeset/shiny-garlics-cough.md | 5 ++ .changeset/sweet-candles-greet.md | 2 +- .../src/apis/definitions/FetchApi.ts | 3 + packages/test-utils/api-report.md | 14 ++++ packages/test-utils/package.json | 4 +- packages/test-utils/src/setupTests.ts | 1 + .../apis/FetchApi/MockFetchApi.test.ts | 78 +++++++++++++++++ .../testUtils/apis/FetchApi/MockFetchApi.ts | 84 +++++++++++++++++++ .../src/testUtils/apis/FetchApi/index.ts | 17 ++++ .../test-utils/src/testUtils/apis/index.ts | 1 + .../test-utils/src/testUtils/msw/index.ts | 15 +--- .../testUtils/msw/setupRequestMockHandlers.ts | 30 +++++++ plugins/scaffolder/api-report.md | 6 +- plugins/scaffolder/dev/index.tsx | 10 +-- plugins/scaffolder/src/api.test.ts | 8 +- plugins/scaffolder/src/api.ts | 34 ++------ plugins/scaffolder/src/plugin.ts | 5 +- plugins/techdocs/api-report.md | 7 +- plugins/techdocs/src/client.test.ts | 28 +++++-- plugins/techdocs/src/client.ts | 31 ++----- plugins/techdocs/src/plugin.ts | 4 +- plugins/techdocs/src/setupTests.ts | 1 - 23 files changed, 284 insertions(+), 106 deletions(-) create mode 100644 .changeset/shiny-garlics-cough.md create mode 100644 packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts create mode 100644 packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.ts create mode 100644 packages/test-utils/src/testUtils/apis/FetchApi/index.ts create mode 100644 packages/test-utils/src/testUtils/msw/setupRequestMockHandlers.ts diff --git a/.changeset/modern-knives-roll.md b/.changeset/modern-knives-roll.md index 4d69f285b2..337fc62ed7 100644 --- a/.changeset/modern-knives-roll.md +++ b/.changeset/modern-knives-roll.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder': minor --- Make `ScaffolderClient` use the `FetchApi`. diff --git a/.changeset/shiny-garlics-cough.md b/.changeset/shiny-garlics-cough.md new file mode 100644 index 0000000000..ea6634506f --- /dev/null +++ b/.changeset/shiny-garlics-cough.md @@ -0,0 +1,5 @@ +--- +'@backstage/test-utils': patch +--- + +Added a `MockFetchApi` diff --git a/.changeset/sweet-candles-greet.md b/.changeset/sweet-candles-greet.md index 1a83f7828a..9fd08c65d6 100644 --- a/.changeset/sweet-candles-greet.md +++ b/.changeset/sweet-candles-greet.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-techdocs': patch +'@backstage/plugin-techdocs': minor --- Make `TechDocsClient` and `TechDocsStorageClient` use the `FetchApi`. diff --git a/packages/core-plugin-api/src/apis/definitions/FetchApi.ts b/packages/core-plugin-api/src/apis/definitions/FetchApi.ts index 67513b113a..b1a4dbbd0b 100644 --- a/packages/core-plugin-api/src/apis/definitions/FetchApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/FetchApi.ts @@ -23,6 +23,9 @@ import { ApiRef, createApiRef } from '../system'; * @public */ export type FetchApi = { + /** + * The `fetch` implementation. + */ fetch: typeof fetch; }; diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index 2039bc60b5..143c88a4c2 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -13,10 +13,13 @@ import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { ComponentType } from 'react'; import { Config } from '@backstage/config'; import { ConfigApi } from '@backstage/core-plugin-api'; +import crossFetch from 'cross-fetch'; import { ErrorApi } from '@backstage/core-plugin-api'; import { ErrorApiError } from '@backstage/core-plugin-api'; import { ErrorApiErrorContext } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; +import { FetchApi } from '@backstage/core-plugin-api'; +import { IdentityApi } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { Observable } from '@backstage/types'; @@ -117,6 +120,17 @@ export type MockErrorApiOptions = { collect?: boolean; }; +// @public +export class MockFetchApi implements FetchApi { + constructor(implementation?: typeof crossFetch); + // (undocumented) + get fetch(): typeof crossFetch; + setAuthorization(options?: { + identityApi?: Pick; + token?: string; + }): this; +} + // @public export class MockPermissionApi implements PermissionApi { constructor( diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 32d7010f53..b3f2b65aa5 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -41,6 +41,7 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.1.8", + "cross-fetch": "^3.0.6", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "zen-observable": "^0.8.15" @@ -52,7 +53,8 @@ "devDependencies": { "@backstage/cli": "^0.12.0-next.0", "@types/jest": "^26.0.7", - "@types/node": "^14.14.32" + "@types/node": "^14.14.32", + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/packages/test-utils/src/setupTests.ts b/packages/test-utils/src/setupTests.ts index 963c0f188b..c1d649f2ad 100644 --- a/packages/test-utils/src/setupTests.ts +++ b/packages/test-utils/src/setupTests.ts @@ -15,3 +15,4 @@ */ import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts b/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts new file mode 100644 index 0000000000..8ae98b83df --- /dev/null +++ b/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2022 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 { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '../../msw'; +import { MockFetchApi } from './MockFetchApi'; + +describe('MockFetchApi', () => { + const worker = setupServer(); + setupRequestMockHandlers(worker); + + it('works with default constructor', async () => { + worker.use( + rest.get('http://example.com/data.json', (_, res, ctx) => + res(ctx.status(200), ctx.json({ a: 'foo' })), + ), + ); + const m = new MockFetchApi(); + const response = await m.fetch('http://example.com/data.json'); + await expect(response.json()).resolves.toEqual({ a: 'foo' }); + }); + + it('works with a mock implementation', async () => { + const inner = jest.fn(); + const m = new MockFetchApi(inner); + await m.fetch('http://example.com/data.json'); + expect(inner).lastCalledWith('http://example.com/data.json'); + }); + + describe('setAuthorization', () => { + it('works with the default', async () => { + const inner = jest.fn(); + const m = new MockFetchApi(inner).setAuthorization(); + await m.fetch('http://example.com/data.json'); + expect(inner.mock.calls[0][0].headers.get('authorization')).toBe( + 'Bearer mocked', + ); + }); + + it('works with a static token', async () => { + const inner = jest.fn(); + const m = new MockFetchApi(inner).setAuthorization({ token: 'hello' }); + await m.fetch('http://example.com/data.json'); + expect(inner.mock.calls[0][0].headers.get('authorization')).toBe( + 'Bearer hello', + ); + }); + + it('works with an identity api', async () => { + const inner = jest.fn(); + const m = new MockFetchApi(inner).setAuthorization({ + identityApi: { + async getCredentials() { + return { token: 'hello2' }; + }, + }, + }); + await m.fetch('http://example.com/data.json'); + expect(inner.mock.calls[0][0].headers.get('authorization')).toBe( + 'Bearer hello2', + ); + }); + }); +}); diff --git a/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.ts b/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.ts new file mode 100644 index 0000000000..7df9b62e0a --- /dev/null +++ b/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2022 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 { FetchApi, IdentityApi } from '@backstage/core-plugin-api'; +import crossFetch, { Request } from 'cross-fetch'; + +/** + * A test helper implementation of {@link @backstage/core-plugin-api#FetchApi}. + * + * @public + */ +export class MockFetchApi implements FetchApi { + #implementation: typeof crossFetch; + + /** + * Creates a {@link MockFetchApi}. + * + * @param mockImplementation - Here you can pass in a `jest.fn()` for example, + * if you want to track the calls being made through the + * {@link @backstage/core-plugin-api#MockFetchApi}. If you pass in no + * mock implementation, the created + * {@link @backstage/core-plugin-api#MockFetchApi} will make actual + * requests using the global `fetch`. + */ + constructor(implementation?: typeof crossFetch) { + this.#implementation = implementation ?? crossFetch; + } + + /** {@inheritdoc @backstage/core-plugin-api#FetchApi.fetch} */ + get fetch(): typeof crossFetch { + return this.#implementation; + } + + /** + * Adds token based Authorization headers to requests, basically simulating + * what {@link @backstage/core-app-api#FetchMiddlewares.injectIdentityAuth} + * does. + * + * @remarks + * + * You can supply either a static mock token or a mock identity API. If + * neither is given, the static token string "mocked" is used. + */ + setAuthorization(options?: { + identityApi?: Pick; + token?: string; + }): this { + const next = this.#implementation; + + const getToken = async () => { + if (options?.token) { + return options.token; + } else if (options?.identityApi) { + const { token } = await options.identityApi.getCredentials(); + return token; + } + return 'mocked'; + }; + + this.#implementation = async (input, init) => { + const request = new Request(input, init); + const token = await getToken(); + if (token && !request.headers.get('authorization')) { + request.headers.set('authorization', `Bearer ${token}`); + } + return next(request); + }; + + return this; + } +} diff --git a/packages/test-utils/src/testUtils/apis/FetchApi/index.ts b/packages/test-utils/src/testUtils/apis/FetchApi/index.ts new file mode 100644 index 0000000000..058d4c4b27 --- /dev/null +++ b/packages/test-utils/src/testUtils/apis/FetchApi/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { MockFetchApi } from './MockFetchApi'; diff --git a/packages/test-utils/src/testUtils/apis/index.ts b/packages/test-utils/src/testUtils/apis/index.ts index e59608d2b0..e122a4b432 100644 --- a/packages/test-utils/src/testUtils/apis/index.ts +++ b/packages/test-utils/src/testUtils/apis/index.ts @@ -17,5 +17,6 @@ export * from './AnalyticsApi'; export * from './ConfigApi'; export * from './ErrorApi'; +export * from './FetchApi'; export * from './PermissionApi'; export * from './StorageApi'; diff --git a/packages/test-utils/src/testUtils/msw/index.ts b/packages/test-utils/src/testUtils/msw/index.ts index 625ef0e0fc..29cbdf854a 100644 --- a/packages/test-utils/src/testUtils/msw/index.ts +++ b/packages/test-utils/src/testUtils/msw/index.ts @@ -14,17 +14,4 @@ * limitations under the License. */ -/** - * Sets up handlers for request mocking - * @public - * @param worker - service worker - */ -export function setupRequestMockHandlers(worker: { - listen: (t: any) => void; - close: () => void; - resetHandlers: () => void; -}) { - beforeAll(() => worker.listen({ onUnhandledRequest: 'error' })); - afterAll(() => worker.close()); - afterEach(() => worker.resetHandlers()); -} +export { setupRequestMockHandlers } from './setupRequestMockHandlers'; diff --git a/packages/test-utils/src/testUtils/msw/setupRequestMockHandlers.ts b/packages/test-utils/src/testUtils/msw/setupRequestMockHandlers.ts new file mode 100644 index 0000000000..625ef0e0fc --- /dev/null +++ b/packages/test-utils/src/testUtils/msw/setupRequestMockHandlers.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2020 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. + */ + +/** + * Sets up handlers for request mocking + * @public + * @param worker - service worker + */ +export function setupRequestMockHandlers(worker: { + listen: (t: any) => void; + close: () => void; + resetHandlers: () => void; +}) { + beforeAll(() => worker.listen({ onUnhandledRequest: 'error' })); + afterAll(() => worker.close()); + afterEach(() => worker.resetHandlers()); +} diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 835712651d..b77fb5f939 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -19,7 +19,6 @@ import { FetchApi } from '@backstage/core-plugin-api'; import { FieldProps } from '@rjsf/core'; import { FieldValidation } from '@rjsf/core'; import { IconButton } from '@material-ui/core'; -import { IdentityApi } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { JSONSchema } from '@backstage/catalog-model'; import { Observable } from '@backstage/types'; @@ -188,8 +187,6 @@ export interface ScaffolderApi { templateName: EntityName, ): Promise; // Warning: (ae-forgotten-export) The symbol "ListActionsResponse" needs to be exported by the entry point index.d.ts - // - // (undocumented) listActions(): Promise; scaffold( templateName: string, @@ -209,8 +206,7 @@ export const scaffolderApiRef: ApiRef; export class ScaffolderClient implements ScaffolderApi { constructor(options: { discoveryApi: DiscoveryApi; - identityApi: IdentityApi; - fetchApi?: FetchApi; + fetchApi: FetchApi; scmIntegrationsApi: ScmIntegrationRegistry; useLongPollingLogs?: boolean; }); diff --git a/plugins/scaffolder/dev/index.tsx b/plugins/scaffolder/dev/index.tsx index f7c6aa62fc..78e6254e09 100644 --- a/plugins/scaffolder/dev/index.tsx +++ b/plugins/scaffolder/dev/index.tsx @@ -26,9 +26,8 @@ import React from 'react'; import { scaffolderApiRef, ScaffolderClient } from '../src'; import { ScaffolderPage } from '../src/plugin'; import { - configApiRef, discoveryApiRef, - identityApiRef, + fetchApiRef, storageApiRef, } from '@backstage/core-plugin-api'; import { CatalogEntityPage } from '@backstage/plugin-catalog'; @@ -52,12 +51,11 @@ createDevApp() api: scaffolderApiRef, deps: { discoveryApi: discoveryApiRef, - identityApi: identityApiRef, - configApi: configApiRef, + fetchApi: fetchApiRef, scmIntegrationsApi: scmIntegrationsApiRef, }, - factory: ({ discoveryApi, identityApi, scmIntegrationsApi }) => - new ScaffolderClient({ discoveryApi, identityApi, scmIntegrationsApi }), + factory: ({ discoveryApi, fetchApi, scmIntegrationsApi }) => + new ScaffolderClient({ discoveryApi, fetchApi, scmIntegrationsApi }), }) .addPage({ path: '/create', diff --git a/plugins/scaffolder/src/api.test.ts b/plugins/scaffolder/src/api.test.ts index 01463a6e1c..369722ec8f 100644 --- a/plugins/scaffolder/src/api.test.ts +++ b/plugins/scaffolder/src/api.test.ts @@ -16,7 +16,7 @@ import { ConfigReader } from '@backstage/core-app-api'; import { ScmIntegrations } from '@backstage/integration'; -import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { ScaffolderClient } from './api'; @@ -32,7 +32,7 @@ describe('api', () => { const mockBaseUrl = 'http://backstage/api'; const discoveryApi = { getBaseUrl: async () => mockBaseUrl }; - const identityApi = {} as any; + const fetchApi = new MockFetchApi(); const scmIntegrationsApi = ScmIntegrations.fromConfig( new ConfigReader({ integrations: { @@ -50,7 +50,7 @@ describe('api', () => { apiClient = new ScaffolderClient({ scmIntegrationsApi, discoveryApi, - identityApi, + fetchApi, }); }); @@ -126,7 +126,7 @@ describe('api', () => { apiClient = new ScaffolderClient({ scmIntegrationsApi, discoveryApi, - identityApi, + fetchApi, useLongPollingLogs: true, }); }); diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 77af1460c3..62fc06a6db 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -19,7 +19,6 @@ import { createApiRef, DiscoveryApi, FetchApi, - IdentityApi, } from '@backstage/core-plugin-api'; import { ResponseError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; @@ -94,7 +93,9 @@ export interface ScaffolderApi { allowedHosts: string[]; }): Promise<{ type: string; title: string; host: string }[]>; - // Returns a list of all installed actions. + /** + * Returns a list of all installed actions. + */ listActions(): Promise; streamLogs(options: { taskId: string; after?: number }): Observable; @@ -107,20 +108,17 @@ export interface ScaffolderApi { */ export class ScaffolderClient implements ScaffolderApi { private readonly discoveryApi: DiscoveryApi; - private readonly identityApi: IdentityApi; private readonly scmIntegrationsApi: ScmIntegrationRegistry; private readonly fetchApi: FetchApi; private readonly useLongPollingLogs: boolean; constructor(options: { discoveryApi: DiscoveryApi; - identityApi: IdentityApi; - fetchApi?: FetchApi; + fetchApi: FetchApi; scmIntegrationsApi: ScmIntegrationRegistry; useLongPollingLogs?: boolean; }) { this.discoveryApi = options.discoveryApi; - this.identityApi = options.identityApi; this.fetchApi = options.fetchApi ?? { fetch }; this.scmIntegrationsApi = options.scmIntegrationsApi; this.useLongPollingLogs = options.useLongPollingLogs ?? false; @@ -142,19 +140,13 @@ export class ScaffolderClient implements ScaffolderApi { ): Promise { const { namespace, kind, name } = templateName; - const { token } = await this.identityApi.getCredentials(); const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); const templatePath = [namespace, kind, name] .map(s => encodeURIComponent(s)) .join('/'); const url = `${baseUrl}/v2/templates/${templatePath}/parameter-schema`; - const response = await this.fetchApi.fetch(url, { - headers: { - ...(token && { Authorization: `Bearer ${token}` }), - }, - }); - + const response = await this.fetchApi.fetch(url); if (!response.ok) { throw await ResponseError.fromResponse(response); } @@ -176,13 +168,11 @@ export class ScaffolderClient implements ScaffolderApi { values: Record, secrets: Record = {}, ): Promise { - const { token } = await this.identityApi.getCredentials(); const url = `${await this.discoveryApi.getBaseUrl('scaffolder')}/v2/tasks`; const response = await this.fetchApi.fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', - ...(token && { Authorization: `Bearer ${token}` }), }, body: JSON.stringify({ templateName, @@ -202,13 +192,10 @@ export class ScaffolderClient implements ScaffolderApi { } async getTask(taskId: string) { - const { token } = await this.identityApi.getCredentials(); const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}`; - const response = await this.fetchApi.fetch(url, { - headers: token ? { Authorization: `Bearer ${token}` } : {}, - }); + const response = await this.fetchApi.fetch(url); if (!response.ok) { throw await ResponseError.fromResponse(response); } @@ -317,16 +304,9 @@ export class ScaffolderClient implements ScaffolderApi { }); } - /** - * @returns ListActionsResponse containing all registered actions. - */ async listActions(): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); - const { token } = await this.identityApi.getCredentials(); - const response = await this.fetchApi.fetch(`${baseUrl}/v2/actions`, { - headers: token ? { Authorization: `Bearer ${token}` } : {}, - }); - + const response = await this.fetchApi.fetch(`${baseUrl}/v2/actions`); if (!response.ok) { throw await ResponseError.fromResponse(response); } diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index 9221fc1bf3..ccf8d2ee34 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -34,7 +34,6 @@ import { createRoutableExtension, discoveryApiRef, fetchApiRef, - identityApiRef, } from '@backstage/core-plugin-api'; import { OwnedEntityPicker } from './components/fields/OwnedEntityPicker'; import { EntityTagsPicker } from './components/fields/EntityTagsPicker'; @@ -46,14 +45,12 @@ export const scaffolderPlugin = createPlugin({ api: scaffolderApiRef, deps: { discoveryApi: discoveryApiRef, - identityApi: identityApiRef, scmIntegrationsApi: scmIntegrationsApiRef, fetchApi: fetchApiRef, }, - factory: ({ discoveryApi, identityApi, scmIntegrationsApi, fetchApi }) => + factory: ({ discoveryApi, scmIntegrationsApi, fetchApi }) => new ScaffolderClient({ discoveryApi, - identityApi, scmIntegrationsApi, fetchApi, }), diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index 7aed7139db..691df74656 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -222,8 +222,7 @@ export class TechDocsClient implements TechDocsApi { constructor(options: { configApi: Config; discoveryApi: DiscoveryApi; - identityApi: IdentityApi; - fetchApi?: FetchApi; + fetchApi: FetchApi; }); // (undocumented) configApi: Config; @@ -233,8 +232,6 @@ export class TechDocsClient implements TechDocsApi { getApiOrigin(): Promise; getEntityMetadata(entityId: EntityName): Promise; getTechDocsMetadata(entityId: EntityName): Promise; - // (undocumented) - identityApi: IdentityApi; } // Warning: (ae-missing-release-tag) "TechDocsCustomHome" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -382,7 +379,7 @@ export class TechDocsStorageClient implements TechDocsStorageApi { configApi: Config; discoveryApi: DiscoveryApi; identityApi: IdentityApi; - fetchApi?: FetchApi; + fetchApi: FetchApi; }); // (undocumented) configApi: Config; diff --git a/plugins/techdocs/src/client.test.ts b/plugins/techdocs/src/client.test.ts index 076d90cdad..8ea2b772b2 100644 --- a/plugins/techdocs/src/client.test.ts +++ b/plugins/techdocs/src/client.test.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { MockConfigApi } from '@backstage/test-utils'; import { UrlPatternDiscovery } from '@backstage/core-app-api'; import { IdentityApi } from '@backstage/core-plugin-api'; import { NotFoundError } from '@backstage/errors'; import { EventSourcePolyfill } from 'event-source-polyfill'; +import { MockConfigApi, MockFetchApi } from '@backstage/test-utils'; import { TechDocsStorageClient } from './client'; const MockedEventSource = EventSourcePolyfill as jest.MockedClass< @@ -36,17 +36,13 @@ const mockEntity = { describe('TechDocsStorageClient', () => { const mockBaseUrl = 'http://backstage:9191/api/techdocs'; const configApi = new MockConfigApi({ - techdocs: { - requestUrl: 'http://backstage:9191/api/techdocs', - }, + techdocs: { requestUrl: 'http://backstage:9191/api/techdocs' }, }); const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); const identityApi: jest.Mocked = { - signOut: jest.fn(), - getProfileInfo: jest.fn(), - getBackstageIdentity: jest.fn(), getCredentials: jest.fn(), - }; + } as unknown as jest.Mocked; + const fetchApi = new MockFetchApi().setAuthorization({ identityApi }); beforeEach(() => { jest.resetAllMocks(); @@ -58,6 +54,7 @@ describe('TechDocsStorageClient', () => { configApi, discoveryApi, identityApi, + fetchApi, }); await expect( @@ -78,6 +75,7 @@ describe('TechDocsStorageClient', () => { configApi, discoveryApi, identityApi, + fetchApi, }); await expect( @@ -93,6 +91,7 @@ describe('TechDocsStorageClient', () => { configApi, discoveryApi, identityApi, + fetchApi, }); MockedEventSource.prototype.addEventListener.mockImplementation( @@ -103,6 +102,7 @@ describe('TechDocsStorageClient', () => { }, ); + identityApi.getCredentials.mockResolvedValue({}); await storageApi.syncEntityDocs(mockEntity); expect(MockedEventSource).toBeCalledWith( @@ -116,6 +116,7 @@ describe('TechDocsStorageClient', () => { configApi, discoveryApi, identityApi, + fetchApi, }); MockedEventSource.prototype.addEventListener.mockImplementation( @@ -127,7 +128,6 @@ describe('TechDocsStorageClient', () => { ); identityApi.getCredentials.mockResolvedValue({ token: 'token' }); - await storageApi.syncEntityDocs(mockEntity); expect(MockedEventSource).toBeCalledWith( @@ -141,6 +141,7 @@ describe('TechDocsStorageClient', () => { configApi, discoveryApi, identityApi, + fetchApi, }); MockedEventSource.prototype.addEventListener.mockImplementation( @@ -151,6 +152,7 @@ describe('TechDocsStorageClient', () => { }, ); + identityApi.getCredentials.mockResolvedValue({}); await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual( 'cached', ); @@ -161,6 +163,7 @@ describe('TechDocsStorageClient', () => { configApi, discoveryApi, identityApi, + fetchApi, }); MockedEventSource.prototype.addEventListener.mockImplementation( @@ -171,6 +174,7 @@ describe('TechDocsStorageClient', () => { }, ); + identityApi.getCredentials.mockResolvedValue({}); await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual( 'updated', ); @@ -181,6 +185,7 @@ describe('TechDocsStorageClient', () => { configApi, discoveryApi, identityApi, + fetchApi, }); MockedEventSource.prototype.addEventListener.mockImplementation( @@ -195,6 +200,7 @@ describe('TechDocsStorageClient', () => { }, ); + identityApi.getCredentials.mockResolvedValue({}); const logHandler = jest.fn(); await expect( storageApi.syncEntityDocs(mockEntity, logHandler), @@ -209,9 +215,11 @@ describe('TechDocsStorageClient', () => { configApi, discoveryApi, identityApi, + fetchApi, }); // we await later after we emitted the error + identityApi.getCredentials.mockResolvedValue({}); const promise = storageApi.syncEntityDocs(mockEntity).then(); // flush the event loop @@ -234,9 +242,11 @@ describe('TechDocsStorageClient', () => { configApi, discoveryApi, identityApi, + fetchApi, }); // we await later after we emitted the error + identityApi.getCredentials.mockResolvedValue({}); const promise = storageApi.syncEntityDocs(mockEntity).then(); // flush the event loop diff --git a/plugins/techdocs/src/client.ts b/plugins/techdocs/src/client.ts index 6f381a5bd9..eac5da0b6b 100644 --- a/plugins/techdocs/src/client.ts +++ b/plugins/techdocs/src/client.ts @@ -34,19 +34,16 @@ import { TechDocsEntityMetadata, TechDocsMetadata } from './types'; export class TechDocsClient implements TechDocsApi { public configApi: Config; public discoveryApi: DiscoveryApi; - public identityApi: IdentityApi; private fetchApi: FetchApi; constructor(options: { configApi: Config; discoveryApi: DiscoveryApi; - identityApi: IdentityApi; - fetchApi?: FetchApi; + fetchApi: FetchApi; }) { this.configApi = options.configApi; this.discoveryApi = options.discoveryApi; - this.identityApi = options.identityApi; - this.fetchApi = options.fetchApi ?? { fetch }; + this.fetchApi = options.fetchApi; } async getApiOrigin(): Promise { @@ -70,12 +67,7 @@ export class TechDocsClient implements TechDocsApi { const apiOrigin = await this.getApiOrigin(); const requestUrl = `${apiOrigin}/metadata/techdocs/${namespace}/${kind}/${name}`; - const { token } = await this.identityApi.getCredentials(); - - const request = await this.fetchApi.fetch(`${requestUrl}`, { - headers: token ? { Authorization: `Bearer ${token}` } : {}, - }); - + const request = await this.fetchApi.fetch(`${requestUrl}`); if (!request.ok) { throw await ResponseError.fromResponse(request); } @@ -98,12 +90,8 @@ export class TechDocsClient implements TechDocsApi { const apiOrigin = await this.getApiOrigin(); const requestUrl = `${apiOrigin}/metadata/entity/${namespace}/${kind}/${name}`; - const { token } = await this.identityApi.getCredentials(); - - const request = await this.fetchApi.fetch(`${requestUrl}`, { - headers: token ? { Authorization: `Bearer ${token}` } : {}, - }); + const request = await this.fetchApi.fetch(`${requestUrl}`); if (!request.ok) { throw await ResponseError.fromResponse(request); } @@ -114,11 +102,8 @@ export class TechDocsClient implements TechDocsApi { /** * API which talks to TechDocs storage to fetch files to render. -<<<<<<< HEAD -======= * * @public ->>>>>>> 31c54b8ea2 (Make the techdocs APIs use the FetchApi) */ export class TechDocsStorageClient implements TechDocsStorageApi { public configApi: Config; @@ -130,12 +115,12 @@ export class TechDocsStorageClient implements TechDocsStorageApi { configApi: Config; discoveryApi: DiscoveryApi; identityApi: IdentityApi; - fetchApi?: FetchApi; + fetchApi: FetchApi; }) { this.configApi = options.configApi; this.discoveryApi = options.discoveryApi; this.identityApi = options.identityApi; - this.fetchApi = options.fetchApi ?? { fetch }; + this.fetchApi = options.fetchApi; } async getApiOrigin(): Promise { @@ -169,13 +154,9 @@ export class TechDocsStorageClient implements TechDocsStorageApi { const storageUrl = await this.getStorageUrl(); const url = `${storageUrl}/${namespace}/${kind}/${name}/${path}`; - const { token } = await this.identityApi.getCredentials(); const request = await this.fetchApi.fetch( `${url.endsWith('/') ? url : `${url}/`}index.html`, - { - headers: token ? { Authorization: `Bearer ${token}` } : {}, - }, ); let errorMessage = ''; diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts index 274d1904d8..a37ad61f80 100644 --- a/plugins/techdocs/src/plugin.ts +++ b/plugins/techdocs/src/plugin.ts @@ -56,14 +56,12 @@ export const techdocsPlugin = createPlugin({ deps: { configApi: configApiRef, discoveryApi: discoveryApiRef, - identityApi: identityApiRef, fetchApi: fetchApiRef, }, - factory: ({ configApi, discoveryApi, identityApi, fetchApi }) => + factory: ({ configApi, discoveryApi, fetchApi }) => new TechDocsClient({ configApi, discoveryApi, - identityApi, fetchApi, }), }), diff --git a/plugins/techdocs/src/setupTests.ts b/plugins/techdocs/src/setupTests.ts index c1d649f2ad..963c0f188b 100644 --- a/plugins/techdocs/src/setupTests.ts +++ b/plugins/techdocs/src/setupTests.ts @@ -15,4 +15,3 @@ */ import '@testing-library/jest-dom'; -import 'cross-fetch/polyfill';