diff --git a/README.md b/README.md index bc3e080a..811fdb31 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ npm i express-openapi-validator ## Usage -Install the openapi validator +1. Install the openapi validator ```javascript new OpenApiValidator({ @@ -29,21 +29,26 @@ new OpenApiValidator({ validateResponses: true, // false by default }).install(app); ``` -_Note: response validation is currently a beta feature_ -Then, register an error handler to customize errors +2. Register an error handler ```javascript app.use((err, req, res, next) => { // format error - res.status(err.status).json({ + res.status(err.status || 500).json({ message: err.message, errors: err.errors, }); }); ``` -#### Alternatively... +_**Note:** Ensure express is configured with all relevant body parsers. See an [example](#example-express-api-server)_ + +## Advanced Usage + +For OpenAPI 3.0.x 3rd party and custom formats, see [Options](#Options). + +#### Optionally inline the spec... The `apiSpec` option may be specified as the spec object itself, rather than a path e.g. @@ -72,13 +77,25 @@ new OpenApiValidator(options).install(app); **`validateRequests:`** enable response validation. -- true - (default) validate requests. -- false - do not validate requests. +- `true` - (default) validate requests. +- `false` - do not validate requests. **`validateResponses:`** enable response validation. -- true - validate responses -- false - (default) do not validate responses +- `true` - validate responses +- `false` - (default) do not validate responses + +**`unknownFormats:`** handling of unknown and/or custom formats. Option values: + +- `true` (default) - if an unknown format is encountered, validation will report a 400 error. +- `[string]` - an array of unknown format names that will be ignored by the validator. This option can be used to allow usage of third party schemas with format(s), but still fail if another unknown format is used. (_Recommended if unknown formats are used_) +- `"ignore"` - to log warning during schema compilation and always pass validation. This option is not recommended, as it allows to mistype format name and it won't be validated without any error message. + + **example:** + + ```javascript + unknownFormats: ['phone-number', 'uuid'] + ``` **`securityHandlers:`** register authentication handlers @@ -125,9 +142,9 @@ new OpenApiValidator(options).install(app); **`coerceTypes:`** change data type of data to match type keyword. See the example in Coercing data types and coercion rules. Option values: -- true - (default) coerce scalar data types. -- false - no type coercion. -- "array" - in addition to coercions between scalar types, coerce scalar data to an array with one element and vice versa (as required by the schema). +- `true` - (default) coerce scalar data types. +- `false` - no type coercion. +- `"array"` - in addition to coercions between scalar types, coerce scalar data to an array with one element and vice versa (as required by the schema). **`multerOpts:`** the [multer opts](https://github.com/expressjs/multer) to passthrough to multer @@ -148,17 +165,21 @@ const app = express(); // 1. Import the express-openapi-validator library const OpenApiValidator = require('express-openapi-validator').OpenApiValidator; +// 2. Set up body parsers for the request body types you expect app.use(bodyParser.json()); +app.use(bodyParser.text()); +app.use(bodyParser.urlencoded()); + app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); -// 2. (optionally) Serve the OpenAPI spec +// 3. (optionally) Serve the OpenAPI spec app.use('/spec', express.static(spec)); -// 3. Install the OpenApiValidator onto your express app +// 4. Install the OpenApiValidator onto your express app new OpenApiValidator({ apiSpec: './openapi.yaml', }).install(app); @@ -176,7 +197,7 @@ app.get('/v1/pets/:id', function(req, res, next) { res.json({ id: req.params.id, name: 'sparky' }); }); -// 4. Define route(s) to upload file(s) +// 5. Define route(s) to upload file(s) app.post('/v1/pets/:id/photos', function(req, res, next) { // files are found in req.files // non-file multipart params can be found as such: req.body['my-param'] @@ -192,10 +213,10 @@ app.post('/v1/pets/:id/photos', function(req, res, next) { }); }); -// 5. Create an Express error handler +// 6. Create an Express error handler app.use((err, req, res, next) => { - // 6. Customize errors - res.status(err.status).json({ + // 7. Customize errors + res.status(err.status || 500).json({ message: err.message, errors: err.errors, }); diff --git a/example/app.js b/example/app.js index 85f974da..b1e1afcd 100644 --- a/example/app.js +++ b/example/app.js @@ -9,6 +9,7 @@ const app = express(); app.use(bodyParser.urlencoded()); app.use(bodyParser.json()); +app.use(bodyParser.text()); app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); diff --git a/package-lock.json b/package-lock.json index 61ab4e5c..400e55eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "express-openapi-validator", - "version": "2.3.0", + "version": "2.4.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2448,9 +2448,9 @@ "dev": true }, "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.2.tgz", + "integrity": "sha512-cIv17+GhL8pHHnRJzGu2wwcthL5sb8uDKBHvZ2Dtu5s1YNt0ljbzKbamnc+gr69y7bzwQiBdr5+hOpRd5pnOdg==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -5184,13 +5184,13 @@ "dev": true }, "uglify-js": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", - "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.1.tgz", + "integrity": "sha512-+dSJLJpXBb6oMHP+Yvw8hUgElz4gLTh82XuX68QiJVTXaE5ibl6buzhNkQdYhBlIhozWOC9ge16wyRmjG4TwVQ==", "dev": true, "optional": true, "requires": { - "commander": "~2.20.0", + "commander": "2.20.0", "source-map": "~0.6.1" }, "dependencies": { diff --git a/package.json b/package.json index caf5883c..125e9368 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "express-openapi-validator", - "version": "2.3.0", + "version": "2.4.1", "description": "Automatically validate API requests using an OpenAPI 3 and Express.", "main": "dist/index.js", "scripts": { diff --git a/src/index.ts b/src/index.ts index a91c43eb..2e290377 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,10 +14,11 @@ export type SecurityHandlers = { }; export interface OpenApiValidatorOpts { apiSpec: OpenAPIV3.Document | string; - coerceTypes?: boolean; validateResponses?: boolean; validateRequests?: boolean; securityHandlers?: SecurityHandlers; + coerceTypes?: boolean; + unknownFormats?: string[] | string | boolean; multerOpts?: {}; } @@ -26,7 +27,9 @@ export class OpenApiValidator { private options: OpenApiValidatorOpts; constructor(options: OpenApiValidatorOpts) { - if (!options.apiSpec) throw ono('apiSpec required.'); + this.validateOptions(options); + + if (options.unknownFormats == null) options.unknownFormats === true; if (options.coerceTypes == null) options.coerceTypes = true; if (options.validateRequests == null) options.validateRequests = true; @@ -58,26 +61,33 @@ export class OpenApiValidator { }); } - const coerceTypes = this.options.coerceTypes; - const aoav = new middlewares.RequestValidator(this.context.apiDoc, { - nullable: true, - coerceTypes, - removeAdditional: false, - useDefaults: true, - }); + const { coerceTypes, unknownFormats } = this.options; + const requestValidator = new middlewares.RequestValidator( + this.context.apiDoc, + { + nullable: true, + coerceTypes, + removeAdditional: false, + useDefaults: true, + unknownFormats, + }, + ); - const requestValidator = (req, res, next) => { - return aoav.validate(req, res, next); + const requestValidatorMw = (req, res, next) => { + return requestValidator.validate(req, res, next); }; const responseValidator = new middlewares.ResponseValidator( this.context.apiDoc, { coerceTypes, + unknownFormats, }, ); - const securityMiddleware = middlewares.security(this.options.securityHandlers); + const securityMiddleware = middlewares.security( + this.options.securityHandlers, + ); const components = this.context.apiDoc.components; const use = [ @@ -86,9 +96,28 @@ export class OpenApiValidator { ]; // TODO validate security functions exist for each security key if (components && components.securitySchemes) use.push(securityMiddleware); - if (this.options.validateRequests) use.push(requestValidator); + if (this.options.validateRequests) use.push(requestValidatorMw); if (this.options.validateResponses) use.push(responseValidator.validate()); app.use(use); } + + private validateOptions(options: OpenApiValidatorOpts): void { + if (!options.apiSpec) throw ono('apiSpec required.'); + const unknownFormats = options.unknownFormats; + if (typeof unknownFormats === 'boolean') { + if (!unknownFormats) { + throw ono( + "unknownFormats must contain an array of unknownFormats, 'ignore' or true", + ); + } + } else if ( + typeof unknownFormats === 'string' && + unknownFormats !== 'ignore' && + !Array.isArray(unknownFormats) + ) + throw ono( + "unknownFormats must contain an array of unknownFormats, 'ignore' or true", + ); + } } diff --git a/src/middlewares/ajv/index.ts b/src/middlewares/ajv/index.ts index cdb2c990..a8e9857c 100644 --- a/src/middlewares/ajv/index.ts +++ b/src/middlewares/ajv/index.ts @@ -13,10 +13,11 @@ export function createResponseAjv(openApiSpec, options: any = {}) { function createAjv(openApiSpec, options: any = {}, request: boolean = true) { const ajv = new Ajv({ ...options, - formats: { ...formats, ...options.formats }, schemaId: 'auto', allErrors: true, meta: draftSchema, + formats: { ...formats, ...options.formats }, + unknownFormats: options.unknownFormats, }); ajv.removeKeyword('propertyNames'); ajv.removeKeyword('contains'); diff --git a/src/middlewares/openapi.response.validator.ts b/src/middlewares/openapi.response.validator.ts index c613c005..bb8cc287 100644 --- a/src/middlewares/openapi.response.validator.ts +++ b/src/middlewares/openapi.response.validator.ts @@ -18,14 +18,13 @@ export class ResponseValidator { constructor(openApiSpec, options: any = {}) { this.spec = openApiSpec; this.ajv = createResponseAjv(openApiSpec, options); - (mung).onError = function(err, req, res, next) { - // monkey patch mung to rethrow exception + (mung).onError = (err, req, res, next) => { return next(err); }; } validate() { - return mung.jsonAsync((body, req: any, res) => { + return mung.json((body, req: any, res) => { if (req.openapi) { const responses = req.openapi.schema && req.openapi.schema.responses; const validators = this._getOrBuildValidator(req, responses); diff --git a/test/additional.props.spec.ts b/test/additional.props.spec.ts index 2311af21..284a97f5 100644 --- a/test/additional.props.spec.ts +++ b/test/additional.props.spec.ts @@ -8,7 +8,6 @@ const packageJson = require('../package.json'); describe(packageJson.name, () => { let app = null; - let basePath = null; before(async () => { // Set up the express app @@ -17,16 +16,14 @@ describe(packageJson.name, () => { 'resources', 'additional.properties.yaml', ); - app = await createApp({ apiSpec }, 3005); - basePath = app.basePath; - - // Define new coercion routes - app.use( - `${basePath}/additional_props`, - express - .Router() - .post(`/false`, (req, res) => res.json(req.body)) - .post(`/true`, (req, res) => res.json(req.body)), + app = await createApp({ apiSpec }, 3005, app => + app.use( + `${app.basePath}/additional_props`, + express + .Router() + .post(`/false`, (req, res) => res.json(req.body)) + .post(`/true`, (req, res) => res.json(req.body)), + ), ); }); @@ -36,14 +33,14 @@ describe(packageJson.name, () => { it('should return 400 if additionalProperties=false, but extra props sent', async () => request(app) - .post(`${basePath}/additional_props/false`) + .post(`${app.basePath}/additional_props/false`) .send({ name: 'test', extra_prop: 'test', }) .expect(400) .then(r => { - expect(r.body.errors).to.be.an('array') + expect(r.body.errors).to.be.an('array'); expect(r.body.errors).to.have.length(1); const message = r.body.errors[0].message; expect(message).to.equal('should NOT have additional properties'); @@ -51,7 +48,7 @@ describe(packageJson.name, () => { it('should return 200 if additonalProperities=true and extra props are sent', async () => request(app) - .post(`${basePath}/additional_props/true`) + .post(`${app.basePath}/additional_props/true`) .send({ name: 'test', extra_prop: 'test', diff --git a/test/coercion.spec.ts b/test/coercion.spec.ts index c1364155..5b71d9a1 100644 --- a/test/coercion.spec.ts +++ b/test/coercion.spec.ts @@ -8,21 +8,18 @@ const packageJson = require('../package.json'); describe(packageJson.name, () => { let app = null; - let basePath = null; before(async () => { // Set up the express app const apiSpec = path.join('test', 'resources', 'coercion.yaml'); - app = await createApp({ apiSpec }, 3005); - basePath = app.basePath; - - // Define new coercion routes - app.use( - `${basePath}/coercion`, - express - .Router() - .post(`/pets`, (req, res) => res.json(req.body)) - .post(`/pets_string_boolean`, (req, res) => res.json(req.body)), + app = await createApp({ apiSpec }, 3005, app => + app.use( + `${app.basePath}/coercion`, + express + .Router() + .post(`/pets`, (req, res) => res.json(req.body)) + .post(`/pets_string_boolean`, (req, res) => res.json(req.body)), + ), ); }); @@ -32,7 +29,7 @@ describe(packageJson.name, () => { it('should coerce is_cat to boolean since it is defined as a boolean in the spec', async () => request(app) - .post(`${basePath}/coercion/pets`) + .post(`${app.basePath}/coercion/pets`) .send({ name: 'test', is_cat: 'true', @@ -45,7 +42,7 @@ describe(packageJson.name, () => { it('should keep is_cat as boolean', async () => request(app) - .post(`${basePath}/coercion/pets`) + .post(`${app.basePath}/coercion/pets`) .send({ name: 'test', is_cat: true, @@ -58,7 +55,7 @@ describe(packageJson.name, () => { it('should coerce a is_cat from boolean to string since it is defined as such in the spec', async () => request(app) - .post(`${basePath}/coercion/pets_string_boolean`) + .post(`${app.basePath}/coercion/pets_string_boolean`) .send({ name: 'test', is_cat: true, diff --git a/test/common/app.common.ts b/test/common/app.common.ts index 2478f102..8d72b033 100644 --- a/test/common/app.common.ts +++ b/test/common/app.common.ts @@ -1,13 +1,11 @@ import * as http from 'http'; import * as express from 'express'; -const BASE_PATH = '/v1'; export function startServer(app, port): Promise { return new Promise((resolve, reject) => { const http = require('http'); const server = http.createServer(app); app.server = server; - app.basePath = BASE_PATH; server.listen(port, () => { console.log(`Listening on port ${port}`); resolve(server); @@ -16,7 +14,7 @@ export function startServer(app, port): Promise { } export function routes(app) { - const basePath = BASE_PATH; + const basePath = app.basePath; const router1 = express .Router() .post('/', function(req, res, next) { diff --git a/test/common/app.ts b/test/common/app.ts index 0b1eb3a2..203ff02e 100644 --- a/test/common/app.ts +++ b/test/common/app.ts @@ -10,11 +10,14 @@ import { startServer, routes } from './app.common'; export async function createApp( opts?: any, port: number = 3000, + customRoutes: (app) => void = () => {}, useRoutes: boolean = true, ) { var app = express(); + (app).basePath = '/v1'; app.use(bodyParser.json()); + app.use(bodyParser.text()); app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); @@ -24,10 +27,18 @@ export async function createApp( new OpenApiValidator(opts).install(app); if (useRoutes) { + // register common routes routes(app); + } + + // register custom routes + customRoutes(app); + + if (useRoutes) { // Register error handler app.use((err, req, res, next) => { res.status(err.status || 500).json({ + message: err.message, errors: err.errors, }); }); diff --git a/test/headers.spec.ts b/test/headers.spec.ts index e97fc657..ae123482 100644 --- a/test/headers.spec.ts +++ b/test/headers.spec.ts @@ -6,13 +6,11 @@ import * as packageJson from '../package.json'; describe(packageJson.name, () => { let app = null; - let basePath = null; before(() => { const apiSpec = path.join('test', 'resources', 'openapi.yaml'); return createApp({ apiSpec }, 3004).then(a => { app = a; - basePath = (app).basePath; }); }); @@ -22,7 +20,7 @@ describe(packageJson.name, () => { it('should throw 400 if required header is missing', async () => request(app) - .get(`${basePath}/pets/10/attributes`) + .get(`${app.basePath}/pets/10/attributes`) .set('Accept', 'application/json') .expect('Content-Type', /json/) .expect(400) diff --git a/test/multipart.spec.ts b/test/multipart.spec.ts index f5ca85d2..939a33ce 100644 --- a/test/multipart.spec.ts +++ b/test/multipart.spec.ts @@ -5,25 +5,21 @@ import { createApp } from './common/app'; import * as packageJson from '../package.json'; describe(packageJson.name, () => { - let app = null; - let basePath = null; + describe(`GET .../pets/:id/photos`, () => { + let app = null; - before(() => { - const apiSpec = path.join('test', 'resources', 'openapi.yaml'); - return createApp({ apiSpec }, 3003).then(a => { - app = a; - basePath = (app).basePath; + before(async () => { + const apiSpec = path.join('test', 'resources', 'openapi.yaml'); + app = await createApp({ apiSpec }, 3003); }); - }); - after(() => { - (app).server.close(); - }); + after(() => { + (app).server.close(); + }); - describe(`GET ${basePath}/pets/:id/photos`, () => { it('should throw 400 when required multipart file field', async () => request(app) - .post(`${basePath}/pets/10/photos`) + .post(`${app.basePath}/pets/10/photos`) .set('Content-Type', 'multipart/form-data') .set('Accept', 'application/json') .expect(400) @@ -38,7 +34,7 @@ describe(packageJson.name, () => { it('should throw 400 when required form field is missing during multipart upload', async () => request(app) - .post(`${basePath}/pets/10/photos`) + .post(`${app.basePath}/pets/10/photos`) .set('Content-Type', 'multipart/form-data') .set('Accept', 'application/json') .attach('file', 'package.json') @@ -46,7 +42,7 @@ describe(packageJson.name, () => { it('should validate multipart file and metadata', async () => request(app) - .post(`${basePath}/pets/10/photos`) + .post(`${app.basePath}/pets/10/photos`) .set('Content-Type', 'multipart/form-data') .set('Accept', 'application/json') .attach('file', 'package.json') @@ -65,7 +61,7 @@ describe(packageJson.name, () => { it('should throw 405 get method not allowed', async () => request(app) - .get(`${basePath}/pets/10/photos`) + .get(`${app.basePath}/pets/10/photos`) .set('Content-Type', 'multipart/form-data') .set('Accept', 'application/json') .expect('Content-Type', /json/) @@ -75,7 +71,7 @@ describe(packageJson.name, () => { it('should throw 415 unsupported media type', async () => request(app) - .post(`${basePath}/pets/10/photos`) + .post(`${app.basePath}/pets/10/photos`) .send({ test: 'test' }) .set('Content-Type', 'application/json') .expect('Content-Type', /json/) diff --git a/test/nullable.spec.ts b/test/nullable.spec.ts index 3ff36ce4..ce7d8b6c 100644 --- a/test/nullable.spec.ts +++ b/test/nullable.spec.ts @@ -13,12 +13,13 @@ describe(packageJson.name, () => { before(async () => { // Set up the express app const apiSpec = path.join('test', 'resources', 'nullable.yaml'); - app = await createApp({ apiSpec, coerceTypes: false }, 3005); - basePath = app.basePath; - - app.use( - `${basePath}`, - express.Router().post(`/pets/nullable`, (req, res) => res.json(req.body)), + app = await createApp({ apiSpec, coerceTypes: false }, 3005, app => + app.use( + `${app.basePath}`, + express + .Router() + .post(`/pets/nullable`, (req, res) => res.json(req.body)), + ), ); }); @@ -28,7 +29,7 @@ describe(packageJson.name, () => { it('should allow null to be set (name: nullable true)', async () => request(app) - .post(`${basePath}/pets/nullable`) + .post(`${app.basePath}/pets/nullable`) .send({ name: null, }) @@ -39,7 +40,7 @@ describe(packageJson.name, () => { it('should not fill an explicity null with default when coerceTypes is false', async () => request(app) - .post(`${basePath}/pets`) + .post(`${app.basePath}/pets`) .send({ name: null, }) @@ -47,7 +48,7 @@ describe(packageJson.name, () => { it('should fill unspecified field with default when coerceTypes is false', async () => request(app) - .post(`${basePath}/pets`) + .post(`${app.basePath}/pets`) .send({ name: 'name', }) @@ -58,7 +59,7 @@ describe(packageJson.name, () => { it('should fail if required and not provided (nullable true)', async () => request(app) - .post(`${basePath}/pets/nullable`) + .post(`${app.basePath}/pets/nullable`) .send({}) .expect(400) .then(r => { @@ -67,7 +68,7 @@ describe(packageJson.name, () => { it('should fail if required and not provided (nullable false', async () => request(app) - .post(`${basePath}/pets`) + .post(`${app.basePath}/pets`) .send({}) .expect(400) .then(r => { @@ -76,7 +77,7 @@ describe(packageJson.name, () => { it('should fail if required and provided as null when nullable is false', async () => request(app) - .post(`${basePath}/pets`) + .post(`${app.basePath}/pets`) .send({ name: null, }) diff --git a/test/path.level.parameters.spec.ts b/test/path.level.parameters.spec.ts index fa751212..3bc60054 100644 --- a/test/path.level.parameters.spec.ts +++ b/test/path.level.parameters.spec.ts @@ -17,13 +17,13 @@ describe(packageJson.name, () => { 'resources', 'path.level.parameters.yaml', ); - app = await createApp({ apiSpec }, 3005); - basePath = app.basePath; - - // Define new coercion routes - app.use( - `${basePath}`, - express.Router().get(`/path_level_parameters`, (_req, res) => res.send()), + app = await createApp({ apiSpec }, 3005, app => + app.use( + `${app.basePath}`, + express + .Router() + .get(`/path_level_parameters`, (_req, res) => res.send()), + ), ); }); @@ -33,7 +33,7 @@ describe(packageJson.name, () => { it('should return 400 if pathLevel query parameter is not provided', async () => request(app) - .get(`${basePath}/path_level_parameters?operationLevel=123`) + .get(`${app.basePath}/path_level_parameters?operationLevel=123`) .send() .expect(400) .then(r => { @@ -45,7 +45,7 @@ describe(packageJson.name, () => { it('should return 400 if operationLevel query parameter is not provided', async () => request(app) - .get(`${basePath}/path_level_parameters?pathLevel=123`) + .get(`${app.basePath}/path_level_parameters?pathLevel=123`) .send() .expect(400) .then(r => { @@ -59,7 +59,7 @@ describe(packageJson.name, () => { it('should return 400 if neither operationLevel, nor pathLevel query parameters are provided', async () => request(app) - .get(`${basePath}/path_level_parameters`) + .get(`${app.basePath}/path_level_parameters`) .send() .expect(400) .then(r => { @@ -74,7 +74,7 @@ describe(packageJson.name, () => { it('should return 200 if both pathLevel and operationLevel query parameter are provided', async () => request(app) - .get(`${basePath}/path_level_parameters?operationLevel=123&pathLevel=123`) + .get(`${app.basePath}/path_level_parameters?operationLevel=123&pathLevel=123`) .send() .expect(200)); }); diff --git a/test/query.params.spec.ts b/test/query.params.spec.ts index 89cd3af2..946a7866 100644 --- a/test/query.params.spec.ts +++ b/test/query.params.spec.ts @@ -13,12 +13,13 @@ describe(packageJson.name, () => { before(async () => { // Set up the express app const apiSpec = path.join('test', 'resources', 'query.params.yaml'); - app = await createApp({ apiSpec }, 3005); - basePath = app.basePath; - - app.use( - `${basePath}`, - express.Router().post(`/pets/nullable`, (req, res) => res.json(req.body)), + app = await createApp({ apiSpec }, 3005, app => + app.use( + `${app.basePath}`, + express + .Router() + .post(`/pets/nullable`, (req, res) => res.json(req.body)), + ), ); }); @@ -28,7 +29,7 @@ describe(packageJson.name, () => { it('should pass if known query params are specified', async () => request(app) - .get(`${basePath}/pets`) + .get(`${app.basePath}/pets`) .query({ tags: 'one,two,three', limit: 10, @@ -39,7 +40,7 @@ describe(packageJson.name, () => { it('should fail if unknown query param is specified', async () => request(app) - .get(`${basePath}/pets`) + .get(`${app.basePath}/pets`) .query({ tags: 'one,two,three', limit: 10, diff --git a/test/request.bodies.ref.spec.ts b/test/request.bodies.ref.spec.ts index a26e8bd3..a9c3b7ee 100644 --- a/test/request.bodies.ref.spec.ts +++ b/test/request.bodies.ref.spec.ts @@ -8,20 +8,32 @@ const packageJson = require('../package.json'); describe(packageJson.name, () => { let app = null; - let basePath = null; before(async () => { // Set up the express app const apiSpec = path.join('test', 'resources', 'request.bodies.ref.yaml'); - app = await createApp({ apiSpec }, 3005); - basePath = app.basePath; - - // Define new coercion routes - app.use( - `${basePath}`, - express - .Router() - .post(`/request_bodies_ref`, (req, res) => res.json(req.body)), + app = await createApp( + { + apiSpec, + validateResponses: true, + unknownFormats: ['phone-number'], + }, + 3005, + app => { + // Define new coercion routes + app.post(`${app.basePath}/request_bodies_ref`, (req, res) => { + if (req.headers['content-type'].indexOf('text/plain') > -1) { + res.type('text').send(req.body); + } else if (req.query.bad_body) { + const r = req.body; + r.unexpected_prop = 'bad'; + res.json(r); + } else { + res.json(req.body); + } + }); + }, + true, ); }); @@ -29,9 +41,21 @@ describe(packageJson.name, () => { app.server.close(); }); + it('should return 200 if text/plain request body is satisfied', async () => { + const stringData = 'my string data'; + return request(app) + .post(`${app.basePath}/request_bodies_ref`) + .set('content-type', 'text/plain') + .send(stringData) + .expect(200) + .then(r => { + expect(r.text).equals(stringData); + }); + }); + it('should return 400 if testProperty body property is not provided', async () => request(app) - .post(`${basePath}/request_bodies_ref`) + .post(`${app.basePath}/request_bodies_ref`) .send({}) .expect(400) .then(r => { @@ -45,15 +69,39 @@ describe(packageJson.name, () => { it('should return 200 if testProperty body property is provided', async () => request(app) - .post(`${basePath}/request_bodies_ref`) + .post(`${app.basePath}/request_bodies_ref`) .send({ testProperty: 'abc', }) - .expect(200)); + .expect(200) + .then(r => { + const { body } = r; + expect(body).to.have.property('testProperty'); + })); + + it('should return 500 if additional response body property is returned', async () => + request(app) + .post(`${app.basePath}/request_bodies_ref`) + .query({ + bad_body: true, + }) + .send({ + testProperty: 'abc', + }) + .expect(500) + .then(r => { + const { body } = r; + expect(body.message).to.include( + '.response should NOT have additional properties', + ); + expect(body.errors[0].message).to.equals( + 'should NOT have additional properties', + ); + })); it('should return 400 if an additional property is encountered', async () => request(app) - .post(`${basePath}/request_bodies_ref`) + .post(`${app.basePath}/request_bodies_ref`) .send({ testProperty: 'abc', invalidProperty: 'abc', diff --git a/test/resources/request.bodies.ref.yaml b/test/resources/request.bodies.ref.yaml index b8f6bdcf..8d86cecf 100644 --- a/test/resources/request.bodies.ref.yaml +++ b/test/resources/request.bodies.ref.yaml @@ -11,24 +11,40 @@ paths: /request_bodies_ref: post: requestBody: - $ref: '#components/requestBodies/TestBody' + $ref: '#/components/requestBodies/TestBody' responses: '200': description: OK + content: + text/plain: + schema: + type: string + application/json: + schema: + $ref: '#/components/schemas/Test' '400': description: Bad Request components: + schemas: + Test: + type: object + additionalProperties: false + properties: + testProperty: + type: string + example: +15017122661 + format: phone-number + required: + - testProperty + requestBodies: TestBody: required: true content: + text/plain: + schema: + type: string application/json: schema: - type: object - additionalProperties: false - properties: - testProperty: - type: string - required: - - testProperty + $ref: '#/components/schemas/Test' diff --git a/test/resources/response.validation.yaml b/test/resources/response.validation.yaml index 7437c443..26274a53 100644 --- a/test/resources/response.validation.yaml +++ b/test/resources/response.validation.yaml @@ -12,7 +12,7 @@ info: name: Apache 2.0 url: https://www.apache.org/licenses/LICENSE-2.0.html servers: - - url: /v1/ + - url: /v1 paths: /pets: description: endpoints for pets diff --git a/test/response.validation.spec.ts b/test/response.validation.spec.ts index 80746715..84cb37c8 100644 --- a/test/response.validation.spec.ts +++ b/test/response.validation.spec.ts @@ -9,30 +9,29 @@ const apiSpecPath = path.join('test', 'resources', 'response.validation.yaml'); describe(packageJson.name, () => { let app = null; - let basePath = null; before(async () => { - // Set up the express app + // set up express app app = await createApp( { apiSpec: apiSpecPath, validateResponses: true }, 3005, + app => { + app.get(`${app.basePath}/pets`, (req, res) => { + let json = {}; + if ((req.query.mode = 'bad_type')) { + json = [{ id: 'bad_id', name: 'name', tag: 'tag' }]; + } + return res.json(json); + }); + app.use((err, req, res, next) => { + res.status(err.status || 500).json({ + message: err.message, + code: err.status || 500, + }); + }); + }, false, ); - basePath = app.basePath; - app.get(`${basePath}/pets`, (req, res) => { - let json = {}; - if ((req.query.mode = 'bad_type')) { - json = [{ id: 'bad_id', name: 'name', tag: 'tag' }]; - } - return res.json(json); - }); - // Register error handler - app.use((err, req, res, next) => { - res.status(err.status || 500).json({ - message: err.message, - code: err.status, - }); - }); }); after(() => { @@ -41,7 +40,7 @@ describe(packageJson.name, () => { it('should fail if response field has a value of incorrect type', async () => request(app) - .get(`${basePath}/pets?mode=bad_type`) + .get(`${app.basePath}/pets?mode=bad_type`) .expect(500) .then((r: any) => { expect(r.body.message).to.contain('should be integer');