@wesleytodd/openapi
Version:
Middleware for generating OpenAPI/Swagger documentation for your Express app
660 lines (568 loc) • 17.8 kB
JavaScript
const { suite, test } = require('mocha')
const assert = require('assert')
const util = require('util')
const supertest = require('supertest')
const SwaggerParser = require('swagger-parser')
const openapi = require('../')
const { name } = require('../package.json')
const YAML = require('yaml')
// We support testing with different major versions of express
let spec = 'express'
if (process.env.EXPRESS_MAJOR) {
if (!['4', '5'].includes(process.env.EXPRESS_MAJOR)) {
throw new Error('EXPRESS_MAJOR contained an invalid value')
}
spec += process.env.EXPRESS_MAJOR
}
const express = require(spec)
function logDocument (doc) {
console.log(util.inspect(doc, { depth: null }))
}
suite(name, function () {
test('accept no options', function () {
const oapi = openapi()
assert.strictEqual(oapi.routePrefix, openapi.defaultRoutePrefix)
assert.deepStrictEqual(oapi.document, openapi.minimumViableDocument)
})
test('accept no document option', function () {
const oapi = openapi('/test')
assert.strictEqual(oapi.routePrefix, '/test')
assert.deepStrictEqual(oapi.document, openapi.minimumViableDocument)
})
test('accept doc w/o path or opts', function () {
const oapi = openapi({
info: {
version: '1.0.0',
title: '@express/openapi'
}
})
assert.strictEqual(oapi.routePrefix, '/openapi')
assert.deepStrictEqual(oapi.document.info.title, '@express/openapi')
assert.deepStrictEqual(oapi.options, {})
})
test('accept both a routePrefix and a document', function () {
const oapi = openapi('/test', {
info: {
title: 'Test App'
}
})
assert.strictEqual(oapi.routePrefix, '/test')
assert.deepStrictEqual(oapi.document, {
openapi: '3.0.0',
info: {
title: 'Test App',
version: '1.0.0'
},
paths: {}
})
})
test('create a basic valid OpenAPI document and serve test json on an express app', function (done) {
const app = express()
app.use(openapi())
supertest(app)
.get(`${openapi.defaultRoutePrefix}.json`)
.expect(200, (err, res) => {
assert(!err, err)
SwaggerParser.validate(res.body, (err, api) => {
assert(!err, err)
assert.deepStrictEqual(api, openapi.minimumViableDocument)
done()
})
})
})
test('create a basic valid OpenAPI document and serve test yaml on an express app', function (done) {
const app = express()
app.use(openapi())
supertest(app)
.get(`${openapi.defaultRoutePrefix}.yaml`)
.expect(200, (err, res) => {
assert(!err, err)
const output = YAML.parse(res.text)
SwaggerParser.validate(output, (err, api) => {
assert(!err, err)
assert.deepStrictEqual(api, openapi.minimumViableDocument)
done()
})
})
})
test('create a basic valid OpenAPI document and serve test yml on an express app', function (done) {
const app = express()
app.use(openapi())
supertest(app)
.get(`${openapi.defaultRoutePrefix}.yml`)
.expect(200, (err, res) => {
assert(!err, err)
const output = YAML.parse(res.text)
SwaggerParser.validate(output, (err, api) => {
assert(!err, err)
assert.deepStrictEqual(api, openapi.minimumViableDocument)
done()
})
})
})
test('create a basic valid Swagger UI document and check the HTML title', function (done) {
const app = express()
app.use(openapi().swaggerui())
supertest(app)
.get(`${openapi.defaultRoutePrefix}.json`)
.end((err, res) => {
assert(!err, err)
assert(res.text.includes('<title>Swagger UI</title>'))
done()
})
})
test('serves onload function in swagger-ui-init.js file', function (done) {
const app = express()
app.use(openapi().swaggerui())
supertest(app)
.get(`${openapi.defaultRoutePrefix}/swagger-ui-init.js`)
.end((err, res) => {
assert(!err, err)
assert(res.text.includes('window.onload = function () {'))
done()
})
})
test('load routes from the express app', function (done) {
const app = express()
const oapi = openapi()
const helloWorldSchema = oapi.path({
responses: {
200: {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
hello: { type: 'string' }
}
}
}
}
}
}
})
app.use(oapi)
app.get('/foo', helloWorldSchema, (req, res) => {
res.json({
hello: 'world'
})
})
supertest(app)
.get(`${openapi.defaultRoutePrefix}.json`)
.expect(200, (err, res) => {
assert(!err, err)
SwaggerParser.validate(res.body, (err, api) => {
assert(!err, err)
done()
})
})
})
test('support express array formats', (done) => {
const app = express()
const oapi = openapi()
const emptySchema = oapi.path({
responses: {
204: {
description: 'Successful response',
content: {
'application/json': { }
}
}
}
})
app.use(oapi)
app.get('/undocumented', (req, res) => {
res.status(204).send()
})
app.get('/array', [emptySchema, (req, res) => {
res.status(204).send()
}])
app.get('/array-of-arrays', [[emptySchema, (req, res) => {
res.status(204).send()
}]])
supertest(app)
.get(`${openapi.defaultRoutePrefix}.json`)
.expect(200, (err, res) => {
assert(!err, err)
SwaggerParser.validate(res.body, (err, api) => {
if (err) {
logDocument(api)
done(err)
}
assert(api.paths['/array'])
assert(api.paths['/array'].get)
assert(api.paths['/array'].get.responses[204])
assert.strictEqual(api.paths['/array'].get.responses[204].description, 'Successful response')
assert(api.paths['/array-of-arrays'])
assert(api.paths['/array-of-arrays'].get)
assert(api.paths['/array-of-arrays'].get.responses[204])
assert.strictEqual(api.paths['/array-of-arrays'].get.responses[204].description, 'Successful response')
assert(!api.paths['/undocumented'])
done()
})
})
})
test('support express route syntax', (done) => {
const app = express()
const oapi = openapi()
const emptySchema = oapi.path({
responses: {
204: {
description: 'Successful response',
content: {
'application/json': { }
}
}
}
})
app.use(oapi)
app.route('/route')
.get(emptySchema, (req, res) => {
res.status(204).send()
})
.put(emptySchema, (req, res) => {
res.status(204).send()
})
app.route('/route-all')
.all(emptySchema)
.all((req, res) => {
res.status(204).send()
})
supertest(app)
.get(`${openapi.defaultRoutePrefix}.json`)
.expect(200, (err, res) => {
assert(!err, err)
SwaggerParser.validate(res.body, (err, api) => {
if (err) {
logDocument(api)
done(err)
}
assert(api.paths['/route'])
assert(api.paths['/route'].get)
assert(api.paths['/route'].get.responses[204])
assert.strictEqual(api.paths['/route'].get.responses[204].description, 'Successful response')
assert(api.paths['/route'].put)
assert(api.paths['/route'].put.responses[204])
assert.strictEqual(api.paths['/route'].put.responses[204].description, 'Successful response')
done()
})
})
})
test('support named path params', (done) => {
const app = express()
const oapi = openapi()
const emptySchema = oapi.path({
responses: {
204: {
description: 'Successful response',
content: {
'application/json': { }
}
}
}
})
app.use(oapi)
app.get('/:foo', emptySchema, (req, res) => {
res.status(204).send()
})
supertest(app)
.get(`${openapi.defaultRoutePrefix}.json`)
.expect(200, (err, res) => {
assert(!err, err)
SwaggerParser.validate(res.body, (err, api) => {
if (err) {
logDocument(api)
done(err)
}
assert(api.paths['/{foo}'])
assert(api.paths['/{foo}'].get)
assert(api.paths['/{foo}'].get.parameters[0])
assert.strictEqual(api.paths['/{foo}'].get.parameters[0].name, 'foo')
done()
})
})
})
test('support parameter components', (done) => {
const app = express()
const oapi = openapi()
oapi.parameters('id', {
in: 'path',
required: true,
description: 'The entity id',
schema: { type: 'string' }
})
app.use(oapi)
app.get('/:id', oapi.path({
description: 'Get thing by id',
parameters: [oapi.parameters('id')],
responses: {
204: {
description: 'Successful response',
content: {
'application/json': { }
}
}
}
}), (req, res) => {
res.status(204).send()
})
supertest(app)
.get(`${openapi.defaultRoutePrefix}/validate`)
.expect(200, (err, res) => {
assert(!err, err)
assert.strictEqual(res.body.valid, true)
assert.strictEqual(res.body.document.components.parameters.id.name, 'id')
assert.strictEqual(res.body.document.components.parameters.id.description, 'The entity id')
assert.strictEqual(res.body.document.components.parameters.id.schema.type, 'string')
assert.strictEqual(res.status, 200)
done()
})
})
test('support a non-string path parameter', (done) => {
const app = express()
const oapi = openapi()
oapi.parameters('id', {
in: 'path',
required: true,
description: 'The numeric User ID',
schema: { type: 'integer' }
})
app.use(oapi)
app.get('/:id', oapi.path({
description: 'Get a user by ID',
parameters: [oapi.parameters('id')],
responses: {
204: {
description: 'Successful response',
content: {
'application/json': { }
}
}
}
}), (req, res) => {
res.status(204).send()
})
supertest(app)
.get(`${openapi.defaultRoutePrefix}/validate`)
.expect(200, (err, res) => {
assert(!err, err)
assert.strictEqual(res.body.valid, true)
assert.strictEqual(res.body.document.components.parameters.id.name, 'id')
assert.strictEqual(res.body.document.components.parameters.id.description, 'The numeric User ID')
assert.strictEqual(res.body.document.components.parameters.id.schema.type, 'integer')
assert.strictEqual(res.status, 200)
done()
})
})
test('support express sub-routes with Router', (done) => {
const app = express()
const router = express.Router()
const oapi = openapi()
const emptySchema = oapi.path({
responses: {
204: {
description: 'Successful response',
content: {
'application/json': {}
}
}
}
})
router.get('/endpoint', emptySchema, (req, res) => {
res.status(204).send()
})
app.use(oapi)
app.use('/sub-route', router)
supertest(app)
.get(`${openapi.defaultRoutePrefix}.json`)
.expect(200, (err, res) => {
assert(!err, err)
SwaggerParser.validate(res.body, (err, api) => {
if (err) {
logDocument(api)
done(err)
}
assert(api.paths['/sub-route/endpoint'])
assert(api.paths['/sub-route/endpoint'].get)
assert(api.paths['/sub-route/endpoint'].get.responses[204])
assert.strictEqual(
api.paths['/sub-route/endpoint'].get.responses[204].description,
'Successful response'
)
done()
})
})
})
test('support express nested sub-routes with Router', (done) => {
const app = express()
const router = express.Router()
const subrouter = express.Router()
const oapi = openapi()
const emptySchema = oapi.path({
responses: {
204: {
description: 'Successful response',
content: {
'application/json': {}
}
}
}
})
subrouter.get('/endpoint', emptySchema, (req, res) => {
res.status(204).send()
})
app.use(oapi)
app.use('/sub-route', router)
router.use('/sub-sub-route', subrouter)
supertest(app)
.get(`${openapi.defaultRoutePrefix}.json`)
.expect(200, (err, res) => {
assert(!err, err)
SwaggerParser.validate(res.body, (err, api) => {
if (err) {
logDocument(api)
done(err)
}
assert(api.paths['/sub-route/sub-sub-route/endpoint'])
assert(api.paths['/sub-route/sub-sub-route/endpoint'].get)
assert(api.paths['/sub-route/sub-sub-route/endpoint'].get.responses[204])
assert.strictEqual(
api.paths['/sub-route/sub-sub-route/endpoint'].get.responses[204].description,
'Successful response'
)
done()
})
})
})
test('support express nested sub-routes with base path', (done) => {
const app = express()
const router = express.Router()
const subrouter = express.Router()
const oapi = openapi(undefined, { basePath: '/sub-route' })
const emptySchema = oapi.path({
responses: {
204: {
description: 'Successful response',
content: {
'application/json': {}
}
}
}
})
subrouter.get('/endpoint', emptySchema, (req, res) => {
res.status(204).send()
})
app.use(oapi)
app.use('/sub-route', router)
router.use('/sub-sub-route', subrouter)
supertest(app)
.get(`${openapi.defaultRoutePrefix}.json`)
.expect(200, (err, res) => {
assert(!err, err)
SwaggerParser.validate(res.body, (err, api) => {
if (err) {
logDocument(api)
done(err)
}
assert(Object.keys(api.paths).length === 1)
assert(api.paths['/sub-sub-route/endpoint'])
assert(api.paths['/sub-sub-route/endpoint'].get)
assert(api.paths['/sub-sub-route/endpoint'].get.responses[204])
assert.strictEqual(
api.paths['/sub-sub-route/endpoint'].get.responses[204].description,
'Successful response'
)
done()
})
})
})
test('sub-routes should only be stripped once', (done) => {
const app = express()
const router = express.Router()
const subrouter = express.Router()
const oapi = openapi(undefined, { basePath: '/sub-route' })
const emptySchema = oapi.path({
responses: {
204: {
description: 'Successful response',
content: {
'application/json': {}
}
}
}
})
subrouter.get('/endpoint', emptySchema, (req, res) => {
res.status(204).send()
})
app.use(oapi)
app.use('/sub-route', router)
router.use('/sub-route', subrouter)
supertest(app)
.get(`${openapi.defaultRoutePrefix}.json`)
.expect(200, (err, res) => {
assert(!err, err)
SwaggerParser.validate(res.body, (err, api) => {
if (err) {
logDocument(api)
done(err)
}
assert(Object.keys(api.paths).length === 1)
assert(api.paths['/sub-route/endpoint'])
assert(api.paths['/sub-route/endpoint'].get)
assert(api.paths['/sub-route/endpoint'].get.responses[204])
assert.strictEqual(
api.paths['/sub-route/endpoint'].get.responses[204].description,
'Successful response'
)
done()
})
})
})
test('the basePath option should only strip the base path from the start of paths', (done) => {
const app = express()
const router = express.Router()
const subrouter = express.Router()
const oapi = openapi(undefined, { basePath: '/base-path' })
const emptySchema = oapi.path({
responses: {
204: {
description: 'Successful response',
content: {
'application/json': {}
}
}
}
})
subrouter.get('/endpoint', emptySchema, (req, res) => {
res.status(204).send()
})
app.use(oapi)
app.use('/route', router)
router.use('/base-path', subrouter)
supertest(app)
.get(`${openapi.defaultRoutePrefix}.json`)
.expect(200, (err, res) => {
assert(!err, err)
SwaggerParser.validate(res.body, (err, api) => {
if (err) {
logDocument(api)
done(err)
}
assert(Object.keys(api.paths).length === 1)
assert(api.paths['/route/base-path/endpoint'])
assert(api.paths['/route/base-path/endpoint'].get)
assert(api.paths['/route/base-path/endpoint'].get.responses[204])
assert.strictEqual(
api.paths['/route/base-path/endpoint'].get.responses[204].description,
'Successful response'
)
done()
})
})
})
// Other tests
require('./_validate')()
require('./_routes')()
})