backend-test-utils: add makeServiceFactoryTester

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2023-08-29 16:40:10 +02:00
parent 7ad7af4ae9
commit 5b8b654bfe
5 changed files with 209 additions and 1 deletions
+13
View File
@@ -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<TService, TScope>
| (() => ServiceFactory<TService, TScope>),
dependencies?: Array<ServiceFactory | (() => ServiceFactory)>,
): 'root' extends TScope
? () => Promise<TService>
: (pluginId: string) => Promise<TService>;
// @public (undocumented)
export namespace mockServices {
// (undocumented)
@@ -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(),
@@ -14,5 +14,6 @@
* limitations under the License.
*/
export { makeServiceFactoryTester } from './makeServiceFactoryTester';
export { startTestBackend } from './TestBackend';
export type { TestBackend, TestBackendOptions } from './TestBackend';
@@ -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<string>({ id: 'a', scope: 'root' });
const pluginServiceRef = createServiceRef<string>({ id: 'b', scope: 'plugin' });
const sharedPluginServiceRef = createServiceRef<string>({
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<string>({ 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<string>({ 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'",
);
});
});
@@ -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<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;
};
}