diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index 143c88a4c2..fade844712 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -122,13 +122,22 @@ export type MockErrorApiOptions = { // @public export class MockFetchApi implements FetchApi { - constructor(implementation?: typeof crossFetch); + constructor(options?: MockFetchApiOptions); // (undocumented) get fetch(): typeof crossFetch; - setAuthorization(options?: { - identityApi?: Pick; - token?: string; - }): this; +} + +// @public +export interface MockFetchApiOptions { + authorization?: + | { + token: string; + } + | { + identityApi: Pick; + } + | undefined; + baseImplementation?: 'fetch' | 'none' | typeof crossFetch | undefined; } // @public diff --git a/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts b/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts index 8ae98b83df..bf81e0ace0 100644 --- a/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts +++ b/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.test.ts @@ -36,24 +36,18 @@ describe('MockFetchApi', () => { it('works with a mock implementation', async () => { const inner = jest.fn(); - const m = new MockFetchApi(inner); + const m = new MockFetchApi({ baseImplementation: 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' }); + const m = new MockFetchApi({ + baseImplementation: inner, + authorization: { token: 'hello' }, + }); await m.fetch('http://example.com/data.json'); expect(inner.mock.calls[0][0].headers.get('authorization')).toBe( 'Bearer hello', @@ -62,10 +56,13 @@ describe('MockFetchApi', () => { it('works with an identity api', async () => { const inner = jest.fn(); - const m = new MockFetchApi(inner).setAuthorization({ - identityApi: { - async getCredentials() { - return { token: 'hello2' }; + const m = new MockFetchApi({ + baseImplementation: inner, + authorization: { + identityApi: { + async getCredentials() { + return { token: 'hello2' }; + }, }, }, }); diff --git a/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.ts b/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.ts index 7df9b62e0a..7afa5a38b1 100644 --- a/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.ts +++ b/packages/test-utils/src/testUtils/apis/FetchApi/MockFetchApi.ts @@ -15,7 +15,46 @@ */ import { FetchApi, IdentityApi } from '@backstage/core-plugin-api'; -import crossFetch, { Request } from 'cross-fetch'; +import crossFetch, { Request, Response } from 'cross-fetch'; + +/** + * The options given when constructing a {@link MockFetchApi}. + * + * @public + */ +export interface MockFetchApiOptions { + /** + * Define the underlying base `fetch` implementation. + * + * @defaultValue 'fetch' + * @remarks + * + * `'fetch'` uses the global `fetch` implementation to make real network + * requests. This is the default. + * + * `'none'` swallows all calls and makes no requests at all. + * + * You can also pass in any `fetch` compatible callback, such as a + * `jest.fn()`, if you want to use a custom implementation or to just track + * and assert on calls. + */ + baseImplementation?: 'fetch' | 'none' | typeof crossFetch | undefined; + + /** + * If defined, adds token based Authorization headers to requests, basically + * simulating what + * {@link @backstage/core-app-api#FetchMiddlewares.injectIdentityAuth} does. + * + * @defaultValue undefined + * @remarks + * + * You can supply either a static token or an identity API. + */ + authorization?: + | { token: string } + | { identityApi: Pick } + | undefined; +} /** * A test helper implementation of {@link @backstage/core-plugin-api#FetchApi}. @@ -23,62 +62,70 @@ import crossFetch, { Request } from 'cross-fetch'; * @public */ export class MockFetchApi implements FetchApi { - #implementation: typeof crossFetch; + private readonly 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`. + * Creates a mock {@link @backstage/core-plugin-api#FetchApi}. */ - constructor(implementation?: typeof crossFetch) { - this.#implementation = implementation ?? crossFetch; + constructor(options?: MockFetchApiOptions) { + this.implementation = build(options); } /** {@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; + return this.implementation; } } + +// +// Helpers +// + +const dummyFetch: typeof crossFetch = () => Promise.resolve(new Response(null)); + +function build(options?: MockFetchApiOptions): typeof crossFetch { + let implementation = baseImplementation(options); + implementation = authorization(options, implementation); + return implementation; +} + +function baseImplementation( + options: MockFetchApiOptions | undefined, +): typeof crossFetch { + const implementation = options?.baseImplementation ?? 'fetch'; + if (implementation === 'fetch') { + return crossFetch; + } else if (implementation === 'none') { + return dummyFetch; + } + return implementation; +} + +function authorization( + options: MockFetchApiOptions | undefined, + next: typeof crossFetch, +): typeof crossFetch { + const auth = options?.authorization; + if (!auth) { + return next; + } + + const getToken = async () => { + if ('token' in auth) { + return auth.token; + } + const { token } = await auth.identityApi.getCredentials(); + return token; + }; + + return async (input, init) => { + const request = new Request(input, init); + if (!request.headers.get('authorization')) { + const token = await getToken(); + if (token) { + request.headers.set('authorization', `Bearer ${token}`); + } + } + return next(request); + }; +} diff --git a/packages/test-utils/src/testUtils/apis/FetchApi/index.ts b/packages/test-utils/src/testUtils/apis/FetchApi/index.ts index 058d4c4b27..5ac0c08ff1 100644 --- a/packages/test-utils/src/testUtils/apis/FetchApi/index.ts +++ b/packages/test-utils/src/testUtils/apis/FetchApi/index.ts @@ -15,3 +15,4 @@ */ export { MockFetchApi } from './MockFetchApi'; +export type { MockFetchApiOptions } from './MockFetchApi'; diff --git a/plugins/techdocs/src/client.test.ts b/plugins/techdocs/src/client.test.ts index 8ea2b772b2..460b104c33 100644 --- a/plugins/techdocs/src/client.test.ts +++ b/plugins/techdocs/src/client.test.ts @@ -42,7 +42,7 @@ describe('TechDocsStorageClient', () => { const identityApi: jest.Mocked = { getCredentials: jest.fn(), } as unknown as jest.Mocked; - const fetchApi = new MockFetchApi().setAuthorization({ identityApi }); + const fetchApi = new MockFetchApi({ authorization: { identityApi } }); beforeEach(() => { jest.resetAllMocks();