backend-test-utils: automatically determine the plugin ID of tested extension points
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -220,4 +220,89 @@ describe('TestBackend', () => {
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.body).toEqual({ message: 'pong' });
|
||||
});
|
||||
|
||||
it('should provide extension point implementations', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
const extensionPointA = createExtensionPoint<string>({ id: 'a' });
|
||||
const extensionPointB = createExtensionPoint<string>({ id: 'b' });
|
||||
await expect(
|
||||
startTestBackend({
|
||||
extensionPoints: [
|
||||
[extensionPointA, 'a'],
|
||||
[extensionPointB, 'b'],
|
||||
],
|
||||
features: [
|
||||
createBackendModule({
|
||||
pluginId: 'testA',
|
||||
moduleId: 'test',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: { ext: extensionPointA },
|
||||
async init({ ext }) {
|
||||
expect(ext).toBe('a');
|
||||
},
|
||||
});
|
||||
},
|
||||
})(),
|
||||
createBackendModule({
|
||||
pluginId: 'testB',
|
||||
moduleId: 'test',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: { ext: extensionPointB },
|
||||
async init({ ext }) {
|
||||
expect(ext).toBe('b');
|
||||
},
|
||||
});
|
||||
},
|
||||
})(),
|
||||
],
|
||||
}),
|
||||
).resolves.not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should reject extension point used by multiple plugins', async () => {
|
||||
const extensionPointA = createExtensionPoint<string>({ id: 'a' });
|
||||
await expect(
|
||||
startTestBackend({
|
||||
extensionPoints: [[extensionPointA, 'a']],
|
||||
features: [
|
||||
createBackendModule({
|
||||
pluginId: 'testA',
|
||||
moduleId: 'test',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: { ext: extensionPointA },
|
||||
async init() {},
|
||||
});
|
||||
},
|
||||
})(),
|
||||
createBackendModule({
|
||||
pluginId: 'testB',
|
||||
moduleId: 'test',
|
||||
register(reg) {
|
||||
reg.registerInit({
|
||||
deps: { ext: extensionPointA },
|
||||
async init() {},
|
||||
});
|
||||
},
|
||||
})(),
|
||||
],
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"Extension point registered for plugin 'testA' may not be used by module for plugin 'testB'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject extension point not used by any plugin', async () => {
|
||||
const extensionPointA = createExtensionPoint<string>({ id: 'a' });
|
||||
await expect(
|
||||
startTestBackend({
|
||||
extensionPoints: [[extensionPointA, 'a']],
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"Unable to determine the plugin ID of extension point(s) 'a'. Tested extension points must be depended on by one or more tested modules.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,11 +30,14 @@ import {
|
||||
BackendFeature,
|
||||
ExtensionPoint,
|
||||
coreServices,
|
||||
createBackendPlugin,
|
||||
createBackendModule,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { mockServices } from '../services';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import express from 'express';
|
||||
// Direct internal import to avoid duplication
|
||||
// eslint-disable-next-line @backstage/no-forbidden-package-imports
|
||||
import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types';
|
||||
|
||||
/** @public */
|
||||
export interface TestBackendOptions<
|
||||
@@ -87,6 +90,93 @@ const defaultServiceFactories = [
|
||||
mockServices.urlReader.factory(),
|
||||
];
|
||||
|
||||
/**
|
||||
* Given a set of extension points and plugins, find
|
||||
* @returns
|
||||
*/
|
||||
function createExtensionPointTestModules(
|
||||
features: BackendFeature[],
|
||||
extensionPointTuples?: readonly [
|
||||
ref: ExtensionPoint<unknown>,
|
||||
impl: unknown,
|
||||
][],
|
||||
): BackendFeature[] {
|
||||
if (!extensionPointTuples) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const registrations = features.flatMap(feature => {
|
||||
if (feature.$$type !== '@backstage/BackendFeature') {
|
||||
throw new Error(
|
||||
`Failed to add feature, invalid type '${feature.$$type}'`,
|
||||
);
|
||||
}
|
||||
const internalFeature = feature as InternalBackendFeature;
|
||||
if (internalFeature.version !== 'v1') {
|
||||
throw new Error(
|
||||
`Failed to add feature, invalid version '${internalFeature.version}'`,
|
||||
);
|
||||
}
|
||||
return internalFeature.getRegistrations();
|
||||
});
|
||||
|
||||
const extensionPointMap = new Map(
|
||||
extensionPointTuples.map(ep => [ep[0].id, ep]),
|
||||
);
|
||||
const extensionPointsToSort = new Set(extensionPointMap.keys());
|
||||
const extensionPointsByPlugin = new Map<string, string[]>();
|
||||
|
||||
for (const registration of registrations) {
|
||||
if (registration.type === 'module') {
|
||||
const testDep = Object.values(registration.init.deps).filter(dep =>
|
||||
extensionPointsToSort.has(dep.id),
|
||||
);
|
||||
if (testDep.length > 0) {
|
||||
let points = extensionPointsByPlugin.get(registration.pluginId);
|
||||
if (!points) {
|
||||
points = [];
|
||||
extensionPointsByPlugin.set(registration.pluginId, points);
|
||||
}
|
||||
for (const { id } of testDep) {
|
||||
points.push(id);
|
||||
extensionPointsToSort.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (extensionPointsToSort.size > 0) {
|
||||
const list = Array.from(extensionPointsToSort)
|
||||
.map(id => `'${id}'`)
|
||||
.join(', ');
|
||||
throw new Error(
|
||||
`Unable to determine the plugin ID of extension point(s) ${list}. ` +
|
||||
'Tested extension points must be depended on by one or more tested modules.',
|
||||
);
|
||||
}
|
||||
|
||||
const modules = [];
|
||||
|
||||
for (const [pluginId, pluginExtensionPointIds] of extensionPointsByPlugin) {
|
||||
modules.push(
|
||||
createBackendModule({
|
||||
pluginId,
|
||||
moduleId: 'testExtensionPointRegistration',
|
||||
register(reg) {
|
||||
for (const id of pluginExtensionPointIds) {
|
||||
const tuple = extensionPointMap.get(id)!;
|
||||
reg.registerExtensionPoint(...tuple);
|
||||
}
|
||||
|
||||
reg.registerInit({ deps: {}, async init() {} });
|
||||
},
|
||||
})(),
|
||||
);
|
||||
}
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
const backendInstancesToCleanUp = new Array<Backend>();
|
||||
|
||||
/** @public */
|
||||
@@ -98,7 +188,7 @@ export async function startTestBackend<
|
||||
): Promise<TestBackend> {
|
||||
const {
|
||||
services = [],
|
||||
extensionPoints = [],
|
||||
extensionPoints,
|
||||
features = [],
|
||||
...otherOptions
|
||||
} = options;
|
||||
@@ -194,18 +284,9 @@ export async function startTestBackend<
|
||||
|
||||
backendInstancesToCleanUp.push(backend);
|
||||
|
||||
backend.add(
|
||||
createBackendPlugin({
|
||||
pluginId: `---test-extension-point-registrar`,
|
||||
register(reg) {
|
||||
for (const [ref, impl] of extensionPoints) {
|
||||
reg.registerExtensionPoint(ref, impl);
|
||||
}
|
||||
|
||||
reg.registerInit({ deps: {}, async init() {} });
|
||||
},
|
||||
})(),
|
||||
);
|
||||
for (const m of createExtensionPointTestModules(features, extensionPoints)) {
|
||||
backend.add(m);
|
||||
}
|
||||
|
||||
for (const feature of features) {
|
||||
backend.add(feature);
|
||||
|
||||
Reference in New Issue
Block a user