From a0d9373c3f04426a8727b00f69dfbc4df5aeaed6 Mon Sep 17 00:00:00 2001 From: aramissennyeydd Date: Tue, 19 Aug 2025 17:09:54 -0700 Subject: [PATCH] update system metadata service to be observable-first Signed-off-by: aramissennyeydd --- packages/backend-defaults/package.json | 1 + .../report-systemMetadata.api.md | 5 +- .../SystemMetadataService.test.ts | 244 +++++++++++------- .../lib/DefaultSystemMetadataService.ts | 120 ++++++++- .../lib/createSystemMetadataRouter.ts | 22 +- packages/backend-plugin-api/report.api.md | 5 +- .../definitions/SystemMetadataService.ts | 4 +- packages/backend-test-utils/package.json | 1 + packages/backend-test-utils/report.api.md | 10 +- .../src/services/MockObservable.ts | 25 ++ .../MockSystemMetadataService.test.ts | 34 +++ .../src/services/MockSystemMetadataService.ts | 44 ++++ .../src/services/mockServices.ts | 23 +- .../src/services/simpleMock.ts | 1 + yarn.lock | 2 + 15 files changed, 420 insertions(+), 121 deletions(-) create mode 100644 packages/backend-test-utils/src/services/MockObservable.ts create mode 100644 packages/backend-test-utils/src/services/MockSystemMetadataService.test.ts create mode 100644 packages/backend-test-utils/src/services/MockSystemMetadataService.ts diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 3e9c5f57cb..5586289e5f 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -198,6 +198,7 @@ "winston-transport": "^4.5.0", "yauzl": "^3.0.0", "yn": "^4.0.0", + "zen-observable": "^0.10.0", "zod": "^3.22.4", "zod-to-json-schema": "^3.20.4" }, diff --git a/packages/backend-defaults/report-systemMetadata.api.md b/packages/backend-defaults/report-systemMetadata.api.md index bd28437f2e..6f0f106039 100644 --- a/packages/backend-defaults/report-systemMetadata.api.md +++ b/packages/backend-defaults/report-systemMetadata.api.md @@ -19,6 +19,7 @@ export const actionsServiceFactory: ServiceFactory< ActionsService, import { BackstageInstance } from '@backstage/backend-plugin-api'; import { LoggerService } from '@backstage/backend-plugin-api'; +import { Observable } from '@backstage/types'; import { RootConfigService } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { SystemMetadataService } from '@backstage/backend-plugin-api'; @@ -32,9 +33,7 @@ export class DefaultSystemMetadataService implements SystemMetadataService { config: RootConfigService; }): DefaultSystemMetadataService; // (undocumented) - introspect(): Promise<{ - instances: BackstageInstance[]; - }>; + instances(): Observable; } // @alpha diff --git a/packages/backend-defaults/src/entrypoints/systemMetadata/SystemMetadataService.test.ts b/packages/backend-defaults/src/entrypoints/systemMetadata/SystemMetadataService.test.ts index 93b0d2dfb5..6bb0ab6fdc 100644 --- a/packages/backend-defaults/src/entrypoints/systemMetadata/SystemMetadataService.test.ts +++ b/packages/backend-defaults/src/entrypoints/systemMetadata/SystemMetadataService.test.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createSpecializedBackend } from '@backstage/backend-app-api'; +import { Backend, createSpecializedBackend } from '@backstage/backend-app-api'; import { systemMetadataServiceFactory } from './systemMetadataServiceFactory'; import { mockServices } from '@backstage/backend-test-utils'; import getPort from 'get-port'; @@ -29,111 +29,171 @@ const baseFactories = [ ]; describe('SystemMetadataService', () => { - it('should list plugins across instances', async () => { - const instance1HttpPort = await getPort(); - const instance2HttpPort = await getPort(); - const instance1 = createSpecializedBackend({ - defaultServiceFactories: [ - ...baseFactories, - systemMetadataServiceFactory, - mockServices.rootConfig.factory({ - data: { - backend: { - listen: { - port: instance1HttpPort, - }, - }, - discovery: { - instances: [ - { - baseUrl: `http://localhost:${instance1HttpPort}`, - }, - { - baseUrl: `http://localhost:${instance2HttpPort}`, - }, - ], + describe('multiple backends testing', () => { + let instance1: Backend; + let instance2: Backend; + let instance1HttpPort: number; + let instance2HttpPort: number; + + const configFactory = (port: number) => + mockServices.rootConfig.factory({ + data: { + backend: { + listen: { + port, }, }, - }), - ], + discovery: { + instances: [ + { + baseUrl: `http://localhost:${instance1HttpPort}`, + }, + { + baseUrl: `http://localhost:${instance2HttpPort}`, + }, + ], + }, + }, + }); + beforeEach(async () => { + instance1HttpPort = await getPort(); + instance2HttpPort = await getPort(); + // Setup code for multiple backend instances + instance1 = createSpecializedBackend({ + defaultServiceFactories: [ + ...baseFactories, + systemMetadataServiceFactory, + configFactory(instance1HttpPort), + ], + }); + + instance2 = createSpecializedBackend({ + defaultServiceFactories: [ + ...baseFactories, + systemMetadataServiceFactory, + configFactory(instance2HttpPort), + ], + }); }); - const instance2 = createSpecializedBackend({ - defaultServiceFactories: [ - ...baseFactories, - systemMetadataServiceFactory, - mockServices.rootConfig.factory({ - data: { - backend: { - listen: { - port: instance2HttpPort, + it('should list plugins across instances', async () => { + instance1.add( + createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: {}, + async init() { + // do nothing }, - }, - discovery: { - instances: [ - { - baseUrl: `http://localhost:${instance1HttpPort}`, - }, - { - baseUrl: `http://localhost:${instance2HttpPort}`, - }, - ], - }, + }); }, }), - ], + ); + + instance2.add( + createBackendPlugin({ + pluginId: 'test-other', + register(reg) { + reg.registerInit({ + deps: {}, + async init() { + // do nothing + }, + }); + }, + }), + ); + + await instance1.start(); + await instance2.start(); + + const instance1Response = await fetch( + `http://localhost:${instance1HttpPort}/.backstage/systemMetadata/v1/features/installed`, + ); + + expect(instance1Response.status).toBe(200); + await expect(instance1Response.json()).resolves.toMatchObject({ + test: [ + { + externalUrl: `http://localhost:${instance1HttpPort}`, + internalUrl: `http://localhost:${instance1HttpPort}`, + }, + ], + 'test-other': [ + { + externalUrl: `http://localhost:${instance2HttpPort}`, + internalUrl: `http://localhost:${instance2HttpPort}`, + }, + ], + }); + + const instance2Response = await fetch( + `http://localhost:${instance2HttpPort}/.backstage/systemMetadata/v1/features/installed`, + ); + + expect(instance2Response.status).toBe(200); + + await expect(instance2Response.json()).resolves.toMatchObject({ + test: [ + { + externalUrl: `http://localhost:${instance1HttpPort}`, + internalUrl: `http://localhost:${instance1HttpPort}`, + }, + ], + 'test-other': [ + { + externalUrl: `http://localhost:${instance2HttpPort}`, + internalUrl: `http://localhost:${instance2HttpPort}`, + }, + ], + }); }); - instance1.add( - createBackendPlugin({ - pluginId: 'test', - register(reg) { - reg.registerInit({ - deps: {}, - async init() { - // do nothing - }, - }); - }, - }), - ); + it('should list all known instances', async () => { + await instance1.start(); + await instance2.start(); - instance2.add( - createBackendPlugin({ - pluginId: 'test-other', - register(reg) { - reg.registerInit({ - deps: {}, - async init() { - // do nothing - }, - }); - }, - }), - ); + const instance1Response = await fetch( + `http://localhost:${instance1HttpPort}/.backstage/systemMetadata/v1/instances`, + ); - await instance1.start(); - await instance2.start(); + expect(instance1Response.status).toBe(200); + await expect(instance1Response.json()).resolves.toMatchObject({ + items: [ + { + externalUrl: `http://localhost:${instance1HttpPort}`, + internalUrl: `http://localhost:${instance1HttpPort}`, + }, + { + externalUrl: `http://localhost:${instance2HttpPort}`, + internalUrl: `http://localhost:${instance2HttpPort}`, + }, + ], + }); - const installedFeatures = await fetch( - `http://localhost:${instance1HttpPort}/.backstage/systemMetadata/v1/features/installed`, - ); + const instance2Response = await fetch( + `http://localhost:${instance2HttpPort}/.backstage/systemMetadata/v1/instances`, + ); - expect(installedFeatures.status).toBe(200); + expect(instance2Response.status).toBe(200); + await expect(instance2Response.json()).resolves.toMatchObject({ + items: [ + { + externalUrl: `http://localhost:${instance1HttpPort}`, + internalUrl: `http://localhost:${instance1HttpPort}`, + }, + { + externalUrl: `http://localhost:${instance2HttpPort}`, + internalUrl: `http://localhost:${instance2HttpPort}`, + }, + ], + }); + }); - await expect(installedFeatures.json()).resolves.toMatchObject({ - test: [ - { - externalUrl: `http://localhost:${instance1HttpPort}`, - internalUrl: `http://localhost:${instance1HttpPort}`, - }, - ], - 'test-other': [ - { - externalUrl: `http://localhost:${instance2HttpPort}`, - internalUrl: `http://localhost:${instance2HttpPort}`, - }, - ], + afterEach(async () => { + await instance1.stop(); + await instance2.stop(); }); }); }); diff --git a/packages/backend-defaults/src/entrypoints/systemMetadata/lib/DefaultSystemMetadataService.ts b/packages/backend-defaults/src/entrypoints/systemMetadata/lib/DefaultSystemMetadataService.ts index 5277e05cae..0ce468e2be 100644 --- a/packages/backend-defaults/src/entrypoints/systemMetadata/lib/DefaultSystemMetadataService.ts +++ b/packages/backend-defaults/src/entrypoints/systemMetadata/lib/DefaultSystemMetadataService.ts @@ -22,18 +22,126 @@ import { BackstageInstance, SystemMetadataService, } from '@backstage/backend-plugin-api'; +import { Observable } from '@backstage/types'; import z from 'zod'; +import ObservableImpl from 'zen-observable'; const targetObjectSchema = z.object({ internal: z.string(), external: z.string(), }); +/** + * A basic implementation of ReactiveX behavior subjects. + * + * A subject is a convenient way to create an observable when you want + * to fan out a single value to all subscribers. + * + * The BehaviorSubject will emit the most recently emitted value or error + * whenever a new observer subscribes to the subject. + * + * See http://reactivex.io/documentation/subject.html + * + * FORKED FROM core-app-api - where should this live? + */ + +export class BehaviorSubject + implements Observable, ZenObservable.SubscriptionObserver +{ + private isClosed: boolean; + private currentValue: T; + private terminatingError: Error | undefined; + private readonly observable: Observable; + + constructor(value: T) { + this.isClosed = false; + this.currentValue = value; + this.terminatingError = undefined; + this.observable = new ObservableImpl(subscriber => { + if (this.isClosed) { + if (this.terminatingError) { + subscriber.error(this.terminatingError); + } else { + subscriber.complete(); + } + return () => {}; + } + + subscriber.next(this.currentValue); + + this.subscribers.add(subscriber); + return () => { + this.subscribers.delete(subscriber); + }; + }); + } + + private readonly subscribers = new Set< + ZenObservable.SubscriptionObserver + >(); + + [Symbol.observable]() { + return this; + } + + get closed() { + return this.isClosed; + } + + next(value: T) { + if (this.isClosed) { + throw new Error('BehaviorSubject is closed'); + } + this.currentValue = value; + this.subscribers.forEach(subscriber => subscriber.next(value)); + } + + error(error: Error) { + if (this.isClosed) { + throw new Error('BehaviorSubject is closed'); + } + this.isClosed = true; + this.terminatingError = error; + this.subscribers.forEach(subscriber => subscriber.error(error)); + } + + complete() { + if (this.isClosed) { + throw new Error('BehaviorSubject is closed'); + } + this.isClosed = true; + this.subscribers.forEach(subscriber => subscriber.complete()); + } + + subscribe(observer: ZenObservable.Observer): ZenObservable.Subscription; + subscribe( + onNext: (value: T) => void, + onError?: (error: any) => void, + onComplete?: () => void, + ): ZenObservable.Subscription; + subscribe( + onNext: ZenObservable.Observer | ((value: T) => void), + onError?: (error: any) => void, + onComplete?: () => void, + ): ZenObservable.Subscription { + const observer = + typeof onNext === 'function' + ? { + next: onNext, + error: onError, + complete: onComplete, + } + : onNext; + + return this.observable.subscribe(observer); + } +} + /** * @alpha */ export class DefaultSystemMetadataService implements SystemMetadataService { - private instances: BackstageInstance[]; + private instance$: BehaviorSubject; constructor( private options: { logger: LoggerService; config: RootConfigService }, ) { @@ -60,9 +168,9 @@ export class DefaultSystemMetadataService implements SystemMetadataService { } return instances; }; - this.instances = getInstances(); + this.instance$ = new BehaviorSubject(getInstances()); this.options.config.subscribe?.(() => { - this.instances = getInstances(); + this.instance$.next(getInstances()); }); } @@ -73,9 +181,7 @@ export class DefaultSystemMetadataService implements SystemMetadataService { return new DefaultSystemMetadataService(pluginEnv); } - async introspect() { - return { - instances: this.instances, - }; + instances(): Observable { + return this.instance$; } } diff --git a/packages/backend-defaults/src/entrypoints/systemMetadata/lib/createSystemMetadataRouter.ts b/packages/backend-defaults/src/entrypoints/systemMetadata/lib/createSystemMetadataRouter.ts index fb7add8ed6..e24870f7d0 100644 --- a/packages/backend-defaults/src/entrypoints/systemMetadata/lib/createSystemMetadataRouter.ts +++ b/packages/backend-defaults/src/entrypoints/systemMetadata/lib/createSystemMetadataRouter.ts @@ -16,7 +16,10 @@ import { LoggerService } from '@backstage/backend-plugin-api'; import { BackendFeatureMeta } from '@backstage/backend-plugin-api/alpha'; -import type { SystemMetadataService } from '@backstage/backend-plugin-api'; +import type { + BackstageInstance, + SystemMetadataService, +} from '@backstage/backend-plugin-api'; import Router from 'express-promise-router'; export async function createSystemMetadataRouter(options: { @@ -25,23 +28,22 @@ export async function createSystemMetadataRouter(options: { }) { const { logger, systemMetadata } = options; - async function getInstances() { - const instances = await systemMetadata.introspect(); - return instances.instances; - } + let instances: BackstageInstance[] = []; + systemMetadata.instances().subscribe({ + next: value => { + instances = value; + }, + }); - logger.info( - `Instances in this system: ${JSON.stringify(await getInstances())}`, - ); + logger.info(`Instances in this system: ${JSON.stringify(instances)}`); const router = Router(); router.get('/instances', async (_, res) => { - res.json(await getInstances()); + res.json({ items: instances }); }); router.get('/features/installed', async (_, res) => { - const instances = await getInstances(); const featurePromises = await Promise.allSettled( instances.map(async instance => { const response = await fetch( diff --git a/packages/backend-plugin-api/report.api.md b/packages/backend-plugin-api/report.api.md index 5bb80fd6a7..abb20539db 100644 --- a/packages/backend-plugin-api/report.api.md +++ b/packages/backend-plugin-api/report.api.md @@ -14,6 +14,7 @@ import { isChildPath } from '@backstage/cli-common'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { Knex } from 'knex'; +import { Observable } from '@backstage/types'; import { Permission } from '@backstage/plugin-permission-common'; import { PermissionAttributes } from '@backstage/plugin-permission-common'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; @@ -762,9 +763,7 @@ export interface ServiceRefOptions< // @public (undocumented) export interface SystemMetadataService { // (undocumented) - introspect(): Promise<{ - instances: BackstageInstance[]; - }>; + instances(): Observable; } // @public diff --git a/packages/backend-plugin-api/src/services/definitions/SystemMetadataService.ts b/packages/backend-plugin-api/src/services/definitions/SystemMetadataService.ts index 83066225fe..36ab1ff0e9 100644 --- a/packages/backend-plugin-api/src/services/definitions/SystemMetadataService.ts +++ b/packages/backend-plugin-api/src/services/definitions/SystemMetadataService.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { Observable } from '@backstage/types'; + /** * @public */ @@ -26,5 +28,5 @@ export interface BackstageInstance { * @public */ export interface SystemMetadataService { - introspect(): Promise<{ instances: BackstageInstance[] }>; + instances(): Observable; } diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index cd4f68ead0..a771b9513f 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -79,6 +79,7 @@ "text-extensions": "^2.4.0", "uuid": "^11.0.0", "yn": "^4.0.0", + "zen-observable": "^0.10.0", "zod": "^3.22.4", "zod-to-json-schema": "^3.20.4" }, diff --git a/packages/backend-test-utils/report.api.md b/packages/backend-test-utils/report.api.md index 6892086505..08e6979928 100644 --- a/packages/backend-test-utils/report.api.md +++ b/packages/backend-test-utils/report.api.md @@ -9,6 +9,7 @@ import { AuthService } from '@backstage/backend-plugin-api'; import { Backend } from '@backstage/backend-app-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { BackstageCredentials } from '@backstage/backend-plugin-api'; +import { BackstageInstance } from '@backstage/backend-plugin-api'; import { BackstageNonePrincipal } from '@backstage/backend-plugin-api'; import { BackstagePrincipalAccessRestrictions } from '@backstage/backend-plugin-api'; import { BackstageServicePrincipal } from '@backstage/backend-plugin-api'; @@ -397,12 +398,17 @@ export namespace mockServices { partialImpl?: Partial | undefined, ) => ServiceMock; } + export function systemMetadata(options: { + instances: BackstageInstance[]; + }): SystemMetadataService; // (undocumented) export namespace systemMetadata { - const factory: () => ServiceFactory< + const factory: (options: { + instances: BackstageInstance[]; + }) => ServiceFactory< SystemMetadataService, 'root', - 'singleton' + 'singleton' | 'multiton' >; const mock: ( partialImpl?: Partial | undefined, diff --git a/packages/backend-test-utils/src/services/MockObservable.ts b/packages/backend-test-utils/src/services/MockObservable.ts new file mode 100644 index 0000000000..765b6ccb53 --- /dev/null +++ b/packages/backend-test-utils/src/services/MockObservable.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2025 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 { Observable } from '@backstage/types'; +import ObservableImpl from 'zen-observable'; + +export function createMockObservable(value: T): Observable { + return new ObservableImpl(observer => { + observer.next(value); + observer.complete(); + }); +} diff --git a/packages/backend-test-utils/src/services/MockSystemMetadataService.test.ts b/packages/backend-test-utils/src/services/MockSystemMetadataService.test.ts new file mode 100644 index 0000000000..8f78ec7567 --- /dev/null +++ b/packages/backend-test-utils/src/services/MockSystemMetadataService.test.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2025 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 { BackstageInstance } from '@backstage/backend-plugin-api'; +import { MockSystemMetadataService } from './MockSystemMetadataService'; + +describe('MockSystemMetadataService', () => { + it('should return the passed in instances', () => { + expect.assertions(1); + const instances: BackstageInstance[] = [ + { internalUrl: 'localhost:7007', externalUrl: 'external.url' }, + { internalUrl: 'localhost:7008', externalUrl: 'other.external.url' }, + ]; + const service = MockSystemMetadataService.create({ instances }); + service.instances().subscribe({ + next: value => { + expect(value).toEqual(instances); + }, + }); + }); +}); diff --git a/packages/backend-test-utils/src/services/MockSystemMetadataService.ts b/packages/backend-test-utils/src/services/MockSystemMetadataService.ts new file mode 100644 index 0000000000..eb8b39daf1 --- /dev/null +++ b/packages/backend-test-utils/src/services/MockSystemMetadataService.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2025 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 { + BackstageInstance, + SystemMetadataService, +} from '@backstage/backend-plugin-api'; +import { Observable } from '@backstage/types'; +import ObservableImpl from 'zen-observable'; + +/** + * @public + */ +export class MockSystemMetadataService implements SystemMetadataService { + #instances: BackstageInstance[]; + + constructor(instances: BackstageInstance[]) { + this.#instances = instances; + } + + public static create(options: { instances: BackstageInstance[] }) { + return new MockSystemMetadataService(options.instances); + } + + instances(): Observable { + return new ObservableImpl(subscriber => { + subscriber.next(this.#instances); + subscriber.complete(); + }); + } +} diff --git a/packages/backend-test-utils/src/services/mockServices.ts b/packages/backend-test-utils/src/services/mockServices.ts index 6047f8ea1c..4f89d62971 100644 --- a/packages/backend-test-utils/src/services/mockServices.ts +++ b/packages/backend-test-utils/src/services/mockServices.ts @@ -27,10 +27,10 @@ import { rootHealthServiceFactory } from '@backstage/backend-defaults/rootHealth import { rootHttpRouterServiceFactory } from '@backstage/backend-defaults/rootHttpRouter'; import { rootLifecycleServiceFactory } from '@backstage/backend-defaults/rootLifecycle'; import { urlReaderServiceFactory } from '@backstage/backend-defaults/urlReader'; -import { systemMetadataServiceFactory } from '@backstage/backend-defaults/systemMetadata'; import { AuthService, BackstageCredentials, + BackstageInstance, BackstageUserInfo, DatabaseService, DiscoveryService, @@ -41,6 +41,7 @@ import { SchedulerService, ServiceFactory, ServiceRef, + SystemMetadataService, UserInfoService, coreServices, createServiceFactory, @@ -62,6 +63,8 @@ import { simpleMock } from './simpleMock'; import { MockSchedulerService } from './MockSchedulerService'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { ObservableConfigProxy } from '../../../config-loader/src/sources/ObservableConfigProxy'; +import { MockSystemMetadataService } from './MockSystemMetadataService'; +import { createMockObservable } from './MockObservable'; /** @internal */ function createLoggerMock() { @@ -574,19 +577,33 @@ export namespace mockServices { rootInstanceMetadata, ); } + /** + * Creates a functional mock implementation for the + * {@link @backstage/backend-plugin-api#coreServices.systemMetadata}. + */ + export function systemMetadata(options: { + instances: BackstageInstance[]; + }): SystemMetadataService { + return MockSystemMetadataService.create(options); + } export namespace systemMetadata { /** * Creates a functional mock factory for the * {@link @backstage/backend-plugin-api#coreServices.systemMetadata}. */ - export const factory = () => systemMetadataServiceFactory; + export const factory = simpleFactoryWithOptions( + coreServices.systemMetadata, + systemMetadata, + ); /** * Creates a mock of the * {@link @backstage/backend-events-node#systemMetadata}, optionally * with some given method implementations. */ export const mock = simpleMock(coreServices.systemMetadata, () => ({ - introspect: jest.fn(), + instances: jest + .fn() + .mockReturnValue(createMockObservable([])), })); } } diff --git a/packages/backend-test-utils/src/services/simpleMock.ts b/packages/backend-test-utils/src/services/simpleMock.ts index 484969f235..137c1cba66 100644 --- a/packages/backend-test-utils/src/services/simpleMock.ts +++ b/packages/backend-test-utils/src/services/simpleMock.ts @@ -39,6 +39,7 @@ export function simpleMock( const mock = mockFactory(); if (partialImpl) { for (const [key, impl] of Object.entries(partialImpl)) { + console.log(key, impl, mock); if (typeof impl === 'function') { (mock as any)[key].mockImplementation(impl); } else { diff --git a/yarn.lock b/yarn.lock index d359ca5cbb..1d7fcf655e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2951,6 +2951,7 @@ __metadata: winston-transport: "npm:^4.5.0" yauzl: "npm:^3.0.0" yn: "npm:^4.0.0" + zen-observable: "npm:^0.10.0" zod: "npm:^3.22.4" zod-to-json-schema: "npm:^3.20.4" peerDependencies: @@ -3103,6 +3104,7 @@ __metadata: text-extensions: "npm:^2.4.0" uuid: "npm:^11.0.0" yn: "npm:^4.0.0" + zen-observable: "npm:^0.10.0" zod: "npm:^3.22.4" zod-to-json-schema: "npm:^3.20.4" languageName: unknown