routing-controllers-openapi
Version:
Runtime OpenAPI v3 spec generation for routing-controllers
657 lines (603 loc) • 17.9 kB
text/typescript
import {
ContentType,
Controller,
Get,
getMetadataArgsStorage,
HttpCode,
JsonController,
Param,
} from 'routing-controllers'
import {
getOperation,
getTags,
IRoute,
OpenAPI,
parseRoutes,
ResponseSchema,
} from '../src'
import { ModelDto } from './fixtures/models'
type IndexedRoutes = {
[method: string]: IRoute
}
describe('decorators', () => {
let routes: IndexedRoutes
beforeEach(() => {
getMetadataArgsStorage().reset()
// @ts-ignore: not referenced
class UsersController {
listUsers() {
return
}
getUser( _userId: number) {
return
}
multipleOpenAPIsWithObjectParam() {
return
}
multipleOpenAPIsWithFunctionParam() {
return
}
multipleOpenAPIsWithMixedParam() {
return
}
responseSchemaDefaults() {
return
}
responseSchemaOptions() {
return
}
responseSchemaDecorators() {
return
}
responseSchemaArray() {
return
}
responseSchemaDecoratorAndSchema() {
return
}
responseSchemaModelAsString() {
return
}
responseSchemaNotOverwritingInnerOpenApiDecorator() {
return
}
responseSchemaNotOverwritingOuterOpenApiDecorator() {
return
}
responseSchemaNoNoModel() {
return
}
multipleResponseSchemas() {
return
}
twoResponseSchemasSameStatusCode() {
return
}
threeResponseSchemasSameStatusCode() {
return
}
twoResponseSchemaSameStatusCodeWithOneArraySchema() {
return
}
fourResponseSchemasMixedStatusCodeWithTwoArraySchemas() {
return
}
}
// @ts-ignore: not referenced
class UsersHtmlController {
responseSchemaDefaultsHtml() {
return
}
}
routes = parseRoutes(getMetadataArgsStorage()).reduce((acc, route) => {
acc[route.action.method] = route
return acc
}, {} as IndexedRoutes)
})
it('merges keywords defined in @OpenAPI decorator into operation', () => {
const operation = getOperation(routes.listUsers, {})
expect(operation.description).toEqual('List all users')
})
it('applies @OpenAPI decorator function parameter to operation', () => {
const operation = getOperation(routes.getUser, {})
expect(operation.tags).toEqual(['Users', 'custom-tag'])
})
it('merges consecutive @OpenAPI object parameters top-down', () => {
const operation = getOperation(routes.multipleOpenAPIsWithObjectParam, {})
expect(operation.summary).toEqual('Some summary')
expect(operation.description).toEqual('Some description')
expect(operation['x-custom-key']).toEqual('Custom value')
})
it('applies consecutive @OpenAPI function parameters top-down', () => {
const operation = getOperation(routes.multipleOpenAPIsWithFunctionParam, {})
expect(operation.summary).toEqual('Some summary')
expect(operation.description).toEqual('Some description')
expect(operation['x-custom-key']).toEqual(20)
})
it('merges and applies consecutive @OpenAPI object and function parameters top-down', () => {
const operation = getOperation(routes.multipleOpenAPIsWithMixedParam, {})
expect(operation.summary).toEqual('Some summary')
expect(operation.description).toEqual('Some description')
expect(operation['x-custom-key']).toEqual(20)
})
it('applies @ResponseSchema merging in response schema into source metadata', () => {
const operation = getOperation(routes.responseSchemaDefaults, {})
// ensure other metadata doesnt get overwritten by decorator
expect(operation.operationId).toEqual(
'UsersController.responseSchemaDefaults'
)
})
it('applies @ResponseSchema using default contentType and statusCode', () => {
const operation = getOperation(routes.responseSchemaDefaults, {})
expect(operation.responses).toEqual({
'200': {
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/ModelDto',
},
},
},
description: '',
},
})
})
it('applies @ResponseSchema using contentType and statusCode from options object', () => {
const operation = getOperation(routes.responseSchemaOptions, {})
expect(operation.responses).toEqual({
'200': {
content: {
'application/json': {},
},
description: 'Successful response',
},
'400': {
content: {
'text/csv': {
schema: {
$ref: '#/components/schemas/ModelDto',
},
},
},
description: 'Bad request',
},
})
})
it('applies @ResponseSchema using contentType and statusCode from decorators', () => {
const operation = getOperation(routes.responseSchemaDecorators, {})
expect(operation.responses['201'].content['application/pdf']).toEqual({
schema: { $ref: '#/components/schemas/ModelDto' },
})
})
it('applies @ResponseSchema using isArray flag set to true', () => {
const operation = getOperation(routes.responseSchemaArray, {})
expect(operation.responses['200'].content['application/json']).toEqual({
schema: {
items: {
$ref: '#/components/schemas/ModelDto',
},
type: 'array',
},
})
})
it('applies @ResponseSchema using contentType and statusCode from options object, overruling options from RC decorators', () => {
const operation = getOperation(routes.responseSchemaDecoratorAndSchema, {})
expect(operation.responses).toEqual({
'201': {
content: {
'application/pdf': {},
},
description: 'Successful response',
},
'400': {
content: {
'text/csv': {
schema: { $ref: '#/components/schemas/ModelDto' },
},
},
description: '',
},
})
})
it('applies @ResponseSchema using a string as ModelName', () => {
const operation = getOperation(routes.responseSchemaModelAsString, {})
expect(operation.responses).toEqual({
'200': {
content: {
'application/json': {},
},
description: 'Successful response',
},
'400': {
content: {
'text/csv': {
schema: { $ref: '#/components/schemas/MyModelName' },
},
},
description: '',
},
})
})
it('applies @ResponseSchema while retaining inner OpenAPI decorator', () => {
const operation = getOperation(
routes.responseSchemaNotOverwritingInnerOpenApiDecorator,
{}
)
expect(operation.description).toEqual('somedescription')
expect(operation.responses).toEqual({
'200': {
content: {
'application/json': {},
},
description: 'Successful response',
},
'400': {
content: {
'text/csv': {
schema: { $ref: '#/components/schemas/MyModelName' },
},
},
description: '',
},
})
})
it('applies @ResponseSchema while retaining outer OpenAPI decorator', () => {
const operation = getOperation(
routes.responseSchemaNotOverwritingOuterOpenApiDecorator,
{}
)
expect(operation.description).toEqual('somedescription')
expect(operation.responses).toEqual({
'200': {
content: {
'application/json': {},
},
description: 'Successful response',
},
'400': {
content: {
'text/csv': {
schema: { $ref: '#/components/schemas/MyModelName' },
},
},
description: '',
},
})
})
it('does not apply @ResponseSchema if empty ModelName is passed', () => {
const operation = getOperation(routes.responseSchemaNoNoModel, {})
expect(operation.responses).toEqual({
'200': {
content: {
'application/json': {},
},
description: 'Successful response',
},
})
})
it('applies @ResponseSchema using default contentType and statusCode from @Controller (non-json)', () => {
const operation = getOperation(routes.responseSchemaDefaultsHtml, {})
expect(operation.responses).toEqual({
'200': {
content: {
'text/html; charset=utf-8': {
schema: {
$ref: '#/components/schemas/ModelDto',
},
},
},
description: '',
},
})
})
it('applies multiple @ResponseSchema on a single handler', () => {
const operation = getOperation(routes.multipleResponseSchemas, {})
expect(operation.responses).toEqual({
'200': {
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/MySuccessObject',
},
},
},
description: 'Some successful response object',
},
'400': {
content: {
'text/html': {
schema: {
$ref: '#/components/schemas/BadRequestErrorObject',
},
},
},
description: '',
},
'404': {
content: {
'text/csv': {
schema: {
$ref: '#/components/schemas/NotFoundErrorObject',
},
},
},
description: '',
},
})
})
it('applies two @ResponseSchema with same status code', () => {
const operation = getOperation(routes.twoResponseSchemasSameStatusCode, {})
expect(operation.responses).toEqual({
'200': {
content: {
'application/json': {
schema: {
oneOf: [
{ $ref: '#/components/schemas/SuccessObject1' },
{ $ref: '#/components/schemas/SuccessObject2' },
],
},
},
},
description: '',
},
})
})
it('applies three @ResponseSchema with same status code', () => {
const operation = getOperation(
routes.threeResponseSchemasSameStatusCode,
{}
)
expect(operation.responses).toEqual({
'200': {
content: {
'application/json': {
schema: {
oneOf: [
{ $ref: '#/components/schemas/SuccessObject1' },
{ $ref: '#/components/schemas/SuccessObject2' },
{ $ref: '#/components/schemas/SuccessObject3' },
],
},
},
},
description: '',
},
})
})
it('applies two @ResponseSchema with same status code, where one of them is an array', () => {
const operation = getOperation(
routes.twoResponseSchemaSameStatusCodeWithOneArraySchema,
{}
)
expect(operation.responses).toEqual({
'200': {
content: {
'application/json': {
schema: {
oneOf: [
{
items: {
$ref: '#/components/schemas/SuccessObjects1',
},
type: 'array',
},
{ $ref: '#/components/schemas/SuccessObject2' },
],
},
},
},
description: '',
},
})
})
it('applies four @ResponseSchema with mixed status code, where two of them are arrays', () => {
const operation = getOperation(
routes.fourResponseSchemasMixedStatusCodeWithTwoArraySchemas,
{}
)
expect(operation.responses).toEqual({
'200': {
content: {
'application/json': {
schema: {
items: {
$ref: '#/components/schemas/SuccessObjects1',
},
type: 'array',
},
},
},
description: '',
},
'201': {
content: {
'application/json': {
schema: {
oneOf: [
{ $ref: '#/components/schemas/CreatedObject2' },
{
items: {
$ref: '#/components/schemas/CreatedObjects3',
},
type: 'array',
},
],
},
},
},
description: '',
},
'400': {
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/BadRequestObject4',
},
},
},
description: '',
},
})
})
})
describe('@OpenAPI-decorated class', () => {
let routes: IndexedRoutes
beforeEach(() => {
getMetadataArgsStorage().reset()
// @ts-ignore: not referenced
class Item {
listItems() {
return
}
getItem() {
return
}
}
routes = parseRoutes(getMetadataArgsStorage()).reduce((acc, route) => {
acc[route.action.method] = route
return acc
}, {} as IndexedRoutes)
})
it('applies controller OpenAPI props to each method with method-specific props taking precedence', () => {
expect(getOperation(routes.listItems, {})).toEqual(
expect.objectContaining({
description: 'List all items',
externalDocs: { url: 'http://docs.com' },
security: [{ basicAuth: [] }],
summary: 'Method-specific summary',
})
)
expect(getOperation(routes.getItem, {})).toEqual(
expect.objectContaining({
description: 'Common description',
externalDocs: { url: 'http://docs.com' },
security: [],
summary: 'Get item',
})
)
})
})