backend-test-utils: refactor into ServiceFactoryTester
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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<TService, TScope>
|
||||
| (() => ServiceFactory<TService, TScope>),
|
||||
dependencies?: Array<ServiceFactory | (() => ServiceFactory)>,
|
||||
): 'root' extends TScope
|
||||
? () => Promise<TService>
|
||||
: (pluginId: string) => Promise<TService>;
|
||||
|
||||
// @public (undocumented)
|
||||
export namespace mockServices {
|
||||
// (undocumented)
|
||||
@@ -195,6 +183,28 @@ export namespace mockServices {
|
||||
}
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export class ServiceFactoryTester<TService, TScope extends 'root' | 'plugin'> {
|
||||
// (undocumented)
|
||||
static from<TService, TScope extends 'root' | 'plugin'>(
|
||||
subject:
|
||||
| ServiceFactory<TService, TScope>
|
||||
| (() => ServiceFactory<TService, TScope>),
|
||||
options?: {
|
||||
dependencies?: Array<ServiceFactory | (() => ServiceFactory)>;
|
||||
},
|
||||
): ServiceFactoryTester<TService, TScope>;
|
||||
// (undocumented)
|
||||
get(
|
||||
...args: 'root' extends TScope ? [] : [pluginId?: string]
|
||||
): Promise<TService>;
|
||||
// (undocumented)
|
||||
getService<TGetService, TGetScope extends 'root' | 'plugin'>(
|
||||
service: ServiceRef<TGetService, TGetScope>,
|
||||
...args: 'root' extends TGetScope ? [] : [pluginId?: string]
|
||||
): Promise<TGetService>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type ServiceMock<TService> = {
|
||||
factory: ServiceFactory<TService>;
|
||||
|
||||
+36
-36
@@ -19,7 +19,7 @@ import {
|
||||
createServiceFactory,
|
||||
createServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { makeServiceFactoryTester } from './makeServiceFactoryTester';
|
||||
import { ServiceFactoryTester } from './ServiceFactoryTester';
|
||||
|
||||
const rootServiceRef = createServiceRef<string>({ id: 'a', scope: 'root' });
|
||||
const pluginServiceRef = createServiceRef<string>({ 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<string>({ 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<string>({ 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'",
|
||||
);
|
||||
});
|
||||
@@ -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<TService, TScope extends 'root' | 'plugin'> {
|
||||
readonly #subject: ServiceRef<TService, TScope>;
|
||||
readonly #registry: ServiceRegistry;
|
||||
|
||||
static from<TService, TScope extends 'root' | 'plugin'>(
|
||||
subject:
|
||||
| ServiceFactory<TService, TScope>
|
||||
| (() => ServiceFactory<TService, TScope>),
|
||||
options?: { dependencies?: Array<ServiceFactory | (() => ServiceFactory)> },
|
||||
) {
|
||||
return new ServiceFactoryTester(
|
||||
typeof subject === 'function' ? subject() : subject,
|
||||
options?.dependencies,
|
||||
);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
subject: ServiceFactory<TService, TScope>,
|
||||
dependencies?: Array<ServiceFactory | (() => 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<TService> {
|
||||
const [pluginId] = args;
|
||||
return this.#registry.get(this.#subject, pluginId ?? 'test')!;
|
||||
}
|
||||
|
||||
async getService<TGetService, TGetScope extends 'root' | 'plugin'>(
|
||||
service: ServiceRef<TGetService, TGetScope>,
|
||||
...args: 'root' extends TGetScope ? [] : [pluginId?: string]
|
||||
): Promise<TGetService> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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<TService, TScope>
|
||||
| (() => ServiceFactory<TService, TScope>),
|
||||
dependencies?: Array<ServiceFactory | (() => ServiceFactory)>,
|
||||
): 'root' extends TScope
|
||||
? () => Promise<TService>
|
||||
: (pluginId: string) => Promise<TService> {
|
||||
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;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user