From 7ad7af4ae9abe8d4a8454d67dde5f357ddbe0f5a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 29 Aug 2023 16:38:00 +0200 Subject: [PATCH 1/9] backend-test-utils: add root http service mock Signed-off-by: Patrik Oldsberg --- packages/backend-test-utils/api-report.md | 13 +++++++++++++ .../src/next/services/mockServices.ts | 7 +++++++ 2 files changed, 20 insertions(+) diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 95f67da35f..4a5d93390f 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -20,6 +20,8 @@ import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoggerService } from '@backstage/backend-plugin-api'; import { PermissionsService } from '@backstage/backend-plugin-api'; import { RootConfigService } from '@backstage/backend-plugin-api'; +import { RootHttpRouterFactoryOptions } from '@backstage/backend-app-api'; +import { RootHttpRouterService } 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'; @@ -113,6 +115,17 @@ export namespace mockServices { ) => ServiceFactory; } // (undocumented) + export namespace rootHttpRouter { + const // (undocumented) + factory: ( + options?: RootHttpRouterFactoryOptions | undefined, + ) => ServiceFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ServiceMock; + } + // (undocumented) export namespace rootLifecycle { const // (undocumented) factory: () => ServiceFactory; diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 3edc3e0894..22fba38fda 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -31,6 +31,7 @@ import { lifecycleServiceFactory, loggerServiceFactory, permissionsServiceFactory, + rootHttpRouterServiceFactory, rootLifecycleServiceFactory, schedulerServiceFactory, urlReaderServiceFactory, @@ -184,6 +185,12 @@ export namespace mockServices { use: jest.fn(), })); } + export namespace rootHttpRouter { + export const factory = rootHttpRouterServiceFactory; + export const mock = simpleMock(coreServices.rootHttpRouter, () => ({ + use: jest.fn(), + })); + } export namespace lifecycle { export const factory = lifecycleServiceFactory; export const mock = simpleMock(coreServices.lifecycle, () => ({ From 5b8b654bfeee47bf89617a9e0841de1e363514a0 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 29 Aug 2023 16:40:10 +0200 Subject: [PATCH 2/9] backend-test-utils: add makeServiceFactoryTester Signed-off-by: Patrik Oldsberg --- packages/backend-test-utils/api-report.md | 13 ++ .../src/next/wiring/TestBackend.ts | 2 +- .../src/next/wiring/index.ts | 1 + .../wiring/makeServiceFactoryTester.test.ts | 145 ++++++++++++++++++ .../next/wiring/makeServiceFactoryTester.ts | 49 ++++++ 5 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 packages/backend-test-utils/src/next/wiring/makeServiceFactoryTester.test.ts create 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 4a5d93390f..41928af40b 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -32,6 +32,19 @@ 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) diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts index 97da3543ae..0b92ec3353 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -61,7 +61,7 @@ export interface TestBackend extends Backend { readonly server: ExtendedHttpServer; } -const defaultServiceFactories = [ +export const defaultServiceFactories = [ mockServices.cache.factory(), mockServices.rootConfig.factory(), mockServices.database.factory(), diff --git a/packages/backend-test-utils/src/next/wiring/index.ts b/packages/backend-test-utils/src/next/wiring/index.ts index 7c39474d4c..451a7aaece 100644 --- a/packages/backend-test-utils/src/next/wiring/index.ts +++ b/packages/backend-test-utils/src/next/wiring/index.ts @@ -14,5 +14,6 @@ * limitations under the License. */ +export { makeServiceFactoryTester } from './makeServiceFactoryTester'; export { startTestBackend } from './TestBackend'; export type { TestBackend, TestBackendOptions } from './TestBackend'; diff --git a/packages/backend-test-utils/src/next/wiring/makeServiceFactoryTester.test.ts b/packages/backend-test-utils/src/next/wiring/makeServiceFactoryTester.test.ts new file mode 100644 index 0000000000..b30700cdec --- /dev/null +++ b/packages/backend-test-utils/src/next/wiring/makeServiceFactoryTester.test.ts @@ -0,0 +1,145 @@ +/* + * 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 { + coreServices, + createServiceFactory, + createServiceRef, +} from '@backstage/backend-plugin-api'; +import { makeServiceFactoryTester } from './makeServiceFactoryTester'; + +const rootServiceRef = createServiceRef({ id: 'a', scope: 'root' }); +const pluginServiceRef = createServiceRef({ id: 'b', scope: 'plugin' }); +const sharedPluginServiceRef = createServiceRef({ + id: 'c', + scope: 'plugin', +}); + +const rootFactory = createServiceFactory({ + service: rootServiceRef, + deps: {}, + factory: async () => 'root', +}); + +const pluginFactory = createServiceFactory({ + service: pluginServiceRef, + deps: { plugin: coreServices.pluginMetadata }, + factory: async ({ plugin }) => `${plugin.getId()}-plugin`, +}); + +const sharedPluginFactory = createServiceFactory({ + service: sharedPluginServiceRef, + deps: { plugin: coreServices.pluginMetadata }, + createRootContext() { + return { counter: 0 }; + }, + factory: async ({ plugin }, state) => { + state.counter += 1; + return `${plugin.getId()}-${state.counter}-plugin`; + }, +}); + +describe('makeServiceFactoryTester', () => { + it('should test a root service factory', async () => { + const factoryTester = makeServiceFactoryTester(rootFactory()); + + await expect(factoryTester()).resolves.toBe('root'); + }); + + it('should test a plugin service factory', async () => { + const factoryTester = makeServiceFactoryTester(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'); + }); + + it('should test a plugin service factory with root context', async () => { + const factoryTester = makeServiceFactoryTester(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'); + + const tester2 = makeServiceFactoryTester(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'); + }); + + it('should use dependencies', async () => { + const factoryTester = makeServiceFactoryTester( + createServiceFactory({ + service: createServiceRef({ id: 'concat' }), + deps: { root: rootServiceRef, plugin: pluginServiceRef }, + factory: async ({ root, plugin }) => `${root}, ${plugin}`, + }), + [rootFactory, pluginFactory()], + ); + + await expect(factoryTester('x')).resolves.toBe('root, x-plugin'); + }); + + it('should use dependencies with root context', async () => { + const factoryTester = makeServiceFactoryTester( + createServiceFactory({ + service: createServiceRef({ id: 'concat' }), + deps: { shared: sharedPluginServiceRef, plugin: pluginServiceRef }, + factory: async ({ shared, plugin }) => `${shared}, ${plugin}`, + }), + [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'); + }); + + it('should prioritize the subject implementation', async () => { + const factoryTester = makeServiceFactoryTester( + createServiceFactory({ + service: rootServiceRef, + deps: {}, + factory: async () => 'other-root', + }), + [rootFactory], + ); + + await expect(factoryTester()).resolves.toBe('other-root'); + }); + + it('should throw on missing dependencies', async () => { + const factoryTester = makeServiceFactoryTester( + createServiceFactory({ + service: pluginServiceRef, + deps: { root: rootServiceRef }, + factory: async () => 'plugin', + }), + ); + + await expect(factoryTester('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/makeServiceFactoryTester.ts b/packages/backend-test-utils/src/next/wiring/makeServiceFactoryTester.ts new file mode 100644 index 0000000000..5bc1aae775 --- /dev/null +++ b/packages/backend-test-utils/src/next/wiring/makeServiceFactoryTester.ts @@ -0,0 +1,49 @@ +/* + * 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; + }; +} From bbff821e545a3fedaa56a3f8027bd60011fb5f80 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 29 Aug 2023 18:38:48 +0200 Subject: [PATCH 3/9] 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; - }; -} From 0423c1d0d9c31f5744db8a01d99257dc60fe0b6c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 29 Aug 2023 18:39:16 +0200 Subject: [PATCH 4/9] backend-app-api: migrate tests to use ServiceFactoryTester Signed-off-by: Patrik Oldsberg --- .../httpRouterServiceFactory.test.ts | 69 +++++++------------ .../scheduler/schedulerServiceFactory.test.ts | 63 ++++++----------- 2 files changed, 44 insertions(+), 88 deletions(-) diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts index d0b40df33b..ff25be98a6 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts @@ -14,39 +14,29 @@ * limitations under the License. */ +import { + ServiceFactoryTester, + mockServices, +} from '@backstage/backend-test-utils'; import { httpRouterServiceFactory } from './httpRouterServiceFactory'; describe('httpRouterFactory', () => { it('should register plugin paths', async () => { - const rootHttpRouter = { use: jest.fn() }; - const factory = httpRouterServiceFactory() as any; + const rootHttpRouter = mockServices.rootHttpRouter.mock(); + const tester = ServiceFactoryTester.from(httpRouterServiceFactory(), { + dependencies: [rootHttpRouter.factory], + }); - const handler1 = () => {}; - const router1 = await factory.factory( - { - rootHttpRouter, - plugin: { getId: () => 'test1' }, - lifecycle: { addStartupHook() {}, addShutdownHook() {} }, - }, - undefined, - ); - router1.use(handler1); + const router1 = await tester.get('test1'); + router1.use(() => {}); expect(rootHttpRouter.use).toHaveBeenCalledTimes(1); expect(rootHttpRouter.use).toHaveBeenCalledWith( '/api/test1', expect.any(Function), ); - const handler2 = () => {}; - const router2 = await factory.factory( - { - rootHttpRouter, - plugin: { getId: () => 'test2' }, - lifecycle: { addStartupHook() {}, addShutdownHook() {} }, - }, - undefined, - ); - router2.use(handler2); + const router2 = await tester.get('test2'); + router2.use(() => {}); expect(rootHttpRouter.use).toHaveBeenCalledTimes(2); expect(rootHttpRouter.use).toHaveBeenCalledWith( '/api/test2', @@ -55,37 +45,24 @@ describe('httpRouterFactory', () => { }); it('should use custom path generator', async () => { - const rootHttpRouter = { use: jest.fn() }; - const factory = httpRouterServiceFactory({ - getPath: id => `/some/${id}/path`, - }) as any; - - const handler1 = () => {}; - const router1 = await factory.factory( - { - rootHttpRouter, - plugin: { getId: () => 'test1' }, - lifecycle: { addStartupHook() {}, addShutdownHook() {} }, - }, - undefined, + const rootHttpRouter = mockServices.rootHttpRouter.mock(); + const tester = ServiceFactoryTester.from( + httpRouterServiceFactory({ + getPath: id => `/some/${id}/path`, + }), + { dependencies: [rootHttpRouter.factory] }, ); - router1.use(handler1); + + const router1 = await tester.get('test1'); + router1.use(() => {}); expect(rootHttpRouter.use).toHaveBeenCalledTimes(1); expect(rootHttpRouter.use).toHaveBeenCalledWith( '/some/test1/path', expect.any(Function), ); - const handler2 = () => {}; - const router2 = await factory.factory( - { - rootHttpRouter, - plugin: { getId: () => 'test2' }, - lifecycle: { addStartupHook() {}, addShutdownHook() {} }, - }, - undefined, - ); - router2.use(handler2); + const router2 = await tester.get('test2'); + router2.use(() => {}); expect(rootHttpRouter.use).toHaveBeenCalledTimes(2); expect(rootHttpRouter.use).toHaveBeenCalledWith( '/some/test2/path', diff --git a/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts index d771c5dd11..7b889b3a0e 100644 --- a/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts @@ -14,54 +14,33 @@ * limitations under the License. */ -import { - coreServices, - createBackendPlugin, -} from '@backstage/backend-plugin-api'; -import { startTestBackend } from '@backstage/backend-test-utils'; +import { coreServices } from '@backstage/backend-plugin-api'; +import { ServiceFactoryTester } from '@backstage/backend-test-utils'; import { schedulerServiceFactory } from './schedulerServiceFactory'; describe('schedulerFactory', () => { it('creates sidecar database features', async () => { - expect.assertions(3); + const tester = ServiceFactoryTester.from(schedulerServiceFactory()); - const subject = schedulerServiceFactory(); - - const plugin = createBackendPlugin({ - pluginId: 'example', - register(reg) { - reg.registerInit({ - deps: { - scheduler: subject.service, - database: coreServices.database, - }, - init: async ({ scheduler, database }) => { - await scheduler.scheduleTask({ - id: 'task1', - timeout: { seconds: 1 }, - frequency: { seconds: 1 }, - fn: async () => {}, - }); - - const client = await database.getClient(); - await expect( - client.from('backstage_backend_tasks__tasks').count(), - ).resolves.toEqual([{ 'count(*)': 1 }]); - await expect( - client.from('backstage_backend_tasks__knex_migrations').count(), - ).resolves.toEqual([{ 'count(*)': expect.any(Number) }]); - await expect( - client - .from('backstage_backend_tasks__knex_migrations_lock') - .count(), - ).resolves.toEqual([{ 'count(*)': expect.any(Number) }]); - }, - }); - }, + const scheduler = await tester.get(); + await scheduler.scheduleTask({ + id: 'task1', + timeout: { seconds: 1 }, + frequency: { seconds: 1 }, + fn: async () => {}, }); - await startTestBackend({ - features: [plugin(), subject], - }); + const database = await tester.getService(coreServices.database); + + const client = await database.getClient(); + await expect( + client.from('backstage_backend_tasks__tasks').count(), + ).resolves.toEqual([{ 'count(*)': 1 }]); + await expect( + client.from('backstage_backend_tasks__knex_migrations').count(), + ).resolves.toEqual([{ 'count(*)': expect.any(Number) }]); + await expect( + client.from('backstage_backend_tasks__knex_migrations_lock').count(), + ).resolves.toEqual([{ 'count(*)': expect.any(Number) }]); }); }); From 58cb5e5cea7b77ef87266f3348742afde6422b64 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 29 Aug 2023 18:40:38 +0200 Subject: [PATCH 5/9] changesets: changeset for ServiceFactoryTester Signed-off-by: Patrik Oldsberg --- .changeset/blue-rats-drop.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/blue-rats-drop.md diff --git a/.changeset/blue-rats-drop.md b/.changeset/blue-rats-drop.md new file mode 100644 index 0000000000..19b732be1a --- /dev/null +++ b/.changeset/blue-rats-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Introduced a new utility for testing service factories, `ServiceFactoryTester`. From 187cc4680e24115656f87553e887a5d795e443f4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 29 Aug 2023 18:48:56 +0200 Subject: [PATCH 6/9] backend-test-utils: document ServiceFactoryTester and extract options type Signed-off-by: Patrik Oldsberg --- packages/backend-test-utils/api-report.md | 14 +++--- .../src/next/wiring/ServiceFactoryTester.ts | 48 ++++++++++++++++++- .../src/next/wiring/index.ts | 5 +- 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 147481f8d8..5831b093c5 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -183,28 +183,28 @@ export namespace mockServices { } } -// @public (undocumented) +// @public export class ServiceFactoryTester { - // (undocumented) static from( subject: | ServiceFactory | (() => ServiceFactory), - options?: { - dependencies?: Array ServiceFactory)>; - }, + options?: ServiceFactoryTesterOptions, ): ServiceFactoryTester; - // (undocumented) get( ...args: 'root' extends TScope ? [] : [pluginId?: string] ): Promise; - // (undocumented) getService( service: ServiceRef, ...args: 'root' extends TGetScope ? [] : [pluginId?: string] ): Promise; } +// @public +export interface ServiceFactoryTesterOptions { + dependencies?: Array ServiceFactory)>; +} + // @public (undocumented) export type ServiceMock = { factory: ServiceFactory; diff --git a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts index 40364756c2..b4a038d001 100644 --- a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts +++ b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts @@ -22,16 +22,43 @@ import { defaultServiceFactories } from './TestBackend'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { ServiceRegistry } from '../../../../backend-app-api/src/wiring/ServiceRegistry'; -/** @public */ +/** + * Options for {@link ServiceFactoryTester}. + * @public + */ +export interface ServiceFactoryTesterOptions { + /** + * Additional service factories to make available as dependencies. + * + * @remarks + * + * If a service factory is provided for a service that already has a default + * implementation, the provided factory will override the default. + */ + dependencies?: Array ServiceFactory)>; +} + +/** + * A utility to help test service factories in isolation. + * + * @public + */ export class ServiceFactoryTester { readonly #subject: ServiceRef; readonly #registry: ServiceRegistry; + /** + * Creates a new {@link ServiceFactoryTester} used to test the provided subject. + * + * @param subject - The service factory to test. + * @param options - Additional options + * @returns A new tester instance for the provided subject. + */ static from( subject: | ServiceFactory | (() => ServiceFactory), - options?: { dependencies?: Array ServiceFactory)> }, + options?: ServiceFactoryTesterOptions, ) { return new ServiceFactoryTester( typeof subject === 'function' ? subject() : subject, @@ -52,6 +79,16 @@ export class ServiceFactoryTester { ]); } + /** + * Returns the service instance for the subject. + * + * @remarks + * + * If the subject is a plugin scoped service factory a plugin ID + * can be provided to instantiate the service for a specific plugin. + * + * By default the plugin ID 'test' is used. + */ async get( ...args: 'root' extends TScope ? [] : [pluginId?: string] ): Promise { @@ -59,6 +96,13 @@ export class ServiceFactoryTester { return this.#registry.get(this.#subject, pluginId ?? 'test')!; } + /** + * Return the service instance for any of the provided dependencies or built-in services. + * + * @remarks + * + * A plugin ID can optionally be provided for plugin scoped services, otherwise the plugin ID 'test' is used. + */ async getService( service: ServiceRef, ...args: 'root' extends TGetScope ? [] : [pluginId?: string] diff --git a/packages/backend-test-utils/src/next/wiring/index.ts b/packages/backend-test-utils/src/next/wiring/index.ts index 949bd3329e..bd6a61317f 100644 --- a/packages/backend-test-utils/src/next/wiring/index.ts +++ b/packages/backend-test-utils/src/next/wiring/index.ts @@ -14,6 +14,9 @@ * limitations under the License. */ -export { ServiceFactoryTester } from './ServiceFactoryTester'; +export { + ServiceFactoryTester, + type ServiceFactoryTesterOptions, +} from './ServiceFactoryTester'; export { startTestBackend } from './TestBackend'; export type { TestBackend, TestBackendOptions } from './TestBackend'; From 6cc775b8c28bf3f9745590148db548eeca5dfd03 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 30 Aug 2023 17:27:04 +0200 Subject: [PATCH 7/9] backend-test-utils: some ServiceFactoryTester tweaks Signed-off-by: Patrik Oldsberg --- .../httpRouterServiceFactory.test.ts | 2 +- .../next/wiring/ServiceFactoryTester.test.ts | 8 +++--- .../src/next/wiring/ServiceFactoryTester.ts | 26 +++++++++---------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts index ff25be98a6..a6c586d806 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts @@ -23,7 +23,7 @@ import { httpRouterServiceFactory } from './httpRouterServiceFactory'; describe('httpRouterFactory', () => { it('should register plugin paths', async () => { const rootHttpRouter = mockServices.rootHttpRouter.mock(); - const tester = ServiceFactoryTester.from(httpRouterServiceFactory(), { + const tester = ServiceFactoryTester.from(httpRouterServiceFactory, { dependencies: [rootHttpRouter.factory], }); diff --git a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.test.ts b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.test.ts index 073b454ad0..14dd427ecf 100644 --- a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.test.ts +++ b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.test.ts @@ -54,13 +54,13 @@ const sharedPluginFactory = createServiceFactory({ describe('ServiceFactoryTester', () => { it('should test a root service factory', async () => { - const tester = ServiceFactoryTester.from(rootFactory()); + const tester = ServiceFactoryTester.from(rootFactory); await expect(tester.get()).resolves.toBe('root'); }); it('should test a plugin service factory', async () => { - const tester = ServiceFactoryTester.from(pluginFactory()); + const tester = ServiceFactoryTester.from(pluginFactory); await expect(tester.get('x')).resolves.toBe('x-plugin'); await expect(tester.get('y')).resolves.toBe('y-plugin'); @@ -68,7 +68,7 @@ describe('ServiceFactoryTester', () => { }); it('should test a plugin service factory with root context', async () => { - const tester = ServiceFactoryTester.from(sharedPluginFactory()); + const tester = ServiceFactoryTester.from(sharedPluginFactory); await expect(tester.get('x')).resolves.toBe('x-1-plugin'); await expect(tester.get('y')).resolves.toBe('y-2-plugin'); @@ -76,7 +76,7 @@ describe('ServiceFactoryTester', () => { await expect(tester.get('y')).resolves.toBe('y-2-plugin'); await expect(tester.get('z')).resolves.toBe('z-3-plugin'); - const tester2 = ServiceFactoryTester.from(sharedPluginFactory()); + const tester2 = ServiceFactoryTester.from(sharedPluginFactory); await expect(tester2.get('z')).resolves.toBe('z-1-plugin'); await expect(tester2.get('y')).resolves.toBe('y-2-plugin'); diff --git a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts index b4a038d001..d583614598 100644 --- a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts +++ b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts @@ -60,23 +60,23 @@ export class ServiceFactoryTester { | (() => ServiceFactory), options?: ServiceFactoryTesterOptions, ) { - return new ServiceFactoryTester( - typeof subject === 'function' ? subject() : subject, - options?.dependencies, - ); + const subjectFactory = typeof subject === 'function' ? subject() : subject; + const registry = new ServiceRegistry([ + ...defaultServiceFactories, + ...(options?.dependencies?.map(f => + typeof f === 'function' ? f() : f, + ) ?? []), + subjectFactory, + ]); + return new ServiceFactoryTester(subjectFactory.service, registry); } private constructor( - subject: ServiceFactory, - dependencies?: Array ServiceFactory)>, + subject: ServiceRef, + registry: ServiceRegistry, ) { - this.#subject = subject.service; - - this.#registry = new ServiceRegistry([ - ...defaultServiceFactories, - ...(dependencies?.map(f => (typeof f === 'function' ? f() : f)) ?? []), - subject, - ]); + this.#subject = subject; + this.#registry = registry; } /** From b7ceb86a0706116b5566f002d863976344ab6d30 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 30 Aug 2023 17:48:33 +0200 Subject: [PATCH 8/9] backend-app-api: minor ServiceRegistry.create refactor Signed-off-by: Patrik Oldsberg --- .../src/wiring/ServiceRegistry.ts | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts index c89916974b..d009a319ba 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts @@ -58,9 +58,9 @@ const pluginMetadataServiceFactory = createServiceFactory( ); export class ServiceRegistry implements EnumerableServiceHolder { - static create(factories: Array): EnumerableServiceHolder { + static create(factories: Array): ServiceRegistry { const registry = new ServiceRegistry(factories); - registry.checkForCircularDeps(); + registry.#checkForCircularDeps(); return registry; } @@ -80,7 +80,6 @@ export class ServiceRegistry implements EnumerableServiceHolder { InternalServiceFactory, Promise >(); - readonly #dependencyGraph: DependencyGraph; private constructor(factories: Array) { this.#providedFactories = new Map( @@ -88,15 +87,6 @@ export class ServiceRegistry implements EnumerableServiceHolder { ); this.#loadedDefaultFactories = new Map(); this.#implementations = new Map(); - this.#dependencyGraph = DependencyGraph.fromIterable( - Array.from(this.#providedFactories).map( - ([serviceId, serviceFactory]) => ({ - value: serviceId, - provides: [serviceId], - consumes: Object.values(serviceFactory.deps).map(d => d.id), - }), - ), - ); } #resolveFactory( @@ -163,10 +153,17 @@ export class ServiceRegistry implements EnumerableServiceHolder { } } - checkForCircularDeps(): void { - const circularDependencies = Array.from( - this.#dependencyGraph.detectCircularDependencies(), + #checkForCircularDeps(): void { + const graph = DependencyGraph.fromIterable( + Array.from(this.#providedFactories).map( + ([serviceId, serviceFactory]) => ({ + value: serviceId, + provides: [serviceId], + consumes: Object.values(serviceFactory.deps).map(d => d.id), + }), + ), ); + const circularDependencies = Array.from(graph.detectCircularDependencies()); if (circularDependencies.length) { const cycles = circularDependencies From e1c4b4494e9a9c744a30bb5c660ca26b4425e72c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 30 Aug 2023 17:48:59 +0200 Subject: [PATCH 9/9] backend-test-utils: update to use ServiceFactory.create Signed-off-by: Patrik Oldsberg --- .../backend-test-utils/src/next/wiring/ServiceFactoryTester.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts index d583614598..0555ab10a4 100644 --- a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts +++ b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts @@ -61,7 +61,7 @@ export class ServiceFactoryTester { options?: ServiceFactoryTesterOptions, ) { const subjectFactory = typeof subject === 'function' ? subject() : subject; - const registry = new ServiceRegistry([ + const registry = ServiceRegistry.create([ ...defaultServiceFactories, ...(options?.dependencies?.map(f => typeof f === 'function' ? f() : f,