catalog-backend: attribute provider connection failures to modules

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2026-01-24 16:52:37 +01:00
parent 24eb7d7933
commit f1d29b4d4d
23 changed files with 649 additions and 137 deletions
@@ -21,6 +21,7 @@ import {
createServiceRef,
coreServices,
createBackendPlugin,
ExtensionPointFactoryContext,
} from '@backstage/backend-plugin-api';
import { Router } from 'express';
import request from 'supertest';
@@ -408,4 +409,114 @@ describe('TestBackend', () => {
}),
).rejects.toThrow('nah');
});
it('should support factory-based extension points', async () => {
expect.assertions(2);
const extensionPoint = createExtensionPoint<{
getValue(): number;
}>({ id: 'test-factory' });
const testPlugin = createBackendPlugin({
pluginId: 'test',
register(env) {
const instances: Array<{ getValue(): number }> = [];
env.registerExtensionPoint({
extensionPoint,
factory: () => {
const instance = {
getValue: () => 42,
};
instances.push(instance);
return instance;
},
});
env.registerInit({
deps: {},
async init() {
// Factory is called during module initialization, which happens before plugin init
expect(instances).toHaveLength(1);
},
});
},
});
const testModule = createBackendModule({
pluginId: 'test',
moduleId: 'test-module',
register(env) {
env.registerInit({
deps: { ext: extensionPoint },
async init({ ext }) {
expect(ext.getValue()).toBe(42);
},
});
},
});
await startTestBackend({
features: [testPlugin, testModule],
});
});
it('should support reportModuleStartupFailure in factory-based extension points', async () => {
const extensionPoint = createExtensionPoint<{
getValue(): number;
}>({ id: 'test-failure' });
const testPlugin = createBackendPlugin({
pluginId: 'test',
register(env) {
let capturedContext: ExtensionPointFactoryContext | undefined;
env.registerExtensionPoint({
extensionPoint,
factory: context => {
capturedContext = context;
return {
getValue: () => 42,
};
},
});
env.registerInit({
deps: {},
async init() {
// Plugin init runs after all modules have been initialized
// At this point, the module result exists and we can report a failure
capturedContext?.reportModuleStartupFailure({
error: new Error('NOPE'),
});
},
});
},
});
const testModule = createBackendModule({
pluginId: 'test',
moduleId: 'test-module',
register(env) {
env.registerInit({
deps: { ext: extensionPoint },
async init({ ext }) {
// Use the extension point - this creates the context
ext.getValue();
},
});
},
});
await expect(
startTestBackend({
features: [testPlugin, testModule],
}),
).rejects.toThrow(
new Error(
`Backend startup failed due to the following errors:
Module 'test-module' for plugin 'test' startup failed; caused by Error: NOPE`,
),
);
});
});
@@ -107,9 +107,15 @@ function createPluginsForOrphanModules(features: Array<BackendFeature>) {
if (isInternalBackendRegistrations(feature)) {
const registrations = feature.getRegistrations();
for (const registration of registrations) {
if (registration.type === 'plugin') {
if (
registration.type === 'plugin' ||
registration.type === 'plugin-v1.1'
) {
pluginIds.add(registration.pluginId);
} else if (registration.type === 'module') {
} else if (
registration.type === 'module' ||
registration.type === 'module-v1.1'
) {
modulePluginIds.add(registration.pluginId);
}
}
@@ -160,7 +166,7 @@ function createExtensionPointTestModules(
const extensionPointsByPlugin = new Map<string, string[]>();
for (const registration of registrations) {
if (registration.type === 'module') {
if (registration.type === 'module' || registration.type === 'module-v1.1') {
const testDep = Object.values(registration.init.deps).filter(dep =>
extensionPointsToSort.has(dep.id),
);