Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rest): add OAS Enhancer service #4554

Merged
merged 2 commits into from
Mar 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ describe('CoffeeShopApplication', () => {
let apiSpec: OpenApiSpec;

before(async () => {
apiSpec = app.lbApp.restServer.getApiSpec();
apiSpec = await app.lbApp.restServer.getApiSpec();
dougal83 marked this conversation as resolved.
Show resolved Hide resolved
});

it('has the same properties in both the LB3 and LB4 specs', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ describe('booter-lb3app', () => {
});

context('generated OpenAPI spec', () => {
it('uses different request-body schema for "create" operation', () => {
const spec = app.restServer.getApiSpec();
it('uses different request-body schema for "create" operation', async () => {
const spec = await app.restServer.getApiSpec();
const createOp: OperationObject = spec.paths['/api/CoffeeShops'].post;
expect(createOp.requestBody).to.containDeep({
content: {
Expand All @@ -66,8 +66,8 @@ describe('booter-lb3app', () => {
});
});

it('includes the target model as a property of the source model in a relation', () => {
const spec = app.restServer.getApiSpec();
it('includes the target model as a property of the source model in a relation', async () => {
const spec = await app.restServer.getApiSpec();
const schemas = (spec.components ?? {}).schemas ?? {};

expect(schemas.CoffeeShop)
Expand Down Expand Up @@ -118,8 +118,8 @@ describe('booter-lb3app', () => {
}
});

it('includes LoopBack 3 endpoints with `/api` base in OpenApiSpec', () => {
const apiSpec = app.restServer.getApiSpec();
it('includes LoopBack 3 endpoints with `/api` base in OpenApiSpec', async () => {
const apiSpec = await app.restServer.getApiSpec();
const paths = Object.keys(apiSpec.paths);
expect(paths).to.containDeep([
'/api/CoffeeShops/{id}',
Expand Down Expand Up @@ -219,8 +219,8 @@ describe('booter-lb3app', () => {
}));
});

it('does apply the spec modification', () => {
const spec = app.restServer.getApiSpec();
it('does apply the spec modification', async () => {
const spec = await app.restServer.getApiSpec();
const createOp: OperationObject = spec.paths['/api/CoffeeShops'].post;
expect(createOp.summary).to.eql('just a very simple modification');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ describe('CrudRestController for a simple Product model', () => {
// a new test suite that will configure a PK with a different name
// and type, e.g. `pk: string` instead of `id: number`.
it('uses correct schema for the id parameter', async () => {
const spec = app.restServer.getApiSpec();
const spec = await app.restServer.getApiSpec();
const findByIdOp = spec.paths['/products/{id}'].get;
expect(findByIdOp).to.containDeep({
parameters: [
Expand Down Expand Up @@ -210,7 +210,7 @@ describe('CrudRestController for a simple Product model', () => {
// a new test suite that will configure a PK with a different name
// and type, e.g. `pk: string` instead of `id: number`.
it('uses correct schema for the id parameter', async () => {
const spec = app.restServer.getApiSpec();
const spec = await app.restServer.getApiSpec();
const findByIdOp = spec.paths['/products/{id}'].patch;
expect(findByIdOp).to.containDeep({
parameters: [
Expand Down Expand Up @@ -245,7 +245,7 @@ describe('CrudRestController for a simple Product model', () => {
// a new test suite that will configure a PK with a different name
// and type, e.g. `pk: string` instead of `id: number`.
it('uses correct schema for the id parameter', async () => {
const spec = app.restServer.getApiSpec();
const spec = await app.restServer.getApiSpec();
const findByIdOp = spec.paths['/products/{id}']['patch'];
expect(findByIdOp).to.containDeep({
parameters: [
Expand Down Expand Up @@ -278,7 +278,7 @@ describe('CrudRestController for a simple Product model', () => {
// a new test suite that will configure a PK with a different name
// and type, e.g. `pk: string` instead of `id: number`.
it('uses correct schema for the id parameter', async () => {
const spec = app.restServer.getApiSpec();
const spec = await app.restServer.getApiSpec();
const findByIdOp = spec.paths['/products/{id}']['delete'];
expect(findByIdOp).to.containDeep({
parameters: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ describe('RestApplication (integration)', () => {
'x-foo': 'bar',
});

const spec = restApp.restServer.getApiSpec();
const spec = await restApp.restServer.getApiSpec();
expect(spec).to.deepEqual({
openapi: '3.0.0',
info: {
Expand Down Expand Up @@ -231,7 +231,7 @@ describe('RestApplication (integration)', () => {
restApp.mountExpressRouter('/dogs', router, spec);
await client.get('/dogs/hello').expect(200, 'Hello dogs!');

const openApiSpec = restApp.restServer.getApiSpec();
const openApiSpec = await restApp.restServer.getApiSpec();
expect(openApiSpec.paths).to.deepEqual({
'/dogs/hello': {
get: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright IBM Corp. 2019. All Rights Reserved.
// Node module: @loopback/openapi-v3
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {bind} from '@loopback/core';
import debugModule from 'debug';
import {inspect} from 'util';
import {
asSpecEnhancer,
mergeOpenAPISpec,
OASEnhancer,
OpenApiSpec,
} from '../../../..';

const debug = debugModule('loopback:openapi:spec-enhancer');

/**
* A spec enhancer to add OpenAPI info spec
*/
@bind(asSpecEnhancer)
export class InfoSpecEnhancer implements OASEnhancer {
name = 'info';

modifySpec(spec: OpenApiSpec): OpenApiSpec {
const InfoPatchSpec = {
info: {title: 'LoopBack Test Application', version: '1.0.1'},
};
const mergedSpec = mergeOpenAPISpec(spec, InfoPatchSpec);
debug(`security spec extension, merged spec: ${inspect(mergedSpec)}`);
return mergedSpec;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Application} from '@loopback/core';
import {Application, createBindingFromClass} from '@loopback/core';
import {anOpenApiSpec, anOperationSpec} from '@loopback/openapi-spec-builder';
import {get, post, requestBody} from '@loopback/openapi-v3';
import {model, property} from '@loopback/repository';
Expand All @@ -14,17 +14,18 @@ import {
RestServer,
} from '../../..';
import {RestTags} from '../../../keys';
import {InfoSpecEnhancer} from './fixtures/info.spec.extension';

describe('RestServer.getApiSpec()', () => {
let app: Application;
let server: RestServer;
beforeEach(givenApplication);

it('comes with a valid default spec', async () => {
await validateApiSpec(server.getApiSpec());
await validateApiSpec(await server.getApiSpec());
});

it('honours API defined via app.api()', () => {
it('honours API defined via app.api()', async () => {
server.api({
openapi: '3.0.0',
info: {
Expand All @@ -36,7 +37,7 @@ describe('RestServer.getApiSpec()', () => {
'x-foo': 'bar',
});

const spec = server.getApiSpec();
const spec = await server.getApiSpec();
expect(spec).to.deepEqual({
openapi: '3.0.0',
info: {
Expand Down Expand Up @@ -81,11 +82,11 @@ describe('RestServer.getApiSpec()', () => {
});
});

it('returns routes registered via app.route(route)', () => {
it('returns routes registered via app.route(route)', async () => {
function greet() {}
server.route('get', '/greet', {responses: {}}, greet);

const spec = server.getApiSpec();
const spec = await server.getApiSpec();
expect(spec.paths).to.eql({
'/greet': {
get: {
Expand All @@ -95,7 +96,7 @@ describe('RestServer.getApiSpec()', () => {
});
});

it('ignores routes marked as "x-visibility" via app.route(route)', () => {
it('ignores routes marked as "x-visibility" via app.route(route)', async () => {
function greet() {}
function meet() {}
server.route(
Expand All @@ -105,7 +106,7 @@ describe('RestServer.getApiSpec()', () => {
greet,
);
server.route('get', '/meet', {responses: {}, spec: {}}, meet);
const spec = server.getApiSpec();
const spec = await server.getApiSpec();
expect(spec.paths).to.eql({
'/meet': {
get: {
Expand All @@ -116,7 +117,7 @@ describe('RestServer.getApiSpec()', () => {
});
});

it('returns routes registered via app.route(..., Controller, method)', () => {
it('returns routes registered via app.route(..., Controller, method)', async () => {
class MyController {
greet() {}
}
Expand All @@ -130,7 +131,7 @@ describe('RestServer.getApiSpec()', () => {
'greet',
);

const spec = server.getApiSpec();
const spec = await server.getApiSpec();
expect(spec.paths).to.eql({
'/greet': {
get: {
Expand All @@ -143,7 +144,7 @@ describe('RestServer.getApiSpec()', () => {
});
});

it('ignores routes marked as "x-visibility" via app.route(..., Controller, method)', () => {
it('ignores routes marked as "x-visibility" via app.route(..., Controller, method)', async () => {
class GreetController {
greet() {}
}
Expand All @@ -170,7 +171,7 @@ describe('RestServer.getApiSpec()', () => {
'meet',
);

const spec = server.getApiSpec();
const spec = await server.getApiSpec();
expect(spec.paths).to.eql({
'/meet': {
get: {
Expand All @@ -183,14 +184,14 @@ describe('RestServer.getApiSpec()', () => {
});
});

it('honors tags in the operation spec', () => {
it('honors tags in the operation spec', async () => {
class MyController {
@get('/greet', {responses: {'200': {description: ''}}, tags: ['MyTag']})
greet() {}
}
app.controller(MyController);

const spec = server.getApiSpec();
const spec = await server.getApiSpec();
expect(spec.paths).to.eql({
'/greet': {
get: {
Expand All @@ -204,7 +205,7 @@ describe('RestServer.getApiSpec()', () => {
});
});

it('emits all media types for request body', () => {
it('emits all media types for request body', async () => {
const expectedOpSpec = anOperationSpec()
.withRequestBody({
description: 'Any object value.',
Expand Down Expand Up @@ -238,18 +239,18 @@ describe('RestServer.getApiSpec()', () => {
}
app.controller(MyController);

const spec = server.getApiSpec();
const spec = await server.getApiSpec();
expect(spec.paths['/show-body'].post).to.containDeep(expectedOpSpec);
});

it('returns routes registered via app.controller()', () => {
it('returns routes registered via app.controller()', async () => {
class MyController {
@get('/greet')
greet() {}
}
app.controller(MyController);

const spec = server.getApiSpec();
const spec = await server.getApiSpec();
expect(spec.paths).to.eql({
'/greet': {
get: {
Expand All @@ -265,7 +266,7 @@ describe('RestServer.getApiSpec()', () => {
});
});

it('returns definitions inferred via app.controller()', () => {
it('returns definitions inferred via app.controller()', async () => {
@model()
class MyModel {
@property()
Expand All @@ -277,7 +278,7 @@ describe('RestServer.getApiSpec()', () => {
}
app.controller(MyController);

const spec = server.getApiSpec();
const spec = await server.getApiSpec();
expect(spec.components && spec.components.schemas).to.deepEqual({
MyModel: {
title: 'MyModel',
Expand All @@ -291,7 +292,7 @@ describe('RestServer.getApiSpec()', () => {
});
});

it('preserves routes specified in app.api()', () => {
it('preserves routes specified in app.api()', async () => {
function status() {}
server.api(
anOpenApiSpec()
Expand All @@ -305,7 +306,7 @@ describe('RestServer.getApiSpec()', () => {
function greet() {}
server.route('get', '/greet', {responses: {}}, greet);

const spec = server.getApiSpec();
const spec = await server.getApiSpec();
expect(spec.paths).to.eql({
'/greet': {
get: {
Expand All @@ -320,6 +321,16 @@ describe('RestServer.getApiSpec()', () => {
});
});

it('invokes registered oas enhancers', async () => {
const EXPECTED_SPEC_INFO = {
title: 'LoopBack Test Application',
version: '1.0.1',
};
server.add(createBindingFromClass(InfoSpecEnhancer));
const spec = await server.getApiSpec();
expect(spec.info).to.eql(EXPECTED_SPEC_INFO);
});

async function givenApplication() {
app = new Application();
app.component(RestComponent);
Expand Down
Loading