diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index 38db9d6413..ae96f17b8e 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -40,6 +40,8 @@ "@backstage/cli": "workspace:^", "@backstage/config": "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 +50,9 @@ "uuid": "^8.0.0" }, "devDependencies": { - "@backstage/cli": "workspace:^" + "@backstage/cli": "workspace:^", + "@types/supertest": "^2.0.8", + "supertest": "^6.1.3" }, "files": [ "dist", diff --git a/packages/backend-test-utils/src/next/implementations/index.ts b/packages/backend-test-utils/src/next/implementations/index.ts index c248502307..2068aebec5 100644 --- a/packages/backend-test-utils/src/next/implementations/index.ts +++ b/packages/backend-test-utils/src/next/implementations/index.ts @@ -15,4 +15,3 @@ */ export { mockTokenManagerFactory } from './mockTokenManagerService'; export { mockConfigFactory } from './mockConfigService'; -export { mockDiscoveryFactory } from './mockDiscoveryService'; diff --git a/packages/backend-test-utils/src/next/implementations/mockDiscoveryService.ts b/packages/backend-test-utils/src/next/implementations/mockDiscoveryService.ts deleted file mode 100644 index e382d23cbd..0000000000 --- a/packages/backend-test-utils/src/next/implementations/mockDiscoveryService.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 { SingleHostDiscovery } from '@backstage/backend-common'; -import { - coreServices, - createServiceFactory, -} from '@backstage/backend-plugin-api'; -import { ConfigReader } from '@backstage/config'; - -/** @public */ -export const mockDiscoveryFactory = createServiceFactory({ - service: coreServices.discovery, - deps: {}, - async factory() { - // todo(blam): we want to grab the port from the httpRouter when that's available here - // to provide a better way to create our mockDiscoveryService. - const discovery = SingleHostDiscovery.fromConfig( - new ConfigReader({ - backend: { baseUrl: 'http://localhost:7007', listen: '0.0.0.0' }, - }), - ); - - return async () => { - return discovery; - }; - }, -}); 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 4c7362880e..6358051c34 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts @@ -22,6 +22,8 @@ import { coreServices, createBackendPlugin, } from '@backstage/backend-plugin-api'; +import { Router } from 'express'; +import request from 'supertest'; import { startTestBackend } from './TestBackend'; @@ -184,7 +186,7 @@ describe('TestBackend', () => { urlReader: coreServices.urlReader, }, async init(deps) { - expect(Object.keys(deps)).toHaveLength(14); + expect(Object.keys(deps)).toHaveLength(15); expect(Object.values(deps)).not.toContain(undefined); }, }); @@ -194,6 +196,30 @@ describe('TestBackend', () => { await startTestBackend({ services: [], features: [testPlugin()], - }).then(backend => backend.stop()); + }); + }); + + 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' }); }); }); diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts index 35f138091d..933257426e 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -26,23 +26,27 @@ import { schedulerFactory, urlReaderFactory, databaseFactory, - rootHttpRouterFactory, httpRouterFactory, } from '@backstage/backend-app-api'; - +import { + createServiceBuilder, + SingleHostDiscovery, +} from '@backstage/backend-common'; +import { Handler } from 'express'; +import * as http from 'http'; +import Router from 'express-promise-router'; import { ServiceFactory, ServiceRef, createServiceFactory, BackendFeature, ExtensionPoint, + coreServices, } from '@backstage/backend-plugin-api'; -import { - mockConfigFactory, - mockTokenManagerFactory, - mockDiscoveryFactory, -} from '../implementations'; +import { mockConfigFactory, mockTokenManagerFactory } from '../implementations'; +import { AddressInfo } from 'net'; +import { ConfigReader } from '@backstage/config'; /** @alpha */ export interface TestBackendOptions< @@ -75,10 +79,8 @@ const defaultServiceFactories = [ lifecycleFactory(), loggerFactory(), mockConfigFactory(), - mockDiscoveryFactory(), mockTokenManagerFactory(), permissionsFactory(), - rootHttpRouterFactory(), rootLifecycleFactory(), rootLoggerFactory(), schedulerFactory(), @@ -99,6 +101,71 @@ export async function startTestBackend< ...otherOptions } = options; + let server: http.Server; + + const rootHttpRouterFactory = createServiceFactory({ + service: coreServices.rootHttpRouter, + deps: { + config: coreServices.config, + lifecycle: coreServices.rootLifecycle, + }, + async factory({ config, lifecycle }) { + const router = Router(); + + const service = createServiceBuilder(module) + .loadConfig(config) + .setPort(0); + + service.addRouter('', router); + + server = await service.start(); + // Stop method isn't part of the public API, let's fix that once we move the implementation here. + const stoppableServer = server as typeof server & { + stop: (cb: (error?: Error) => void) => void; + }; + + lifecycle.addShutdownHook({ + async fn() { + await new Promise((resolve, reject) => { + stoppableServer.stop((error?: Error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); + }, + labels: { service: 'rootHttpRouter' }, + }); + + return { + use: (path: string, handler: Handler) => { + router.use(path, handler); + }, + }; + }, + }); + + 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.address() as AddressInfo; + 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? @@ -131,7 +198,7 @@ export async function startTestBackend< const backend = createSpecializedBackend({ ...otherOptions, - services: factories, + services: [...factories, rootHttpRouterFactory, discoveryFactory], }); backendInstancesToCleanUp.push(backend); @@ -153,7 +220,7 @@ export async function startTestBackend< await backend.start(); - return backend; + return Object.assign(backend, { server: server! }) as Backend; } let registered = false; diff --git a/yarn.lock b/yarn.lock index 693b5cc3dd..f8957bfc35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3558,11 +3558,15 @@ __metadata: "@backstage/backend-plugin-api": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "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