From cb6efa99c1556c76881dfec747f8c01493421716 Mon Sep 17 00:00:00 2001 From: idantovi Date: Wed, 27 Dec 2017 16:09:57 +0200 Subject: [PATCH] add support for swagger base path --- src/middleware.js | 2 +- test/middleware-test.js | 351 +++++++++++++++++++++ test/pet-store-swagger-inheritance.yaml | 4 +- test/pet-store-swagger-with-base-path.yaml | 213 +++++++++++++ test/pet-store-swagger.yaml | 2 +- test/router.js | 13 + test/test-simple-server-with-base-path.js | 29 ++ 7 files changed, 610 insertions(+), 4 deletions(-) create mode 100644 test/pet-store-swagger-with-base-path.yaml create mode 100644 test/router.js create mode 100644 test/test-simple-server-with-base-path.js diff --git a/src/middleware.js b/src/middleware.js index 32f0470..4155e1a 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -22,7 +22,7 @@ function init(swaggerPath, options) { var dereferenced = swaggers[0]; Object.keys(dereferenced.paths).forEach(function (currentPath) { let pathParameters = dereferenced.paths[currentPath].parameters || []; - let parsedPath = currentPath.replace(/{/g, ':').replace(/}/g, ''); + let parsedPath = dereferenced.basePath !== '/' ? dereferenced.basePath.concat(currentPath.replace(/{/g, ':').replace(/}/g, '')) : currentPath.replace(/{/g, ':').replace(/}/g, ''); schemas[parsedPath] = {}; Object.keys(dereferenced.paths[currentPath]).filter(function (parameter) { return parameter !== 'parameters' }) .forEach(function (currentMethod) { diff --git a/test/middleware-test.js b/test/middleware-test.js index b44d1d0..a71a4e5 100644 --- a/test/middleware-test.js +++ b/test/middleware-test.js @@ -379,6 +379,357 @@ describe('input-validation middleware tests', function () { }); }); }); + describe('Simple server - with base path', function () { + var app; + before(function () { + return require('./test-simple-server-with-base-path').then(function (testServer) { + app = testServer; + }); + }); + it('valid request - should pass validation', function (done) { + request(app) + .get('/v1/pets') + .set('api-version', '1.0') + .set('request-id', '123456') + .query({ page: 0 }) + .expect(200, function (err, res) { + if (err) { + throw err; + } + expect(res.body.result).to.equal('OK'); + done(); + }); + }); + it('missing header - should fail', function (done) { + request(app) + .get('/v1/pets') + .set('request-id', '123456') + .query({ page: 0 }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + let moreInfoAsJson = JSON.parse(res.body.more_info); + expect(moreInfoAsJson).to.be.instanceof(Array); + expect(res.body.more_info).to.includes('api-version'); + expect(res.body.more_info).to.includes('should have required property \'api-version\''); + done(); + }); + }); + it('bad header - invalid pattern', function (done) { + request(app) + .get('/v1/pets') + .set('request-id', '123456') + .set('api-version', '1') + .query({ page: 0 }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + let moreInfoAsJson = JSON.parse(res.body.more_info); + expect(moreInfoAsJson).to.be.instanceof(Array); + expect(res.body.more_info).to.includes('api-version'); + expect(res.body.more_info).to.includes('should match pattern'); + done(); + }); + }); + it('bad header - empty header', function (done) { + request(app) + .get('/v1/pets') + .set('request-id', '') + .set('api-version', '1.0') + .query({ page: 0 }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + let moreInfoAsJson = JSON.parse(res.body.more_info); + expect(moreInfoAsJson).to.be.instanceof(Array); + expect(res.body.more_info).to.includes('request-id'); + expect(res.body.more_info).to.includes('should NOT be shorter than 1 characters'); + done(); + }); + }); + it('bad body - wrong type', function (done) { + request(app) + .post('/v1/pets') + .set('request-id', '123234') + .set('api-version', '1.0') + .send({ + name: '111', + tag: 12344, + 'test': { + field1: '1' + } + }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + let moreInfoAsJson = JSON.parse(res.body.more_info); + expect(moreInfoAsJson).to.be.instanceof(Array); + expect(res.body.more_info).to.includes('tag'); + done(); + }); + }); + it('bad body - missing required params', function (done) { + request(app) + .post('/v1/pets') + .set('request-id', '123324') + .set('api-version', '1.0') + .send({ + tag: 'tag', + 'test': { + field1: '1' + } + }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + let moreInfoAsJson = JSON.parse(res.body.more_info); + expect(moreInfoAsJson).to.be.instanceof(Array); + expect(res.body.more_info).to.includes('name'); + done(); + }); + }); + it('bad body - missing required object attribute', function (done) { + request(app) + .post('/v1/pets') + .set('request-id', '123434') + .set('api-version', '1.0') + .send({ + name: 'name', + tag: 'tag' + }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + let moreInfoAsJson = JSON.parse(res.body.more_info); + expect(moreInfoAsJson).to.be.instanceof(Array); + expect(res.body.more_info).to.includes('test'); + done(); + }); + }); + it('bad body - wrong type object attribute', function (done) { + request(app) + .post('/v1/pets') + .set('request-id', '12334') + .set('api-version', '1.0') + .send({ + name: 'name', + tag: 'tag', + test: '' + }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + let moreInfoAsJson = JSON.parse(res.body.more_info); + expect(moreInfoAsJson).to.be.instanceof(Array); + expect(res.body.more_info).to.includes('test'); + done(); + }); + }); + it('bad body - missing required nested attribute', function (done) { + request(app) + .post('/v1/pets') + .set('request-id', '12343') + .set('api-version', '1.0') + .send({ + name: 'name', + tag: 'tag', + test: {} + }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + let moreInfoAsJson = JSON.parse(res.body.more_info); + expect(moreInfoAsJson).to.be.instanceof(Array); + expect(res.body.more_info).to.includes('field1'); + done(); + }); + }); + it('bad body - wrong format nested attribute', function (done) { + request(app) + .post('/v1/pets') + .set('request-id', '12343') + .set('api-version', '1.0') + .send({ + name: 'name', + tag: 'tag', + test: { + field1: 1234 + } + }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + let moreInfoAsJson = JSON.parse(res.body.more_info); + expect(moreInfoAsJson).to.be.instanceof(Array); + expect(res.body.more_info).to.includes('field1'); + done(); + }); + }); + it('bad body - wrong enum value', function (done) { + request(app) + .post('/v1/pets') + .set('request-id', '1234') + .set('api-version', '1.0') + .send({ + name: 'name', + tag: 'tag', + test: { + field1: 'field1' + } + }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + let moreInfoAsJson = JSON.parse(res.body.more_info); + expect(moreInfoAsJson).to.be.instanceof(Array); + expect(res.body.more_info).to.includes('should be equal to one of the allowed values'); + done(); + }); + }); + it('bad query param - missing required params', function (done) { + request(app) + .get('/v1/pets') + .set('request-id', '1234') + .set('api-version', '1.0') + .query({ limit: 100 }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + let moreInfoAsJson = JSON.parse(res.body.more_info); + expect(moreInfoAsJson).to.be.instanceof(Array); + expect(res.body.more_info).to.includes('page'); + done(); + }); + }); + it('bad query param - over limit', function (done) { + request(app) + .get('/v1/pets') + .set('request-id', '1234') + .set('api-version', '1.0') + .query({ limit: 150, page: 0 }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + let moreInfoAsJson = JSON.parse(res.body.more_info); + expect(moreInfoAsJson).to.be.instanceof(Array); + expect(res.body.more_info).to.includes('limit'); + done(); + }); + }); + it('bad query param - under limit', function (done) { + request(app) + .get('/v1/pets') + .set('request-id', '1234') + .set('api-version', '1.0') + .query({ limit: 0, page: 0 }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + let moreInfoAsJson = JSON.parse(res.body.more_info); + expect(moreInfoAsJson).to.be.instanceof(Array); + expect(res.body.more_info).to.includes('limit'); + done(); + }); + }); + it('bad path param - wrong format', function (done) { + request(app) + .get('/v1/pets/12') + .set('request-id', '1234') + .set('api-version', '1.0') + .query({ limit: '50', page: 0 }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + let moreInfoAsJson = JSON.parse(res.body.more_info); + expect(moreInfoAsJson).to.be.instanceof(Array); + expect(res.body.more_info).to.includes('petId'); + done(); + }); + }); + it('bad body - wrong format nested attribute (not parameters)', function (done) { + request(app) + .put('/v1/pets') + .send([{ + name: 'name', + tag: 'tag', + test: { + field1: 1234 + } + }]) + .expect(400, function (err, res) { + if (err) { + throw err; + } + let moreInfoAsJson = JSON.parse(res.body.more_info); + expect(moreInfoAsJson).to.be.instanceof(Array); + expect(moreInfoAsJson.length).to.equal(2); + expect(res.body.more_info).to.includes('field1'); + done(); + }); + }); + it('bad body - wrong format in array item body (second item)', function (done) { + request(app) + .put('/v1/pets') + .send([ + { + name: 'name', + tag: 'tag', + test: { + field1: 'enum1' + } + }, + { + name: 'name', + tag: 'tag', + test: { + field1: 1234 + } + }]) + .expect(400, function (err, res) { + if (err) { + throw err; + } + let moreInfoAsJson = JSON.parse(res.body.more_info); + expect(moreInfoAsJson).to.be.instanceof(Array); + expect(res.body.more_info).to.includes('[1].test.field1'); + done(); + }); + }); + it('bad body - wrong format body (should be an array)', function (done) { + request(app) + .put('/v1/pets') + .send({ + name: 'name', + tag: 'tag', + test: { + field1: '1234' + } + }) + .expect(400, function (err, res) { + if (err) { + throw err; + } + let moreInfoAsJson = JSON.parse(res.body.more_info); + expect(moreInfoAsJson).to.be.instanceof(Array); + expect(res.body.more_info).to.includes('should be array'); + done(); + }); + }); + }); describe('Simple server using routes', function () { var app; before(function () { diff --git a/test/pet-store-swagger-inheritance.yaml b/test/pet-store-swagger-inheritance.yaml index 56dace0..e0b5e17 100644 --- a/test/pet-store-swagger-inheritance.yaml +++ b/test/pet-store-swagger-inheritance.yaml @@ -5,7 +5,7 @@ info: license: name: MIT host: petstore.swagger.io -basePath: /v1 +basePath: / schemes: - http consumes: @@ -215,4 +215,4 @@ parameters: in: path description: 'api version in path' type: string - enum: ['v1'] + enum: ['v1'] \ No newline at end of file diff --git a/test/pet-store-swagger-with-base-path.yaml b/test/pet-store-swagger-with-base-path.yaml new file mode 100644 index 0000000..76aedc6 --- /dev/null +++ b/test/pet-store-swagger-with-base-path.yaml @@ -0,0 +1,213 @@ +swagger: "2.0" +info: + version: 1.0.0 + title: Swagger Petstore + license: + name: MIT +host: petstore.swagger.io +basePath: /v1 +schemes: + - http +consumes: + - application/json +produces: + - application/json +paths: + /pets: + get: + summary: List all pets + operationId: listPets + tags: + - pets + parameters: + - $ref: '#/parameters/ApiRequestId' + - $ref: '#/parameters/ApiVersion' + - name: limit + in: query + description: How many items to return at one time (max 100) + required: false + type: integer + # format: int32 + minimum: 1 + maximum: 100 + - name: page + in: query + description: page number + required: true + type: integer + # format: int32 + minimum: 0 + responses: + "200": + description: An paged array of pets + headers: + x-next: + type: string + description: A link to the next page of responses + schema: + $ref: '#/definitions/Pets' + default: + description: unexpected error + schema: + $ref: '#/definitions/Error' + post: + summary: Create a pet + operationId: createPets + parameters: + - $ref: '#/parameters/ApiRequestId' + - name: body + in: body + required: true + schema: + required: + - name + - test + properties: + name: + type: string + tag: + type: string + test: + type: object + required: + - field1 + properties: + field1: + type: string + enum: ['enum1', 'enum2'] + responses: + "201": + description: Null response + default: + description: unexpected error + schema: + $ref: '#/definitions/Error' + put: + summary: Info for a specific pet + operationId: updatePats + tags: + - pets + parameters: + - name: body + in: body + required: true + schema: + type: array + items: + type: object + required: + - name + - test + properties: + name: + type: string + tag: + type: string + test: + type: object + required: + - field1 + properties: + field1: + type: string + enum: ['enum1', 'enum2'] + responses: + "200": + description: Expected response to a valid request + schema: + $ref: '#/definitions/Pets' + default: + description: unexpected error + schema: + $ref: '#/definitions/Error' + /pets/{petId}: + get: + summary: Info for a specific pet + operationId: showPetById + tags: + - pets + parameters: + - $ref: '#/parameters/ApiVersion' + - $ref: '#/parameters/ApiRequestId' + - name: petId + in: path + required: true + description: The id of the pet to retrieve + type: string + minLength: 3 + maxLength: 10 + responses: + "200": + description: Expected response to a valid request + schema: + $ref: '#/definitions/Pets' + default: + description: unexpected error + schema: + $ref: '#/definitions/Error' + /heartbeat: + get: + summary: Info for current system status + operationId: getHearbeat + responses: + "200": + description: Expected response to a valid request + schema: + $ref: '#/definitions/StatusReport' + default: + description: unexpected error + schema: + $ref: '#/definitions/Error' +definitions: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Pets: + type: array + items: + $ref: '#/definitions/Pet' + Error: + required: + - code + - message + properties: + code: + type: integer + # format: int32 + message: + type: string + StatusReport: + required: + - text + - code + properties: + text: + type: string + code: + type: string +parameters: + ApiVersion: + name: 'api-version' + in: header + description: 'The api version. It overwrites the account settings.' + required: true + type: string + minLength: 1 + pattern: '^\d{1,3}\.\d{1,3}$' + x-example: '3.0' + ApiRequestId: + name: 'request-id' + in: header + description: 'global request id through the system.' + type: string + minLength: 1 + x-example: '123456' \ No newline at end of file diff --git a/test/pet-store-swagger.yaml b/test/pet-store-swagger.yaml index 76aedc6..473fdc7 100644 --- a/test/pet-store-swagger.yaml +++ b/test/pet-store-swagger.yaml @@ -5,7 +5,7 @@ info: license: name: MIT host: petstore.swagger.io -basePath: /v1 +basePath: / schemes: - http consumes: diff --git a/test/router.js b/test/router.js new file mode 100644 index 0000000..72ab759 --- /dev/null +++ b/test/router.js @@ -0,0 +1,13 @@ +'use strict'; +var express = require('express'); +var router = express.Router(); +var inputValidation = require('../src/middleware'); + +router.get('/pets/:petId', inputValidation.validate, function (req, res, next) { + res.json({ result: 'OK' }); +}); +router.put('/pets', inputValidation.validate, function (req, res, next) { + res.json({ result: 'OK' }); +}); + +module.exports = router; \ No newline at end of file diff --git a/test/test-simple-server-with-base-path.js b/test/test-simple-server-with-base-path.js new file mode 100644 index 0000000..5249d6b --- /dev/null +++ b/test/test-simple-server-with-base-path.js @@ -0,0 +1,29 @@ +'use strict'; + +var express = require('express'); +var bodyParser = require('body-parser'); +var inputValidation = require('../src/middleware'); +var router = require('./router'); + +module.exports = inputValidation.init('test/pet-store-swagger-with-base-path.yaml') + .then(function () { + var app = express(); + app.use(bodyParser.json()); + + app.get('/v1/pets', inputValidation.validate, function (req, res, next) { + res.json({ result: 'OK' }); + }); + app.post('/v1/pets', inputValidation.validate, function (req, res, next) { + res.json({ result: 'OK' }); + }); + + app.use('/v1', router); + + app.use(function (err, req, res, next) { + if (err instanceof inputValidation.InputValidationError) { + res.status(400).json({ more_info: JSON.stringify(err.errors) }); + } + }); + + return Promise.resolve(app); + }); \ No newline at end of file