├── test ├── assets │ ├── example_file.txt │ └── project │ │ ├── api │ │ ├── mocks │ │ │ └── hello_world.js │ │ ├── helpers │ │ │ └── securityHandlers.js │ │ ├── controllers │ │ │ ├── hello_deps_injected.js │ │ │ ├── overrides_ctrl_interface_pipe.js │ │ │ └── hello_world.js │ │ ├── pipes │ │ │ └── hello_world.js │ │ └── swagger │ │ │ └── swagger.yaml │ │ ├── config │ │ └── default.yaml │ │ ├── config_auto │ │ └── default.yaml │ │ └── config_pipe │ │ └── default.yaml ├── mocha.opts ├── lib │ ├── express_middleware.js │ ├── connect_middleware.js │ ├── hapi_middleware.js │ ├── restify_middleware.js │ ├── common_mock.js │ └── common.js ├── fittings │ ├── express_compatibility.js │ ├── connect_middleware.js │ ├── swagger_raw.js │ └── json_error_handler.js └── index.js ├── .npmignore ├── fittings ├── swagger_cors.js ├── cors.js ├── swagger_validator.js ├── express_compatibility.js ├── json_error_handler.js ├── swagger_raw.js ├── swagger_security.js ├── swagger_params_parser.js └── swagger_router.js ├── lib ├── sails_middleware.js ├── helpers.js ├── restify_middleware.js ├── hapi_middleware.js └── connect_middleware.js ├── .jshintrc ├── .gitignore ├── LICENSE ├── README.md ├── package.json └── index.js /test/assets/example_file.txt: -------------------------------------------------------------------------------- 1 | Example -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | .gitignore 3 | .jshintrc 4 | .npmignore 5 | -------------------------------------------------------------------------------- /fittings/swagger_cors.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./cors'); -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | test test/lib test/fittings -------------------------------------------------------------------------------- /test/assets/project/api/mocks/hello_world.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | hello_mock: hello_mock 5 | }; 6 | 7 | function hello_mock(req, res, next) { 8 | res.json({ message: 'mocking from the controller!'}); 9 | next(); 10 | } 11 | -------------------------------------------------------------------------------- /test/assets/project/api/helpers/securityHandlers.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | api_key: function failure(req, res, next) { 3 | if (req.swagger.params.name.value === 'Scott') { 4 | next(); 5 | } else { 6 | next(new Error('no way!')); 7 | } 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /test/assets/project/api/controllers/hello_deps_injected.js: -------------------------------------------------------------------------------- 1 | module.exports = function(dependencies){ 2 | if (!dependencies.FooFactory) 3 | throw new Error('Foo Factory not found'); 4 | var FooFactory = dependencies.FooFactory; 5 | 6 | return { 7 | hello: function(req, res) { 8 | var name = req.swagger.params.name.value; 9 | res.json(FooFactory.hello(name)); 10 | } 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /lib/sails_middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = init; 4 | 5 | var debug = require('debug')('swagger:sails_middleware'); 6 | 7 | 8 | function init(runner) { 9 | return new Middleware(runner); 10 | } 11 | 12 | function Middleware(runner) { 13 | 14 | this.runner = runner; 15 | 16 | var connectMiddleware = runner.connectMiddleware(); 17 | this.chain = connectMiddleware.middleware; 18 | } 19 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | queryString: queryString 5 | }; 6 | 7 | var qs = require('qs'); 8 | var parseUrl = require('parseurl'); 9 | var debug = require('debug')('swagger'); 10 | 11 | // side-effect: stores in query property on req 12 | function queryString(req) { 13 | if (!req.query) { 14 | var url = parseUrl(req); 15 | req.query = (url.query) ? qs.parse(url.query) : {}; 16 | } 17 | return req.query; 18 | } 19 | -------------------------------------------------------------------------------- /fittings/cors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('swagger:cors'); 4 | var CORS = require('cors'); 5 | 6 | // config options: https://www.npmjs.com/package/cors 7 | 8 | module.exports = function create(fittingDef, bagpipes) { 9 | 10 | debug('config: %j', fittingDef); 11 | var middleware = CORS(fittingDef); 12 | 13 | return function cors(context, cb) { 14 | debug('exec'); 15 | middleware(context.request, context.response, cb); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise":true, 3 | "curly":true, 4 | "eqeqeq":true, 5 | "forin":true, 6 | "newcap":true, 7 | "noarg":true, 8 | "noempty":true, 9 | "nonew":true, 10 | "undef":true, 11 | "strict":true, 12 | "node":true, 13 | "indent":2, 14 | "expr":true, 15 | "globals" : { 16 | /* MOCHA */ 17 | "describe" : false, 18 | "it" : false, 19 | "before" : false, 20 | "beforeEach" : false, 21 | "after" : false, 22 | "afterEach" : false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE files 2 | .idea 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # Commenting this out is preferred by some people, see 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | # Users Environment Variables 31 | .lock-wscript 32 | -------------------------------------------------------------------------------- /test/assets/project/api/controllers/overrides_ctrl_interface_pipe.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | pipeInterface: pipeInterface, 3 | middlewareInterface: middlewareInterface, 4 | pipeInterfaceNoBody: pipeInterfaceNoBody 5 | } 6 | 7 | function pipeInterface(ctx, next) { 8 | ctx.statusCode = 200; 9 | ctx.headers = { 10 | 'content-type': 'application/json', 11 | 'x-interface': 'pipe' 12 | }; 13 | next(null, { interface: 'pipe' }); 14 | } 15 | 16 | function middlewareInterface(req, res, next) { 17 | res.setHeader('x-interface', 'middleware'); 18 | res.json({ interface: "middleware" }); 19 | } 20 | 21 | function pipeInterfaceNoBody(ctx, next) { 22 | ctx.statusCode = 200; 23 | ctx.headers = { 24 | 'content-type': 'application/json', 25 | 'x-interface': 'pipe' 26 | }; 27 | next(null, null); 28 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Apigee Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/restify_middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = init; 4 | 5 | var debug = require('debug')('swagger:restify_middleware'); 6 | var ALL_METHODS = ['del', 'get', 'head', 'opts', 'post', 'put', 'patch']; 7 | 8 | function init(runner) { 9 | return new Middleware(runner); 10 | } 11 | 12 | function Middleware(runner) { 13 | 14 | this.runner = runner; 15 | 16 | var connectMiddleware = runner.connectMiddleware(); 17 | var chain = connectMiddleware.middleware(); 18 | 19 | this.register = function register(app) { 20 | 21 | // this bit of oddness forces Restify to route all requests through the middleware 22 | ALL_METHODS.forEach(function(method) { 23 | app[method]('.*', function(req, res, next) { 24 | req.query = undefined; // oddly, req.query is a function in Restify, kill it 25 | chain(req, res, function(err) { 26 | if (err) { return next(err); } 27 | if (!res.finished) { 28 | res.statusCode = 404; 29 | res.end('Not found'); 30 | } 31 | next(); 32 | }); 33 | }); 34 | }); 35 | 36 | connectMiddleware.register(app); 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /fittings/swagger_validator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('swagger:swagger_validator'); 4 | var _ = require('lodash'); 5 | var util = require('util'); 6 | 7 | module.exports = function create(fittingDef, bagpipes) { 8 | 9 | debug('config: %j', fittingDef); 10 | 11 | return function swagger_validator(context, cb) { 12 | 13 | debug('exec'); 14 | 15 | // todo: add support for validating accept header against produces declarations 16 | // see: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html 17 | //var accept = req.headers['accept']; 18 | //var produces = _.union(operation.api.definition.produces, operation.definition.produces); 19 | 20 | if (context.request.swagger.operation) { 21 | var validateResult = context.request.swagger.operation.validateRequest(context.request); 22 | if (validateResult.errors.length) { 23 | var error = new Error('Validation errors'); 24 | error.statusCode = 400; 25 | error.errors = validateResult.errors; 26 | } 27 | } else { 28 | debug('not a swagger operation, will not validate response'); 29 | } 30 | 31 | cb(error); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Middleware for Swagger projects 2 | 3 | [![Coverage Status](https://coveralls.io/repos/theganyo/swagger-node-runner/badge.svg?branch=sway&service=github)](https://coveralls.io/github/theganyo/swagger-node-runner) 4 | 5 | This is the middleware engine used by the [Swagger](https://www.npmjs.com/package/swagger) project. It is designed to handle all your Swagger-driven API project needs with minimal fuss - and maximal flexibility. 6 | 7 | ### Important upgrade note! 8 | 9 | If you're upgrading a swagger-node generated project, you must follow the upgrade instructions in the [release notes](https://github.com/theganyo/swagger-node-runner/releases/tag/v0.6.0) for the upgrade to succeed. 10 | 11 | Also, be sure to read the following release notes for more information on other changes and enhancements: 12 | 13 | https://github.com/theganyo/swagger-node-runner/releases/tag/v0.6.4 14 | https://github.com/theganyo/swagger-node-runner/releases/tag/v0.6.10 15 | https://github.com/theganyo/swagger-node-runner/releases/tag/v0.6.11 16 | https://github.com/theganyo/swagger-node-runner/releases/tag/v0.7.0 17 | https://github.com/theganyo/swagger-node-runner/releases/tag/v0.7.1 18 | -------------------------------------------------------------------------------- /fittings/express_compatibility.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('swagger:cors'); 4 | var Url = require('url'); 5 | 6 | module.exports = function create(fittingDef, bagpipes) { 7 | 8 | debug('config: %j', fittingDef); 9 | 10 | return function express_compatibility(context, cb) { 11 | debug('exec'); 12 | expressCompatibility(context.request, context.response, cb); 13 | } 14 | }; 15 | 16 | function expressCompatibility(req, res, next) { 17 | 18 | if (!req.query || !req.path) { 19 | var url = Url.parse(req.url, !req.query); 20 | req.path = url.path; 21 | req.query = url.query; 22 | } 23 | 24 | if (!res.json) { 25 | res.json = function(obj) { 26 | res.statusCode = res.statusCode || 200; 27 | res.setHeader('Content-Type', 'application/json'); 28 | res.end(JSON.stringify(obj)); 29 | }; 30 | } 31 | 32 | if (!req.get) req.get = function(name) { 33 | return this.headers[name]; 34 | }; 35 | 36 | if (!res.set) { res.set = res.setHeader; } 37 | if (!res.get) { res.get = res.getHeader; } 38 | if (!res.status) { 39 | res.status = function(status) { 40 | res.statusCode = status; 41 | }; 42 | } 43 | 44 | next(); 45 | } 46 | -------------------------------------------------------------------------------- /test/assets/project/config/default.yaml: -------------------------------------------------------------------------------- 1 | # values in the swagger hash are system configuration for swagger-node 2 | swagger: 3 | 4 | fittingsDirs: [ api/fittings ] 5 | defaultPipe: null 6 | swaggerControllerPipe: swagger_controllers # defines the standard processing pipe for controllers 7 | 8 | # values defined in the bagpipes key are the bagpipes pipes and fittings definitions 9 | # (see https://github.com/apigee-127/bagpipes) 10 | bagpipes: 11 | 12 | _router: 13 | name: swagger_router 14 | mockMode: false 15 | mockControllersDirs: [ api/mocks ] 16 | controllersDirs: [ api/controllers ] 17 | 18 | _swagger_validate: 19 | name: swagger_validator 20 | validateResponse: true 21 | 22 | _swagger_security: 23 | name: swagger_security 24 | securityHandlersModule: api/helpers/securityHandlers 25 | 26 | # pipe for all swagger-node controllers 27 | swagger_controllers: 28 | - onError: json_error_handler 29 | - cors 30 | - swagger_params_parser 31 | - _swagger_security 32 | - _swagger_validate 33 | - express_compatibility 34 | - _router 35 | 36 | # pipe to serve swagger (endpoint is in swagger.yaml) 37 | swagger_raw: 38 | name: swagger_raw -------------------------------------------------------------------------------- /test/assets/project/api/controllers/hello_world.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | 5 | module.exports = { 6 | hello: hello, 7 | hello_body: hello_body, 8 | hello_file: hello_file, 9 | get: hello, 10 | multiple_writes: multiple_writes, 11 | hello_text_body: hello_text_body 12 | }; 13 | 14 | function hello(req, res) { 15 | var name = req.swagger.params.name.value || 'stranger'; 16 | var hello = util.format('Hello, %s!', name); 17 | res.json(hello); 18 | } 19 | 20 | function hello_body(req, res) { 21 | var name = req.swagger.params.nameRequest.value.name || 'stranger'; 22 | var hello = util.format('Hello, %s!', name); 23 | res.json(hello); 24 | } 25 | 26 | function hello_file(req, res) { 27 | var name = req.swagger.params.name.value || 'stranger'; 28 | var file = req.swagger.params.example_file.value; 29 | var hello = util.format('Hello, %s! Thanks for the %d byte file!', name, file.size); 30 | res.json(hello); 31 | } 32 | 33 | function multiple_writes(req, res) { 34 | res.write('hello'); 35 | res.write('world'); 36 | res.end('yo'); 37 | } 38 | 39 | function hello_text_body(req, res) { 40 | var name = req.swagger.params.name.value || 'stranger'; 41 | var hello = util.format('Hello, %s!', name); 42 | res.json(hello); 43 | } -------------------------------------------------------------------------------- /test/assets/project/config_auto/default.yaml: -------------------------------------------------------------------------------- 1 | # values in the swagger hash are system configuration for swagger-node 2 | swagger: 3 | 4 | fittingsDirs: [ api/fittings ] 5 | defaultPipe: null 6 | swaggerControllerPipe: swagger_controllers # defines the standard processing pipe for controllers 7 | 8 | # values defined in the bagpipes key are the bagpipes pipes and fittings definitions 9 | # (see https://github.com/apigee-127/bagpipes) 10 | bagpipes: 11 | 12 | _router: 13 | name: swagger_router 14 | mockMode: false 15 | mockControllersDirs: [ api/mocks ] 16 | controllersDirs: [ api/controllers ] 17 | controllersInterface: auto-detect 18 | 19 | _swagger_validate: 20 | name: swagger_validator 21 | validateResponse: true 22 | 23 | _swagger_security: 24 | name: swagger_security 25 | securityHandlersModule: api/helpers/securityHandlers 26 | 27 | # pipe for all swagger-node controllers 28 | swagger_controllers: 29 | - onError: json_error_handler 30 | - cors 31 | - swagger_params_parser 32 | - _swagger_security 33 | - _swagger_validate 34 | - express_compatibility 35 | - _router 36 | 37 | # pipe to serve swagger (endpoint is in swagger.yaml) 38 | swagger_raw: 39 | name: swagger_raw -------------------------------------------------------------------------------- /test/assets/project/config_pipe/default.yaml: -------------------------------------------------------------------------------- 1 | # values in the swagger hash are system configuration for swagger-node 2 | swagger: 3 | 4 | fittingsDirs: [ node_modules, api/fittings ] 5 | defaultPipe: null 6 | swaggerControllerPipe: swagger_controllers # defines the standard processing pipe for controllers 7 | 8 | # values defined in the bagpipes key are the bagpipes pipes and fittings definitions 9 | # (see https://github.com/apigee-127/bagpipes) 10 | bagpipes: 11 | 12 | _router: 13 | name: swagger_router 14 | mockMode: false 15 | mockControllersDirs: [ api/mocks ] 16 | controllersDirs: [ api/pipes ] 17 | controllersInterface: pipe 18 | 19 | _swagger_validate: 20 | name: swagger_validator 21 | validateResponse: true 22 | 23 | _swagger_security: 24 | name: swagger_security 25 | securityHandlersModule: api/helpers/securityHandlers 26 | 27 | # pipe for all swagger-node controllers 28 | swagger_controllers: 29 | - onError: json_error_handler 30 | - swagger_cors 31 | - swagger_params_parser 32 | - _swagger_security 33 | - _swagger_validate 34 | - express_compatibility 35 | - _router 36 | 37 | # pipe to serve swagger (endpoint is in swagger.yaml) 38 | swagger_raw: 39 | name: swagger_raw -------------------------------------------------------------------------------- /test/lib/express_middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'); 4 | var request = require('supertest'); 5 | var path = require('path'); 6 | var _ = require('lodash'); 7 | 8 | var SwaggerRunner = require('../..'); 9 | 10 | var TEST_PROJECT_ROOT = path.resolve(__dirname, '..', 'assets', 'project'); 11 | var TEST_PROJECT_CONFIG = { appRoot: TEST_PROJECT_ROOT }; 12 | var MOCK_CONFIG = { 13 | appRoot: TEST_PROJECT_ROOT, 14 | bagpipes: {_router: {mockMode: true}} 15 | }; 16 | 17 | describe('express_middleware', function() { 18 | 19 | describe('standard', function() { 20 | before(function(done) { 21 | createServer.call(this, TEST_PROJECT_CONFIG, done); 22 | }); 23 | 24 | require('./common')(); 25 | }); 26 | 27 | describe('mock', function() { 28 | 29 | before(function(done) { 30 | createServer.call(this, MOCK_CONFIG, done); 31 | }); 32 | 33 | require('./common_mock')(); 34 | }); 35 | }); 36 | 37 | function createServer(config, done) { 38 | this.app = require('express')(); 39 | var self = this; 40 | SwaggerRunner.create(config, function(err, r) { 41 | if (err) { 42 | console.error(err); 43 | return done(err); 44 | } 45 | self.runner = r; 46 | var middleware = self.runner.expressMiddleware(); 47 | middleware.register(self.app); 48 | done(); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /test/lib/connect_middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'); 4 | var request = require('supertest'); 5 | var path = require('path'); 6 | var _ = require('lodash'); 7 | 8 | var SwaggerRunner = require('../..'); 9 | 10 | var TEST_PROJECT_ROOT = path.resolve(__dirname, '..', 'assets', 'project'); 11 | var TEST_PROJECT_CONFIG = { appRoot: TEST_PROJECT_ROOT }; 12 | var MOCK_CONFIG = { 13 | appRoot: TEST_PROJECT_ROOT, 14 | bagpipes: {_router: {mockMode: true}} 15 | }; 16 | 17 | describe('connect_middleware', function() { 18 | 19 | describe('standard', function() { 20 | 21 | before(function(done) { 22 | createServer.call(this, TEST_PROJECT_CONFIG, done); 23 | }); 24 | 25 | require('./common')(); 26 | }); 27 | 28 | describe('mock', function() { 29 | 30 | before(function(done) { 31 | createServer.call(this, MOCK_CONFIG, done); 32 | }); 33 | 34 | require('./common_mock')(); 35 | }); 36 | }); 37 | 38 | function createServer(config, done) { 39 | this.app = require('connect')(); 40 | var self = this; 41 | SwaggerRunner.create(config, function(err, r) { 42 | if (err) { 43 | console.error(err); 44 | return done(err); 45 | } 46 | self.runner = r; 47 | var middleware = self.runner.connectMiddleware(); 48 | middleware.register(self.app); 49 | done(); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /test/assets/project/api/pipes/hello_world.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var util = require('util'); 3 | 4 | module.exports = { 5 | hello: hello, 6 | hello_body: hello_body, 7 | hello_file: hello_file, 8 | get: hello, 9 | self_multiple_writes : self_multiple_writes, 10 | hello_text_body: hello_text_body 11 | }; 12 | 13 | function hello(ctx, next) { 14 | var req = ctx.request; 15 | var name = req.swagger.params.name.value || 'stranger'; 16 | var hello = { message: util.format('Hello, %s!', name) }; 17 | ctx.statusCode = 200; 18 | ctx.headers = { 'content-type' : 'application/json' }; 19 | next(null, hello) 20 | } 21 | 22 | function hello_body(ctx, next) { 23 | var req = ctx.request; 24 | var name = req.swagger.params.nameRequest.value.name || 'stranger'; 25 | var hello = { message: util.format('Hello, %s!', name) }; 26 | ctx.statusCode = 200; 27 | ctx.headers = {}; 28 | next(null, hello) 29 | } 30 | 31 | function hello_file(ctx, next) { 32 | var req = ctx.request; 33 | var name = req.swagger.params.name.value || 'stranger'; 34 | var file = req.swagger.params.example_file.value; 35 | var hello = { message: util.format('Hello, %s!', name) }; 36 | ctx.statusCode = 200; 37 | ctx.headers = {}; 38 | next(null, hello) 39 | } 40 | 41 | function self_multiple_writes(ctx, next) { 42 | var res = ctx.response; 43 | res.write('hello'); 44 | res.write('world'); 45 | res.end('yo'); 46 | next(); 47 | } 48 | 49 | function hello_text_body(ctx, next) { 50 | var req = ctx.request; 51 | var name = req.swagger.params.name.value || 'stranger'; 52 | var hello = { message: util.format('Hello, %s!', name) }; 53 | ctx.statusCode = 200; 54 | ctx.headers = {}; 55 | next(null, hello) 56 | } -------------------------------------------------------------------------------- /test/lib/hapi_middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'); 4 | var path = require('path'); 5 | var _ = require('lodash'); 6 | 7 | var SwaggerRunner = require('../..'); 8 | 9 | var TEST_PROJECT_ROOT = path.resolve(__dirname, '..', 'assets', 'project'); 10 | var TEST_PROJECT_CONFIG = { appRoot: TEST_PROJECT_ROOT }; 11 | var MOCK_CONFIG = { 12 | appRoot: TEST_PROJECT_ROOT, 13 | bagpipes: {_router: {mockMode: true}} 14 | }; 15 | 16 | describe('hapi_middleware', function() { 17 | 18 | describe('standard', function() { 19 | before(function(done) { 20 | createServer.call(this, TEST_PROJECT_CONFIG, done); 21 | }); 22 | 23 | after(function(done) { 24 | this.app.stop(done); 25 | }); 26 | 27 | require('./common')(); 28 | }); 29 | 30 | describe('mock', function() { 31 | 32 | before(function(done) { 33 | createServer.call(this, MOCK_CONFIG, done); 34 | }); 35 | 36 | after(function(done) { 37 | this.app.stop(done); 38 | }); 39 | 40 | require('./common_mock')(); 41 | }); 42 | }); 43 | 44 | function createServer(config, done) { 45 | var hapi = require('hapi'); 46 | this.app = new hapi.Server(); 47 | var self = this; 48 | SwaggerRunner.create(config, function(err, r) { 49 | if (err) { 50 | console.error(err); 51 | return done(err); 52 | } 53 | self.runner = r; 54 | var middleware = self.runner.hapiMiddleware(); 55 | 56 | self.app.address = function() { return { port: 7236 }; }; 57 | self.app.connection(self.app.address()); 58 | 59 | self.app.register(middleware.plugin, function(err) { 60 | if (err) { return console.error('Failed to load plugin:', err); } 61 | self.app.start(function() { 62 | done(); 63 | }); 64 | }); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swagger-node-runner", 3 | "version": "0.7.3", 4 | "description": "Swagger loader and middleware utilities", 5 | "keywords": [ 6 | "swagger", 7 | "api", 8 | "apis", 9 | "swagger-connect", 10 | "swagger-express", 11 | "swagger-restify", 12 | "swagger-hapi", 13 | "swagger-sails" 14 | ], 15 | "author": "Scott Ganyo ", 16 | "license": "MIT", 17 | "main": "index.js", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/theganyo/swagger-node-runner.git" 21 | }, 22 | "dependencies": { 23 | "async": "^1.5.0", 24 | "bagpipes": "^0.1.0", 25 | "body-parser": "^1.14.1", 26 | "config": "^1.16.0", 27 | "cors": "^2.5.3", 28 | "debug": "^2.1.3", 29 | "js-yaml": "^3.3.0", 30 | "lodash": "^3.6.0", 31 | "multer": "^1.0.6", 32 | "parseurl": "^1.3.0", 33 | "qs": "^6.4.0", 34 | "sway": "^1.0.0", 35 | "type-is": "^1.6.9" 36 | }, 37 | "devDependencies": { 38 | "connect": "^3.4.0", 39 | "coveralls": "^2.11.4", 40 | "express": "^4.13.3", 41 | "hapi": "^10.0.0", 42 | "istanbul": "^0.4.0", 43 | "mocha": "^2.3.0", 44 | "mocha-lcov-reporter": "^1.0.0", 45 | "restify": "^5.2.0", 46 | "should": "^7.1.0", 47 | "sinon": "^1.17.2", 48 | "supertest": "^1.1.0" 49 | }, 50 | "scripts": { 51 | "test": "node_modules/mocha/bin/_mocha -u exports -R spec test test/lib test/fittings", 52 | "coverage": "node node_modules/istanbul/lib/cli.js cover node_modules/mocha/bin/_mocha -- -u exports -R spec test test/lib test/fittings", 53 | "coveralls": "node node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec test test/lib test/fittings && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js --verbose" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/hapi_middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = init; 4 | 5 | var debug = require('debug')('swagger:hapi_middleware'); 6 | 7 | function init(runner) { 8 | return new Hapi(runner); 9 | } 10 | 11 | function Hapi(runner) { 12 | this.runner = runner; 13 | this.config = runner.config; 14 | 15 | var connectMiddleware = runner.connectMiddleware(); 16 | var chain = connectMiddleware.middleware(); 17 | 18 | this.plugin = { 19 | register: function(server, options, next) { 20 | 21 | server.ext('onRequest', function(request, reply) { 22 | 23 | var req = request.raw.req; 24 | var res = newResponse(reply); 25 | 26 | chain(req, res, function(err) { 27 | if (err) { 28 | if (err.statusCode) { res.statusCode = err.statusCode; } 29 | res.end(err.message); 30 | } 31 | res.finish(); 32 | }); 33 | }); 34 | 35 | /* istanbul ignore next */ 36 | server.on('request-error', function (request, err) { 37 | debug('Request: %s error: %s', request.id, err.stack); 38 | }); 39 | 40 | next(); 41 | } 42 | }; 43 | this.plugin.register.attributes = { name: 'swagger-node-runner', version: version() }; 44 | } 45 | 46 | function version() { 47 | return require('../package.json').version; 48 | } 49 | 50 | function newResponse(reply) { 51 | return { 52 | getHeader: function getHeader(name) { 53 | return this.headers ? this.headers[name.toLowerCase()] : null; 54 | }, 55 | setHeader: function setHeader(name, value) { 56 | if (!this.headers) { this.headers = {}; } 57 | this.headers[name.toLowerCase()] = value; 58 | }, 59 | end: function end(string) { 60 | this.res = reply(string); 61 | this.res.statusCode = this.statusCode; 62 | if (this.headers) { 63 | for (var header in this.headers) { 64 | this.res.header(header, this.headers[header]); 65 | } 66 | } 67 | }, 68 | finish: function finish() { 69 | if (!this.res) { 70 | reply.continue(); 71 | } 72 | } 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /test/lib/restify_middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'); 4 | var request = require('supertest'); 5 | var path = require('path'); 6 | var _ = require('lodash'); 7 | var sinon = require('sinon'); 8 | 9 | var SwaggerRunner = require('../..'); 10 | 11 | var TEST_PROJECT_ROOT = path.resolve(__dirname, '..', 'assets', 'project'); 12 | var TEST_PROJECT_CONFIG = { appRoot: TEST_PROJECT_ROOT }; 13 | var MOCK_CONFIG = { 14 | appRoot: TEST_PROJECT_ROOT, 15 | bagpipes: {_router: {mockMode: true}} 16 | }; 17 | 18 | describe('restify_middleware', function() { 19 | 20 | describe('standard', function() { 21 | 22 | before(function(done) { 23 | createServer.call(this, TEST_PROJECT_CONFIG, done); 24 | }); 25 | 26 | after(function(done) { 27 | try { 28 | this.app.close(done); 29 | } catch (err) { 30 | done(); 31 | } 32 | }); 33 | 34 | require('./common')(); 35 | }); 36 | 37 | describe('mock', function() { 38 | var afterEvent; 39 | 40 | before(function(done) { 41 | createServer.call(this, MOCK_CONFIG, done); 42 | afterEvent = sinon.spy(); 43 | this.app.on('after', afterEvent); 44 | }); 45 | 46 | describe('after event', function () { 47 | it('should been emitted', function (done) { 48 | request(this.app) 49 | .get('/hello') 50 | .set('Accept', 'application/json') 51 | .expect(200) 52 | .end(function(err, res) { 53 | sinon.assert.called(afterEvent); 54 | done(); 55 | }); 56 | }); 57 | }); 58 | 59 | after(function(done) { 60 | try { 61 | this.app.close(done); 62 | } catch (err) { 63 | done(); 64 | } 65 | }); 66 | 67 | require('./common_mock')(); 68 | }); 69 | }); 70 | 71 | function createServer(config, done) { 72 | this.app = require('restify').createServer(); 73 | var self = this; 74 | SwaggerRunner.create(config, function(err, r) { 75 | if (err) { 76 | console.error(err); 77 | return done(err); 78 | } 79 | self.runner = r; 80 | var middleware = self.runner.restifyMiddleware(); 81 | middleware.register(self.app); 82 | done(); 83 | }); 84 | } 85 | -------------------------------------------------------------------------------- /fittings/json_error_handler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('swagger:json_error_handler'); 4 | var util = require('util'); 5 | 6 | module.exports = function create(fittingDef, bagpipes) { 7 | 8 | debug('config: %j', fittingDef); 9 | 10 | return function error_handler(context, next) { 11 | 12 | if (!util.isError(context.error)) { return next(); } 13 | 14 | var err = context.error; 15 | var log; 16 | var body; 17 | 18 | debug('exec: %s', context.error.message); 19 | 20 | if (!context.statusCode || context.statusCode < 400) { 21 | if (context.response && context.response.statusCode && context.response.statusCode >= 400) { 22 | context.statusCode = context.response.statusCode; 23 | } else if (err.statusCode && err.statusCode >= 400) { 24 | context.statusCode = err.statusCode; 25 | delete(err.statusCode); 26 | } else { 27 | context.statusCode = 500; 28 | } 29 | } 30 | 31 | try { 32 | //TODO: find what's throwing here... 33 | if (context.statusCode === 500 && !fittingDef.handle500Errors) { return next(err); } 34 | //else - from here we commit to emitting error as JSON, no matter what. 35 | 36 | context.headers['Content-Type'] = 'application/json'; 37 | Object.defineProperty(err, 'message', { enumerable: true }); // include message property in response 38 | if (fittingDef.includeErrStack) 39 | Object.defineProperty(err, 'stack', { enumerable: true }); // include stack property in response 40 | 41 | delete(context.error); 42 | next(null, JSON.stringify(err)); 43 | } catch (err2) { 44 | log = context.request && ( 45 | context.request.log 46 | || context.request.app && context.request.app.log 47 | ) 48 | || context.response && context.response.log; 49 | 50 | body = { 51 | message: "unable to stringify error properly", 52 | stringifyErr: err2.message, 53 | originalErrInspect: util.inspect(err) 54 | }; 55 | context.statusCode = 500; 56 | 57 | debug('jsonErrorHandler unable to stringify error: ', err); 58 | if (log) log.error(err2, "onError: json_error_handler - unable to stringify error", err); 59 | 60 | next(null, JSON.stringify(body)); 61 | } 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /test/lib/common_mock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'); 4 | var request = require('supertest'); 5 | var path = require('path'); 6 | var _ = require('lodash'); 7 | var yaml = require('js-yaml'); 8 | 9 | module.exports = function() { 10 | 11 | it('should return from mock controller handler if exists', function(done) { 12 | request(this.app) 13 | .get('/hello_with_mock') 14 | .set('Accept', 'application/json') 15 | .expect(200) 16 | .expect('Content-Type', /json/) 17 | .end(function(err, res) { 18 | should.not.exist(err); 19 | res.body.should.eql({ message: 'mocking from the controller!'}); 20 | done(); 21 | }); 22 | }); 23 | 24 | it('should return example if exists and no mock controller', function(done) { 25 | request(this.app) 26 | .get('/hello') 27 | .set('Accept', 'application/json') 28 | .expect(200) 29 | .expect('Content-Type', /json/) 30 | .end(function(err, res) { 31 | should.not.exist(err); 32 | res.body.should.eql({ message: 'An example message' }); 33 | done(); 34 | }); 35 | }); 36 | 37 | it('should return example if exists based on accept header', function(done) { 38 | 39 | var YAML = require('js-yaml'); 40 | var msg = YAML.safeDump({ message: 'A yaml example' }, { indent: 2 }); 41 | 42 | request(this.app) 43 | .get('/hello') 44 | .set('Accept', 'application/x-yaml') 45 | .expect(200) 46 | .expect('Content-Type', 'application/x-yaml') 47 | .end(function(err, res) { 48 | should.not.exist(err); 49 | res.text.should.be.a.String; 50 | res.text.should.eql(msg); 51 | done(); 52 | }); 53 | }); 54 | 55 | it('should return example based on _mockReturnStatus header', function(done) { 56 | request(this.app) 57 | .get('/hello_form') 58 | .send('name=Scott') 59 | .set('Accept', 'application/json') 60 | .set('_mockReturnStatus', '201') 61 | .expect(201) 62 | .expect('Content-Type', /json/) 63 | .end(function(err, res) { 64 | should.not.exist(err); 65 | res.body.should.not.eql({ message: 'An example message' }); 66 | res.body.should.not.eql({ message: 'mocking from the controller!'}); 67 | res.body.should.have.property('string'); 68 | res.body.string.should.be.a.String; 69 | res.body.should.have.property('integer'); 70 | res.body.integer.should.be.a.Integer; 71 | done(); 72 | }); 73 | }); 74 | 75 | }; -------------------------------------------------------------------------------- /fittings/swagger_raw.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('swagger:swagger_raw'); 4 | var YAML = require('js-yaml'); 5 | var _ = require('lodash'); 6 | 7 | // default filter just drops all the x- labels 8 | var DROP_SWAGGER_EXTENSIONS = /^(?!x-.*)/; 9 | 10 | // default filter drops anything labeled x-private 11 | var X_PRIVATE = ['x-private']; 12 | 13 | module.exports = function create(fittingDef, bagpipes) { 14 | 15 | debug('config: %j', fittingDef); 16 | 17 | var filter = DROP_SWAGGER_EXTENSIONS; 18 | if (fittingDef.filter) { 19 | filter = new RegExp(fittingDef.filter); 20 | } 21 | debug('swagger doc filter: %s', filter); 22 | var privateTags = fittingDef.privateTags || X_PRIVATE; 23 | var filteredSwagger = filterKeysRecursive(bagpipes.config.swaggerNodeRunner.swagger, filter, privateTags); 24 | 25 | if (!filteredSwagger) { return next(null, ''); } 26 | 27 | // should this just be based on accept type? 28 | var yaml = YAML.safeDump(filteredSwagger, { indent: 2 }); 29 | var json = JSON.stringify(filteredSwagger, null, 2); 30 | 31 | return function swagger_raw(context, next) { 32 | 33 | debug('exec'); 34 | 35 | var req = context.request; 36 | 37 | var accept = req.headers['accept']; 38 | if (accept && accept.indexOf('yaml') != -1) { 39 | context.headers['Content-Type'] = 'application/yaml'; 40 | next(null, yaml); 41 | } else { 42 | context.headers['Content-Type'] = 'application/json'; 43 | next(null, json); 44 | } 45 | } 46 | }; 47 | 48 | function filterKeysRecursive(object, dropTagRegex, privateTags) { 49 | if (_.isPlainObject(object)) { 50 | if (_.any(privateTags, function(tag) { return object[tag]; })) { 51 | object = undefined; 52 | } else { 53 | var result = {}; 54 | _.each(object, function(value, key) { 55 | if (dropTagRegex.test(key)) { 56 | var v = filterKeysRecursive(value, dropTagRegex, privateTags); 57 | if (v !== undefined) { 58 | result[key] = v; 59 | } else { 60 | debug('dropping object at %s', key); 61 | delete(result[key]); 62 | } 63 | } else { 64 | debug("dropping value at %s", key) 65 | } 66 | }); 67 | return result; 68 | } 69 | } else if (Array.isArray(object) ) { 70 | object = object.reduce(function(reduced, value) { 71 | var v = filterKeysRecursive(value, dropTagRegex, privateTags); 72 | if (v !== undefined) reduced.push(v); 73 | return reduced 74 | }, []) 75 | } 76 | return object; 77 | } 78 | -------------------------------------------------------------------------------- /test/fittings/express_compatibility.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var express_compatibility = require('../../fittings/express_compatibility'); 3 | 4 | describe('express_compatibility', function() { 5 | 6 | var expressCompatibility; 7 | var url = 'http://localhost:10010/test?query1=val1&query2=val2'; 8 | 9 | before(function() { 10 | expressCompatibility = express_compatibility(); 11 | }); 12 | 13 | it('should add missing properties to request and response', function(done) { 14 | 15 | var requestProps = ['path', 'query', 'get']; 16 | var responseProps = ['json', 'get', 'set', 'status']; 17 | 18 | var request = { url: url }; 19 | var response = {}; 20 | var context = { request: request, response: response }; 21 | 22 | expressCompatibility(context, function(err) { 23 | should.not.exist(err); 24 | context.request.should.have.properties(requestProps); 25 | context.response.should.have.properties(responseProps); 26 | 27 | done(); 28 | }); 29 | }); 30 | 31 | it('should properly handle json()', function(done) { 32 | 33 | var setHeaderCalled = false; 34 | var testObject = { this: 'is', a: 'test' }; 35 | 36 | var request = { url: url }; 37 | var response = { 38 | setHeader: function(name, value) { 39 | name.should.eql('Content-Type'); 40 | value.should.eql('application/json'); 41 | setHeaderCalled = true; 42 | }, 43 | end: function(value) { 44 | setHeaderCalled.should.be.true; 45 | response.statusCode.should.eql(200); 46 | JSON.parse(value).should.eql(testObject); 47 | done(); 48 | } 49 | }; 50 | var context = { request: request, response: response }; 51 | 52 | expressCompatibility(context, function(err) { 53 | should.not.exist(err); 54 | response.json(testObject); 55 | should.fail; // should never get here 56 | }); 57 | }); 58 | 59 | it('request.get should get a header', function(done) { 60 | 61 | var request = { url: url, headers: { myheader: 'myvalue' } }; 62 | var response = {}; 63 | var context = { request: request, response: response }; 64 | 65 | should.not.exist(request.get); 66 | expressCompatibility(context, function() { 67 | request.get('myheader').should.eql(request.headers['myheader']); 68 | done(); 69 | }); 70 | }); 71 | 72 | it('request.status should set status', function(done) { 73 | 74 | var request = { url: url }; 75 | var response = {}; 76 | var context = { request: request, response: response }; 77 | 78 | should.not.exist(response.statusCode); 79 | expressCompatibility(context, function() { 80 | response.status(200); 81 | response.statusCode.should.eql(200); 82 | done(); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /fittings/swagger_security.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('swagger:swagger_security'); 4 | var async = require('async'); 5 | var helpers = require('../lib/helpers'); 6 | var _ = require('lodash'); 7 | var path = require('path'); 8 | 9 | module.exports = function create(fittingDef, bagpipes) { 10 | 11 | debug('config: %j', fittingDef); 12 | 13 | var runner = bagpipes.config.swaggerNodeRunner; 14 | 15 | if (fittingDef.securityHandlersModule && !runner.config.securityHandlers) { 16 | 17 | var appRoot = runner.config.swagger.appRoot; 18 | var handlersPath = path.resolve(appRoot, fittingDef.securityHandlersModule); 19 | 20 | runner.securityHandlers = require(handlersPath); 21 | debug('loaded handlers: %s from: %s', Object.keys(runner.securityHandlers), handlersPath); 22 | } 23 | 24 | return function swagger_security(context, cb) { 25 | 26 | debug('exec'); 27 | 28 | var handlers = runner.securityHandlers || {}; 29 | var req = context.request; 30 | var operation = req.swagger.operation; 31 | if (!operation) { return cb(); } 32 | 33 | var security = operation.getSecurity(); 34 | if (!security || security.length == 0) { return cb(); } 35 | 36 | async.map(security, // logical OR - any one can allow 37 | function orCheck(securityRequirement, cb) { 38 | var secName; 39 | 40 | async.map(Object.keys(securityRequirement), // logical AND - all must allow 41 | function andCheck(name, cb) { 42 | // Check both route and global security definitions 43 | var secDef = operation.securityDefinitions[name] || operation.pathObject.api.securityDefinitions[name]; 44 | var handler = handlers[name]; 45 | 46 | secName = name; 47 | 48 | if (!handler) { return cb(new Error('Unknown security handler: ' + name));} 49 | 50 | if (handler.length === 4) { 51 | // swagger-tools style handler 52 | return handler(req, secDef, getScopeOrAPIKey(req, secDef, name, securityRequirement), cb); 53 | } else { 54 | // connect-style handler 55 | return handler(req, context.response, cb); 56 | } 57 | }, 58 | function andCheckDone(err) { 59 | debug('Security check (%s): %s', secName, _.isNull(err) ? 'allowed' : 'denied'); 60 | 61 | // swap normal err and result to short-circuit the logical OR 62 | if (err) { return cb(undefined, err); } 63 | 64 | return cb(new Error('OK')); 65 | }); 66 | }, 67 | function orCheckDone(ok, errors) { // note swapped results 68 | 69 | var allowed = !_.isNull(ok) && ok.message === 'OK'; 70 | debug('Request allowed: %s', allowed); 71 | 72 | allowed ? cb() : sendSecurityError(errors[0], context.response, cb); 73 | }); 74 | } 75 | }; 76 | 77 | function getScopeOrAPIKey(req, securityDefinition, name, securityRequirement) { 78 | 79 | var scopeOrKey; 80 | 81 | if (securityDefinition.type === 'oauth2') { 82 | scopeOrKey = securityRequirement[name]; 83 | } else if (securityDefinition.type === 'apiKey') { 84 | if (securityDefinition.in === 'query') { 85 | scopeOrKey = helpers.queryString(req)[securityDefinition.name]; 86 | } else if (securityDefinition.in === 'header') { 87 | scopeOrKey = req.headers[securityDefinition.name.toLowerCase()]; 88 | } 89 | } 90 | 91 | return scopeOrKey; 92 | } 93 | 94 | function sendSecurityError(err, res, next) { 95 | 96 | if (!err.code) { err.code = 'server_error'; } 97 | if (!err.statusCode) { err.statusCode = 403; } 98 | 99 | if (err.headers) { 100 | _.each(err.headers, function (header, name) { 101 | res.setHeader(name, header); 102 | }); 103 | } 104 | 105 | res.statusCode = err.statusCode; 106 | 107 | next(err); 108 | } 109 | -------------------------------------------------------------------------------- /fittings/swagger_params_parser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('swagger:swagger_params_parser'); 4 | var debugContent = require('debug')('swagger:content'); 5 | var path = require('path'); 6 | var helpers = require('../lib/helpers'); 7 | 8 | var bodyParser = require('body-parser'); 9 | var async = require('async'); 10 | var _ = require('lodash'); 11 | 12 | module.exports = function create(fittingDef, bagpipes) { 13 | 14 | debug('config: %j', fittingDef); 15 | 16 | _.defaults(fittingDef, { 17 | jsonOptions: { 18 | type: ['json', 'application/*+json'] 19 | }, 20 | urlencodedOptions: { 21 | extended: false 22 | }, 23 | multerOptions: { 24 | inMemory: true 25 | }, 26 | textOptions: { 27 | type: '*/*' 28 | } 29 | }); 30 | 31 | return function swagger_params_parser(context, next) { 32 | debug('exec'); 33 | 34 | var req = context.request; 35 | parseRequest(req, fittingDef, function(err) { 36 | if (err) { /* istanbul ignore next */ return next(err); } 37 | 38 | var params = req.swagger.params = {}; 39 | req.swagger.operation.parameterObjects.forEach(function(parameter) { 40 | params[parameter.name] = parameter.getValue(req); // note: we do not check for errors here 41 | }); 42 | 43 | next(null, params); 44 | }); 45 | } 46 | }; 47 | 48 | function parseRequest(req, fittingDef, cb) { 49 | 50 | if (req.query && req.body && req.files) { return cb(); } 51 | 52 | var shouldParseBody = false; 53 | var shouldParseForm = false; 54 | var shouldParseQuery = false; 55 | var multFields = []; 56 | 57 | req.swagger.operation.parameterObjects.forEach(function(parameter) { 58 | 59 | switch (parameter.in) { 60 | 61 | case 'body': 62 | shouldParseBody = true; 63 | break; 64 | 65 | case 'formData': 66 | shouldParseForm = true; 67 | if (parameter.type === 'file') { 68 | multFields.push({ name: parameter.name }); 69 | } 70 | break; 71 | 72 | case 'query': 73 | shouldParseQuery = true; 74 | break; 75 | } 76 | }); 77 | 78 | if (!req.query && shouldParseQuery) { helpers.queryString(req); } 79 | 80 | if (req.body || (!shouldParseBody && !shouldParseForm)) { return cb(); } 81 | 82 | var res = null; 83 | debugContent('parsing req.body for content-type: %s', req.headers['content-type']); 84 | async.series([ 85 | function parseMultipart(cb) { 86 | if (multFields.length === 0) { return cb(); } 87 | var mult = require('multer')(fittingDef.multerOptions); 88 | mult.fields(multFields)(req, res, function(err) { 89 | if (err) { /* istanbul ignore next */ 90 | if (err.code === 'LIMIT_UNEXPECTED_FILE') { err.statusCode = 400 } 91 | return cb(err); 92 | } 93 | if (req.files) { 94 | _.forEach(req.files, function(file, name) { 95 | req.files[name] = (Array.isArray(file) && file.length === 1) ? file[0] : file; 96 | }); 97 | } 98 | debugContent('multer parsed req.body:', req.body); 99 | cb(); 100 | }); 101 | }, 102 | function parseUrlencoded(cb) { 103 | if (req.body || !shouldParseForm) { return cb(); } 104 | if (skipParse(fittingDef.urlencodedOptions, req)) { return cb(); } // hack: see skipParse function 105 | var urlEncodedBodyParser = bodyParser.urlencoded(fittingDef.urlencodedOptions); 106 | urlEncodedBodyParser(req, res, cb); 107 | }, 108 | function parseJson(cb) { 109 | if (req.body) { 110 | debugContent('urlencoded parsed req.body:', req.body); 111 | return cb(); 112 | } 113 | if (skipParse(fittingDef.jsonOptions, req)) { return cb(); } // hack: see skipParse function 114 | bodyParser.json(fittingDef.jsonOptions)(req, res, cb); 115 | }, 116 | function parseText(cb) { 117 | if (req.body) { 118 | debugContent('json parsed req.body:', req.body); 119 | return cb(); 120 | } 121 | if (skipParse(fittingDef.textOptions, req)) { return cb(); } // hack: see skipParse function 122 | bodyParser.text(fittingDef.textOptions)(req, res, function(err) { 123 | if (req.body) { debugContent('text parsed req.body:', req.body); } 124 | cb(err); 125 | }); 126 | } 127 | ], function finishedParseBody(err) { 128 | return cb(err); 129 | }); 130 | 131 | } 132 | 133 | // hack: avoids body-parser issue: https://github.com/expressjs/body-parser/issues/128 134 | var typeis = require('type-is').is; 135 | function skipParse(options, req) { 136 | return typeof options.type !== 'function' && !Boolean(typeis(req, options.type)); 137 | } 138 | -------------------------------------------------------------------------------- /fittings/swagger_router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('swagger:swagger_router'); 4 | var path = require('path'); 5 | var assert = require('assert'); 6 | var SWAGGER_ROUTER_CONTROLLER = 'x-swagger-router-controller'; 7 | var CONTROLLER_INTERFACE_TYPE = 'x-controller-interface'; 8 | var allowedCtrlInterfaces = ["middleware", "pipe", "auto-detect"]; 9 | var util = require('util'); 10 | 11 | module.exports = function create(fittingDef, bagpipes) { 12 | 13 | debug('config: %j', fittingDef); 14 | 15 | assert(Array.isArray(fittingDef.controllersDirs), 'controllersDirs must be an array'); 16 | assert(Array.isArray(fittingDef.mockControllersDirs), 'mockControllersDirs must be an array'); 17 | 18 | if (!fittingDef.controllersInterface) fittingDef.controllersInterface = "middleware"; 19 | assert( ~allowedCtrlInterfaces.indexOf(fittingDef.controllersInterface), 20 | 'value in swagger_router config.controllersInterface - can be one of ' + allowedCtrlInterfaces + ' but got: ' + fittingDef.controllersInterface 21 | ); 22 | 23 | var swaggerNodeRunner = bagpipes.config.swaggerNodeRunner; 24 | swaggerNodeRunner.api.getOperations().forEach(function(operation) { 25 | var interfaceType = 26 | operation.controllerInterface = 27 | operation.definition[CONTROLLER_INTERFACE_TYPE] || 28 | operation.pathObject.definition[CONTROLLER_INTERFACE_TYPE] || 29 | swaggerNodeRunner.api.definition[CONTROLLER_INTERFACE_TYPE] || 30 | fittingDef.controllersInterface; 31 | 32 | assert( ~allowedCtrlInterfaces.indexOf(interfaceType), 33 | 'whenever provided, value of ' + CONTROLLER_INTERFACE_TYPE + ' directive in openapi doc must be one of ' + allowedCtrlInterfaces + ' but got: ' + interfaceType); 34 | }) 35 | 36 | var appRoot = swaggerNodeRunner.config.swagger.appRoot; 37 | var dependencies = swaggerNodeRunner.config.swagger.dependencies 38 | 39 | var mockMode = !!fittingDef.mockMode || !!swaggerNodeRunner.config.swagger.mockMode; 40 | 41 | var controllersDirs = mockMode ? fittingDef.mockControllersDirs : fittingDef.controllersDirs; 42 | 43 | controllersDirs = controllersDirs.map(function(dir) { 44 | return path.resolve(appRoot, dir); 45 | }); 46 | 47 | var controllerFunctionsCache = {}; 48 | 49 | return function swagger_router(context, cb) { 50 | debug('exec'); 51 | 52 | var operation = context.request.swagger.operation; 53 | var controllerName = operation[SWAGGER_ROUTER_CONTROLLER] || operation.pathObject[SWAGGER_ROUTER_CONTROLLER]; 54 | 55 | var controller; 56 | 57 | if (controllerName in controllerFunctionsCache) { 58 | 59 | debug('controller in cache', controllerName); 60 | controller = controllerFunctionsCache[controllerName]; 61 | 62 | } else if (controllerName) { 63 | 64 | debug('loading controller %s from fs: %s', controllerName, controllersDirs); 65 | for (var i = 0; i < controllersDirs.length; i++) { 66 | var controllerPath = path.resolve(controllersDirs[i], controllerName); 67 | try { 68 | var ctrlObj = require(controllerPath) 69 | controller = dependencies && typeof ctrlObj === 'function' ? ctrlObj(dependencies) : ctrlObj 70 | controllerFunctionsCache[controllerName] = controller; 71 | debug('controller found', controllerPath); 72 | break; 73 | } catch (err) { 74 | if (!mockMode && i === controllersDirs.length - 1) { 75 | return cb(err); 76 | } 77 | debug('controller not in', controllerPath); 78 | } 79 | } 80 | } 81 | 82 | if (controller) { 83 | 84 | var operationId = operation.definition.operationId || context.request.method.toLowerCase(); 85 | var ctrlType = 86 | operation.definition['x-controller-type'] || 87 | operation.pathObject.definition['x-controller-type'] || 88 | operation.pathObject.api.definition['x-controller-type']; 89 | 90 | var controllerFunction = controller[operationId]; 91 | 92 | if (controllerFunction && typeof controllerFunction === 'function') { 93 | if (operation.controllerInterface == 'auto-detect') { 94 | operation.controllerInterface = 95 | controllerFunction.length == 3 96 | ? 'middleware' 97 | : 'pipe'; 98 | debug("auto-detected interface-type for operation '%s' at [%s] as '%s'", operationId, operation.pathToDefinition, operation.controllerInterface) 99 | } 100 | 101 | debug('running controller, as %s', operation.controllerInterface); 102 | return operation.controllerInterface == 'pipe' 103 | ? controllerFunction(context, cb) 104 | : controllerFunction(context.request, context.response, cb); 105 | } 106 | 107 | var msg = util.format('Controller %s doesn\'t export handler function %s', controllerName, operationId); 108 | if (mockMode) { 109 | debug(msg); 110 | } else { 111 | return cb(new Error(msg)); 112 | } 113 | } 114 | 115 | if (mockMode) { 116 | 117 | var statusCode = parseInt(context.request.get('_mockreturnstatus')) || 200; 118 | 119 | var mimetype = context.request.get('accept') || 'application/json'; 120 | var response = operation.getResponse(statusCode) || operation.getResponse('default'); 121 | var mock = response.getExample(mimetype); 122 | 123 | if (mock) { 124 | debug('returning mock example value', mock); 125 | } else { 126 | var operationResponse = operation.getResponse(statusCode) || operation.getResponse('default'); 127 | mock = operationResponse.getSample(); 128 | debug('returning mock sample value', mock); 129 | } 130 | 131 | context.headers['Content-Type'] = mimetype; 132 | context.statusCode = statusCode; 133 | 134 | return cb(null, mock); 135 | } 136 | 137 | // for completeness, we should never actually get here 138 | cb(new Error(util.format('No controller found for %s in %j', controllerName, controllersDirs))); 139 | } 140 | }; 141 | -------------------------------------------------------------------------------- /test/fittings/connect_middleware.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var connect_middleware = require('../../lib/connect_middleware'); 3 | 4 | 5 | describe("connect_middleware", function() { 6 | it("should be a factory function that names 1 argument: runner", function() { 7 | should(connect_middleware).be.a.Function(); 8 | connect_middleware.length.should.eql(1); 9 | }); 10 | 11 | describe("when called with a runner object", function() { 12 | var mockRunner = {}; 13 | var mw_provider; 14 | 15 | before(function() { 16 | mw_provider = connect_middleware(mockRunner) 17 | }) 18 | 19 | describe("the returned provider", function() { 20 | it("should be an initiated provider module", function() { 21 | should(mw_provider).be.an.Object(); 22 | }); 23 | 24 | it("should have member .runner - as the injected runner", function() { 25 | should(mw_provider.runner).equal(mockRunner); 26 | }); 27 | 28 | it("should have method .middleware()", function() { 29 | should(mw_provider.middleware).be.a.Function(); 30 | should(mw_provider.middleware.length).eql(0); 31 | }); 32 | describe(".middleware()", function(){ 33 | describe("when called", function() { 34 | it("should return a middleware function(req,res,next)") 35 | describe("the returned middleware", function() { 36 | describe("when used with request that matches no operation nor path", function() { 37 | it("should call next with no side effects") 38 | }); 39 | 40 | describe("when used with request that matches no operation AND path has no 'x-swagger-pipe' AND method is not OPTIONS", function() { 41 | it("should not fail"); 42 | it("should call next"); 43 | describe("the yielded error", function() { 44 | it("should have .statusCode: 405"); 45 | it("should have .status: 405 (for sails)"); 46 | it("should have .message like 'Path [] defined in Swagger, but operation is not'") 47 | it("should have .allowedMethods"); 48 | it("should setHeader('Allow') properly"); 49 | }); 50 | }); 51 | 52 | describe("when used with request that matches no operation but path has 'x-swagger-pipe'", function() { 53 | it("TBD") 54 | }); 55 | 56 | describe("when used with request that matches a concrete operation", function() { 57 | describe("and pipe NOT found", function() { 58 | it("should yield an error"); 59 | describe("the yielded error", function() { 60 | it("should have .message like 'No implementation found for this path'"); 61 | it("should have .statusCode: 405") 62 | }) 63 | }); 64 | describe("and pipe is found", function() { 65 | it("should play the pipe"); 66 | describe("and pipe executes to _finnish", function() { 67 | describe("and context.error is set by the pipe", function() { 68 | it("should yield the error") 69 | }); 70 | describe("and context.statusCode is set", function() { 71 | it("should set response.statusCode"); 72 | }); 73 | describe("and context.headers is set", function() { 74 | it("should set each header in context.headers"); 75 | }); 76 | describe("and context.output is set", function() { 77 | describe("and response content-type is set", function() { 78 | describe("to application/json", function() { 79 | it("should emit response body as JSON serialization of context.output") 80 | it("should call next"); 81 | }); 82 | }) 83 | describe("and response content-type is not set", function() { 84 | describe("and request accept type set", function() { 85 | describe("to application/json", function() { 86 | it("should emit response body as JSON serialization of context.output") 87 | it("should call next"); 88 | }); 89 | }); 90 | describe("and request accept type set to */* or not set", function() { 91 | it("should use the default mimetype in operation.produces[0]"); 92 | it("should yield no error"); 93 | }); 94 | }); 95 | }); 96 | describe("and context.output is set not set", function() { 97 | it("should yield no error without writing anything"); 98 | }); 99 | }); 100 | 101 | describe("and pipe is not executed to _finnish", function() { 102 | it("TBD"); 103 | }); 104 | 105 | describe("and 'responseValidationError' event on the runner is watched", function() { 106 | it("should place response validation hooks") 107 | }); 108 | describe("and 'responseValidationError' event on the runner not watched", function() { 109 | it("should not place response validation hooks") 110 | }); 111 | }); 112 | }); 113 | }); 114 | }); 115 | }); 116 | 117 | it("should have method .register(app)", function() { 118 | should(mw_provider.register).be.a.Function(); 119 | should(mw_provider.register.length).eql(1); 120 | }); 121 | describe(".register(app)", function() { 122 | describe("when called with a server instance", function() { 123 | it("TBD"); 124 | }); 125 | }); 126 | }); 127 | }); 128 | }); -------------------------------------------------------------------------------- /lib/connect_middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = init; 4 | 5 | var debug = require('debug')('swagger'); 6 | var debugContent = require('debug')('swagger:content'); 7 | var _ = require('lodash'); 8 | var util = require('util'); 9 | var EventEmitter = require('events').EventEmitter; 10 | 11 | function init(runner) { 12 | return new Middleware(runner); 13 | } 14 | 15 | function Middleware(runner) { 16 | 17 | this.runner = runner; 18 | 19 | this.middleware = function middleware() { 20 | 21 | return function middleware(req, res, next) { // flow back to connect pipe 22 | 23 | var operation = runner.getOperation(req); 24 | 25 | if (!operation) { 26 | var path = runner.getPath(req); 27 | if (!path) { return next(); } 28 | 29 | if (!path['x-swagger-pipe'] && req.method !== 'OPTIONS') { 30 | var msg = util.format('Path [%s] defined in Swagger, but %s operation is not.', path.path, req.method); 31 | var err = new Error(msg); 32 | err.statusCode = 405; 33 | err.status = err.statusCode; // for Sails, see: https://github.com/theganyo/swagger-node-runner/pull/31 34 | 35 | var allowedMethods = _.map(path.operationObjects, function(operation) { 36 | return operation.method.toUpperCase(); 37 | }); 38 | err.allowedMethods = allowedMethods; 39 | 40 | res.setHeader('Allow', allowedMethods.sort().join(', ')); 41 | return next(err); 42 | } 43 | } 44 | 45 | runner.applyMetadata(req, operation, function(err) { 46 | if (err) { /* istanbul ignore next */ return next(err); } 47 | 48 | var pipe = runner.getPipe(req); 49 | if (!pipe) { 50 | var err = new Error('No implementation found for this path.'); 51 | err.statusCode = 405; 52 | return next(err); 53 | } 54 | 55 | var context = { 56 | // system values 57 | _errorHandler: runner.defaultErrorHandler(), 58 | request: req, 59 | response: res, 60 | 61 | // user-modifiable values 62 | input: undefined, 63 | statusCode: undefined, 64 | headers: {}, 65 | output: undefined 66 | }; 67 | 68 | context._finish = function finishConnect(ignore1, ignore2) { // must have arity of 2 69 | debugContent("exec", context.error); 70 | if (context.error) { return next(context.error); } 71 | 72 | try { 73 | var response = context.response; 74 | 75 | if (context.statusCode) { 76 | debug('setting response statusCode: %d', context.statusCode); 77 | response.statusCode = context.statusCode; 78 | } 79 | 80 | if (context.headers) { 81 | debugContent('setting response headers: %j', context.headers); 82 | _.each(context.headers, function(value, name) { 83 | response.setHeader(name, value); 84 | }); 85 | } 86 | 87 | if (undefined === context.output) { return next(); } 88 | 89 | var contentType = response.getHeader('content-type'); 90 | if (!contentType) { 91 | contentType = request.headers.accept; 92 | if ("*/*" == contentType) contentType = operation.produces[0]; 93 | if (contentType) response.setHeader("content-type", contentType); 94 | } 95 | 96 | var body = translate(context.output, contentType); 97 | 98 | debugContent('sending response body: %s', body); 99 | response.end(body); 100 | } 101 | catch (err) { 102 | /* istanbul ignore next */ 103 | next(err); 104 | } 105 | }; 106 | 107 | /* istanbul ignore next */ 108 | var listenerCount = (runner.listenerCount) ? 109 | runner.listenerCount('responseValidationError') : // Node >= 4.0 110 | EventEmitter.listenerCount(runner, 'responseValidationError'); // Node < 4.0 111 | if (listenerCount) { 112 | hookResponseForValidation(context, runner); 113 | } 114 | 115 | runner.bagpipes.play(pipe, context); 116 | }); 117 | }; 118 | }; 119 | 120 | this.register = function register(app) { 121 | app.use(this.middleware()); 122 | }; 123 | } 124 | 125 | function translate(output, mimeType) { 126 | 127 | if (typeof output !== 'object') { return output; } 128 | 129 | switch(true) { 130 | 131 | case /json/.test(mimeType): 132 | return JSON.stringify(output); 133 | 134 | default: 135 | return util.inspect(output) 136 | } 137 | } 138 | 139 | function hookResponseForValidation(context, eventEmitter) { 140 | 141 | debug('add response validation hook'); 142 | var res = context.response; 143 | var end = res.end; 144 | var write = res.write; 145 | var written; 146 | res.write = function hookWrite(chunk, encoding, callback) { 147 | if (written) { 148 | written = ''; 149 | res.write = write; 150 | res.end = end; 151 | debug('multiple writes, will not validate response'); 152 | } else { 153 | written = chunk; 154 | } 155 | write.apply(res, arguments); 156 | }; 157 | res.end = function hookEnd(data, encoding, callback) { 158 | res.write = write; 159 | res.end = end; 160 | if (written && data) { 161 | debug('multiple writes, will not validate response'); 162 | } else if (!context.request.swagger.operation) { 163 | debug('not a swagger operation, will not validate response'); 164 | } else { 165 | debug('validating response'); 166 | try { 167 | var headers = res._headers || res.headers || {}; 168 | var body = data || written; 169 | debugContent('response body type: %s value: %s', typeof body, body); 170 | var validateResult = context.request.swagger.operation.validateResponse({ 171 | statusCode: res.statusCode, 172 | headers: headers, 173 | body: body 174 | }); 175 | debug('validation result:', validateResult); 176 | if (validateResult.errors.length || validateResult.warnings.length) { 177 | debug('emitting responseValidationError'); 178 | eventEmitter.emit('responseValidationError', validateResult, context.request, res); 179 | } 180 | } catch (err) { 181 | /* istanbul ignore next */ 182 | console.error(err.stack); 183 | } 184 | } 185 | end.apply(res, arguments); 186 | }; 187 | } 188 | -------------------------------------------------------------------------------- /test/fittings/swagger_raw.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var SwaggerRunner = require('../../'); 3 | var swagger_raw = require('../../fittings/swagger_raw'); 4 | var fs = require('fs'); 5 | var YAML = require('js-yaml'); 6 | var path = require('path'); 7 | var _ = require('lodash'); 8 | var request = require('supertest'); 9 | 10 | 11 | describe('swagger_raw', function() { 12 | 13 | var swagger, yaml, json, swaggerDoc; 14 | 15 | before(function() { 16 | var data = fs.readFileSync(path.resolve(__dirname, '../assets/project/api/swagger/swagger.yaml'), 'utf8'); 17 | swagger = YAML.safeLoad(data); 18 | 19 | var bagpipes = { config: { swaggerNodeRunner: { swagger: swagger }}}; 20 | swaggerDoc = swagger_raw({}, bagpipes); 21 | 22 | var filteredSwagger = filterDoc(swagger); 23 | yaml = YAML.safeDump(filteredSwagger, { indent: 2 }); 24 | json = JSON.stringify(filteredSwagger, null, 2); 25 | }); 26 | 27 | it('should retrieve swagger json', function(done) { 28 | 29 | var context = { 30 | headers: {}, 31 | request: { 32 | headers: { } 33 | } 34 | }; 35 | 36 | swaggerDoc(context, function(err, output) { 37 | should.not.exist(err); 38 | 'application/json'.should.eql(context.headers['Content-Type']); 39 | output.should.eql(json); 40 | done(); 41 | }); 42 | }); 43 | 44 | it('should retrieve swagger yaml', function(done) { 45 | 46 | var context = { 47 | headers: {}, 48 | request: { 49 | headers: { accept: 'application/yaml' } 50 | } 51 | }; 52 | 53 | swaggerDoc(context, function(err, output) { 54 | should.not.exist(err); 55 | 'application/yaml'.should.eql(context.headers['Content-Type']); 56 | output.should.eql(yaml); 57 | done(); 58 | }); 59 | }); 60 | 61 | it('should be able to set the filter', function(done) { 62 | 63 | var context = { 64 | headers: {}, 65 | request: { 66 | headers: { accept: 'application/yaml' } 67 | } 68 | }; 69 | 70 | var config = { 71 | filter: '.*' 72 | }; 73 | 74 | var bagpipes = { config: { swaggerNodeRunner: { swagger: swagger }}}; 75 | var swaggerDoc = swagger_raw(config, bagpipes); 76 | 77 | swaggerDoc(context, function(err, output) { 78 | should.not.exist(err); 79 | 'application/yaml'.should.eql(context.headers['Content-Type']); 80 | 81 | var filteredSwagger = _.cloneDeep(swagger); 82 | delete(filteredSwagger.paths['/invalid_header']); 83 | should.exist(filteredSwagger.paths['/invalid_response_code'].get) 84 | var yaml = YAML.safeDump(filteredSwagger, { indent: 2 }); 85 | 86 | output.should.eql(yaml); 87 | done(); 88 | }); 89 | }); 90 | 91 | it('should be able to modify privateTags & apply to operations', function(done) { 92 | 93 | var context = { 94 | headers: {}, 95 | request: { 96 | headers: { accept: 'application/yaml' } 97 | } 98 | }; 99 | 100 | var config = { 101 | filter: '.*', 102 | privateTags: [ 'x-private', 'x-hidden' ] 103 | }; 104 | 105 | var bagpipes = { config: { swaggerNodeRunner: { swagger: swagger }}}; 106 | var swaggerDoc = swagger_raw(config, bagpipes); 107 | 108 | swaggerDoc(context, function(err, output) { 109 | should.not.exist(err); 110 | 'application/yaml'.should.eql(context.headers['Content-Type']); 111 | 112 | var filteredSwagger = _.cloneDeep(swagger); 113 | delete(filteredSwagger.paths['/invalid_header']); 114 | delete(filteredSwagger.paths['/invalid_response_code'].get); 115 | var yaml = YAML.safeDump(filteredSwagger, { indent: 2 }); 116 | 117 | output.should.eql(yaml); 118 | done(); 119 | }); 120 | }); 121 | 122 | describe("end-to-end", function() { 123 | var app, filteredSwagger; 124 | before(function(done) { 125 | var data = fs.readFileSync(path.resolve(__dirname, '../assets/project/api/swagger/swagger.yaml'), 'utf8'); 126 | var attrs; 127 | 128 | //brings the size of the doc to 129 | // yaml - 125425 130 | // json - 120868 131 | var swagger = inflateDocWithAVeryBigTypeDefinition(YAML.safeLoad(data)); 132 | 133 | filteredSwagger = filterDoc(swagger); 134 | yaml = YAML.safeDump(filteredSwagger, { indent: 2 }); 135 | 136 | app = createServer(swagger, done) 137 | }); 138 | 139 | describe('when requested with accept:application/json', function() { 140 | it('should yield the document as json', function(done) { 141 | request(app) 142 | .put('/swagger') 143 | .set('accept', 'application/json') 144 | .expect(200) 145 | .expect('Content-Type', /json/) 146 | .end(function(err, res) { 147 | should.not.exist(err); 148 | 149 | should(res.body).eql(filteredSwagger); 150 | 151 | done(); 152 | }) 153 | }) 154 | }); 155 | 156 | describe('when requested with accept:application/yaml', function() { 157 | it('should yield the document as json', function(done) { 158 | request(app) 159 | .put('/swagger') 160 | .set('accept', 'application/yaml') 161 | .expect(200) 162 | .expect('Content-Type', /yaml/) 163 | .end(function(err, res) { 164 | should.not.exist(err); 165 | 166 | should(res.text).eql(yaml); 167 | 168 | done(); 169 | }) 170 | }) 171 | }) 172 | }) 173 | }); 174 | 175 | 176 | function createServer(swagger, done) { 177 | var TEST_PROJECT_ROOT = path.resolve(__dirname, '..', 'assets', 'project'); 178 | var config = { 179 | appRoot: TEST_PROJECT_ROOT, 180 | swagger: swagger 181 | }; 182 | 183 | var app = require('connect')(); 184 | SwaggerRunner.create(config, function(err, r) { 185 | if (err) { 186 | console.error(err); 187 | return done(err); 188 | } 189 | r.connectMiddleware().register(app); 190 | done(); 191 | }); 192 | return app 193 | } 194 | 195 | function filterDoc(swagger) { 196 | var filteredSwagger = _.cloneDeep(swagger); 197 | delete(filteredSwagger.paths['/invalid_header']); 198 | delete(filteredSwagger.paths['/hello'].get.parameters[0]['x-remove-me']) 199 | 200 | // hokey algorithm, but at least it's different than the one it's testing 201 | var OMIT = ['x-swagger-router-controller', 'x-swagger-pipe', 'x-hidden', 'x-private', 'x-controller-interface']; 202 | _.forEach(filteredSwagger.paths, function(element, name) { 203 | filteredSwagger.paths[name] = _.omit(element, OMIT); 204 | _.forEach(filteredSwagger.paths[name], function(element, subName) { 205 | filteredSwagger.paths[name][subName] = _.omit(element, OMIT); 206 | }); 207 | }); 208 | 209 | return filteredSwagger 210 | } 211 | 212 | function inflateDocWithAVeryBigTypeDefinition(swagger) { 213 | var props; 214 | swagger.definitions.veryVeryBigCompoundType = { 215 | type: "object", 216 | properties: props = { 217 | some_string: { 218 | type: "string", 219 | description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." 220 | } 221 | } 222 | }; 223 | 224 | var i = 200; 225 | while(--i) props["some_string" + i] = _.clone(props.some_string); 226 | 227 | return swagger 228 | } 229 | -------------------------------------------------------------------------------- /test/fittings/json_error_handler.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | var json_error_handler = require('../../fittings/json_error_handler'); 3 | var _ = require('lodash'); 4 | 5 | describe('json_error_handler', function() { 6 | 7 | var jsonErrorHandler = json_error_handler({}); 8 | 9 | var err = new Error('this is a test'); 10 | Object.defineProperty(err, 'message', { enumerable: true }); 11 | var errorString = JSON.stringify(err); 12 | 13 | describe('error in context', function() { 14 | 15 | var context; 16 | beforeEach(function() { 17 | context = { 18 | headers: {}, 19 | error: new Error('this is a test') 20 | } 21 | }); 22 | 23 | it('should set headers', function(done) { 24 | 25 | context.error.statusCode = 400; 26 | jsonErrorHandler(context, function(err) { 27 | should.not.exist(err); 28 | should.not.exist(context.error); 29 | 'application/json'.should.eql(context.headers['Content-Type']); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('should set status code', function(done) { 35 | 36 | context.error.statusCode = 400; 37 | jsonErrorHandler(context, function(err) { 38 | should.not.exist(err); 39 | should.not.exist(context.error); 40 | context.statusCode.should.eql(400); 41 | done(); 42 | }); 43 | }); 44 | 45 | it('should emit appropriate json', function(done) { 46 | 47 | context.error.statusCode = 400; 48 | 49 | jsonErrorHandler(context, function(err, output) { 50 | should.not.exist(err); 51 | should.not.exist(context.error); 52 | errorString.should.eql(output); 53 | done(); 54 | }); 55 | }); 56 | 57 | it('should not handle unexpected errors by default', function(done) { 58 | 59 | jsonErrorHandler(context, function(err) { 60 | should.exist(err); 61 | should.exist(context.error); 62 | should.not.exist(context.headers['Content-Type']); 63 | context.statusCode.should.eql(500); 64 | done(); 65 | }); 66 | }); 67 | 68 | it('should handle unexpected errors if configured to do so', function(done) { 69 | 70 | var jsonErrorHandler = json_error_handler({ handle500Errors: true }); 71 | jsonErrorHandler(context, function(err, output) { 72 | should.not.exist(err); 73 | should.not.exist(context.error); 74 | 'application/json'.should.eql(context.headers['Content-Type']); 75 | context.statusCode.should.eql(500); 76 | errorString.should.eql(output); 77 | done(); 78 | }); 79 | }); 80 | }); 81 | 82 | describe('error with statusCode in context', function() { 83 | 84 | var context; 85 | beforeEach(function() { 86 | var err = new Error('this is a test'); 87 | err.statusCode = 401; 88 | context = { 89 | headers: {}, 90 | error: err 91 | }; 92 | }); 93 | 94 | it('should set headers', function(done) { 95 | 96 | jsonErrorHandler(context, function(err) { 97 | should.not.exist(err); 98 | 'application/json'.should.eql(context.headers['Content-Type']); 99 | done(); 100 | }); 101 | }); 102 | 103 | it('should set status code', function(done) { 104 | 105 | jsonErrorHandler(context, function(err) { 106 | should.not.exist(err); 107 | context.statusCode.should.eql(401); 108 | done(); 109 | }); 110 | }); 111 | 112 | it('should emit appropriate json', function(done) { 113 | 114 | var err = new Error('this is a test'); 115 | Object.defineProperty(err, 'message', { enumerable: true }); 116 | var errorString = JSON.stringify(err); 117 | 118 | jsonErrorHandler(context, function(err, output) { 119 | should.not.exist(err); 120 | should.not.exist(context.error); 121 | errorString.should.eql(output); 122 | done(); 123 | }); 124 | }); 125 | }); 126 | 127 | describe('includeErrStack:true', function() { 128 | 129 | var context; 130 | beforeEach(function() { 131 | var err = new Error('this is a test'); 132 | err.statusCode = 401; 133 | err.someAttr = 'value'; 134 | context = { 135 | headers: {}, 136 | error: err 137 | }; 138 | }); 139 | 140 | it('should allow the stack in the response body', function(done) { 141 | 142 | var jsonErrorHandler = json_error_handler({ includeErrStack: true }); 143 | 144 | jsonErrorHandler(context, function(err, output) { 145 | should.not.exist(err); 146 | should.not.exist(context.error); 147 | 148 | var e; 149 | try { 150 | var body = JSON.parse(output); 151 | body.should.have.property('message', 'this is a test'); 152 | body.should.have.property('someAttr','value'); 153 | body.should.have.property('stack') 154 | } catch(x) { e = x } 155 | done(e) 156 | }); 157 | }) 158 | }) 159 | 160 | describe('handle500Errors:true and error fails to stringify', function() { 161 | var jsonErrorHandler; 162 | var mockErr; 163 | var context; 164 | var err; 165 | 166 | before(function() { 167 | jsonErrorHandler = json_error_handler({ handle500Errors: true }); 168 | mockErr = new Error('this is a test'); 169 | mockErr.circular = mockErr; //force stringification error 170 | }); 171 | 172 | describe('and context has a logger on request', function() { 173 | before(function(done) { 174 | context = { 175 | headers: {}, 176 | request: { 177 | log: { 178 | error: function() { this.lastErr = arguments } 179 | } 180 | }, 181 | error: mockErr 182 | }; 183 | jsonErrorHandler(context, function(e) { 184 | err = e; 185 | done() 186 | }); 187 | }); 188 | it('should not fail', function() { 189 | should.not.exist(err); 190 | }) 191 | it('should remove the error from the context', function() { 192 | should.not.exist(context.error); 193 | }); 194 | it('should pass stringification error to the logger', function() { 195 | should.exist(context.request.log.lastErr, "error was not passed to log"); 196 | should(context.request.log.lastErr.length).eql(3); 197 | should(context.request.log.lastErr[2]).equal(mockErr); 198 | }); 199 | }); 200 | 201 | describe('and context has no logger on req.log, but has on request.app.log', function() { 202 | before(function(done) { 203 | context = { 204 | headers: {}, 205 | request: { 206 | app: { 207 | log: { 208 | error: function() { this.lastErr = arguments } 209 | } 210 | } 211 | }, 212 | error: mockErr 213 | }; 214 | jsonErrorHandler(context, function(e) { 215 | err = e; 216 | done() 217 | }); 218 | }); 219 | it('should not fail', function() { 220 | should.not.exist(err); 221 | }) 222 | it('should remove the error from the context', function() { 223 | should.not.exist(context.error); 224 | }); 225 | it('should pass stringification error to the logger', function() { 226 | should.exist(context.request.app.log.lastErr, "error was not passed to log"); 227 | should(context.request.app.log.lastErr.length).eql(3); 228 | should(context.request.app.log.lastErr[2]).equal(mockErr); 229 | }); 230 | }); 231 | 232 | describe('and context has no logger on request, but has on response', function() { 233 | 234 | beforeEach(function(done) { 235 | context = { 236 | headers: {}, 237 | response: { 238 | log: { 239 | error: function() { this.lastErr = arguments } 240 | } 241 | }, 242 | error: mockErr 243 | }; 244 | jsonErrorHandler(context, function(e) { 245 | err = e; 246 | done() 247 | }); 248 | 249 | }); 250 | 251 | it('should not fail', function() { 252 | should.not.exist(err); 253 | }); 254 | it('should remove the error from the context', function() { 255 | should.not.exist(context.error); 256 | }); 257 | it('should pass stringification error to the logger', function() { 258 | should.exist(context.response.log.lastErr, "error was not passed to log"); 259 | should(context.response.log.lastErr.length).eql(3); 260 | should(context.response.log.lastErr[2]).equal(mockErr); 261 | }); 262 | }); 263 | }); 264 | 265 | 266 | describe('no error in context', function() { 267 | 268 | var context; 269 | before(function() { 270 | context = { 271 | headers: {} 272 | } 273 | }); 274 | 275 | it('should not set headers', function(done) { 276 | jsonErrorHandler(context, function(err) { 277 | should.not.exist(err); 278 | should.not.exist(context.headers['Content-Type']); 279 | done(); 280 | }); 281 | }); 282 | 283 | it('should not set status code', function(done) { 284 | jsonErrorHandler(context, function(err) { 285 | should.not.exist(err); 286 | should.not.exist(context.statusCode); 287 | done(); 288 | }); 289 | }); 290 | 291 | it('should not emit error json', function(done) { 292 | 293 | jsonErrorHandler(context, function(err, output) { 294 | should.not.exist(err); 295 | should.not.exist(output); 296 | done(); 297 | }); 298 | }); 299 | }); 300 | 301 | }); 302 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | Runner properties: 5 | config 6 | swagger 7 | api // (sway) 8 | connectMiddleware() 9 | resolveAppPath() 10 | securityHandlers 11 | bagpipes 12 | 13 | Runner events: 14 | responseValidationError 15 | 16 | config properties: 17 | appRoot 18 | mockMode 19 | configDir 20 | controllersDirs 21 | mockControllersDirs 22 | securityHandlers 23 | */ 24 | 25 | module.exports = { 26 | create: create 27 | }; 28 | 29 | var _ = require('lodash'); 30 | var yaml = require('js-yaml'); 31 | var path = require('path'); 32 | var sway = require('sway'); 33 | var debug = require('debug')('swagger'); 34 | var bagpipes = require('bagpipes'); 35 | var EventEmitter = require('events').EventEmitter; 36 | var util = require('util'); 37 | 38 | var SWAGGER_SELECTED_PIPE = 'x-swagger-pipe'; 39 | var SWAGGER_ROUTER_CONTROLLER = 'x-swagger-router-controller'; 40 | var DEFAULT_FITTINGS_DIRS = [ 'api/fittings' ]; 41 | var DEFAULT_VIEWS_DIRS = [ 'api/views' ]; 42 | var DEFAULT_SWAGGER_FILE = 'api/swagger/swagger.yaml'; // relative to appRoot 43 | 44 | /* 45 | SwaggerNode config priority: 46 | 1. swagger_* environment vars 47 | 2. config passed to create() 48 | 3. read from swagger node in default.yaml in config directory 49 | 4. defaults in this file 50 | */ 51 | 52 | function create(config, cb) { 53 | 54 | if (!_.isFunction(cb)) { throw new Error('callback is required'); } 55 | if (!config || !config.appRoot) { return cb(new Error('config.appRoot is required')); } 56 | 57 | new Runner(config, cb); 58 | } 59 | 60 | util.inherits(Runner, EventEmitter); 61 | 62 | function Runner(appJsConfig, cb) { 63 | 64 | EventEmitter.call(this); 65 | 66 | this.resolveAppPath = function resolveAppPath(to) { 67 | return path.resolve(appJsConfig.appRoot, to); 68 | }; 69 | 70 | this.connectMiddleware = function connectMiddleware() { 71 | return require('./lib/connect_middleware')(this); 72 | }; 73 | 74 | this.expressMiddleware = this.connectMiddleware; 75 | 76 | this.restifyMiddleware = function restifyMiddleware() { 77 | return require('./lib/restify_middleware')(this); 78 | }; 79 | 80 | this.sailsMiddleware = function sailsMiddleware() { 81 | return require('./lib/sails_middleware')(this); 82 | }; 83 | 84 | this.hapiMiddleware = function hapiMiddleware() { 85 | return require('./lib/hapi_middleware')(this); 86 | }; 87 | 88 | this.defaultErrorHandler = function() { 89 | 90 | return this.bagpipes.createPipeFromFitting(defaultErrorFitting, { name: 'defaultErrorHandler' }); 91 | 92 | function defaultErrorFitting(context, next) { 93 | 94 | debug('default error handler: %s', context.error.message); 95 | next(); 96 | } 97 | }; 98 | 99 | this.getOperation = function getOperation(req) { 100 | return this.api.getOperation(req); 101 | }; 102 | 103 | this.getPath = function getPath(req) { 104 | return this.api.getPath(req); 105 | }; 106 | 107 | // adds req.swagger to the request 108 | this.applyMetadata = function applyMetadata(req, operation, cb) { 109 | 110 | var swagger = req.swagger = {}; 111 | swagger.operation = operation; 112 | cb(); 113 | }; 114 | 115 | // must assign req.swagger (see #applyMetadata) before calling 116 | this.getPipe = function getPipe(req) { 117 | 118 | var operation = req.swagger.operation; 119 | 120 | var path = operation ? operation.pathObject : this.getPath(req); 121 | var config = this.config.swagger; 122 | 123 | // prefer explicit pipe 124 | var pipeName; 125 | if (operation) { 126 | pipeName = operation[SWAGGER_SELECTED_PIPE]; 127 | } 128 | if (!pipeName) { 129 | pipeName = path[SWAGGER_SELECTED_PIPE]; 130 | } 131 | 132 | // no explicit pipe, but there's a controller 133 | if (!pipeName) { 134 | if ((operation && operation[SWAGGER_ROUTER_CONTROLLER]) || path[SWAGGER_ROUTER_CONTROLLER]) 135 | { 136 | pipeName = config.swaggerControllerPipe; 137 | } 138 | } 139 | debug('pipe requested:', pipeName); 140 | 141 | // default pipe 142 | if (!pipeName) { pipeName = config.defaultPipe; } 143 | 144 | if (!pipeName) { 145 | debug('no default pipe'); 146 | return null; 147 | } 148 | 149 | var pipe = this.bagpipes.pipes[pipeName]; 150 | 151 | if (!pipe) { 152 | debug('no defined pipe: ', pipeName); 153 | return null; 154 | } 155 | 156 | debug('executing pipe %s', pipeName); 157 | 158 | return pipe; 159 | }; 160 | 161 | // don't override if env var already set 162 | if (!process.env.NODE_CONFIG_DIR) { 163 | if (!appJsConfig.configDir) { appJsConfig.configDir = 'config'; } 164 | process.env.NODE_CONFIG_DIR = path.resolve(appJsConfig.appRoot, appJsConfig.configDir); 165 | } 166 | var Config = require('config'); 167 | 168 | var swaggerConfigDefaults = { 169 | enforceUniqueOperationId: false, 170 | startWithErrors: false, 171 | startWithWarnings: true 172 | }; 173 | 174 | this.config = Config.util.cloneDeep(Config); 175 | this.config.swagger = 176 | Config.util.extendDeep( 177 | swaggerConfigDefaults, 178 | this.config.swagger, 179 | appJsConfig, 180 | readEnvConfig()); 181 | 182 | debug('resolved config: %j', this.config); 183 | 184 | var self = this; 185 | var swayOpts = { 186 | definition: appJsConfig.swagger || appJsConfig.swaggerFile || this.resolveAppPath(DEFAULT_SWAGGER_FILE) 187 | }; 188 | 189 | debug('initializing Sway'); 190 | // sway uses Promises 191 | sway.create(swayOpts) 192 | .then(function(api) { 193 | 194 | debug('validating api'); 195 | var validateResult = api.validate(); 196 | debug('done validating api. errors: %d, warnings: %d', validateResult.errors.length, validateResult.warnings.length); 197 | 198 | var errors = validateResult.errors; 199 | if (errors && errors.length > 0) { 200 | if (!self.config.swagger.enforceUniqueOperationId) { 201 | errors = errors.filter(function(err) { 202 | return (err.code !== 'DUPLICATE_OPERATIONID'); 203 | }); 204 | } 205 | if (errors.length > 0) { 206 | if (self.config.swagger.startWithErrors) { 207 | var errorText = JSON.stringify(errors); 208 | console.error(errorText, 2); 209 | } else { 210 | var err = new Error('Swagger validation errors:'); 211 | err.validationErrors = errors; 212 | throw err; 213 | } 214 | } 215 | } 216 | 217 | var warnings = validateResult.warnings; 218 | if (warnings && warnings.length > 0) { 219 | var warningText = JSON.stringify(warnings); 220 | if (self.config.swagger.startWithWarnings) { 221 | console.error(warningText, 2); 222 | } else { 223 | var err = new Error('Swagger validation warnings:'); 224 | err.validationWarnings = warnings; 225 | throw err; 226 | } 227 | } 228 | 229 | self.api = api; 230 | self.swagger = api.definition; 231 | self.securityHandlers = appJsConfig.securityHandlers || appJsConfig.swaggerSecurityHandlers; // legacy name 232 | self.bagpipes = createPipes(self); 233 | 234 | cb(null, self); 235 | }) 236 | .catch(function(err) { 237 | cb(err); 238 | }) 239 | .catch(function(err) { 240 | console.error('Error in callback! Tossing to global error handler.', err.stack); 241 | 242 | if (err.validationErrors) { 243 | console.error('Details: '); 244 | for (var i= 0; i" + err.validationErrors[i].path.join('/') + "<"); 246 | } 247 | } 248 | 249 | process.nextTick(function() { throw err; }); 250 | }) 251 | } 252 | 253 | function createPipes(self) { 254 | var config = self.config.swagger; 255 | 256 | var fittingsDirs = (config.fittingsDirs || DEFAULT_FITTINGS_DIRS).map(function(dir) { 257 | return path.resolve(config.appRoot, dir); 258 | }); 259 | var swaggerNodeFittingsDir = path.resolve(__dirname, './fittings'); 260 | fittingsDirs.push(swaggerNodeFittingsDir); 261 | 262 | var viewsDirs = (config.viewsDirs || DEFAULT_VIEWS_DIRS).map(function(dir) { 263 | return path.resolve(config.appRoot, dir); 264 | }); 265 | 266 | // legacy support: set up a default piping for traditional swagger-node if nothing is specified 267 | if (!config.bagpipes || config.bagpipes ==='DEFAULTS_TEST') { 268 | 269 | debug('**** No bagpipes defined in config. Using default setup. ****'); 270 | 271 | config.swaggerControllerPipe = 'swagger_controllers'; 272 | 273 | config.bagpipes = { 274 | _router: { 275 | name: 'swagger_router', 276 | mockMode: false, 277 | mockControllersDirs: [ 'api/mocks' ], 278 | controllersDirs: [ 'api/controllers' ] 279 | }, 280 | _swagger_validate: { 281 | name: 'swagger_validator', 282 | validateReponse: true 283 | }, 284 | swagger_controllers: [ 285 | 'cors', 286 | 'swagger_params_parser', 287 | 'swagger_security', 288 | '_swagger_validate', 289 | 'express_compatibility', 290 | '_router' 291 | ] 292 | }; 293 | 294 | if (config.mapErrorsToJson) { 295 | config.bagpipes.swagger_controllers.unshift({ onError: 'json_error_handler' }); 296 | } 297 | } 298 | 299 | var pipesDefs = config.bagpipes; 300 | 301 | var pipesConfig = { 302 | userFittingsDirs: fittingsDirs, 303 | userViewsDirs: viewsDirs, 304 | swaggerNodeRunner: self 305 | }; 306 | return bagpipes.create(pipesDefs, pipesConfig); 307 | } 308 | 309 | function readEnvConfig() { 310 | 311 | var config = {}; 312 | _.each(process.env, function(value, key) { 313 | var split = key.split('_'); 314 | if (split[0] === 'swagger') { 315 | var configItem = config; 316 | for (var i = 1; i < split.length; i++) { 317 | var subKey = split[i]; 318 | if (i < split.length - 1) { 319 | if (!configItem[subKey]) { configItem[subKey] = {}; } 320 | configItem = configItem[subKey]; 321 | } else { 322 | try { 323 | configItem[subKey] = JSON.parse(value); 324 | } catch (err) { 325 | configItem[subKey] = value; 326 | } 327 | } 328 | } 329 | } 330 | }); 331 | debug('loaded env vars: %j', config); 332 | return config; 333 | } -------------------------------------------------------------------------------- /test/assets/project/api/swagger/swagger.yaml: -------------------------------------------------------------------------------- 1 | swagger: "2.0" 2 | info: 3 | version: "0.0.1" 4 | title: Hello World App 5 | host: localhost:10010 6 | basePath: / 7 | schemes: 8 | - http 9 | consumes: 10 | - application/json 11 | produces: 12 | - application/json 13 | paths: 14 | /hello: 15 | x-swagger-router-controller: hello_world 16 | get: 17 | description: Returns 'Hello' to the caller 18 | operationId: hello 19 | parameters: 20 | - name: name 21 | in: query 22 | description: The name of the person to whom to say hello 23 | required: false 24 | type: string 25 | x-remove-me: lol 26 | responses: 27 | 200: 28 | description: Success 29 | schema: 30 | type: object 31 | $ref: "#/definitions/HelloWorldResponse" 32 | examples: 33 | application/json: 34 | message: 'An example message' 35 | application/x-yaml: 36 | message: 'A yaml example' 37 | default: 38 | description: Error 39 | schema: 40 | $ref: "#/definitions/ErrorResponse" 41 | /multiple_writes: 42 | x-swagger-router-controller: hello_world 43 | get: 44 | description: Returns 'Hello' to the caller 45 | operationId: multiple_writes 46 | responses: 47 | 200: 48 | description: Success 49 | schema: 50 | $ref: "#/definitions/HelloWorldResponse" 51 | default: 52 | description: Error 53 | schema: 54 | $ref: "#/definitions/ErrorResponse" 55 | /hello_no_operationid: 56 | x-swagger-router-controller: hello_world 57 | get: 58 | description: Returns 'Hello' to the caller 59 | parameters: 60 | - name: name 61 | in: query 62 | description: The name of the person to whom to say hello 63 | required: false 64 | type: string 65 | responses: 66 | 200: 67 | description: Success 68 | schema: 69 | $ref: "#/definitions/HelloWorldResponse" 70 | default: 71 | description: Error 72 | schema: 73 | $ref: "#/definitions/ErrorResponse" 74 | /hello_secured: 75 | x-swagger-router-controller: hello_world 76 | get: 77 | description: Returns 'Hello' to the caller 78 | operationId: hello 79 | parameters: 80 | - name: name 81 | in: query 82 | description: The name of the person to whom to say hello 83 | required: false 84 | type: string 85 | responses: 86 | 200: 87 | description: Success 88 | schema: 89 | $ref: "#/definitions/HelloWorldResponse" 90 | default: 91 | description: Error 92 | schema: 93 | $ref: "#/definitions/ErrorResponse" 94 | security: 95 | - api_key: [] 96 | /hello_form: 97 | x-swagger-router-controller: hello_world 98 | get: 99 | description: Returns 'Hello' to the caller 100 | operationId: hello 101 | parameters: 102 | - name: name 103 | in: formData 104 | description: The name of the person to whom to say hello 105 | required: true 106 | type: string 107 | responses: 108 | 200: 109 | description: Success 110 | schema: 111 | $ref: "#/definitions/HelloWorldResponse" 112 | "201": 113 | description: Success 114 | schema: 115 | $ref: "#/definitions/Sample201Response" 116 | default: 117 | description: Error 118 | schema: 119 | $ref: "#/definitions/ErrorResponse" 120 | /hello_body: 121 | x-swagger-router-controller: hello_world 122 | get: 123 | description: Returns 'Hello' to the caller 124 | operationId: hello_body 125 | parameters: 126 | - name: nameRequest 127 | in: body 128 | description: The name of the person to whom to say hello 129 | required: false 130 | schema: 131 | $ref: "#/definitions/NameRequest" 132 | responses: 133 | 200: 134 | description: Success 135 | schema: 136 | $ref: "#/definitions/HelloWorldResponse" 137 | default: 138 | description: Error 139 | schema: 140 | $ref: "#/definitions/ErrorResponse" 141 | /hello_file: 142 | x-swagger-router-controller: hello_world 143 | get: 144 | description: Returns 'Hello' to the caller 145 | operationId: hello_file 146 | parameters: 147 | - name: name 148 | in: formData 149 | description: The name of the person to whom to say hello 150 | required: false 151 | type: string 152 | - name: example_file 153 | in: formData 154 | description: An example file 155 | required: true 156 | type: file 157 | responses: 158 | 200: 159 | description: Success 160 | schema: 161 | $ref: "#/definitions/HelloWorldResponse" 162 | default: 163 | description: Error 164 | schema: 165 | $ref: "#/definitions/ErrorResponse" 166 | /hello_text_body: 167 | x-swagger-router-controller: hello_world 168 | get: 169 | description: Returns 'Hello' to the caller 170 | operationId: hello_text_body 171 | parameters: 172 | - name: name 173 | in: body 174 | description: The name of the person to whom to say hello 175 | schema: 176 | type: string 177 | responses: 178 | 200: 179 | description: Success 180 | schema: 181 | $ref: "#/definitions/HelloWorldResponse" 182 | default: 183 | description: Error 184 | schema: 185 | $ref: "#/definitions/ErrorResponse" 186 | /expect_integer: 187 | x-swagger-router-controller: hello_world 188 | put: 189 | description: Returns 'Hello' to the caller 190 | operationId: hello 191 | parameters: 192 | - name: name 193 | in: query 194 | description: The name of the person to whom to say hello 195 | required: false 196 | type: integer 197 | responses: 198 | 200: 199 | description: Success 200 | schema: 201 | $ref: "#/definitions/HelloWorldResponse" 202 | default: 203 | description: Error 204 | schema: 205 | $ref: "#/definitions/ErrorResponse" 206 | /hello_with_mock: 207 | x-swagger-router-controller: hello_world 208 | get: 209 | description: Returns 'Hello' to the caller 210 | operationId: hello_mock 211 | responses: 212 | 200: 213 | description: Success 214 | schema: 215 | $ref: "#/definitions/HelloWorldResponse" 216 | examples: 217 | application/json: 218 | message: 'An example message' 219 | default: 220 | description: Error 221 | schema: 222 | $ref: "#/definitions/ErrorResponse" 223 | /hello_missing_controller: 224 | x-swagger-router-controller: missing_controller 225 | get: 226 | description: Returns 'Hello' to the caller 227 | operationId: missing_operation 228 | responses: 229 | default: 230 | description: Error 231 | schema: 232 | $ref: "#/definitions/ErrorResponse" 233 | /hello_missing_operation: 234 | x-swagger-router-controller: hello_world 235 | get: 236 | description: Returns 'Hello' to the caller 237 | operationId: missing_operation 238 | responses: 239 | default: 240 | description: Error 241 | schema: 242 | $ref: "#/definitions/ErrorResponse" 243 | /hello_injected_dependencies: 244 | x-swagger-router-controller: hello_deps_injected 245 | get: 246 | description: Returns 'Hello' to the caller 247 | operationId: hello 248 | parameters: 249 | - name: name 250 | in: query 251 | description: The name of the person to whom to say hello 252 | required: false 253 | type: string 254 | responses: 255 | 200: 256 | description: Success 257 | schema: 258 | $ref: "#/definitions/HelloWorldResponse" 259 | examples: 260 | application/json: 261 | message: 'An example message' 262 | /swagger: 263 | x-swagger-pipe: swagger_raw 264 | /pipe_on_get: 265 | get: 266 | x-swagger-pipe: swagger_raw 267 | responses: 268 | default: 269 | description: Error 270 | schema: 271 | $ref: "#/definitions/ErrorResponse" 272 | /empty_path: {} 273 | /no_router_controller: 274 | get: 275 | description: Returns 'Hello' to the caller 276 | operationId: no_operation 277 | responses: 278 | default: 279 | description: Error 280 | schema: 281 | $ref: "#/definitions/ErrorResponse" 282 | /invalid_response_code: 283 | x-swagger-router-controller: hello_world 284 | get: 285 | x-hidden: true 286 | description: Returns 'Hello' to the caller 287 | operationId: hello 288 | parameters: 289 | - name: name 290 | in: query 291 | description: The name of the person to whom to say hello 292 | required: false 293 | type: string 294 | responses: 295 | 333: 296 | description: Error 297 | schema: 298 | $ref: "#/definitions/ErrorResponse" 299 | /invalid_header: 300 | x-private: true 301 | x-swagger-router-controller: hello_world 302 | get: 303 | description: Returns 'Hello' to the caller 304 | operationId: hello 305 | parameters: 306 | - name: name 307 | in: query 308 | description: The name of the person to whom to say hello 309 | required: false 310 | type: string 311 | responses: 312 | 200: 313 | description: Whatever 314 | headers: 315 | content-type: 316 | type: integer 317 | schema: {} 318 | /controller_interface_auto_detected_as_pipe: 319 | x-swagger-router-controller: overrides_ctrl_interface_pipe 320 | x-controller-interface: auto-detect 321 | get: 322 | description: well, what do you know... 323 | operationId: pipeInterface 324 | responses: 325 | 200: 326 | description: Whatever 327 | schema: {} 328 | /controller_interface_auto_detected_as_middleware: 329 | x-swagger-router-controller: overrides_ctrl_interface_pipe 330 | x-controller-interface: auto-detect 331 | get: 332 | description: well, what do you know... 333 | operationId: middlewareInterface 334 | responses: 335 | 200: 336 | description: Whatever 337 | schema: {} 338 | /controller_interface_on_path_cascades: 339 | x-swagger-router-controller: overrides_ctrl_interface_pipe 340 | x-controller-interface: pipe 341 | get: 342 | operationId: pipeInterface 343 | responses: 344 | 200: 345 | description: Whatever 346 | schema: {} 347 | /controller_interface_on_operation_cascades: 348 | x-swagger-router-controller: overrides_ctrl_interface_pipe 349 | x-controller-interface: pipe 350 | get: 351 | x-controller-interface: middleware 352 | operationId: middlewareInterface 353 | responses: 354 | 200: 355 | description: Whatever 356 | schema: {} 357 | /controller_interface_pipe_operation_with_no_body: 358 | x-swagger-router-controller: overrides_ctrl_interface_pipe 359 | get: 360 | x-controller-interface: pipe 361 | operationId: pipeInterfaceNoBody 362 | responses: 363 | 200: 364 | description: Success 365 | default: 366 | description: Error 367 | schema: 368 | $ref: "#/definitions/ErrorResponse" 369 | 370 | definitions: 371 | HelloWorldResponse: 372 | type: object 373 | required: 374 | - message 375 | properties: 376 | message: 377 | type: string 378 | Sample201Response: 379 | required: 380 | - string 381 | - integer 382 | properties: 383 | string: 384 | type: string 385 | integer: 386 | type: integer 387 | ErrorResponse: 388 | required: 389 | - message 390 | properties: 391 | message: 392 | type: string 393 | NameRequest: 394 | required: 395 | - name 396 | properties: 397 | name: 398 | type: string 399 | securityDefinitions: 400 | api_key: 401 | type: "apiKey" 402 | name: "api_key" 403 | in: "header" 404 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'); 4 | var path = require('path'); 5 | var _ = require('lodash'); 6 | var util = require('util'); 7 | 8 | var SwaggerRunner = require('..'); 9 | 10 | var DEFAULT_PROJECT_ROOT = path.resolve(__dirname, 'assets', 'project'); 11 | var DEFAULT_PROJECT_CONFIG = { appRoot: DEFAULT_PROJECT_ROOT }; 12 | 13 | var SWAGGER_WITH_ERRORS = { 14 | swagger: "2.0" 15 | }; 16 | 17 | var SWAGGER_WITH_WARNINGS = { 18 | swagger: "2.0", 19 | info: { 20 | title: '', 21 | version: '' 22 | }, 23 | paths: {}, 24 | definitions: { 25 | SomeUnusedDefinition: { 26 | properties: { 27 | name: { 28 | type: 'string' 29 | } 30 | } 31 | } 32 | } 33 | }; 34 | 35 | var SWAGGER_WITH_GLOBAL_SECURITY = { 36 | "swagger": "2.0", 37 | "info": { 38 | "version": "0.0.1", 39 | "title": "Hello World App" 40 | }, 41 | "host": "localhost:10010", 42 | "basePath": "/", 43 | "schemes": [ 44 | "http" 45 | ], 46 | "consumes": [ 47 | "application/json" 48 | ], 49 | "produces": [ 50 | "application/json" 51 | ], 52 | "security": [ 53 | { 54 | "api_key": [] 55 | } 56 | ], 57 | "paths": { 58 | "/hello_secured": { 59 | "x-swagger-router-controller": "hello_world", 60 | "get": { 61 | "description": "Returns 'Hello' to the caller", 62 | "operationId": "hello", 63 | "parameters": [ 64 | { 65 | "name": "name", 66 | "in": "query", 67 | "description": "The name of the person to whom to say hello", 68 | "required": false, 69 | "type": "string" 70 | } 71 | ], 72 | "responses": { 73 | "200": { 74 | "description": "Success", 75 | "schema": { 76 | "$ref": "#/definitions/HelloWorldResponse" 77 | } 78 | }, 79 | "default": { 80 | "description": "Error", 81 | "schema": { 82 | "$ref": "#/definitions/ErrorResponse" 83 | } 84 | } 85 | } 86 | } 87 | } 88 | }, 89 | "definitions": { 90 | "HelloWorldResponse": { 91 | "type": "object", 92 | "required": [ 93 | "message" 94 | ], 95 | "properties": { 96 | "message": { 97 | "type": "string" 98 | } 99 | } 100 | }, 101 | "ErrorResponse": { 102 | "required": [ 103 | "message" 104 | ], 105 | "properties": { 106 | "message": { 107 | "type": "string" 108 | } 109 | } 110 | } 111 | }, 112 | "securityDefinitions": { 113 | "api_key": { 114 | "type": "apiKey", 115 | "name": "api_key", 116 | "in": "header" 117 | } 118 | } 119 | }; 120 | 121 | 122 | describe('index', function() { 123 | 124 | describe('instantiation', function() { 125 | 126 | it('should fail without config', function(done) { 127 | 128 | var config = undefined; 129 | SwaggerRunner.create(config, function(err, runner) { 130 | should.exist(err); 131 | err.message.should.eql('config.appRoot is required'); 132 | done(); 133 | }); 134 | }); 135 | 136 | it('should fail without config.appRoot', function(done) { 137 | 138 | var config = {}; 139 | SwaggerRunner.create(config, function(err, runner) { 140 | should.exist(err); 141 | err.message.should.eql('config.appRoot is required'); 142 | done(); 143 | }); 144 | }); 145 | 146 | it('should accept passed in configuration', function(done) { 147 | 148 | var config = _.clone(DEFAULT_PROJECT_CONFIG); 149 | config.something = 'x'; 150 | SwaggerRunner.create(config, function(err, runner) { 151 | if (err) { return done(err); } 152 | config.something.should.eql(runner.config.swagger.something); 153 | done(); 154 | }); 155 | }); 156 | 157 | it('should accept env configuration', function(done) { 158 | 159 | var test = 'me'; 160 | var test2 = { x: [ 'y'] }; 161 | process.env.swagger_test = test; 162 | process.env.swagger_test2_test = JSON.stringify(test2); 163 | SwaggerRunner.create(DEFAULT_PROJECT_CONFIG, function(err, runner) { 164 | if (err) { return done(err); } 165 | runner.config.swagger.should.have.properties({ 166 | test: test, 167 | test2: { test: test2 } 168 | }); 169 | done(); 170 | }); 171 | }); 172 | 173 | it('should create default config when missing', function(done) { 174 | 175 | var config = _.clone(DEFAULT_PROJECT_CONFIG); 176 | config.mapErrorsToJson = true; 177 | config.bagpipes = 'DEFAULTS_TEST'; 178 | SwaggerRunner.create(config, function(err, runner) { 179 | if (err) { return done(err); } 180 | runner.config.swagger.bagpipes.should.have.property('swagger_controllers'); 181 | 182 | var app = require('connect')(); 183 | runner.connectMiddleware().register(app); 184 | 185 | var request = require('supertest'); 186 | 187 | request(app) 188 | .get('/hello') 189 | .set('Accept', 'application/json') 190 | .expect(200) 191 | .expect('Content-Type', /json/) 192 | .end(function(err, res) { 193 | should.not.exist(err); 194 | res.body.should.eql('Hello, stranger!'); 195 | done(); 196 | }); 197 | }); 198 | }); 199 | 200 | it('should create with injected dependencies controllers', function(done) { 201 | 202 | var config = _.clone(DEFAULT_PROJECT_CONFIG); 203 | var fooFactory = { 204 | hello: function(name){ 205 | if(!name) 206 | name = 'stranger'; 207 | return util.format('Hello, %s!', name); 208 | } 209 | } 210 | config.dependencies = {FooFactory: fooFactory} 211 | SwaggerRunner.create(config, function(err, runner) { 212 | if (err) { return done(err); } 213 | runner.config.swagger.bagpipes.should.have.property('swagger_controllers'); 214 | 215 | var app = require('connect')(); 216 | runner.connectMiddleware().register(app); 217 | 218 | var request = require('supertest'); 219 | 220 | request(app) 221 | .get('/hello_injected_dependencies') 222 | .set('Accept', 'application/json') 223 | .expect(200) 224 | .expect('Content-Type', /json/) 225 | .end(function(err, res) { 226 | should.not.exist(err); 227 | res.body.should.eql('Hello, stranger!'); 228 | done(); 229 | }); 230 | }); 231 | }); 232 | 233 | beforeEach( function() { 234 | //force to load fresh of require('config') 235 | var xConfigModulePath = /node_modules[\\\/]config[\\\/]/; 236 | Object.keys(require.cache).forEach(function(path) { 237 | if ( xConfigModulePath.test(path) ) 238 | delete require.cache[path]; 239 | }); 240 | }); 241 | afterEach(function() { 242 | delete process.env.NODE_CONFIG_DIR; 243 | }); 244 | 245 | it('should use pipe interface when _router.controllersInterface is set to `pipe`', function(done) { 246 | var config = _.clone(DEFAULT_PROJECT_CONFIG); 247 | config.configDir = path.resolve(DEFAULT_PROJECT_ROOT, "config_pipe"); 248 | 249 | SwaggerRunner.create(config, function(err, runner) { 250 | if (err) { return done(err); } 251 | runner.config.swagger.bagpipes.should.have.property('swagger_controllers'); 252 | 253 | var app = require('connect')(); 254 | runner.connectMiddleware().register(app); 255 | 256 | var request = require('supertest'); 257 | 258 | request(app) 259 | .get('/hello') 260 | .set('Accept', 'application/json') 261 | .expect(200) 262 | .expect('Content-Type', /json/) 263 | .end(function(err, res) { 264 | should.not.exist(err, err && err.stack); 265 | res.body.should.eql({ message: 'Hello, stranger!' }); 266 | done(); 267 | }); 268 | }); 269 | }); 270 | 271 | it('should use pipe interface when _router.controllersInterface is set to `auto` and operation.length is 2', function(done) { 272 | var config = _.clone(DEFAULT_PROJECT_CONFIG); 273 | config.configDir = path.resolve(DEFAULT_PROJECT_ROOT, "config_auto"); 274 | 275 | SwaggerRunner.create(config, function(err, runner) { 276 | if (err) { return done(err); } 277 | runner.config.swagger.bagpipes.should.have.property('swagger_controllers'); 278 | 279 | var app = require('connect')(); 280 | runner.connectMiddleware().register(app); 281 | 282 | var request = require('supertest'); 283 | 284 | request(app) 285 | .get('/controller_interface_auto_detected_as_pipe') 286 | .set('Accept', 'application/json') 287 | .expect(200) 288 | .expect('Content-Type', /json/) 289 | .expect('x-interface', /pipe/) 290 | .end(function(err, res) { 291 | should.not.exist(err, err && err.stack); 292 | res.body.should.eql({ interface: "pipe" }); 293 | done(); 294 | }); 295 | }); 296 | }); 297 | 298 | it('should use middleware interface when _router.controllersInterface is set to `auto` and operation.length is 3', function(done) { 299 | var config = _.clone(DEFAULT_PROJECT_CONFIG); 300 | config.configDir = path.resolve(DEFAULT_PROJECT_ROOT, "config_auto"); 301 | 302 | SwaggerRunner.create(config, function(err, runner) { 303 | if (err) { return done(err); } 304 | runner.config.swagger.bagpipes.should.have.property('swagger_controllers'); 305 | 306 | var app = require('connect')(); 307 | runner.connectMiddleware().register(app); 308 | 309 | var request = require('supertest'); 310 | 311 | request(app) 312 | .get('/controller_interface_auto_detected_as_middleware') 313 | .set('Accept', 'application/json') 314 | .expect(200) 315 | .expect('Content-Type', /json/) 316 | .expect('x-interface', /middleware/) 317 | .end(function(err, res) { 318 | should.not.exist(err, err && err.stack); 319 | res.body.should.eql({ interface: "middleware" }); 320 | done(); 321 | }); 322 | }); 323 | }); 324 | 325 | it('should use adhere to cascading directgive `x-interface-type` found on path', function(done) { 326 | var config = _.clone(DEFAULT_PROJECT_CONFIG); 327 | config.configDir = path.resolve(DEFAULT_PROJECT_ROOT, "config_auto"); 328 | 329 | SwaggerRunner.create(config, function(err, runner) { 330 | if (err) { return done(err); } 331 | runner.config.swagger.bagpipes.should.have.property('swagger_controllers'); 332 | 333 | var app = require('connect')(); 334 | runner.connectMiddleware().register(app); 335 | 336 | var request = require('supertest'); 337 | 338 | request(app) 339 | .get('/controller_interface_on_path_cascades') 340 | .set('Accept', 'application/json') 341 | .expect(200) 342 | .expect('Content-Type', /json/) 343 | .expect('x-interface', /pipe/) 344 | .end(function(err, res) { 345 | should.not.exist(err, err && err.stack); 346 | res.body.should.eql({ interface: "pipe" }); 347 | done(); 348 | }); 349 | }); 350 | }); 351 | 352 | it('should use adhere to cascading directgive `x-interface-type` found on operation over one found on path', function(done) { 353 | var config = _.clone(DEFAULT_PROJECT_CONFIG); 354 | config.configDir = path.resolve(DEFAULT_PROJECT_ROOT, "config_auto"); 355 | 356 | SwaggerRunner.create(config, function(err, runner) { 357 | if (err) { return done(err); } 358 | runner.config.swagger.bagpipes.should.have.property('swagger_controllers'); 359 | 360 | var app = require('connect')(); 361 | runner.connectMiddleware().register(app); 362 | 363 | var request = require('supertest'); 364 | 365 | request(app) 366 | .get('/controller_interface_on_operation_cascades') 367 | .set('Accept', 'application/json') 368 | .expect(200) 369 | .expect('Content-Type', /json/) 370 | .expect('x-interface', /middleware/) 371 | .end(function(err, res) { 372 | should.not.exist(err, err && err.stack); 373 | res.body.should.eql({ interface: "middleware" }); 374 | done(); 375 | }); 376 | }); 377 | }); 378 | 379 | it('should accept null body from pipe interface', function(done) { 380 | var config = _.clone(DEFAULT_PROJECT_CONFIG); 381 | config.configDir = path.resolve(DEFAULT_PROJECT_ROOT, "config_auto"); 382 | 383 | SwaggerRunner.create(config, function(err, runner) { 384 | if (err) { return done(err); } 385 | runner.config.swagger.bagpipes.should.have.property('swagger_controllers'); 386 | 387 | var app = require('connect')(); 388 | runner.connectMiddleware().register(app); 389 | 390 | var request = require('supertest'); 391 | 392 | request(app) 393 | .get('/controller_interface_pipe_operation_with_no_body') 394 | .set('Accept', 'application/json') 395 | .expect(200) 396 | .end(function(err, res) { 397 | should.not.exist(err, err && err.stack); 398 | done(); 399 | }); 400 | }); 401 | }); 402 | 403 | 404 | it('should fail without callback', function() { 405 | (function() { SwaggerRunner.create(DEFAULT_PROJECT_CONFIG) }).should.throw('callback is required'); 406 | }); 407 | }); 408 | 409 | it('should continue with bad swagger if startWithErrors is true', function(done) { 410 | var config = _.clone(DEFAULT_PROJECT_CONFIG); 411 | config.startWithErrors = true; 412 | config.swagger = SWAGGER_WITH_ERRORS; 413 | SwaggerRunner.create(config, function(err, runner) { 414 | should.not.exist(err); 415 | done(); 416 | }); 417 | }); 418 | 419 | it('should fail with bad swagger if startWithErrors is false', function(done) { 420 | var config = _.clone(DEFAULT_PROJECT_CONFIG); 421 | config.swagger = SWAGGER_WITH_ERRORS; 422 | SwaggerRunner.create(config, function(err, runner) { 423 | should.exist(err); 424 | err.message.should.eql('Swagger validation errors:'); 425 | err.should.have.property('validationErrors'); 426 | err.validationErrors.should.be.an.Array; 427 | err.validationErrors.length.should.eql(2); 428 | done(); 429 | }); 430 | }); 431 | 432 | it('should fail with swagger warnings if startWithWarnings is false', function(done) { 433 | var config = _.clone(DEFAULT_PROJECT_CONFIG); 434 | config.startWithWarnings = false; 435 | config.swagger = SWAGGER_WITH_WARNINGS; 436 | SwaggerRunner.create(config, function(err, runner) { 437 | should.exist(err); 438 | err.message.should.eql('Swagger validation warnings:'); 439 | err.should.have.property('validationWarnings'); 440 | err.validationWarnings.should.be.an.Array; 441 | err.validationWarnings.length.should.eql(1); 442 | done(); 443 | }); 444 | }); 445 | 446 | it('should continue with swagger warnings if startWithWarnings is true', function(done) { 447 | var config = _.clone(DEFAULT_PROJECT_CONFIG); 448 | config.startWithWarnings = true; 449 | config.swagger = SWAGGER_WITH_WARNINGS; 450 | SwaggerRunner.create(config, function(err, runner) { 451 | should.not.exist(err); 452 | done(); 453 | }); 454 | }); 455 | 456 | it('should allow paths using global security', function(done) { 457 | var config = _.clone(DEFAULT_PROJECT_CONFIG); 458 | config.startWithWarnings = true; 459 | config.swagger = SWAGGER_WITH_GLOBAL_SECURITY; 460 | SwaggerRunner.create(config, function(err, runner) { 461 | 462 | var app = require('connect')(); 463 | runner.connectMiddleware().register(app); 464 | 465 | var request = require('supertest'); 466 | 467 | request(app) 468 | .get('/hello_secured?name=Scott') 469 | .set('Accept', 'application/json') 470 | .expect(200) 471 | .expect('Content-Type', /json/) 472 | .end(function(err, res) { 473 | should.not.exist(err); 474 | res.body.should.eql('Hello, Scott!'); 475 | done(); 476 | }); 477 | }); 478 | }); 479 | }); 480 | -------------------------------------------------------------------------------- /test/lib/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'); 4 | var request = require('supertest'); 5 | var path = require('path'); 6 | var _ = require('lodash'); 7 | var yaml = require('js-yaml'); 8 | 9 | module.exports = function() { 10 | 11 | describe('controllers', function() { 12 | 13 | it('should execute', function(done) { 14 | request(this.app) 15 | .get('/hello') 16 | .set('Accept', 'application/json') 17 | .expect(200) 18 | .expect('Content-Type', /json/) 19 | .end(function(err, res) { 20 | should.not.exist(err); 21 | res.body.should.eql('Hello, stranger!'); 22 | done(); 23 | }); 24 | }); 25 | 26 | it('should execute without operationId', function(done) { 27 | request(this.app) 28 | .get('/hello_no_operationid') 29 | .set('Accept', 'application/json') 30 | .expect(200) 31 | .expect('Content-Type', /json/) 32 | .end(function(err, res) { 33 | should.not.exist(err); 34 | res.body.should.eql('Hello, stranger!'); 35 | done(); 36 | }); 37 | }); 38 | 39 | it('should get query parameter', function(done) { 40 | request(this.app) 41 | .get('/hello?name=Scott') 42 | .set('Accept', 'application/json') 43 | .expect(200) 44 | .expect('Content-Type', /json/) 45 | .end(function(err, res) { 46 | should.not.exist(err); 47 | res.body.should.eql('Hello, Scott!'); 48 | done(); 49 | }); 50 | }); 51 | 52 | it('should get formData parameter', function(done) { 53 | request(this.app) 54 | .get('/hello_form') 55 | .send('name=Scott') 56 | .set('Accept', 'application/json') 57 | .expect(200) 58 | .expect('Content-Type', /json/) 59 | .end(function(err, res) { 60 | should.not.exist(err); 61 | res.body.should.eql('Hello, Scott!'); 62 | done(); 63 | }); 64 | }); 65 | 66 | it('should get body parameter', function(done) { 67 | request(this.app) 68 | .get('/hello_body') 69 | .send({name: 'Scott'}) 70 | .set('Accept', 'application/json') 71 | .expect(200) 72 | .expect('Content-Type', /json/) 73 | .end(function(err, res) { 74 | should.not.exist(err); 75 | res.body.should.eql('Hello, Scott!'); 76 | done(); 77 | }); 78 | }); 79 | 80 | it('should get file parameter', function(done) { 81 | request(this.app) 82 | .get('/hello_file') 83 | .field('name', 'Scott') 84 | .attach('example_file', path.resolve(__dirname, '../assets/example_file.txt')) 85 | .set('Accept', 'application/json') 86 | .expect(200) 87 | .expect('Content-Type', /json/) 88 | .end(function(err, res) { 89 | should.not.exist(err); 90 | res.body.should.eql('Hello, Scott! Thanks for the 7 byte file!'); 91 | done(); 92 | }); 93 | }); 94 | 95 | it('should get text body', function(done) { 96 | request(this.app) 97 | .get('/hello_text_body') 98 | .send('Scott') 99 | .type('text') 100 | .set('Accept', 'application/json') 101 | .expect(200) 102 | .expect('Content-Type', /json/) 103 | .end(function(err, res) { 104 | should.not.exist(err); 105 | res.body.should.eql('Hello, Scott!'); 106 | done(); 107 | }); 108 | }); 109 | 110 | it('should get a 404 for unknown path and operation', function(done) { 111 | request(this.app) 112 | .get('/not_there') 113 | .expect(404) 114 | .end(function(err, res) { 115 | should.not.exist(err); 116 | done(); 117 | }); 118 | }); 119 | 120 | it('should get a 405 for known path and unknown operation', function(done) { 121 | request(this.app) 122 | .put('/hello') 123 | .expect(405) 124 | .end(function(err, res) { 125 | should.not.exist(err); 126 | done(); 127 | }); 128 | }); 129 | 130 | it('should not get a 204 for known path and undeclared options operation', function(done) { 131 | request(this.app) 132 | .options('/hello') 133 | .expect(204) 134 | .end(function(err, res) { 135 | should.not.exist(err); 136 | done(); 137 | }); 138 | }); 139 | 140 | it('should get a 500 for missing controller', function(done) { 141 | request(this.app) 142 | .put('/hello_missing_controller') 143 | .expect(405) 144 | .end(function(err, res) { 145 | should.not.exist(err); 146 | done(); 147 | }); 148 | }); 149 | 150 | it('should get a 405 for missing operation function', function(done) { 151 | request(this.app) 152 | .put('/hello_missing_operation') 153 | .expect(405) 154 | .end(function(err, res) { 155 | should.not.exist(err); 156 | done(); 157 | }); 158 | }); 159 | }); 160 | 161 | describe('request validation', function() { 162 | 163 | it('should reject when invalid parameter type', function(done) { 164 | request(this.app) 165 | .put('/expect_integer?name=Scott') 166 | .set('Content-Type', 'application/json') 167 | .expect(400) 168 | .expect('Content-Type', /json/) 169 | .end(function(err, res) { 170 | should.not.exist(err); 171 | res.body.message.should.eql('Validation errors'); 172 | res.body.errors.should.be.an.Array; 173 | res.body.errors[0].should.have.properties({ 174 | code: 'INVALID_REQUEST_PARAMETER', 175 | in: 'query', 176 | message: 'Invalid parameter (name): Expected type integer but found type string', 177 | name: 'name' 178 | }); 179 | done(); 180 | }); 181 | }); 182 | 183 | it('should reject when missing parameter', function(done) { 184 | request(this.app) 185 | .get('/hello_form') 186 | .send('xxx=Scott') 187 | .set('Accept', 'application/json') 188 | .expect(400) 189 | .expect('Content-Type', /json/) 190 | .end(function(err, res) { 191 | should.not.exist(err); 192 | res.body.should.have.property('errors'); 193 | res.body.message.should.eql('Validation errors'); 194 | res.body.errors.should.be.an.Array; 195 | res.body.errors[0].should.have.properties({ 196 | code: 'INVALID_REQUEST_PARAMETER', 197 | in: 'formData', 198 | message: 'Invalid parameter (name): Value is required but was not provided', 199 | name: 'name' 200 | }); 201 | done(); 202 | }); 203 | }); 204 | 205 | it('should reject when invalid content', function(done) { 206 | request(this.app) 207 | .put('/expect_integer') 208 | .set('Content-Type', 'text/plain') 209 | .expect(400) 210 | .expect('Content-Type', /json/) 211 | .end(function(err, res) { 212 | should.not.exist(err); 213 | res.body.message.should.eql('Validation errors'); 214 | res.body.errors.should.be.an.Array; 215 | res.body.errors[0].should.have.properties({ 216 | code: 'INVALID_CONTENT_TYPE', 217 | message: 'Invalid Content-Type (text/plain). These are supported: application/json' 218 | }); 219 | done(); 220 | }); 221 | }); 222 | }); 223 | 224 | describe('security', function() { 225 | 226 | describe('loaded from path', function() { 227 | 228 | it('should deny when swagger-tools handler denies', function(done) { 229 | 230 | request(this.app) 231 | .get('/hello_secured') 232 | .set('Accept', 'application/json') 233 | .expect(403) 234 | .expect('Content-Type', /json/) 235 | .end(function(err, res) { 236 | should.not.exist(err); 237 | 238 | res.body.should.have.properties({ 239 | code: 'server_error', 240 | message: 'no way!' 241 | }); 242 | 243 | done(); 244 | }); 245 | }); 246 | 247 | it('should allow when swagger-tools handler accepts', function(done) { 248 | 249 | request(this.app) 250 | .get('/hello_secured?name=Scott') 251 | .set('Accept', 'application/json') 252 | .expect(200) 253 | .expect('Content-Type', /json/) 254 | .end(function(err, res) { 255 | should.not.exist(err); 256 | res.body.should.eql('Hello, Scott!'); 257 | 258 | done(); 259 | }); 260 | }); 261 | }); 262 | 263 | describe('explicit in config', function() { 264 | 265 | it('should deny when missing handler', function(done) { 266 | 267 | this.runner.securityHandlers = { }; 268 | 269 | request(this.app) 270 | .get('/hello_secured') 271 | .set('Accept', 'application/json') 272 | .expect(403) 273 | .expect('Content-Type', /json/) 274 | .end(function(err, res) { 275 | should.not.exist(err); 276 | 277 | res.body.should.have.properties({ 278 | code: 'server_error', 279 | message: 'Unknown security handler: api_key' 280 | }); 281 | 282 | done(); 283 | }); 284 | }); 285 | 286 | it('should deny when swagger-tools handler denies', function(done) { 287 | 288 | this.runner.securityHandlers = { 289 | api_key: function(req, secDef, key, cb) { 290 | cb(new Error('no way!')); 291 | } 292 | }; 293 | 294 | request(this.app) 295 | .get('/hello_secured') 296 | .set('Accept', 'application/json') 297 | .expect(403) 298 | .expect('Content-Type', /json/) 299 | .end(function(err, res) { 300 | should.not.exist(err); 301 | 302 | res.body.should.have.properties({ 303 | code: 'server_error', 304 | message: 'no way!' 305 | }); 306 | 307 | done(); 308 | }); 309 | }); 310 | 311 | it('should allow when swagger-tools handler accepts', function(done) { 312 | 313 | this.runner.securityHandlers = { 314 | api_key: function(req, secDef, key, cb) { 315 | cb(); 316 | } 317 | }; 318 | 319 | request(this.app) 320 | .get('/hello_secured') 321 | .set('Accept', 'application/json') 322 | .expect(200) 323 | .expect('Content-Type', /json/) 324 | .end(function(err, res) { 325 | should.not.exist(err); 326 | res.body.should.eql('Hello, stranger!'); 327 | 328 | done(); 329 | }); 330 | }); 331 | }); 332 | }); 333 | 334 | describe('non-controller routing', function() { 335 | 336 | describe('/swagger should respond', function() { 337 | 338 | it('with json', function(done) { 339 | request(this.app) 340 | .get('/swagger') 341 | .set('Accept', 'application/json') 342 | .expect(200) 343 | .expect('Content-Type', /json/) 344 | .end(function(err, res) { 345 | should.not.exist(err); 346 | res.body.swagger.should.eql('2.0'); 347 | done(); 348 | }); 349 | }); 350 | 351 | it('with yaml', function(done) { 352 | request(this.app) 353 | .get('/swagger') 354 | .expect(200) 355 | .set('Accept', 'text/yaml') 356 | .expect('Content-Type', /yaml/) 357 | .end(function(err, res) { 358 | should.not.exist(err); 359 | var swagger = yaml.safeLoad(res.text); 360 | swagger.swagger.should.eql('2.0'); 361 | done(); 362 | }); 363 | }); 364 | }); 365 | 366 | describe('/pipe_on_get should respond', function() { 367 | 368 | it('to get operation', function(done) { 369 | request(this.app) 370 | .get('/pipe_on_get') 371 | .set('Accept', 'application/json') 372 | .expect(200) 373 | .expect('Content-Type', /json/) 374 | .end(function(err, res) { 375 | should.not.exist(err); 376 | res.body.swagger.should.eql('2.0'); 377 | done(); 378 | }); 379 | }); 380 | 381 | it('with 405 on put operation', function(done) { 382 | request(this.app) 383 | .put('/pipe_on_get') 384 | .set('Accept', 'application/json') 385 | .expect(405) 386 | .end(function(err, res) { 387 | should.not.exist(err); 388 | done(); 389 | }); 390 | }); 391 | }); 392 | 393 | it('empty path', function(done) { 394 | 395 | request(this.app) 396 | .put('/empty_path') 397 | .set('Accept', 'application/json') 398 | .expect(405) 399 | .end(function(err, res) { 400 | should.not.exist(err); 401 | done(); 402 | }); 403 | }); 404 | 405 | it('no controller specified', function(done) { 406 | 407 | request(this.app) 408 | .get('/no_router_controller') 409 | .set('Accept', 'application/json') 410 | .expect(405) 411 | .end(function(err, res) { 412 | should.not.exist(err); 413 | done(); 414 | }); 415 | }); 416 | }); 417 | 418 | describe('response validation listeners', function() { 419 | 420 | afterEach(function() { 421 | this.runner.removeAllListeners(); 422 | }); 423 | 424 | it('should receive invalid response code errors', function(done) { 425 | 426 | this.runner.once('responseValidationError', function(validationResponse, req, res) { 427 | should.exist(validationResponse); 428 | should.exist(req); 429 | should.exist(res); 430 | validationResponse.errors.should.be.an.Array; 431 | validationResponse.errors.length.should.eql(1); 432 | validationResponse.errors[0].should.have.properties({ 433 | code: 'INVALID_RESPONSE_CODE' 434 | }); 435 | done(); 436 | }); 437 | 438 | request(this.app) 439 | .get('/invalid_response_code') 440 | .set('Accept', 'application/json') 441 | .expect(200) 442 | .expect('Content-Type', /json/) 443 | .end(function(err, res) { 444 | should.not.exist(err); 445 | }); 446 | }); 447 | 448 | it('should receive invalid header errors', function(done) { 449 | 450 | this.runner.once('responseValidationError', function(validationResponse, req, res) { 451 | should.exist(validationResponse); 452 | should.exist(req); 453 | should.exist(res); 454 | validationResponse.errors.should.be.an.Array; 455 | validationResponse.errors.length.should.eql(1); 456 | validationResponse.errors[0].should.containDeep({ 457 | code: 'INVALID_RESPONSE_HEADER', 458 | errors: 459 | [ { code: 'INVALID_TYPE', 460 | message: 'Expected type integer but found type string', 461 | path: [] } ], 462 | message: 'Invalid header (content-type): Expected type integer but found type string', 463 | name: 'content-type', 464 | path: [] 465 | }); 466 | done(); 467 | }); 468 | 469 | request(this.app) 470 | .get('/invalid_header') 471 | .set('Accept', 'application/json') 472 | .expect(200) 473 | .expect('Content-Type', /json/) 474 | .end(function(err, res) { 475 | should.not.exist(err); 476 | }); 477 | }); 478 | 479 | it('should receive schema validation errors', function(done) { 480 | 481 | this.runner.once('responseValidationError', function(validationResponse, req, res) { 482 | should.exist(validationResponse); 483 | should.exist(req); 484 | should.exist(res); 485 | validationResponse.errors.should.be.an.Array; 486 | validationResponse.errors.length.should.eql(1); 487 | validationResponse.errors[0].should.containDeep({ 488 | code: 'INVALID_RESPONSE_BODY', 489 | errors: 490 | [ { code: 'INVALID_TYPE', 491 | message: 'Expected type object but found type string', 492 | path: [] } ], 493 | message: 'Invalid body: Expected type object but found type string', 494 | path: [] 495 | }); 496 | done(); 497 | }); 498 | 499 | request(this.app) 500 | .get('/hello') 501 | .set('Accept', 'application/json') 502 | .expect(200) 503 | .expect('Content-Type', /json/) 504 | .end(function(err, res) { 505 | should.not.exist(err); 506 | }); 507 | }); 508 | 509 | it('should not validate multiple writes', function(done) { 510 | 511 | var responseValidationError; 512 | 513 | this.runner.once('responseValidationError', function(validationResponse, req, res) { 514 | responseValidationError = true; 515 | }); 516 | 517 | request(this.app) 518 | .get('/multiple_writes') 519 | .set('Accept', 'application/json') 520 | .expect(200) 521 | .end(function(err, res) { 522 | should.not.exist(err); 523 | process.nextTick(function() { 524 | should.not.exist(responseValidationError); 525 | done(); 526 | }); 527 | }); 528 | }); 529 | }); 530 | }; 531 | --------------------------------------------------------------------------------