From caf1af65415adc77ad6a8fc14096bc82094a5d14 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 17 Feb 2024 12:57:39 +0100 Subject: [PATCH] backend-test-utils: add default credentials for mock http auth + explicit none credentials header Signed-off-by: Patrik Oldsberg --- .../next/services/MockHttpAuthService.test.ts | 50 ++++++++++++++++++- .../src/next/services/MockHttpAuthService.ts | 14 ++++-- .../src/next/services/mockCredentials.test.ts | 4 ++ .../src/next/services/mockCredentials.ts | 20 ++++++++ .../src/next/services/mockServices.ts | 49 +++++++++++++++--- 5 files changed, 126 insertions(+), 11 deletions(-) diff --git a/packages/backend-test-utils/src/next/services/MockHttpAuthService.test.ts b/packages/backend-test-utils/src/next/services/MockHttpAuthService.test.ts index 62810604a6..d441fd0108 100644 --- a/packages/backend-test-utils/src/next/services/MockHttpAuthService.test.ts +++ b/packages/backend-test-utils/src/next/services/MockHttpAuthService.test.ts @@ -19,7 +19,7 @@ import { MockHttpAuthService } from './MockHttpAuthService'; import { mockCredentials } from './mockCredentials'; describe('MockHttpAuthService', () => { - const httpAuth = new MockHttpAuthService('test'); + const httpAuth = new MockHttpAuthService('test', mockCredentials.none()); function makeAuthReq(header?: string) { return { headers: { authorization: header } } as Request; @@ -93,6 +93,54 @@ describe('MockHttpAuthService', () => { ).rejects.toThrow("This endpoint does not allow 'service' credentials"); }); + it('should default to different credential types', async () => { + const noneAuth = new MockHttpAuthService('test', mockCredentials.none()); + const userAuth = new MockHttpAuthService('test', mockCredentials.user()); + const serviceAuth = new MockHttpAuthService( + 'test', + mockCredentials.service(), + ); + + await expect(noneAuth.credentials(makeAuthReq())).resolves.toEqual( + mockCredentials.none(), + ); + await expect( + noneAuth.credentials(makeAuthReq(mockCredentials.none.header())), + ).resolves.toEqual(mockCredentials.none()); + await expect( + noneAuth.credentials(makeAuthReq(mockCredentials.user.header())), + ).resolves.toEqual(mockCredentials.user()); + await expect( + noneAuth.credentials(makeAuthReq(mockCredentials.service.header())), + ).resolves.toEqual(mockCredentials.service()); + + await expect(userAuth.credentials(makeAuthReq())).resolves.toEqual( + mockCredentials.user(), + ); + await expect( + userAuth.credentials(makeAuthReq(mockCredentials.none.header())), + ).resolves.toEqual(mockCredentials.none()); + await expect( + userAuth.credentials(makeAuthReq(mockCredentials.user.header())), + ).resolves.toEqual(mockCredentials.user()); + await expect( + userAuth.credentials(makeAuthReq(mockCredentials.service.header())), + ).resolves.toEqual(mockCredentials.service()); + + await expect(serviceAuth.credentials(makeAuthReq())).resolves.toEqual( + mockCredentials.service(), + ); + await expect( + serviceAuth.credentials(makeAuthReq(mockCredentials.none.header())), + ).resolves.toEqual(mockCredentials.none()); + await expect( + serviceAuth.credentials(makeAuthReq(mockCredentials.user.header())), + ).resolves.toEqual(mockCredentials.user()); + await expect( + serviceAuth.credentials(makeAuthReq(mockCredentials.service.header())), + ).resolves.toEqual(mockCredentials.service()); + }); + it('should reject invalid credentials', async () => { await expect( httpAuth.credentials(makeAuthReq('Bearer bad')), diff --git a/packages/backend-test-utils/src/next/services/MockHttpAuthService.ts b/packages/backend-test-utils/src/next/services/MockHttpAuthService.ts index 02eb305915..79a940789a 100644 --- a/packages/backend-test-utils/src/next/services/MockHttpAuthService.ts +++ b/packages/backend-test-utils/src/next/services/MockHttpAuthService.ts @@ -21,26 +21,34 @@ import { HttpAuthService, } from '@backstage/backend-plugin-api'; import { Request, Response } from 'express'; -import { mockCredentials } from './mockCredentials'; import { MockAuthService } from './MockAuthService'; import { NotAllowedError, NotImplementedError } from '@backstage/errors'; +import { mockCredentials } from './mockCredentials'; // TODO: support mock cookie auth? export class MockHttpAuthService implements HttpAuthService { #auth: AuthService; + #defaultCredentials: BackstageCredentials; - constructor(pluginId: string) { + constructor(pluginId: string, defaultCredentials: BackstageCredentials) { this.#auth = new MockAuthService(pluginId); + this.#defaultCredentials = defaultCredentials; } async #getCredentials(req: Request) { const header = req.headers.authorization; + + if (header === mockCredentials.none.header()) { + return mockCredentials.none(); + } + const token = typeof header === 'string' ? header.match(/^Bearer[ ]+(\S+)$/i)?.[1] : undefined; + if (!token) { - return mockCredentials.none(); + return this.#defaultCredentials; } return await this.#auth.authenticate(token); diff --git a/packages/backend-test-utils/src/next/services/mockCredentials.test.ts b/packages/backend-test-utils/src/next/services/mockCredentials.test.ts index 3d7540a0ee..7b0ff1e9be 100644 --- a/packages/backend-test-utils/src/next/services/mockCredentials.test.ts +++ b/packages/backend-test-utils/src/next/services/mockCredentials.test.ts @@ -48,6 +48,10 @@ describe('mockCredentials', () => { }); }); + it('creates unauthenticated tokens and headers', () => { + expect(mockCredentials.none.header()).toBe('Bearer mock-none-token'); + }); + it('creates user tokens and headers', () => { expect(mockCredentials.user.token()).toBe('mock-user-token'); expect(mockCredentials.user.token('user:default/other')).toBe( diff --git a/packages/backend-test-utils/src/next/services/mockCredentials.ts b/packages/backend-test-utils/src/next/services/mockCredentials.ts index cf516c403a..3b440b3a31 100644 --- a/packages/backend-test-utils/src/next/services/mockCredentials.ts +++ b/packages/backend-test-utils/src/next/services/mockCredentials.ts @@ -24,6 +24,7 @@ import { export const DEFAULT_MOCK_USER_ENTITY_REF = 'user:default/mock'; export const DEFAULT_MOCK_SERVICE_SUBJECT = 'external:test-service'; +export const MOCK_NONE_TOKEN = 'mock-none-token'; export const MOCK_USER_TOKEN = 'mock-user-token'; export const MOCK_USER_TOKEN_PREFIX = 'mock-user-token:'; export const MOCK_INVALID_USER_TOKEN = 'mock-invalid-user-token'; @@ -53,6 +54,25 @@ export namespace mockCredentials { }; } + /** + * Utilities related to none credentials. + */ + export namespace none { + /** + * Returns an authorization header that translates to unauthenticated + * credentials. + * + * This is useful when one wants to explicitly test unauthenticated requests + * while still using the default behavior of the mock HttpAuthService where + * it defaults to user credentials. + */ + export function header(): string { + // NOTE: there is no .token() version of this because only the + // HttpAuthService should know about and consume this token + return `Bearer ${MOCK_NONE_TOKEN}`; + } + } + /** * Creates a mocked credentials object for a user principal. * diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 0d88c6b377..e87319c669 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -26,6 +26,7 @@ import { AuthService, DiscoveryService, HttpAuthService, + BackstageCredentials, } from '@backstage/backend-plugin-api'; import { cacheServiceFactory, @@ -47,6 +48,7 @@ import { MockIdentityService } from './MockIdentityService'; import { MockRootLoggerService } from './MockRootLoggerService'; import { MockAuthService } from './MockAuthService'; import { MockHttpAuthService } from './MockHttpAuthService'; +import { mockCredentials } from './mockCredentials'; /** @internal */ function simpleFactory< @@ -203,15 +205,48 @@ export namespace mockServices { })); } - export function httpAuth(options?: { pluginId?: string }): HttpAuthService { - return new MockHttpAuthService(options?.pluginId ?? 'test'); + /** + * Creates a mock implementation of the `HttpAuthService`. + * + * By default all requests without credentials are treated as requests from + * the default mock user principal. This behavior can be configured with the + * `defaultCredentials` option. + */ + export function httpAuth(options?: { + pluginId?: string; + /** + * The default credentials to use if there are no credentials present in the + * incoming request. + * + * By default all requests without credentials are treated as authenticated + * as the default mock user as returned from `mockCredentials.user()`. + */ + defaultCredentials?: BackstageCredentials; + }): HttpAuthService { + return new MockHttpAuthService( + options?.pluginId ?? 'test', + options?.defaultCredentials ?? mockCredentials.user(), + ); } export namespace httpAuth { - export const factory = createServiceFactory({ - service: coreServices.httpAuth, - deps: { plugin: coreServices.pluginMetadata }, - factory: ({ plugin }) => new MockHttpAuthService(plugin.getId()), - }); + /** + * Creates a mock service factory for the `HttpAuthService`. + * + * By default all requests without credentials are treated as requests from + * the default mock user principal. This behavior can be configured with the + * `defaultCredentials` option. + */ + export const factory = createServiceFactory( + (options?: { defaultCredentials?: BackstageCredentials }) => ({ + service: coreServices.httpAuth, + deps: { plugin: coreServices.pluginMetadata }, + factory: ({ plugin }) => + new MockHttpAuthService( + plugin.getId(), + options?.defaultCredentials ?? mockCredentials.user(), + ), + }), + ); export const mock = simpleMock(coreServices.httpAuth, () => ({ credentials: jest.fn(), issueUserCookie: jest.fn(),