diff --git a/packages/rest-explorer/src/__tests__/acceptance/rest-explorer.acceptance.ts b/packages/rest-explorer/src/__tests__/acceptance/rest-explorer.acceptance.ts index dc27d939a174..53c7c011c036 100644 --- a/packages/rest-explorer/src/__tests__/acceptance/rest-explorer.acceptance.ts +++ b/packages/rest-explorer/src/__tests__/acceptance/rest-explorer.acceptance.ts @@ -20,6 +20,11 @@ describe('API Explorer (acceptance)', () => { let app: RestApplication; let request: Client; + afterEach(async () => { + if (app) await app.stop(); + (app as unknown) = undefined; + }); + context('with default config', () => { beforeEach(async () => { app = givenRestApplication(); @@ -109,6 +114,26 @@ describe('API Explorer (acceptance)', () => { } }); + context('with custom basePath', () => { + beforeEach(async () => { + app = givenRestApplication(); + app.basePath('/api'); + app.component(RestExplorerComponent); + await app.start(); + request = createRestAppClient(app); + }); + + it('uses correct URLs', async () => { + // static assets (including swagger-ui) honor basePath + await request + .get('/api/explorer/') + .expect(200) + .expect('content-type', /html/) + // OpenAPI endpoints DO NOT honor basePath + .expect(/url\: '\/openapi\.json'\,/); + }); + }); + function givenRestApplication(config?: RestServerConfig) { const rest = Object.assign({}, givenHttpServerConfig(), config); return new RestApplication({rest}); diff --git a/packages/rest-explorer/src/__tests__/acceptance/rest-explorer.express.acceptance.ts b/packages/rest-explorer/src/__tests__/acceptance/rest-explorer.express.acceptance.ts index 60a1962be6fe..97c0a8dbd9b6 100644 --- a/packages/rest-explorer/src/__tests__/acceptance/rest-explorer.express.acceptance.ts +++ b/packages/rest-explorer/src/__tests__/acceptance/rest-explorer.express.acceptance.ts @@ -3,12 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import { - HttpRequestListener, - RestApplication, - RestServer, - RestServerConfig, -} from '@loopback/rest'; +import {RestApplication, RestServer, RestServerConfig} from '@loopback/rest'; import { Client, createClientForHandler, @@ -21,7 +16,6 @@ describe('REST Explorer mounted as an express router', () => { let client: Client; let expressApp: express.Application; let server: RestServer; - let handler: HttpRequestListener; beforeEach(givenLoopBackApp); beforeEach(givenExpressApp); beforeEach(givenClient); @@ -41,6 +35,17 @@ describe('REST Explorer mounted as an express router', () => { .expect('location', '/api/explorer/'); }); + it('honors basePath config', async () => { + server.basePath('/v1'); + await client + // static assets (including swagger-ui) honor basePath + .get('/api/v1/explorer/') + .expect(200) + .expect('content-type', /html/) + // OpenAPI endpoints DO NOT honor basePath + .expect(/url\: '\/api\/openapi\.json'\,/); + }); + async function givenLoopBackApp( options: {rest: RestServerConfig} = {rest: {port: 0}}, ) { @@ -48,7 +53,6 @@ describe('REST Explorer mounted as an express router', () => { const app = new RestApplication(options); app.component(RestExplorerComponent); server = await app.getServer(RestServer); - handler = server.requestHandler; } /** @@ -56,7 +60,10 @@ describe('REST Explorer mounted as an express router', () => { */ function givenExpressApp() { expressApp = express(); - expressApp.use('/api', handler); + expressApp.use('/api', (req, res, _next) => { + // defer calling of `server.requestHandler` until a request arrives + server.requestHandler(req, res); + }); } function givenClient() { diff --git a/packages/rest-explorer/src/rest-explorer.controller.ts b/packages/rest-explorer/src/rest-explorer.controller.ts index 149b7fe4f8c9..acc67b893407 100644 --- a/packages/rest-explorer/src/rest-explorer.controller.ts +++ b/packages/rest-explorer/src/rest-explorer.controller.ts @@ -5,11 +5,11 @@ import {inject} from '@loopback/context'; import { - RestBindings, - RestServerConfig, OpenApiSpecForm, Request, Response, + RestBindings, + RestServerConfig, } from '@loopback/rest'; import * as ejs from 'ejs'; import * as fs from 'fs'; @@ -26,6 +26,7 @@ export class ExplorerController { constructor( @inject(RestBindings.CONFIG, {optional: true}) restConfig: RestServerConfig = {}, + @inject(RestBindings.BASE_PATH) private serverBasePath: string, @inject(RestBindings.Http.REQUEST) private request: Request, @inject(RestBindings.Http.RESPONSE) private response: Response, ) { @@ -39,8 +40,20 @@ export class ExplorerController { index() { let openApiSpecUrl = this.openApiSpecUrl; - if (this.request.baseUrl && this.request.baseUrl !== '/') { - openApiSpecUrl = this.request.baseUrl + openApiSpecUrl; + + // baseURL is composed from mountPath and basePath + // OpenAPI endpoints ignore basePath but do honor mountPath + let rootPath = this.request.baseUrl; + if ( + this.serverBasePath && + this.serverBasePath !== '/' && + rootPath.endsWith(this.serverBasePath) + ) { + rootPath = rootPath.slice(0, -this.serverBasePath.length); + } + + if (rootPath && rootPath !== '/') { + openApiSpecUrl = rootPath + openApiSpecUrl; } const data = { openApiSpecUrl,