chore: moving some files around and exporting things as alpha services

Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
benjdlambert
2025-06-16 16:34:38 +02:00
parent 36417fbe26
commit 8a75a84fc9
35 changed files with 344 additions and 149 deletions
+2 -1
View File
@@ -21,7 +21,8 @@
"license": "Apache-2.0",
"exports": {
".": "./src/index.ts",
"./package.json": "./package.json"
"./package.json": "./package.json",
"./alpha": "./src/alpha/index.ts"
},
"main": "src/index.ts",
"types": "src/index.ts",
@@ -0,0 +1,85 @@
## API Report File for "@backstage/backend-test-utils"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ActionsRegistryActionOptions } from '@backstage/backend-plugin-api/alpha';
import { ActionsRegistryService } from '@backstage/backend-plugin-api/alpha';
import { ActionsService } from '@backstage/backend-plugin-api/alpha';
import { ActionsServiceAction } from '@backstage/backend-plugin-api/alpha';
import { AnyZodObject } from 'zod';
import { BackstageCredentials } from '@backstage/backend-plugin-api';
import { JsonObject } from '@backstage/types';
import { JsonValue } from '@backstage/types';
import { LoggerService } from '@backstage/backend-plugin-api';
import { ServiceFactory } from '@backstage/backend-plugin-api';
// @alpha (undocumented)
export function actionsRegistryServiceMock(options?: {
logger: LoggerService;
}): MockActionsRegistry;
// @alpha (undocumented)
export namespace actionsRegistryServiceMock {
const // (undocumented)
factory: () => ServiceFactory<
ActionsRegistryService,
'plugin',
'singleton'
>;
const // (undocumented)
mock: (
partialImpl?: Partial<ActionsRegistryService> | undefined,
) => ServiceMock<ActionsRegistryService>;
}
// @alpha (undocumented)
export namespace actionsServiceMock {
const // (undocumented)
factory: () => ServiceFactory<ActionsService, 'plugin', 'singleton'>;
const // (undocumented)
mock: (
partialImpl?: Partial<ActionsService> | undefined,
) => ServiceMock<ActionsService>;
}
// @alpha
export class MockActionsRegistry
implements ActionsRegistryService, ActionsService
{
// (undocumented)
readonly actions: Map<string, ActionsRegistryActionOptions<any, any>>;
// (undocumented)
static create(opts: { logger: LoggerService }): MockActionsRegistry;
// (undocumented)
invoke(opts: {
id: string;
input?: JsonObject;
credentials?: BackstageCredentials;
}): Promise<{
output: JsonValue;
}>;
// (undocumented)
list(): Promise<{
actions: ActionsServiceAction[];
}>;
// (undocumented)
register<
TInputSchema extends AnyZodObject,
TOutputSchema extends AnyZodObject,
>(options: ActionsRegistryActionOptions<TInputSchema, TOutputSchema>): void;
}
// @alpha (undocumented)
export type ServiceMock<TService> = {
factory: ServiceFactory<TService>;
} & {
[Key in keyof TService]: TService[Key] extends (
...args: infer Args
) => infer Return
? TService[Key] & jest.MockInstance<Return, Args>
: TService[Key];
};
// (No @packageDocumentation comment for this package)
```
-59
View File
@@ -3,11 +3,6 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ActionsRegistryActionOptions } from '@backstage/backend-plugin-api';
import { ActionsRegistryService } from '@backstage/backend-plugin-api';
import { ActionsService } from '@backstage/backend-plugin-api';
import { ActionsServiceAction } from '@backstage/backend-plugin-api';
import { AnyZodObject } from 'zod';
import { AuditorService } from '@backstage/backend-plugin-api';
import { AuthorizeResult } from '@backstage/plugin-permission-common';
import { AuthService } from '@backstage/backend-plugin-api';
@@ -29,7 +24,6 @@ import { ExtensionPoint } from '@backstage/backend-plugin-api';
import { HttpAuthService } from '@backstage/backend-plugin-api';
import { HttpRouterService } from '@backstage/backend-plugin-api';
import { JsonObject } from '@backstage/types';
import { JsonValue } from '@backstage/types';
import Keyv from 'keyv';
import { Knex } from 'knex';
import { LifecycleService } from '@backstage/backend-plugin-api';
@@ -60,33 +54,6 @@ export interface CreateMockDirectoryOptions {
mockOsTmpDir?: boolean;
}
// @public
export class MockActionsRegistry
implements ActionsRegistryService, ActionsService
{
// (undocumented)
readonly actions: Map<string, ActionsRegistryActionOptions<any, any>>;
// (undocumented)
static create(opts: { logger: LoggerService }): MockActionsRegistry;
// (undocumented)
invoke(opts: {
id: string;
input?: JsonObject;
credentials?: BackstageCredentials;
}): Promise<{
output: JsonValue;
}>;
// (undocumented)
list(): Promise<{
actions: ActionsServiceAction[];
}>;
// (undocumented)
register<
TInputSchema extends AnyZodObject,
TOutputSchema extends AnyZodObject,
>(options: ActionsRegistryActionOptions<TInputSchema, TOutputSchema>): void;
}
// @public (undocumented)
export namespace mockCredentials {
export function limitedUser(
@@ -195,32 +162,6 @@ export function mockErrorHandler(): ErrorRequestHandler<
// @public
export namespace mockServices {
// (undocumented)
export namespace actions {
const // (undocumented)
factory: () => ServiceFactory<ActionsService, 'plugin', 'singleton'>;
const // (undocumented)
mock: (
partialImpl?: Partial<ActionsService> | undefined,
) => ServiceMock<ActionsService>;
}
// (undocumented)
export function actionsRegistry(options?: {
logger: LoggerService;
}): MockActionsRegistry;
// (undocumented)
export namespace actionsRegistry {
const // (undocumented)
factory: () => ServiceFactory<
ActionsRegistryService,
'plugin',
'singleton'
>;
const // (undocumented)
mock: (
partialImpl?: Partial<ActionsRegistryService> | undefined,
) => ServiceMock<ActionsRegistryService>;
}
// (undocumented)
export namespace auditor {
const // (undocumented)
@@ -14,5 +14,4 @@
* limitations under the License.
*/
export * from './wiring';
export * from './services';
@@ -0,0 +1,49 @@
/*
* 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 { mockServices } from '../../services';
import { LoggerService } from '@backstage/backend-plugin-api';
import { MockActionsRegistry } from './MockActionsRegistry';
import { simpleMock } from './simpleMock';
import {
ActionsRegistryService,
actionsRegistryServiceRef,
} from '@backstage/backend-plugin-api/alpha';
import { actionsRegistryServiceFactory } from '@backstage/backend-defaults/alpha';
/**
* @alpha
*/
export function actionsRegistryServiceMock(options?: {
logger: LoggerService;
}): MockActionsRegistry {
return MockActionsRegistry.create({
logger: options?.logger ?? mockServices.logger.mock(),
});
}
/**
* @alpha
*/
export namespace actionsRegistryServiceMock {
export const factory = () => actionsRegistryServiceFactory;
export const mock = simpleMock<ActionsRegistryService>(
actionsRegistryServiceRef,
() => ({
register: jest.fn(),
}),
);
}
@@ -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 { simpleMock } from './simpleMock';
import {
ActionsService,
actionsServiceRef,
} from '@backstage/backend-plugin-api/alpha';
import { actionsServiceFactory } from '@backstage/backend-defaults/alpha';
/**
* @alpha
*/
export namespace actionsServiceMock {
export const factory = () => actionsServiceFactory;
export const mock = simpleMock<ActionsService>(actionsServiceRef, () => ({
invoke: jest.fn(),
list: jest.fn(),
}));
}
@@ -17,15 +17,19 @@ import {
coreServices,
createBackendPlugin,
} from '@backstage/backend-plugin-api';
import { startTestBackend } from '../wiring';
import { mockServices } from './mockServices';
import { mockCredentials } from './mockCredentials';
import { startTestBackend } from '../../wiring';
import { actionsRegistryServiceMock } from './ActionsRegistryServiceMock';
import { mockCredentials } from '../../services';
import { Router } from 'express';
import supertest from 'supertest';
import {
actionsRegistryServiceRef,
actionsServiceRef,
} from '@backstage/backend-plugin-api/alpha';
describe('MockActionsRegistry', () => {
it('should be able to register and invoke actions', async () => {
const registry = mockServices.actionsRegistry();
const registry = actionsRegistryServiceMock();
registry.register({
name: 'my-demo-action',
@@ -53,7 +57,7 @@ describe('MockActionsRegistry', () => {
});
it('should throw an error when the input is invalid to the action', async () => {
const registry = mockServices.actionsRegistry();
const registry = actionsRegistryServiceMock();
registry.register({
name: 'my-demo-action',
@@ -72,7 +76,7 @@ describe('MockActionsRegistry', () => {
});
it('should throw an error when the action is not found', async () => {
const registry = mockServices.actionsRegistry();
const registry = actionsRegistryServiceMock();
await expect(registry.invoke({ id: 'test' })).rejects.toThrow(
'Action "test" not found, available actions: none',
@@ -80,7 +84,7 @@ describe('MockActionsRegistry', () => {
});
it('should throw an error when the action is not found with recommended actions', async () => {
const registry = mockServices.actionsRegistry();
const registry = actionsRegistryServiceMock();
registry.register({
name: 'my-demo-action',
@@ -99,7 +103,7 @@ describe('MockActionsRegistry', () => {
});
it('should throw an error when the output is invalid', async () => {
const registry = mockServices.actionsRegistry();
const registry = actionsRegistryServiceMock();
registry.register({
name: 'my-demo-action',
@@ -119,7 +123,7 @@ describe('MockActionsRegistry', () => {
});
it('should list the actions correctly', async () => {
const registry = mockServices.actionsRegistry();
const registry = actionsRegistryServiceMock();
registry.register({
name: 'my-demo-action',
@@ -165,13 +169,13 @@ describe('MockActionsRegistry', () => {
});
});
describe('mockServices.actions + mockService.actionsRegistry', () => {
describe('actionsRegistryServiceMock + mockService.actionsRegistry', () => {
it('should be able to register and invoke actions', async () => {
const pluginWithAction = createBackendPlugin({
pluginId: 'my-plugin',
register(reg) {
reg.registerInit({
deps: { actionsRegistry: coreServices.actionsRegistry },
deps: { actionsRegistry: actionsRegistryServiceRef },
async init({ actionsRegistry }) {
actionsRegistry.register({
name: 'test',
@@ -196,7 +200,7 @@ describe('MockActionsRegistry', () => {
register(reg) {
reg.registerInit({
deps: {
actions: coreServices.actions,
actions: actionsServiceRef,
router: coreServices.httpRouter,
},
async init({ actions, router }) {
@@ -14,10 +14,6 @@
* limitations under the License.
*/
import {
ActionsRegistryActionOptions,
ActionsRegistryService,
ActionsService,
ActionsServiceAction,
BackstageCredentials,
LoggerService,
} from '@backstage/backend-plugin-api';
@@ -25,7 +21,13 @@ import { ForwardedError, InputError, NotFoundError } from '@backstage/errors';
import { JsonObject, JsonValue } from '@backstage/types';
import { z, AnyZodObject } from 'zod';
import zodToJsonSchema from 'zod-to-json-schema';
import { mockCredentials } from './mockCredentials';
import { mockCredentials } from '../../services';
import {
ActionsRegistryActionOptions,
ActionsRegistryService,
ActionsService,
ActionsServiceAction,
} from '@backstage/backend-plugin-api/alpha';
/**
* A mock implementation of the ActionsRegistryService and ActionsService that can be used in tests.
@@ -58,7 +60,7 @@ import { mockCredentials } from './mockCredentials';
* expect(result).toEqual({ output: { name: 'test' } });
* ```
*
* @public
* @alpha
*/
export class MockActionsRegistry
implements ActionsRegistryService, ActionsService
@@ -0,0 +1,20 @@
/*
* 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.
*/
export { actionsRegistryServiceMock } from './ActionsRegistryServiceMock';
export { MockActionsRegistry } from './MockActionsRegistry';
export { actionsServiceMock } from './ActionsServiceMock';
export { type ServiceMock } from './simpleMock';
@@ -0,0 +1,57 @@
/*
* 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 {
createServiceFactory,
ServiceFactory,
ServiceRef,
} from '@backstage/backend-plugin-api';
/** @alpha */
export type ServiceMock<TService> = {
factory: ServiceFactory<TService>;
} & {
[Key in keyof TService]: TService[Key] extends (
...args: infer Args
) => infer Return
? TService[Key] & jest.MockInstance<Return, Args>
: TService[Key];
};
/** @internal */
export function simpleMock<TService>(
ref: ServiceRef<TService, any>,
mockFactory: () => jest.Mocked<TService>,
): (partialImpl?: Partial<TService>) => ServiceMock<TService> {
return partialImpl => {
const mock = mockFactory();
if (partialImpl) {
for (const [key, impl] of Object.entries(partialImpl)) {
if (typeof impl === 'function') {
(mock as any)[key].mockImplementation(impl);
} else {
(mock as any)[key] = impl;
}
}
}
return Object.assign(mock, {
factory: createServiceFactory({
service: ref,
deps: {},
factory: () => mock,
}),
}) as ServiceMock<TService>;
};
}
+2 -1
View File
@@ -24,5 +24,6 @@ export * from './cache';
export * from './database';
export * from './msw';
export * from './filesystem';
export * from './next';
export * from './services';
export * from './wiring';
export { mockErrorHandler } from './util';
@@ -13,6 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { mockServices, type ServiceMock } from './mockServices';
export { mockServices } from './mockServices';
export { mockCredentials } from './mockCredentials';
export { type MockActionsRegistry } from './MockActionsRegistry';
export { type ServiceMock } from './simpleMock';
@@ -55,10 +55,8 @@ import { MockRootLoggerService } from './MockRootLoggerService';
import { MockUserInfoService } from './MockUserInfoService';
import { mockCredentials } from './mockCredentials';
import { MockEventsService } from './MockEventsService';
import { actionsServiceFactory } from '@backstage/backend-defaults/actions';
import { actionsRegistryServiceFactory } from '@backstage/backend-defaults/actionsRegistry';
import { MockPermissionsService } from './MockPermissionsService';
import { MockActionsRegistry } from './MockActionsRegistry';
import { simpleMock } from './simpleMock';
/** @internal */
function createLoggerMock() {
@@ -95,43 +93,6 @@ function simpleFactoryWithOptions<
((...options: TOptions) => ServiceFactory<TService, TScope>);
}
/** @public */
export type ServiceMock<TService> = {
factory: ServiceFactory<TService>;
} & {
[Key in keyof TService]: TService[Key] extends (
...args: infer Args
) => infer Return
? TService[Key] & jest.MockInstance<Return, Args>
: TService[Key];
};
/** @internal */
function simpleMock<TService>(
ref: ServiceRef<TService, any>,
mockFactory: () => jest.Mocked<TService>,
): (partialImpl?: Partial<TService>) => ServiceMock<TService> {
return partialImpl => {
const mock = mockFactory();
if (partialImpl) {
for (const [key, impl] of Object.entries(partialImpl)) {
if (typeof impl === 'function') {
(mock as any)[key].mockImplementation(impl);
} else {
(mock as any)[key] = impl;
}
}
}
return Object.assign(mock, {
factory: createServiceFactory({
service: ref,
deps: {},
factory: () => mock,
}),
}) as ServiceMock<TService>;
};
}
/**
* Mock implementations of the core services, to be used in tests.
*
@@ -553,28 +514,6 @@ export namespace mockServices {
search: jest.fn(),
}));
}
export namespace actions {
export const factory = () => actionsServiceFactory;
export const mock = simpleMock(coreServices.actions, () => ({
list: jest.fn(),
invoke: jest.fn(),
}));
}
export function actionsRegistry(options?: {
logger: LoggerService;
}): MockActionsRegistry {
return MockActionsRegistry.create({
logger: options?.logger ?? mockServices.logger.mock(),
});
}
export namespace actionsRegistry {
export const factory = () => actionsRegistryServiceFactory;
export const mock = simpleMock(coreServices.actionsRegistry, () => ({
register: jest.fn(),
}));
}
/**
* Creates a functional mock implementation of the
@@ -0,0 +1,57 @@
/*
* 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 {
createServiceFactory,
ServiceFactory,
ServiceRef,
} from '@backstage/backend-plugin-api';
/** @public */
export type ServiceMock<TService> = {
factory: ServiceFactory<TService>;
} & {
[Key in keyof TService]: TService[Key] extends (
...args: infer Args
) => infer Return
? TService[Key] & jest.MockInstance<Return, Args>
: TService[Key];
};
/** @internal */
export function simpleMock<TService>(
ref: ServiceRef<TService, any>,
mockFactory: () => jest.Mocked<TService>,
): (partialImpl?: Partial<TService>) => ServiceMock<TService> {
return partialImpl => {
const mock = mockFactory();
if (partialImpl) {
for (const [key, impl] of Object.entries(partialImpl)) {
if (typeof impl === 'function') {
(mock as any)[key].mockImplementation(impl);
} else {
(mock as any)[key] = impl;
}
}
}
return Object.assign(mock, {
factory: createServiceFactory({
service: ref,
deps: {},
factory: () => mock,
}),
}) as ServiceMock<TService>;
};
}
@@ -15,7 +15,7 @@
*/
import { MiddlewareFactory } from '@backstage/backend-defaults/rootHttpRouter';
import { mockServices } from '../next';
import { mockServices } from '../services';
/**
* A mock for error handler middleware that can be used in router tests.
@@ -20,7 +20,7 @@ import { defaultServiceFactories } from './TestBackend';
// 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';
import { ServiceRegistry } from '../../../backend-app-api/src/wiring/ServiceRegistry';
/**
* Options for {@link ServiceFactoryTester}.
@@ -31,7 +31,7 @@ import express from 'express';
import {
InternalBackendFeature,
InternalBackendRegistrations,
} from '../../../../backend-plugin-api/src/wiring/types';
} from '../../../backend-plugin-api/src/wiring/types';
import {
DefaultRootHttpRouter,
ExtendedHttpServer,
@@ -40,6 +40,10 @@ import {
createHttpServer,
} from '@backstage/backend-defaults/rootHttpRouter';
import { HostDiscovery } from '@backstage/backend-defaults/discovery';
import {
actionsRegistryServiceMock,
actionsServiceMock,
} from '../alpha/services';
/** @public */
export interface TestBackendOptions<TExtensionPoints extends any[]> {
@@ -84,8 +88,10 @@ export const defaultServiceFactories = [
mockServices.userInfo.factory(),
mockServices.urlReader.factory(),
mockServices.events.factory(),
mockServices.actionsRegistry.factory(),
mockServices.actions.factory(),
// Alpha services
actionsRegistryServiceMock.factory(),
actionsServiceMock.factory(),
];
/**