backend-test-utils: add default credentials for mock http auth + explicit none credentials header

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2024-02-17 12:57:39 +01:00
parent 663fce7457
commit caf1af6541
5 changed files with 126 additions and 11 deletions
@@ -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')),
@@ -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);
@@ -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(
@@ -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.
*
@@ -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(),