From 9fb3b5373c453a257817c71cce5df68e1f064ab9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 17 Aug 2023 14:40:29 +0200 Subject: [PATCH] backend-test-utils: add service mock helpers Signed-off-by: Patrik Oldsberg --- .changeset/old-geese-drive.md | 5 + packages/backend-test-utils/api-report.md | 253 ++++++++++++++++++ packages/backend-test-utils/package.json | 3 + .../src/next/services/mockServices.ts | 85 ++++++ yarn.lock | 2 + 5 files changed, 348 insertions(+) create mode 100644 .changeset/old-geese-drive.md diff --git a/.changeset/old-geese-drive.md b/.changeset/old-geese-drive.md new file mode 100644 index 0000000000..ddaa7ad30a --- /dev/null +++ b/.changeset/old-geese-drive.md @@ -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. diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index d086b1d2ae..f87181f635 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -3,24 +3,52 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + +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; + const // (undocumented) + mock: (partialImpl?: Partial | undefined) => { + factory: ServiceFactory; + } & { + get: jest.MockInstance< + Promise, + [key: string], + unknown + >; + set: jest.MockInstance< + Promise, + [ + key: string, + value: JsonValue, + options?: CacheServiceSetOptions | undefined, + ], + unknown + >; + delete: jest.MockInstance, [key: string], unknown>; + withOptions: jest.MockInstance< + CacheService, + [options: CacheServiceOptions], + unknown + >; + } & CacheService; } // (undocumented) export namespace database { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: (partialImpl?: Partial | undefined) => { + factory: ServiceFactory; + } & { + getClient: jest.MockInstance>, [], unknown>; + migrations?: + | { + skip?: boolean | undefined; + } + | undefined; + } & DatabaseService; } // (undocumented) export namespace httpRouter { @@ -45,6 +109,12 @@ export namespace mockServices { factory: ( options?: HttpRouterFactoryOptions | undefined, ) => ServiceFactory; + const // (undocumented) + mock: (partialImpl?: Partial | undefined) => { + factory: ServiceFactory; + } & { + use: jest.MockInstance; + } & HttpRouterService; } // (undocumented) export function identity(): IdentityService; @@ -52,21 +122,99 @@ export namespace mockServices { export namespace identity { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: (partialImpl?: Partial | undefined) => { + factory: ServiceFactory; + } & { + getIdentity: jest.MockInstance< + Promise, + [options: IdentityApiGetIdentityRequest], + unknown + >; + } & IdentityService; } // (undocumented) export namespace lifecycle { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: (partialImpl?: Partial | undefined) => { + factory: ServiceFactory; + } & { + 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; + const // (undocumented) + mock: (partialImpl?: Partial | undefined) => { + factory: ServiceFactory; + } & { + 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; } // (undocumented) export namespace permissions { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: (partialImpl?: Partial | undefined) => { + factory: ServiceFactory; + } & { + authorize: jest.MockInstance< + Promise, + [ + requests: AuthorizePermissionRequest[], + options?: EvaluatorRequestOptions | undefined, + ], + unknown + >; + authorizeConditional: jest.MockInstance< + Promise, + [ + 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; + const // (undocumented) + mock: (partialImpl?: Partial | undefined) => { + factory: ServiceFactory; + } & { + 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; + const // (undocumented) + mock: (partialImpl?: Partial | undefined) => { + factory: ServiceFactory; + } & { + 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; + } & RootLoggerService; } // (undocumented) export namespace scheduler { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: (partialImpl?: Partial | undefined) => { + factory: ServiceFactory; + } & { + triggerTask: jest.MockInstance, [id: string], unknown>; + scheduleTask: jest.MockInstance< + Promise, + [task: TaskScheduleDefinition & TaskInvocationDefinition], + unknown + >; + createScheduledTaskRunner: jest.MockInstance< + TaskRunner, + [schedule: TaskScheduleDefinition], + unknown + >; + getScheduledTasks: jest.MockInstance< + Promise, + [], + unknown + >; + } & SchedulerService; } // (undocumented) export function tokenManager(): TokenManagerService; @@ -110,11 +326,48 @@ export namespace mockServices { export namespace tokenManager { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: (partialImpl?: Partial | undefined) => { + factory: ServiceFactory; + } & { + getToken: jest.MockInstance< + Promise<{ + token: string; + }>, + [], + unknown + >; + authenticate: jest.MockInstance< + Promise, + [token: string], + unknown + >; + } & TokenManagerService; } // (undocumented) export namespace urlReader { const // (undocumented) factory: () => ServiceFactory; + const // (undocumented) + mock: (partialImpl?: Partial | undefined) => { + factory: ServiceFactory; + } & { + readUrl: jest.MockInstance< + Promise, + [url: string, options?: ReadUrlOptions | undefined], + unknown + >; + readTree: jest.MockInstance< + Promise, + [url: string, options?: ReadTreeOptions | undefined], + unknown + >; + search: jest.MockInstance< + Promise, + [url: string, options?: SearchOptions | undefined], + unknown + >; + } & UrlReaderService; } } diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 02cb1d5cf9..21946be36c 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -57,6 +57,9 @@ "testcontainers": "^8.1.2", "uuid": "^8.0.0" }, + "peerDependencies": { + "@types/jest": "*" + }, "devDependencies": { "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 0455b9f1ae..d0bb79dd2e 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -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; } +/** @internal */ +function simpleMock( + ref: ServiceRef, + mockFactory: () => jest.Mocked, +): ( + partialImpl?: Partial, +) => { factory: ServiceFactory } & jest.Mocked { + 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(), + })); } } diff --git a/yarn.lock b/yarn.lock index ec0379375a..fd3d589b8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3514,6 +3514,8 @@ __metadata: supertest: ^6.1.3 testcontainers: ^8.1.2 uuid: ^8.0.0 + peerDependencies: + "@types/jest": "*" languageName: unknown linkType: soft