Merge pull request #15530 from backstage/blam/add-mocks-test-backend
Add all `coreServices` to `startTestBackend`
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-tasks': patch
|
||||
---
|
||||
|
||||
Added a new static `TaskScheduler.forPlugin` method.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-test-utils': patch
|
||||
---
|
||||
|
||||
The backend started by `startTestBackend` now has default implementations of all core services. It now also returns a `TestBackend` instance, which provides access to the underlying `server` that can be used with testing libraries such as `supertest`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-app-api': patch
|
||||
---
|
||||
|
||||
Exported the default root HTTP router implementation as `DefaultRootHttpRouter`. It only implements the routing layer and needs to be exposed via an HTTP server similar to the built-in setup in the `rootHttpRouterFactory`.
|
||||
@@ -12,6 +12,7 @@ import { CorsOptions } from 'cors';
|
||||
import { ErrorRequestHandler } from 'express';
|
||||
import { Express as Express_2 } from 'express';
|
||||
import { ExtensionPoint } from '@backstage/backend-plugin-api';
|
||||
import { Handler } from 'express';
|
||||
import { HelmetOptions } from 'helmet';
|
||||
import * as http from 'http';
|
||||
import { HttpRouterService } from '@backstage/backend-plugin-api';
|
||||
@@ -79,6 +80,21 @@ export const databaseFactory: (
|
||||
options?: undefined,
|
||||
) => ServiceFactory<PluginDatabaseManager>;
|
||||
|
||||
// @public
|
||||
export class DefaultRootHttpRouter implements RootHttpRouterService {
|
||||
// (undocumented)
|
||||
static create(options?: DefaultRootHttpRouterOptions): DefaultRootHttpRouter;
|
||||
// (undocumented)
|
||||
handler(): Handler;
|
||||
// (undocumented)
|
||||
use(path: string, handler: Handler): void;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface DefaultRootHttpRouterOptions {
|
||||
indexPath?: string | false;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export const discoveryFactory: (
|
||||
options?: undefined,
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"express-promise-router": "^4.1.0",
|
||||
"fs-extra": "10.1.0",
|
||||
"helmet": "^6.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"minimatch": "^5.0.0",
|
||||
"morgan": "^1.10.0",
|
||||
"node-forge": "^1.3.1",
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
/** @public */
|
||||
export const databaseFactory = createServiceFactory({
|
||||
@@ -28,7 +29,16 @@ export const databaseFactory = createServiceFactory({
|
||||
plugin: coreServices.pluginMetadata,
|
||||
},
|
||||
async factory({ config }) {
|
||||
const databaseManager = DatabaseManager.fromConfig(config);
|
||||
const databaseManager = config.getOptional('backend.database')
|
||||
? DatabaseManager.fromConfig(config)
|
||||
: DatabaseManager.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
database: { client: 'better-sqlite3', connection: ':memory:' },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return async ({ plugin }) => {
|
||||
return databaseManager.forPlugin(plugin.getId());
|
||||
};
|
||||
|
||||
+10
-4
@@ -14,9 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RestrictedIndexedRouter } from './RestrictedIndexedRouter';
|
||||
import { DefaultRootHttpRouter } from './DefaultRootHttpRouter';
|
||||
|
||||
describe('RestrictedIndexedRouter', () => {
|
||||
describe('DefaultRootHttpRouter', () => {
|
||||
it.each([
|
||||
[['/b'], '/a'],
|
||||
[['/a'], '/aa/b'],
|
||||
@@ -25,7 +25,7 @@ describe('RestrictedIndexedRouter', () => {
|
||||
[['/b/a'], '/a'],
|
||||
[['/a'], '/aa'],
|
||||
])(`with existing paths %s, adds %s without conflict`, (existing, added) => {
|
||||
const router = new RestrictedIndexedRouter(false);
|
||||
const router = DefaultRootHttpRouter.create();
|
||||
for (const path of existing) {
|
||||
router.use(path, () => {});
|
||||
}
|
||||
@@ -39,7 +39,7 @@ describe('RestrictedIndexedRouter', () => {
|
||||
])(
|
||||
`find conflict when existing paths %s, adds %s`,
|
||||
(existing, added, conflict) => {
|
||||
const router = new RestrictedIndexedRouter(false);
|
||||
const router = DefaultRootHttpRouter.create();
|
||||
for (const path of existing) {
|
||||
router.use(path, () => {});
|
||||
}
|
||||
@@ -48,4 +48,10 @@ describe('RestrictedIndexedRouter', () => {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('should not be possible to supply an empty indexPath', () => {
|
||||
expect(() => DefaultRootHttpRouter.create({ indexPath: '' })).toThrow(
|
||||
'indexPath option may not be an empty string',
|
||||
);
|
||||
});
|
||||
});
|
||||
+41
-5
@@ -16,23 +16,59 @@
|
||||
|
||||
import { RootHttpRouterService } from '@backstage/backend-plugin-api';
|
||||
import { Handler, Router } from 'express';
|
||||
import trimEnd from 'lodash/trimEnd';
|
||||
|
||||
function normalizePath(path: string): string {
|
||||
return path.replace(/\/*$/, '/');
|
||||
return `${trimEnd(path, '/')}/`;
|
||||
}
|
||||
|
||||
export class RestrictedIndexedRouter implements RootHttpRouterService {
|
||||
#indexPath?: false | string;
|
||||
/**
|
||||
* Options for the {@link DefaultRootHttpRouter} class.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface DefaultRootHttpRouterOptions {
|
||||
/**
|
||||
* The path to forward all unmatched requests to. Defaults to '/api/app' if
|
||||
* not given. Disables index path behavior if false is given.
|
||||
*/
|
||||
indexPath?: string | false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The default implementation of the {@link @backstage/backend-plugin-api#RootHttpRouterService} interface for
|
||||
* {@link @backstage/backend-plugin-api#coreServices.rootHttpRouter}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export class DefaultRootHttpRouter implements RootHttpRouterService {
|
||||
#indexPath?: string;
|
||||
|
||||
#router = Router();
|
||||
#namedRoutes = Router();
|
||||
#indexRouter = Router();
|
||||
#existingPaths = new Array<string>();
|
||||
|
||||
constructor(indexPath?: false | string) {
|
||||
static create(options?: DefaultRootHttpRouterOptions) {
|
||||
let indexPath;
|
||||
if (options?.indexPath === false) {
|
||||
indexPath = undefined;
|
||||
} else if (options?.indexPath === undefined) {
|
||||
indexPath = '/api/app';
|
||||
} else if (options?.indexPath === '') {
|
||||
throw new Error('indexPath option may not be an empty string');
|
||||
} else {
|
||||
indexPath = options.indexPath;
|
||||
}
|
||||
return new DefaultRootHttpRouter(indexPath);
|
||||
}
|
||||
|
||||
private constructor(indexPath?: string) {
|
||||
this.#indexPath = indexPath;
|
||||
this.#router.use(this.#namedRoutes);
|
||||
this.#router.use(this.#indexRouter);
|
||||
if (this.#indexPath) {
|
||||
this.#router.use(this.#indexRouter);
|
||||
}
|
||||
}
|
||||
|
||||
use(path: string, handler: Handler) {
|
||||
@@ -14,8 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { rootHttpRouterFactory } from './rootHttpRouterFactory';
|
||||
export type {
|
||||
RootHttpRouterFactoryOptions,
|
||||
RootHttpRouterConfigureOptions,
|
||||
export {
|
||||
rootHttpRouterFactory,
|
||||
type RootHttpRouterFactoryOptions,
|
||||
type RootHttpRouterConfigureOptions,
|
||||
} from './rootHttpRouterFactory';
|
||||
export {
|
||||
DefaultRootHttpRouter,
|
||||
type DefaultRootHttpRouterOptions,
|
||||
} from './DefaultRootHttpRouter';
|
||||
|
||||
+4
-4
@@ -27,7 +27,7 @@ import {
|
||||
MiddlewareFactory,
|
||||
readHttpServerOptions,
|
||||
} from '../../../http';
|
||||
import { RestrictedIndexedRouter } from './RestrictedIndexedRouter';
|
||||
import { DefaultRootHttpRouter } from './DefaultRootHttpRouter';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -46,7 +46,8 @@ export interface RootHttpRouterConfigureOptions {
|
||||
*/
|
||||
export type RootHttpRouterFactoryOptions = {
|
||||
/**
|
||||
* The path to forward all unmatched requests to. Defaults to '/api/app'
|
||||
* The path to forward all unmatched requests to. Defaults to '/api/app' if
|
||||
* not given. Disables index path behavior if false is given.
|
||||
*/
|
||||
indexPath?: string | false;
|
||||
|
||||
@@ -82,11 +83,10 @@ export const rootHttpRouterFactory = createServiceFactory({
|
||||
configure = defaultConfigure,
|
||||
}: RootHttpRouterFactoryOptions = {},
|
||||
) {
|
||||
const router = new RestrictedIndexedRouter(indexPath ?? '/api/app');
|
||||
const logger = rootLogger.child({ service: 'rootHttpRouter' });
|
||||
|
||||
const app = express();
|
||||
|
||||
const router = DefaultRootHttpRouter.create({ indexPath });
|
||||
const middleware = MiddlewareFactory.create({ config, logger });
|
||||
|
||||
configure({
|
||||
|
||||
+10
-5
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { loggerToWinstonLogger } from '@backstage/backend-common';
|
||||
import {
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
@@ -24,13 +25,17 @@ import { TaskScheduler } from '@backstage/backend-tasks';
|
||||
export const schedulerFactory = createServiceFactory({
|
||||
service: coreServices.scheduler,
|
||||
deps: {
|
||||
config: coreServices.config,
|
||||
plugin: coreServices.pluginMetadata,
|
||||
databaseManager: coreServices.database,
|
||||
logger: coreServices.logger,
|
||||
},
|
||||
async factory({ config }) {
|
||||
const taskScheduler = TaskScheduler.fromConfig(config);
|
||||
return async ({ plugin }) => {
|
||||
return taskScheduler.forPlugin(plugin.getId());
|
||||
async factory() {
|
||||
return async ({ plugin, databaseManager, logger }) => {
|
||||
return TaskScheduler.forPlugin({
|
||||
pluginId: plugin.getId(),
|
||||
databaseManager,
|
||||
logger: loggerToWinstonLogger(logger),
|
||||
});
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DatabaseManager } from '@backstage/backend-common';
|
||||
import { Duration } from 'luxon';
|
||||
import { HumanDuration as HumanDuration_2 } from '@backstage/types';
|
||||
import { Logger } from 'winston';
|
||||
import { PluginDatabaseManager } from '@backstage/backend-common';
|
||||
|
||||
// @public @deprecated
|
||||
export type HumanDuration = HumanDuration_2;
|
||||
@@ -74,6 +75,12 @@ export class TaskScheduler {
|
||||
constructor(databaseManager: DatabaseManager, logger: Logger);
|
||||
forPlugin(pluginId: string): PluginTaskScheduler;
|
||||
// (undocumented)
|
||||
static forPlugin(opts: {
|
||||
pluginId: string;
|
||||
databaseManager: PluginDatabaseManager;
|
||||
logger: Logger;
|
||||
}): PluginTaskScheduler;
|
||||
// (undocumented)
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
options?: {
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { DatabaseManager, getRootLogger } from '@backstage/backend-common';
|
||||
import {
|
||||
DatabaseManager,
|
||||
getRootLogger,
|
||||
PluginDatabaseManager,
|
||||
} from '@backstage/backend-common';
|
||||
import { Config } from '@backstage/config';
|
||||
import { once } from 'lodash';
|
||||
import { Duration } from 'luxon';
|
||||
@@ -57,27 +61,35 @@ export class TaskScheduler {
|
||||
* @returns A {@link PluginTaskScheduler} instance
|
||||
*/
|
||||
forPlugin(pluginId: string): PluginTaskScheduler {
|
||||
const databaseFactory = once(async () => {
|
||||
const databaseManager = this.databaseManager.forPlugin(pluginId);
|
||||
const knex = await databaseManager.getClient();
|
||||
return TaskScheduler.forPlugin({
|
||||
pluginId,
|
||||
databaseManager: this.databaseManager.forPlugin(pluginId),
|
||||
logger: this.logger,
|
||||
});
|
||||
}
|
||||
|
||||
if (!databaseManager.migrations?.skip) {
|
||||
static forPlugin(opts: {
|
||||
pluginId: string;
|
||||
databaseManager: PluginDatabaseManager;
|
||||
logger: Logger;
|
||||
}): PluginTaskScheduler {
|
||||
const databaseFactory = once(async () => {
|
||||
const knex = await opts.databaseManager.getClient();
|
||||
|
||||
if (!opts.databaseManager.migrations?.skip) {
|
||||
await migrateBackendTasks(knex);
|
||||
}
|
||||
|
||||
const janitor = new PluginTaskSchedulerJanitor({
|
||||
knex,
|
||||
waitBetweenRuns: Duration.fromObject({ minutes: 1 }),
|
||||
logger: this.logger,
|
||||
logger: opts.logger,
|
||||
});
|
||||
janitor.start();
|
||||
|
||||
return knex;
|
||||
});
|
||||
|
||||
return new PluginTaskSchedulerImpl(
|
||||
databaseFactory,
|
||||
this.logger.child({ plugin: pluginId }),
|
||||
);
|
||||
return new PluginTaskSchedulerImpl(databaseFactory, opts.logger);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
```ts
|
||||
import { Backend } from '@backstage/backend-app-api';
|
||||
import { BackendFeature } from '@backstage/backend-plugin-api';
|
||||
import { ConfigService } from '@backstage/backend-plugin-api';
|
||||
import { ExtendedHttpServer } from '@backstage/backend-app-api';
|
||||
import { ExtensionPoint } from '@backstage/backend-plugin-api';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { Knex } from 'knex';
|
||||
import { ServiceFactory } from '@backstage/backend-plugin-api';
|
||||
import { ServiceRef } from '@backstage/backend-plugin-api';
|
||||
@@ -13,6 +16,15 @@ import { ServiceRef } from '@backstage/backend-plugin-api';
|
||||
// @public (undocumented)
|
||||
export function isDockerDisabledForTests(): boolean;
|
||||
|
||||
// @public (undocumented)
|
||||
export const mockConfigFactory: (
|
||||
options?:
|
||||
| {
|
||||
data?: JsonObject | undefined;
|
||||
}
|
||||
| undefined,
|
||||
) => ServiceFactory<ConfigService>;
|
||||
|
||||
// @public
|
||||
export function setupRequestMockHandlers(worker: {
|
||||
listen: (t: any) => void;
|
||||
@@ -24,7 +36,14 @@ export function setupRequestMockHandlers(worker: {
|
||||
export function startTestBackend<
|
||||
TServices extends any[],
|
||||
TExtensionPoints extends any[],
|
||||
>(options: TestBackendOptions<TServices, TExtensionPoints>): Promise<Backend>;
|
||||
>(
|
||||
options: TestBackendOptions<TServices, TExtensionPoints>,
|
||||
): Promise<TestBackend>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface TestBackend extends Backend {
|
||||
readonly server: ExtendedHttpServer;
|
||||
}
|
||||
|
||||
// @alpha (undocumented)
|
||||
export interface TestBackendOptions<
|
||||
|
||||
@@ -39,7 +39,10 @@
|
||||
"@backstage/backend-plugin-api": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"better-sqlite3": "^8.0.0",
|
||||
"express": "^4.17.1",
|
||||
"express-promise-router": "^4.1.0",
|
||||
"knex": "^2.0.0",
|
||||
"msw": "^0.49.0",
|
||||
"mysql2": "^2.2.5",
|
||||
@@ -48,7 +51,9 @@
|
||||
"uuid": "^8.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"supertest": "^6.1.3"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2023 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 { mockConfigFactory } from './mockConfigService';
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2022 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 {
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
|
||||
/** @public */
|
||||
export const mockConfigFactory = createServiceFactory({
|
||||
service: coreServices.config,
|
||||
deps: {},
|
||||
async factory(_, options?: { data?: JsonObject }) {
|
||||
return new ConfigReader(options?.data, 'mock-config');
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2023 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 { TokenManager } from '@backstage/backend-common';
|
||||
import {
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
class TokenManagerMock implements TokenManager {
|
||||
async getToken(): Promise<{ token: string }> {
|
||||
return { token: 'mock-token' };
|
||||
}
|
||||
async authenticate(token: string): Promise<void> {
|
||||
if (token !== 'mock-token') {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const mockTokenManagerFactory = createServiceFactory({
|
||||
service: coreServices.tokenManager,
|
||||
deps: {},
|
||||
async factory() {
|
||||
return async () => {
|
||||
return new TokenManagerMock();
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -15,3 +15,4 @@
|
||||
*/
|
||||
|
||||
export * from './wiring';
|
||||
export * from './implementations';
|
||||
|
||||
@@ -20,7 +20,11 @@ import {
|
||||
createServiceFactory,
|
||||
createServiceRef,
|
||||
coreServices,
|
||||
createBackendPlugin,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { Router } from 'express';
|
||||
import request from 'supertest';
|
||||
|
||||
import { startTestBackend } from './TestBackend';
|
||||
|
||||
// This bit makes sure that test backends are cleaned up properly
|
||||
@@ -156,4 +160,66 @@ describe('TestBackend', () => {
|
||||
await backend.stop();
|
||||
expect(shutdownSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should provide a set of default services', async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const testPlugin = createBackendPlugin({
|
||||
id: 'test',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
cache: coreServices.cache,
|
||||
config: coreServices.config,
|
||||
database: coreServices.database,
|
||||
discovery: coreServices.discovery,
|
||||
httpRouter: coreServices.httpRouter,
|
||||
lifecycle: coreServices.lifecycle,
|
||||
logger: coreServices.logger,
|
||||
permissions: coreServices.permissions,
|
||||
pluginMetadata: coreServices.pluginMetadata,
|
||||
rootHttpRouter: coreServices.rootHttpRouter,
|
||||
rootLifecycle: coreServices.rootLifecycle,
|
||||
rootLogger: coreServices.rootLogger,
|
||||
scheduler: coreServices.scheduler,
|
||||
tokenManager: coreServices.tokenManager,
|
||||
urlReader: coreServices.urlReader,
|
||||
},
|
||||
async init(deps) {
|
||||
expect(Object.keys(deps)).toHaveLength(15);
|
||||
expect(Object.values(deps)).not.toContain(undefined);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await startTestBackend({
|
||||
services: [],
|
||||
features: [testPlugin()],
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow making requests via supertest', async () => {
|
||||
const testPlugin = createBackendPlugin({
|
||||
id: 'test',
|
||||
register(env) {
|
||||
env.registerInit({
|
||||
deps: {
|
||||
httpRouter: coreServices.httpRouter,
|
||||
},
|
||||
async init({ httpRouter }) {
|
||||
const router = Router();
|
||||
router.use('/ping-me', (_, res) => res.json({ message: 'pong' }));
|
||||
httpRouter.use(router);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { server } = await startTestBackend({ features: [testPlugin()] });
|
||||
|
||||
const res = await request(server).get('/api/test/ping-me');
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.body).toEqual({ message: 'pong' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,15 +21,32 @@ import {
|
||||
rootLifecycleFactory,
|
||||
loggerFactory,
|
||||
rootLoggerFactory,
|
||||
cacheFactory,
|
||||
permissionsFactory,
|
||||
schedulerFactory,
|
||||
urlReaderFactory,
|
||||
databaseFactory,
|
||||
httpRouterFactory,
|
||||
MiddlewareFactory,
|
||||
createHttpServer,
|
||||
ExtendedHttpServer,
|
||||
DefaultRootHttpRouter,
|
||||
} from '@backstage/backend-app-api';
|
||||
import { SingleHostDiscovery } from '@backstage/backend-common';
|
||||
import {
|
||||
ServiceFactory,
|
||||
ServiceRef,
|
||||
createServiceFactory,
|
||||
BackendFeature,
|
||||
ExtensionPoint,
|
||||
coreServices,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
|
||||
import { mockConfigFactory } from '../implementations/mockConfigService';
|
||||
import { mockTokenManagerFactory } from '../implementations/mockTokenManagerService';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import express from 'express';
|
||||
|
||||
/** @alpha */
|
||||
export interface TestBackendOptions<
|
||||
TServices extends any[],
|
||||
@@ -54,11 +71,30 @@ export interface TestBackendOptions<
|
||||
features?: BackendFeature[];
|
||||
}
|
||||
|
||||
/** @alpha */
|
||||
export interface TestBackend extends Backend {
|
||||
/**
|
||||
* Provides access to the underling HTTP server for use with utilities
|
||||
* such as `supertest`.
|
||||
*
|
||||
* If the root http router service has been replaced, this will throw an error.
|
||||
*/
|
||||
readonly server: ExtendedHttpServer;
|
||||
}
|
||||
|
||||
const defaultServiceFactories = [
|
||||
rootLoggerFactory(),
|
||||
loggerFactory(),
|
||||
cacheFactory(),
|
||||
databaseFactory(),
|
||||
httpRouterFactory(),
|
||||
lifecycleFactory(),
|
||||
loggerFactory(),
|
||||
mockConfigFactory(),
|
||||
mockTokenManagerFactory(),
|
||||
permissionsFactory(),
|
||||
rootLifecycleFactory(),
|
||||
rootLoggerFactory(),
|
||||
schedulerFactory(),
|
||||
urlReaderFactory(),
|
||||
];
|
||||
|
||||
const backendInstancesToCleanUp = new Array<Backend>();
|
||||
@@ -67,7 +103,9 @@ const backendInstancesToCleanUp = new Array<Backend>();
|
||||
export async function startTestBackend<
|
||||
TServices extends any[],
|
||||
TExtensionPoints extends any[],
|
||||
>(options: TestBackendOptions<TServices, TExtensionPoints>): Promise<Backend> {
|
||||
>(
|
||||
options: TestBackendOptions<TServices, TExtensionPoints>,
|
||||
): Promise<TestBackend> {
|
||||
const {
|
||||
services = [],
|
||||
extensionPoints = [],
|
||||
@@ -75,6 +113,65 @@ export async function startTestBackend<
|
||||
...otherOptions
|
||||
} = options;
|
||||
|
||||
let server: ExtendedHttpServer;
|
||||
|
||||
const rootHttpRouterFactory = createServiceFactory({
|
||||
service: coreServices.rootHttpRouter,
|
||||
deps: {
|
||||
config: coreServices.config,
|
||||
lifecycle: coreServices.rootLifecycle,
|
||||
rootLogger: coreServices.rootLogger,
|
||||
},
|
||||
async factory({ config, lifecycle, rootLogger }) {
|
||||
const router = DefaultRootHttpRouter.create();
|
||||
const logger = rootLogger.child({ service: 'rootHttpRouter' });
|
||||
|
||||
const app = express();
|
||||
|
||||
const middleware = MiddlewareFactory.create({ config, logger });
|
||||
|
||||
app.use(router.handler());
|
||||
app.use(middleware.notFound());
|
||||
app.use(middleware.error());
|
||||
|
||||
server = await createHttpServer(
|
||||
app,
|
||||
{ listen: { host: '', port: 0 } },
|
||||
{ logger },
|
||||
);
|
||||
|
||||
lifecycle.addShutdownHook({
|
||||
async fn() {
|
||||
await server.stop();
|
||||
},
|
||||
logger,
|
||||
});
|
||||
|
||||
await server.start();
|
||||
|
||||
return router;
|
||||
},
|
||||
});
|
||||
|
||||
const discoveryFactory = createServiceFactory({
|
||||
service: coreServices.discovery,
|
||||
deps: {
|
||||
rootHttpRouter: coreServices.rootHttpRouter,
|
||||
},
|
||||
async factory() {
|
||||
if (!server) {
|
||||
throw new Error('Test server not started yet');
|
||||
}
|
||||
const port = server.port();
|
||||
const discovery = SingleHostDiscovery.fromConfig(
|
||||
new ConfigReader({
|
||||
backend: { baseUrl: `http://localhost:${port}`, listen: { port } },
|
||||
}),
|
||||
);
|
||||
return async () => discovery;
|
||||
},
|
||||
});
|
||||
|
||||
const factories = services.map(serviceDef => {
|
||||
if (Array.isArray(serviceDef)) {
|
||||
// if type is ExtensionPoint?
|
||||
@@ -107,7 +204,7 @@ export async function startTestBackend<
|
||||
|
||||
const backend = createSpecializedBackend({
|
||||
...otherOptions,
|
||||
services: factories,
|
||||
services: [...factories, rootHttpRouterFactory, discoveryFactory],
|
||||
});
|
||||
|
||||
backendInstancesToCleanUp.push(backend);
|
||||
@@ -129,7 +226,14 @@ export async function startTestBackend<
|
||||
|
||||
await backend.start();
|
||||
|
||||
return backend;
|
||||
return Object.assign(backend, {
|
||||
get server() {
|
||||
if (!server) {
|
||||
throw new Error('TestBackend server is not available');
|
||||
}
|
||||
return server;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
let registered = false;
|
||||
|
||||
@@ -15,4 +15,4 @@
|
||||
*/
|
||||
|
||||
export { startTestBackend } from './TestBackend';
|
||||
export type { TestBackendOptions } from './TestBackend';
|
||||
export type { TestBackend, TestBackendOptions } from './TestBackend';
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"get-port": "^6.1.2",
|
||||
"mock-fs": "^5.1.0",
|
||||
"msw": "^0.49.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
|
||||
@@ -17,18 +17,14 @@
|
||||
import mockFs from 'mock-fs';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import fetch from 'node-fetch';
|
||||
import { coreServices } from '@backstage/backend-plugin-api';
|
||||
import { startTestBackend } from '@backstage/backend-test-utils';
|
||||
import { appPlugin } from './appPlugin';
|
||||
import {
|
||||
databaseFactory,
|
||||
httpRouterFactory,
|
||||
rootHttpRouterFactory,
|
||||
loggerFactory,
|
||||
rootLoggerFactory,
|
||||
} from '@backstage/backend-app-api';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import getPort from 'get-port';
|
||||
|
||||
describe('appPlugin', () => {
|
||||
beforeEach(() => {
|
||||
@@ -48,23 +44,12 @@ describe('appPlugin', () => {
|
||||
});
|
||||
|
||||
it('boots', async () => {
|
||||
const port = await getPort();
|
||||
await startTestBackend({
|
||||
const { server } = await startTestBackend({
|
||||
services: [
|
||||
[
|
||||
coreServices.config,
|
||||
new ConfigReader({
|
||||
backend: {
|
||||
listen: { port },
|
||||
database: { client: 'better-sqlite3', connection: ':memory:' },
|
||||
},
|
||||
}),
|
||||
],
|
||||
loggerFactory(),
|
||||
rootLoggerFactory(),
|
||||
databaseFactory(),
|
||||
httpRouterFactory(),
|
||||
rootHttpRouterFactory(),
|
||||
],
|
||||
features: [
|
||||
appPlugin({
|
||||
@@ -75,12 +60,12 @@ describe('appPlugin', () => {
|
||||
});
|
||||
|
||||
await expect(
|
||||
fetch(`http://localhost:${port}/api/app/derp.html`).then(res =>
|
||||
fetch(`http://localhost:${server.port()}/api/app/derp.html`).then(res =>
|
||||
res.text(),
|
||||
),
|
||||
).resolves.toBe('winning');
|
||||
await expect(
|
||||
fetch(`http://localhost:${port}`).then(res => res.text()),
|
||||
fetch(`http://localhost:${server.port()}`).then(res => res.text()),
|
||||
).resolves.toBe('winning');
|
||||
});
|
||||
});
|
||||
|
||||
+19
-27
@@ -14,18 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
getVoidLogger,
|
||||
PluginEndpointDiscovery,
|
||||
TokenManager,
|
||||
} from '@backstage/backend-common';
|
||||
import { coreServices } from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
PluginTaskScheduler,
|
||||
TaskScheduleDefinition,
|
||||
} from '@backstage/backend-tasks';
|
||||
import { startTestBackend } from '@backstage/backend-test-utils';
|
||||
import {
|
||||
startTestBackend,
|
||||
mockConfigFactory,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { catalogProcessingExtensionPoint } from '@backstage/plugin-catalog-node';
|
||||
import { eventsExtensionPoint } from '@backstage/plugin-events-node';
|
||||
import { Duration } from 'luxon';
|
||||
@@ -55,22 +52,6 @@ describe('bitbucketCloudEntityProviderCatalogModule', () => {
|
||||
return runner;
|
||||
},
|
||||
} as unknown as PluginTaskScheduler;
|
||||
const discovery = jest.fn() as any as PluginEndpointDiscovery;
|
||||
const tokenManager = jest.fn() as any as TokenManager;
|
||||
|
||||
const config = new ConfigReader({
|
||||
catalog: {
|
||||
providers: {
|
||||
bitbucketCloud: {
|
||||
schedule: {
|
||||
frequency: 'P1M',
|
||||
timeout: 'PT3M',
|
||||
},
|
||||
workspace: 'test-ws',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await startTestBackend({
|
||||
extensionPoints: [
|
||||
@@ -78,11 +59,22 @@ describe('bitbucketCloudEntityProviderCatalogModule', () => {
|
||||
[eventsExtensionPoint, eventsExtensionPointImpl],
|
||||
],
|
||||
services: [
|
||||
[coreServices.config, config],
|
||||
[coreServices.discovery, discovery],
|
||||
[coreServices.logger, getVoidLogger()],
|
||||
mockConfigFactory({
|
||||
data: {
|
||||
catalog: {
|
||||
providers: {
|
||||
bitbucketCloud: {
|
||||
schedule: {
|
||||
frequency: 'P1M',
|
||||
timeout: 'PT3M',
|
||||
},
|
||||
workspace: 'test-ws',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
[coreServices.scheduler, scheduler],
|
||||
[coreServices.tokenManager, tokenManager],
|
||||
],
|
||||
features: [bitbucketCloudEntityProviderCatalogModule()],
|
||||
});
|
||||
|
||||
@@ -56,8 +56,7 @@
|
||||
"devDependencies": {
|
||||
"@backstage/backend-app-api": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/plugin-catalog-backend": "workspace:^",
|
||||
"get-port": "^6.1.2"
|
||||
"@backstage/plugin-catalog-backend": "workspace:^"
|
||||
},
|
||||
"files": [
|
||||
"alpha",
|
||||
|
||||
@@ -14,11 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { PluginEndpointDiscovery } from '@backstage/backend-common';
|
||||
import {
|
||||
createBackendModule,
|
||||
coreServices,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { createBackendModule } from '@backstage/backend-plugin-api';
|
||||
import { startTestBackend } from '@backstage/backend-test-utils';
|
||||
import { CatalogClient } from '@backstage/catalog-client';
|
||||
import { catalogServiceRef } from './catalogService';
|
||||
@@ -42,9 +38,6 @@ describe('catalogServiceRef', () => {
|
||||
});
|
||||
|
||||
await startTestBackend({
|
||||
services: [
|
||||
[coreServices.discovery, {} as unknown as PluginEndpointDiscovery],
|
||||
],
|
||||
features: [testModule()],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3400,6 +3400,7 @@ __metadata:
|
||||
fs-extra: 10.1.0
|
||||
helmet: ^6.0.0
|
||||
http-errors: ^2.0.0
|
||||
lodash: ^4.17.21
|
||||
minimatch: ^5.0.0
|
||||
morgan: ^1.10.0
|
||||
node-forge: ^1.3.1
|
||||
@@ -3558,11 +3559,16 @@ __metadata:
|
||||
"@backstage/backend-plugin-api": "workspace:^"
|
||||
"@backstage/cli": "workspace:^"
|
||||
"@backstage/config": "workspace:^"
|
||||
"@backstage/types": "workspace:^"
|
||||
"@types/supertest": ^2.0.8
|
||||
better-sqlite3: ^8.0.0
|
||||
express: ^4.17.1
|
||||
express-promise-router: ^4.1.0
|
||||
knex: ^2.0.0
|
||||
msw: ^0.49.0
|
||||
mysql2: ^2.2.5
|
||||
pg: ^8.3.0
|
||||
supertest: ^6.1.3
|
||||
testcontainers: ^8.1.2
|
||||
uuid: ^8.0.0
|
||||
languageName: unknown
|
||||
@@ -4513,7 +4519,6 @@ __metadata:
|
||||
express: ^4.17.1
|
||||
express-promise-router: ^4.1.0
|
||||
fs-extra: 10.1.0
|
||||
get-port: ^6.1.2
|
||||
globby: ^11.0.0
|
||||
helmet: ^6.0.0
|
||||
knex: ^2.0.0
|
||||
@@ -5112,7 +5117,6 @@ __metadata:
|
||||
"@types/luxon": ^3.0.0
|
||||
express: ^4.17.1
|
||||
express-promise-router: ^4.1.0
|
||||
get-port: ^6.1.2
|
||||
knex: ^2.0.0
|
||||
lodash: ^4.17.21
|
||||
luxon: ^3.0.0
|
||||
@@ -23809,13 +23813,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"get-port@npm:^6.1.2":
|
||||
version: 6.1.2
|
||||
resolution: "get-port@npm:6.1.2"
|
||||
checksum: e3c3d591492a11393455ef220f24c812a28f7da56ec3e4a2512d931a1f196d42850b50ac6138349a44622eda6dc3c0ccd8495cd91376d968e2d9e6f6f849e0a9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"get-stdin@npm:^8.0.0":
|
||||
version: 8.0.0
|
||||
resolution: "get-stdin@npm:8.0.0"
|
||||
|
||||
Reference in New Issue
Block a user