backend-test-utils: add service mock helpers

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-08-17 14:40:29 +02:00
parent b027fe4cb3
commit 9fb3b5373c
5 changed files with 348 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-test-utils': patch
---
Extended `mockService` to also include mocked variants, for example `mockServices.lifecycle.mock()`. The returned mocked implementation will have a `factory` property which is a service factory for itself. You can also pass a partial implementation of the service to the mock function to use a mock implementation of specific methods.
+253
View File
@@ -3,24 +3,52 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="jest" />
import { AuthorizePermissionRequest } from '@backstage/plugin-permission-common';
import { Backend } from '@backstage/backend-app-api';
import { BackendFeature } from '@backstage/backend-plugin-api';
import { BackstageIdentityResponse } from '@backstage/plugin-auth-node';
import { CacheService } from '@backstage/backend-plugin-api';
import { CacheServiceOptions } from '@backstage/backend-plugin-api';
import { CacheServiceSetOptions } from '@backstage/backend-plugin-api';
import { DatabaseService } from '@backstage/backend-plugin-api';
import { DefinitivePolicyDecision } from '@backstage/plugin-permission-common';
import { EvaluatorRequestOptions } from '@backstage/plugin-permission-common';
import { ExtendedHttpServer } from '@backstage/backend-app-api';
import { ExtensionPoint } from '@backstage/backend-plugin-api';
import { Handler } from 'express';
import { HttpRouterFactoryOptions } from '@backstage/backend-app-api';
import { HttpRouterService } from '@backstage/backend-plugin-api';
import { IdentityApiGetIdentityRequest } from '@backstage/plugin-auth-node';
import { IdentityService } from '@backstage/backend-plugin-api';
import { JsonObject } from '@backstage/types';
import { JsonValue } from '@backstage/types';
import { Knex } from 'knex';
import { LifecycleService } from '@backstage/backend-plugin-api';
import { LifecycleServiceShutdownHook } from '@backstage/backend-plugin-api';
import { LifecycleServiceShutdownOptions } from '@backstage/backend-plugin-api';
import { LifecycleServiceStartupHook } from '@backstage/backend-plugin-api';
import { LifecycleServiceStartupOptions } from '@backstage/backend-plugin-api';
import { LoggerService } from '@backstage/backend-plugin-api';
import { PermissionsService } from '@backstage/backend-plugin-api';
import { PolicyDecision } from '@backstage/plugin-permission-common';
import { QueryPermissionRequest } from '@backstage/plugin-permission-common';
import { ReadTreeOptions } from '@backstage/backend-plugin-api';
import { ReadTreeResponse } from '@backstage/backend-plugin-api';
import { ReadUrlOptions } from '@backstage/backend-plugin-api';
import { ReadUrlResponse } from '@backstage/backend-plugin-api';
import { RootConfigService } from '@backstage/backend-plugin-api';
import { RootLifecycleService } from '@backstage/backend-plugin-api';
import { RootLoggerService } from '@backstage/backend-plugin-api';
import { SchedulerService } from '@backstage/backend-plugin-api';
import { SearchOptions } from '@backstage/backend-plugin-api';
import { SearchResponse } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
import { TaskDescriptor } from '@backstage/backend-tasks';
import { TaskInvocationDefinition } from '@backstage/backend-tasks';
import { TaskRunner } from '@backstage/backend-tasks';
import { TaskScheduleDefinition } from '@backstage/backend-tasks';
import { TokenManagerService } from '@backstage/backend-plugin-api';
import { UrlReaderService } from '@backstage/backend-plugin-api';
@@ -33,11 +61,47 @@ export namespace mockServices {
export namespace cache {
const // (undocumented)
factory: () => ServiceFactory<CacheService, 'plugin'>;
const // (undocumented)
mock: (partialImpl?: Partial<CacheService> | undefined) => {
factory: ServiceFactory<CacheService, 'root' | 'plugin'>;
} & {
get: jest.MockInstance<
Promise<JsonValue | undefined>,
[key: string],
unknown
>;
set: jest.MockInstance<
Promise<void>,
[
key: string,
value: JsonValue,
options?: CacheServiceSetOptions | undefined,
],
unknown
>;
delete: jest.MockInstance<Promise<void>, [key: string], unknown>;
withOptions: jest.MockInstance<
CacheService,
[options: CacheServiceOptions],
unknown
>;
} & CacheService;
}
// (undocumented)
export namespace database {
const // (undocumented)
factory: () => ServiceFactory<DatabaseService, 'plugin'>;
const // (undocumented)
mock: (partialImpl?: Partial<DatabaseService> | undefined) => {
factory: ServiceFactory<DatabaseService, 'root' | 'plugin'>;
} & {
getClient: jest.MockInstance<Promise<Knex<any, any[]>>, [], unknown>;
migrations?:
| {
skip?: boolean | undefined;
}
| undefined;
} & DatabaseService;
}
// (undocumented)
export namespace httpRouter {
@@ -45,6 +109,12 @@ export namespace mockServices {
factory: (
options?: HttpRouterFactoryOptions | undefined,
) => ServiceFactory<HttpRouterService, 'plugin'>;
const // (undocumented)
mock: (partialImpl?: Partial<HttpRouterService> | undefined) => {
factory: ServiceFactory<HttpRouterService, 'root' | 'plugin'>;
} & {
use: jest.MockInstance<void, [handler: Handler], unknown>;
} & HttpRouterService;
}
// (undocumented)
export function identity(): IdentityService;
@@ -52,21 +122,99 @@ export namespace mockServices {
export namespace identity {
const // (undocumented)
factory: () => ServiceFactory<IdentityService, 'plugin'>;
const // (undocumented)
mock: (partialImpl?: Partial<IdentityService> | undefined) => {
factory: ServiceFactory<IdentityService, 'root' | 'plugin'>;
} & {
getIdentity: jest.MockInstance<
Promise<BackstageIdentityResponse | undefined>,
[options: IdentityApiGetIdentityRequest],
unknown
>;
} & IdentityService;
}
// (undocumented)
export namespace lifecycle {
const // (undocumented)
factory: () => ServiceFactory<LifecycleService, 'plugin'>;
const // (undocumented)
mock: (partialImpl?: Partial<LifecycleService> | undefined) => {
factory: ServiceFactory<LifecycleService, 'root' | 'plugin'>;
} & {
addStartupHook: jest.MockInstance<
void,
[
hook: LifecycleServiceStartupHook,
options?: LifecycleServiceStartupOptions | undefined,
],
unknown
>;
addShutdownHook: jest.MockInstance<
void,
[
hook: LifecycleServiceShutdownHook,
options?: LifecycleServiceShutdownOptions | undefined,
],
unknown
>;
} & LifecycleService;
}
// (undocumented)
export namespace logger {
const // (undocumented)
factory: () => ServiceFactory<LoggerService, 'plugin'>;
const // (undocumented)
mock: (partialImpl?: Partial<LoggerService> | undefined) => {
factory: ServiceFactory<LoggerService, 'root' | 'plugin'>;
} & {
error: jest.MockInstance<
void,
[message: string, meta?: Error | JsonObject | undefined],
unknown
>;
warn: jest.MockInstance<
void,
[message: string, meta?: Error | JsonObject | undefined],
unknown
>;
info: jest.MockInstance<
void,
[message: string, meta?: Error | JsonObject | undefined],
unknown
>;
debug: jest.MockInstance<
void,
[message: string, meta?: Error | JsonObject | undefined],
unknown
>;
child: jest.MockInstance<LoggerService, [meta: JsonObject], unknown>;
} & LoggerService;
}
// (undocumented)
export namespace permissions {
const // (undocumented)
factory: () => ServiceFactory<PermissionsService, 'plugin'>;
const // (undocumented)
mock: (partialImpl?: Partial<PermissionsService> | undefined) => {
factory: ServiceFactory<PermissionsService, 'root' | 'plugin'>;
} & {
authorize: jest.MockInstance<
Promise<DefinitivePolicyDecision[]>,
[
requests: AuthorizePermissionRequest[],
options?: EvaluatorRequestOptions | undefined,
],
unknown
>;
authorizeConditional: jest.MockInstance<
Promise<PolicyDecision[]>,
[
requests: QueryPermissionRequest[],
options?: EvaluatorRequestOptions | undefined,
],
unknown
>;
} & PermissionsService;
}
// (undocumented)
export function rootConfig(options?: rootConfig.Options): RootConfigService;
@@ -85,6 +233,27 @@ export namespace mockServices {
export namespace rootLifecycle {
const // (undocumented)
factory: () => ServiceFactory<RootLifecycleService, 'root'>;
const // (undocumented)
mock: (partialImpl?: Partial<RootLifecycleService> | undefined) => {
factory: ServiceFactory<RootLifecycleService, 'root' | 'plugin'>;
} & {
addStartupHook: jest.MockInstance<
void,
[
hook: LifecycleServiceStartupHook,
options?: LifecycleServiceStartupOptions | undefined,
],
unknown
>;
addShutdownHook: jest.MockInstance<
void,
[
hook: LifecycleServiceShutdownHook,
options?: LifecycleServiceShutdownOptions | undefined,
],
unknown
>;
} & RootLifecycleService;
}
// (undocumented)
export function rootLogger(options?: rootLogger.Options): LoggerService;
@@ -98,11 +267,58 @@ export namespace mockServices {
factory: (
options?: Options | undefined,
) => ServiceFactory<LoggerService, 'root'>;
const // (undocumented)
mock: (partialImpl?: Partial<RootLoggerService> | undefined) => {
factory: ServiceFactory<RootLoggerService, 'root' | 'plugin'>;
} & {
error: jest.MockInstance<
void,
[message: string, meta?: Error | JsonObject | undefined],
unknown
>;
warn: jest.MockInstance<
void,
[message: string, meta?: Error | JsonObject | undefined],
unknown
>;
info: jest.MockInstance<
void,
[message: string, meta?: Error | JsonObject | undefined],
unknown
>;
debug: jest.MockInstance<
void,
[message: string, meta?: Error | JsonObject | undefined],
unknown
>;
child: jest.MockInstance<LoggerService, [meta: JsonObject], unknown>;
} & RootLoggerService;
}
// (undocumented)
export namespace scheduler {
const // (undocumented)
factory: () => ServiceFactory<SchedulerService, 'plugin'>;
const // (undocumented)
mock: (partialImpl?: Partial<SchedulerService> | undefined) => {
factory: ServiceFactory<SchedulerService, 'root' | 'plugin'>;
} & {
triggerTask: jest.MockInstance<Promise<void>, [id: string], unknown>;
scheduleTask: jest.MockInstance<
Promise<void>,
[task: TaskScheduleDefinition & TaskInvocationDefinition],
unknown
>;
createScheduledTaskRunner: jest.MockInstance<
TaskRunner,
[schedule: TaskScheduleDefinition],
unknown
>;
getScheduledTasks: jest.MockInstance<
Promise<TaskDescriptor[]>,
[],
unknown
>;
} & SchedulerService;
}
// (undocumented)
export function tokenManager(): TokenManagerService;
@@ -110,11 +326,48 @@ export namespace mockServices {
export namespace tokenManager {
const // (undocumented)
factory: () => ServiceFactory<TokenManagerService, 'plugin'>;
const // (undocumented)
mock: (partialImpl?: Partial<TokenManagerService> | undefined) => {
factory: ServiceFactory<TokenManagerService, 'root' | 'plugin'>;
} & {
getToken: jest.MockInstance<
Promise<{
token: string;
}>,
[],
unknown
>;
authenticate: jest.MockInstance<
Promise<void>,
[token: string],
unknown
>;
} & TokenManagerService;
}
// (undocumented)
export namespace urlReader {
const // (undocumented)
factory: () => ServiceFactory<UrlReaderService, 'plugin'>;
const // (undocumented)
mock: (partialImpl?: Partial<UrlReaderService> | undefined) => {
factory: ServiceFactory<UrlReaderService, 'root' | 'plugin'>;
} & {
readUrl: jest.MockInstance<
Promise<ReadUrlResponse>,
[url: string, options?: ReadUrlOptions | undefined],
unknown
>;
readTree: jest.MockInstance<
Promise<ReadTreeResponse>,
[url: string, options?: ReadTreeOptions | undefined],
unknown
>;
search: jest.MockInstance<
Promise<SearchResponse>,
[url: string, options?: SearchOptions | undefined],
unknown
>;
} & UrlReaderService;
}
}
+3
View File
@@ -57,6 +57,9 @@
"testcontainers": "^8.1.2",
"uuid": "^8.0.0"
},
"peerDependencies": {
"@types/jest": "*"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@types/supertest": "^2.0.8",
@@ -40,6 +40,7 @@ import { JsonObject } from '@backstage/types';
import { MockIdentityService } from './MockIdentityService';
import { MockRootLoggerService } from './MockRootLoggerService';
/** @internal */
function simpleFactory<
TService,
TScope extends 'root' | 'plugin',
@@ -57,6 +58,34 @@ function simpleFactory<
})) as (...options: TOptions) => ServiceFactory<TService, any>;
}
/** @internal */
function simpleMock<TService>(
ref: ServiceRef<TService, any>,
mockFactory: () => jest.Mocked<TService>,
): (
partialImpl?: Partial<TService>,
) => { factory: ServiceFactory<TService> } & jest.Mocked<TService> {
return partialImpl => {
const mock = mockFactory();
if (partialImpl) {
for (const [key, impl] of Object.entries(partialImpl)) {
if (typeof impl === 'function') {
(mock as any)[key].mockImplementation(impl);
} else {
(mock as any)[key] = impl;
}
}
}
return Object.assign(mock, {
factory: createServiceFactory({
service: ref,
deps: {},
factory: () => mock,
})(),
});
};
}
/**
* @public
*/
@@ -79,6 +108,13 @@ export namespace mockServices {
};
export const factory = simpleFactory(coreServices.rootLogger, rootLogger);
export const mock = simpleMock(coreServices.rootLogger, () => ({
child: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
}));
}
export function tokenManager(): TokenManagerService {
@@ -98,6 +134,10 @@ export namespace mockServices {
coreServices.tokenManager,
tokenManager,
);
export const mock = simpleMock(coreServices.tokenManager, () => ({
authenticate: jest.fn(),
getToken: jest.fn(),
}));
}
export function identity(): IdentityService {
@@ -105,6 +145,9 @@ export namespace mockServices {
}
export namespace identity {
export const factory = simpleFactory(coreServices.identity, identity);
export const mock = simpleMock(coreServices.identity, () => ({
getIdentity: jest.fn(),
}));
}
// TODO(Rugvip): Not all core services have implementations available here yet.
@@ -112,29 +155,71 @@ export namespace mockServices {
// re-implement functioning mock versions here.
export namespace cache {
export const factory = cacheServiceFactory;
export const mock = simpleMock(coreServices.cache, () => ({
delete: jest.fn(),
get: jest.fn(),
set: jest.fn(),
withOptions: jest.fn(),
}));
}
export namespace database {
export const factory = databaseServiceFactory;
export const mock = simpleMock(coreServices.database, () => ({
getClient: jest.fn(),
}));
}
export namespace httpRouter {
export const factory = httpRouterServiceFactory;
export const mock = simpleMock(coreServices.httpRouter, () => ({
use: jest.fn(),
}));
}
export namespace lifecycle {
export const factory = lifecycleServiceFactory;
export const mock = simpleMock(coreServices.lifecycle, () => ({
addShutdownHook: jest.fn(),
addStartupHook: jest.fn(),
}));
}
export namespace logger {
export const factory = loggerServiceFactory;
export const mock = simpleMock(coreServices.logger, () => ({
child: jest.fn(),
debug: jest.fn(),
error: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
}));
}
export namespace permissions {
export const factory = permissionsServiceFactory;
export const mock = simpleMock(coreServices.permissions, () => ({
authorize: jest.fn(),
authorizeConditional: jest.fn(),
}));
}
export namespace rootLifecycle {
export const factory = rootLifecycleServiceFactory;
export const mock = simpleMock(coreServices.rootLifecycle, () => ({
addShutdownHook: jest.fn(),
addStartupHook: jest.fn(),
}));
}
export namespace scheduler {
export const factory = schedulerServiceFactory;
export const mock = simpleMock(coreServices.scheduler, () => ({
createScheduledTaskRunner: jest.fn(),
getScheduledTasks: jest.fn(),
scheduleTask: jest.fn(),
triggerTask: jest.fn(),
}));
}
export namespace urlReader {
export const factory = urlReaderServiceFactory;
export const mock = simpleMock(coreServices.urlReader, () => ({
readTree: jest.fn(),
readUrl: jest.fn(),
search: jest.fn(),
}));
}
}
+2
View File
@@ -3514,6 +3514,8 @@ __metadata:
supertest: ^6.1.3
testcontainers: ^8.1.2
uuid: ^8.0.0
peerDependencies:
"@types/jest": "*"
languageName: unknown
linkType: soft