use a one-argument constructor instead of setters

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2022-01-07 17:28:48 +01:00
parent 6bf7826258
commit 19ea047412
5 changed files with 126 additions and 72 deletions
+14 -5
View File
@@ -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<IdentityApi, 'getCredentials'>;
token?: string;
}): this;
}
// @public
export interface MockFetchApiOptions {
authorization?:
| {
token: string;
}
| {
identityApi: Pick<IdentityApi, 'getCredentials'>;
}
| undefined;
baseImplementation?: 'fetch' | 'none' | typeof crossFetch | undefined;
}
// @public
@@ -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' };
},
},
},
});
@@ -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<IdentityApi, 'getCredentials'> }
| 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<IdentityApi, 'getCredentials'>;
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);
};
}
@@ -15,3 +15,4 @@
*/
export { MockFetchApi } from './MockFetchApi';
export type { MockFetchApiOptions } from './MockFetchApi';
+1 -1
View File
@@ -42,7 +42,7 @@ describe('TechDocsStorageClient', () => {
const identityApi: jest.Mocked<IdentityApi> = {
getCredentials: jest.fn(),
} as unknown as jest.Mocked<IdentityApi>;
const fetchApi = new MockFetchApi().setAuthorization({ identityApi });
const fetchApi = new MockFetchApi({ authorization: { identityApi } });
beforeEach(() => {
jest.resetAllMocks();