From bbff821e545a3fedaa56a3f8027bd60011fb5f80 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 29 Aug 2023 18:38:48 +0200 Subject: [PATCH] backend-test-utils: refactor into ServiceFactoryTester Signed-off-by: Patrik Oldsberg --- packages/backend-test-utils/api-report.md | 36 +++++---- ...r.test.ts => ServiceFactoryTester.test.ts} | 72 +++++++++--------- .../src/next/wiring/ServiceFactoryTester.ts | 73 +++++++++++++++++++ .../src/next/wiring/index.ts | 2 +- .../next/wiring/makeServiceFactoryTester.ts | 49 ------------- 5 files changed, 133 insertions(+), 99 deletions(-) rename packages/backend-test-utils/src/next/wiring/{makeServiceFactoryTester.test.ts => ServiceFactoryTester.test.ts} (57%) create mode 100644 packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts delete mode 100644 packages/backend-test-utils/src/next/wiring/makeServiceFactoryTester.ts diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 41928af40b..147481f8d8 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -26,25 +26,13 @@ import { RootLifecycleService } from '@backstage/backend-plugin-api'; import { RootLoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; +import { ServiceRef } from '@backstage/backend-plugin-api'; import { TokenManagerService } from '@backstage/backend-plugin-api'; import { UrlReaderService } from '@backstage/backend-plugin-api'; // @public (undocumented) export function isDockerDisabledForTests(): boolean; -// @public (undocumented) -export function makeServiceFactoryTester< - TService, - TScope extends 'root' | 'plugin', ->( - subject: - | ServiceFactory - | (() => ServiceFactory), - dependencies?: Array ServiceFactory)>, -): 'root' extends TScope - ? () => Promise - : (pluginId: string) => Promise; - // @public (undocumented) export namespace mockServices { // (undocumented) @@ -195,6 +183,28 @@ export namespace mockServices { } } +// @public (undocumented) +export class ServiceFactoryTester { + // (undocumented) + static from( + subject: + | ServiceFactory + | (() => ServiceFactory), + options?: { + dependencies?: Array ServiceFactory)>; + }, + ): ServiceFactoryTester; + // (undocumented) + get( + ...args: 'root' extends TScope ? [] : [pluginId?: string] + ): Promise; + // (undocumented) + getService( + service: ServiceRef, + ...args: 'root' extends TGetScope ? [] : [pluginId?: string] + ): Promise; +} + // @public (undocumented) export type ServiceMock = { factory: ServiceFactory; diff --git a/packages/backend-test-utils/src/next/wiring/makeServiceFactoryTester.test.ts b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.test.ts similarity index 57% rename from packages/backend-test-utils/src/next/wiring/makeServiceFactoryTester.test.ts rename to packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.test.ts index b30700cdec..073b454ad0 100644 --- a/packages/backend-test-utils/src/next/wiring/makeServiceFactoryTester.test.ts +++ b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.test.ts @@ -19,7 +19,7 @@ import { createServiceFactory, createServiceRef, } from '@backstage/backend-plugin-api'; -import { makeServiceFactoryTester } from './makeServiceFactoryTester'; +import { ServiceFactoryTester } from './ServiceFactoryTester'; const rootServiceRef = createServiceRef({ id: 'a', scope: 'root' }); const pluginServiceRef = createServiceRef({ id: 'b', scope: 'plugin' }); @@ -52,85 +52,85 @@ const sharedPluginFactory = createServiceFactory({ }, }); -describe('makeServiceFactoryTester', () => { +describe('ServiceFactoryTester', () => { it('should test a root service factory', async () => { - const factoryTester = makeServiceFactoryTester(rootFactory()); + const tester = ServiceFactoryTester.from(rootFactory()); - await expect(factoryTester()).resolves.toBe('root'); + await expect(tester.get()).resolves.toBe('root'); }); it('should test a plugin service factory', async () => { - const factoryTester = makeServiceFactoryTester(pluginFactory()); + const tester = ServiceFactoryTester.from(pluginFactory()); - await expect(factoryTester('x')).resolves.toBe('x-plugin'); - await expect(factoryTester('y')).resolves.toBe('y-plugin'); - await expect(factoryTester('z')).resolves.toBe('z-plugin'); + await expect(tester.get('x')).resolves.toBe('x-plugin'); + await expect(tester.get('y')).resolves.toBe('y-plugin'); + await expect(tester.get('z')).resolves.toBe('z-plugin'); }); it('should test a plugin service factory with root context', async () => { - const factoryTester = makeServiceFactoryTester(sharedPluginFactory()); + const tester = ServiceFactoryTester.from(sharedPluginFactory()); - await expect(factoryTester('x')).resolves.toBe('x-1-plugin'); - await expect(factoryTester('y')).resolves.toBe('y-2-plugin'); - await expect(factoryTester('y')).resolves.toBe('y-2-plugin'); - await expect(factoryTester('y')).resolves.toBe('y-2-plugin'); - await expect(factoryTester('z')).resolves.toBe('z-3-plugin'); + await expect(tester.get('x')).resolves.toBe('x-1-plugin'); + await expect(tester.get('y')).resolves.toBe('y-2-plugin'); + await expect(tester.get('y')).resolves.toBe('y-2-plugin'); + await expect(tester.get('y')).resolves.toBe('y-2-plugin'); + await expect(tester.get('z')).resolves.toBe('z-3-plugin'); - const tester2 = makeServiceFactoryTester(sharedPluginFactory()); + const tester2 = ServiceFactoryTester.from(sharedPluginFactory()); - await expect(tester2('z')).resolves.toBe('z-1-plugin'); - await expect(tester2('y')).resolves.toBe('y-2-plugin'); - await expect(tester2('x')).resolves.toBe('x-3-plugin'); - await expect(tester2('x')).resolves.toBe('x-3-plugin'); - await expect(tester2('y')).resolves.toBe('y-2-plugin'); - await expect(tester2('z')).resolves.toBe('z-1-plugin'); + await expect(tester2.get('z')).resolves.toBe('z-1-plugin'); + await expect(tester2.get('y')).resolves.toBe('y-2-plugin'); + await expect(tester2.get('x')).resolves.toBe('x-3-plugin'); + await expect(tester2.get('x')).resolves.toBe('x-3-plugin'); + await expect(tester2.get('y')).resolves.toBe('y-2-plugin'); + await expect(tester2.get('z')).resolves.toBe('z-1-plugin'); }); it('should use dependencies', async () => { - const factoryTester = makeServiceFactoryTester( + const tester = ServiceFactoryTester.from( createServiceFactory({ service: createServiceRef({ id: 'concat' }), deps: { root: rootServiceRef, plugin: pluginServiceRef }, factory: async ({ root, plugin }) => `${root}, ${plugin}`, }), - [rootFactory, pluginFactory()], + { dependencies: [rootFactory, pluginFactory()] }, ); - await expect(factoryTester('x')).resolves.toBe('root, x-plugin'); + await expect(tester.get('x')).resolves.toBe('root, x-plugin'); }); it('should use dependencies with root context', async () => { - const factoryTester = makeServiceFactoryTester( + const tester = ServiceFactoryTester.from( createServiceFactory({ service: createServiceRef({ id: 'concat' }), deps: { shared: sharedPluginServiceRef, plugin: pluginServiceRef }, factory: async ({ shared, plugin }) => `${shared}, ${plugin}`, }), - [sharedPluginFactory(), pluginFactory], + { dependencies: [sharedPluginFactory(), pluginFactory] }, ); - await expect(factoryTester('x')).resolves.toBe('x-1-plugin, x-plugin'); - await expect(factoryTester('y')).resolves.toBe('y-2-plugin, y-plugin'); - await expect(factoryTester('y')).resolves.toBe('y-2-plugin, y-plugin'); - await expect(factoryTester('y')).resolves.toBe('y-2-plugin, y-plugin'); - await expect(factoryTester('z')).resolves.toBe('z-3-plugin, z-plugin'); + await expect(tester.get('x')).resolves.toBe('x-1-plugin, x-plugin'); + await expect(tester.get('y')).resolves.toBe('y-2-plugin, y-plugin'); + await expect(tester.get('y')).resolves.toBe('y-2-plugin, y-plugin'); + await expect(tester.get('y')).resolves.toBe('y-2-plugin, y-plugin'); + await expect(tester.get('z')).resolves.toBe('z-3-plugin, z-plugin'); }); it('should prioritize the subject implementation', async () => { - const factoryTester = makeServiceFactoryTester( + const tester = ServiceFactoryTester.from( createServiceFactory({ service: rootServiceRef, deps: {}, factory: async () => 'other-root', }), - [rootFactory], + { dependencies: [rootFactory] }, ); - await expect(factoryTester()).resolves.toBe('other-root'); + await expect(tester.get()).resolves.toBe('other-root'); }); it('should throw on missing dependencies', async () => { - const factoryTester = makeServiceFactoryTester( + const tester = ServiceFactoryTester.from( createServiceFactory({ service: pluginServiceRef, deps: { root: rootServiceRef }, @@ -138,7 +138,7 @@ describe('makeServiceFactoryTester', () => { }), ); - await expect(factoryTester('x')).rejects.toThrow( + await expect(tester.get('x')).rejects.toThrow( "Failed to instantiate service 'b' for 'x' because the following dependent services are missing: 'a'", ); }); diff --git a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts new file mode 100644 index 0000000000..40364756c2 --- /dev/null +++ b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ServiceFactory, ServiceRef } from '@backstage/backend-plugin-api'; +import { defaultServiceFactories } from './TestBackend'; +// Direct internal import to avoid duplication. +// This is a relative import in order to make sure that the implementation is duplicated +// rather than leading to an import from @backstage/backend-app-api. +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { ServiceRegistry } from '../../../../backend-app-api/src/wiring/ServiceRegistry'; + +/** @public */ +export class ServiceFactoryTester { + readonly #subject: ServiceRef; + readonly #registry: ServiceRegistry; + + static from( + subject: + | ServiceFactory + | (() => ServiceFactory), + options?: { dependencies?: Array ServiceFactory)> }, + ) { + return new ServiceFactoryTester( + typeof subject === 'function' ? subject() : subject, + options?.dependencies, + ); + } + + private constructor( + subject: ServiceFactory, + dependencies?: Array ServiceFactory)>, + ) { + this.#subject = subject.service; + + this.#registry = new ServiceRegistry([ + ...defaultServiceFactories, + ...(dependencies?.map(f => (typeof f === 'function' ? f() : f)) ?? []), + subject, + ]); + } + + async get( + ...args: 'root' extends TScope ? [] : [pluginId?: string] + ): Promise { + const [pluginId] = args; + return this.#registry.get(this.#subject, pluginId ?? 'test')!; + } + + async getService( + service: ServiceRef, + ...args: 'root' extends TGetScope ? [] : [pluginId?: string] + ): Promise { + const [pluginId] = args; + const instance = await this.#registry.get(service, pluginId ?? 'test'); + if (instance === undefined) { + throw new Error(`Service '${service.id}' not found`); + } + return instance; + } +} diff --git a/packages/backend-test-utils/src/next/wiring/index.ts b/packages/backend-test-utils/src/next/wiring/index.ts index 451a7aaece..949bd3329e 100644 --- a/packages/backend-test-utils/src/next/wiring/index.ts +++ b/packages/backend-test-utils/src/next/wiring/index.ts @@ -14,6 +14,6 @@ * limitations under the License. */ -export { makeServiceFactoryTester } from './makeServiceFactoryTester'; +export { ServiceFactoryTester } from './ServiceFactoryTester'; export { startTestBackend } from './TestBackend'; export type { TestBackend, TestBackendOptions } from './TestBackend'; diff --git a/packages/backend-test-utils/src/next/wiring/makeServiceFactoryTester.ts b/packages/backend-test-utils/src/next/wiring/makeServiceFactoryTester.ts deleted file mode 100644 index 5bc1aae775..0000000000 --- a/packages/backend-test-utils/src/next/wiring/makeServiceFactoryTester.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2023 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { ServiceFactory } from '@backstage/backend-plugin-api'; -import { defaultServiceFactories } from './TestBackend'; -// Direct internal import to avoid duplication. -// This is a relative import in order to make sure that the implementation is duplicated -// rather than leading to an import from @backstage/backend-app-api. -// eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { ServiceRegistry } from '../../../../backend-app-api/src/wiring/ServiceRegistry'; - -/** @public */ -export function makeServiceFactoryTester< - TService, - TScope extends 'root' | 'plugin', ->( - subject: - | ServiceFactory - | (() => ServiceFactory), - dependencies?: Array ServiceFactory)>, -): 'root' extends TScope - ? () => Promise - : (pluginId: string) => Promise { - const subjectFactory = typeof subject === 'function' ? subject() : subject; - const services = [ - ...defaultServiceFactories, - ...(dependencies?.map(f => (typeof f === 'function' ? f() : f)) ?? []), - subjectFactory, - ]; - - const registry = new ServiceRegistry(services); - - return async (pluginId?: string) => { - return registry.get(subjectFactory.service, pluginId ?? '')! as TService; - }; -}