diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts index cc916d4309..13b57f54ba 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts @@ -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({ id: 'a' }); + const extensionPointB = createExtensionPoint({ 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({ 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({ 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.", + ); + }); }); diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts index 8a4e1d8471..06e27e5c07 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -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, + 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(); + + 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(); /** @public */ @@ -98,7 +188,7 @@ export async function startTestBackend< ): Promise { 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);