├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── api.swagger.json ├── index.js └── server.js ├── index.js ├── lib ├── extra-parameters.js ├── hippie-swagger.js ├── middleware.js ├── parameters.js ├── response.js ├── settings.js ├── unknown-formats.js └── validate-swagger.js ├── package-lock.json ├── package.json └── test ├── delete.test.js ├── extra-parameters.test.js ├── get.test.js ├── hippie-swagger.test.js ├── middleware.test.js ├── mocha.opts ├── parameters.test.js ├── post.test.js ├── responses.test.js ├── support ├── bootstrap.js ├── data.js ├── server.js └── swagger.js └── swagger-validator.test.js /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x, 14.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm test 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 3.3.1 / 2020-07-17 2 | ================== 3 | 4 | * Upgrade lodash to latest 5 | 6 | 3.3.0 / 2020-07-17 7 | ================== 8 | 9 | * Upgrade hippie to latest; multiple header support 10 | 11 | 3.2.0 / 2018-04-19 12 | ================== 13 | 14 | * Upgrade ajv and qs to latest 15 | * Fix unhandled promises in test suite 16 | 17 | 3.1.1 / 2017-08-11 18 | ================== 19 | 20 | * Fix unknown format errors for swagger files using swagger-specific formats (float, double, date, etc) 21 | 22 | 3.1.0 / 2017-08-04 23 | ================== 24 | 25 | * add validateRequiredParameters option 26 | 27 | 3.0.1 / 2017-08-04 28 | ================== 29 | 30 | * Allow int32 and int64 formats 31 | * Upgrade dependencies 32 | 33 | 3.0.0 / 2017-06-19 34 | ================== 35 | 36 | * Handle integer and boolean types (@netzkind) 37 | 38 | 2.1.0 / 2017-01-28 39 | ================== 40 | 41 | * Update dependencies 42 | 43 | 2.0.1 / 2016-10-29 44 | ================== 45 | 46 | * Path parameter support (@andrew-waters) 47 | 48 | 2.0.0 / 2016-09-30 49 | ================== 50 | 51 | * Make hippie a dependency (@toshi38) 52 | 53 | 1.3.1 / 2016-07-19 54 | ================== 55 | 56 | * Upgraded ajv to latest (@vhenriet) 57 | 58 | 1.3.0 / 2016-03-20 59 | ================== 60 | 61 | * Add support for testing remote APIs 62 | 63 | 1.2.1 / 2016-02-09 64 | ================== 65 | 66 | * properly handle baseUrl 67 | 68 | 1.2.0 / 2015-08-29 69 | ================== 70 | 71 | * formData support 72 | 73 | 1.1.0 / 2015-08-29 74 | ================== 75 | 76 | * Post Body Validation 77 | * Readme documentation 78 | 79 | 1.0.0 / 2015-08-07 80 | ================== 81 | 82 | * Initial implementation 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2017 Cache Hamm 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![hippie-swagger](http://i.imgur.com/icjd94P.png) 2 | 3 | _"The confident hippie"_ 4 | 5 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) 6 | [![Build Status](https://github.com/cachecontrol/hippie-swagger/workflows/Node.js%20CI/badge.svg?branch=master)](https://github.com/cachecontrol/hippie-swagger/workflows/Node.js%20CI/badge.svg?branch=master) 7 | [![npm version](https://badge.fury.io/js/hippie-swagger.svg)](https://badge.fury.io/js/hippie-swagger) 8 | 9 | ## Synopsis 10 | 11 | ```hippie-swagger``` is a tool for testing RESTful APIs. In addition to validating api behavior, it will fail tests when swagger documentation is missing or inaccurate. 12 | 13 | As the test suite runs, any request or response details *not* matching the swagger file will throw an appropriate exception, failing the spec. This ensures the swagger definition accurately describes application behavior, keeping documentation in sync with reality. 14 | 15 | ```hippie-swagger``` uses [hippie](https://github.com/vesln/hippie) under the hood, an excellent API testing tool. 16 | 17 | ## Features 18 | 19 | * All [hippie](https://github.com/vesln/hippie) features included 20 | * All aspects of swagger file validated; parameters, request/response body, paths, etc. 21 | * Checks for extra parameters, paths, headers, etc not mentioned in the swagger file 22 | * Ensures swagger file accurately describes API behavior 23 | * Accurate, human readable assertion messages 24 | 25 | ## Installation 26 | 27 | ``` 28 | npm install hippie-swagger --save-dev 29 | ``` 30 | 31 | ## Basic Usage 32 | 33 | ```js 34 | var hippie = require('hippie-swagger'), 35 | swagger = require('./my-dereferenced-swagger-file'); // see example for how to dereference swagger 36 | 37 | hippie(app, swagger) 38 | .get('/users/{username}') 39 | .pathParams({ 40 | username: 'cachecontrol' 41 | }) 42 | .expectStatus(200) 43 | .expectValue('user.first', 'John') 44 | .expectHeader('cache-control', 'no-cache') 45 | .end(function(err, res, body) { 46 | if (err) throw err; 47 | }); 48 | ``` 49 | 50 | ## Usage 51 | * See [hippie](https://github.com/vesln/hippie) documentation for a description of the base api 52 | * When specifying a url(.get, .post, .patch, .url, etc), use the [swagger path](http://swagger.io/specification/#pathsObject) 53 | * Provide any path variables using [pathParams](#pathparams) 54 | 55 | These aside, use hippie as you normally would; see the [example](example/index.js). 56 | 57 | ## Methods 58 | 59 | ### #constructor (Object app, Object swagger, Object [options]) 60 | 61 | Test an HTTP app (like express) directly 62 | 63 | ```js 64 | hippie(app, swagger, options) 65 | .get('/projects') 66 | .end(fn); 67 | ``` 68 | 69 | ### #constructor (Object swagger, Object [options]) 70 | 71 | Test a remote HTTP app using a fully qualified url 72 | 73 | ```js 74 | hippie(swagger, options) 75 | .get('http://localhost:3000/projects') 76 | .end(fn); 77 | ``` 78 | 79 | ### #pathParams(Object hash) 80 | 81 | Replaces variables contained in the swagger path. 82 | 83 | ```js 84 | hippie(app, swagger) 85 | .get('/projects/{projectId}/tasks/{taskId}') 86 | .pathParams({ 87 | projectId: 123, 88 | taskId: 99 89 | }) 90 | .end(fn); 91 | ``` 92 | 93 | ## Options 94 | 95 | To customize behavior, an ```options``` hash may be passed to the constructor. Typically, ```options``` only need to be specified in situations where the test covers responses to improper requests (e.g. validating the application returns a 422 when a required parameter is not provided). 96 | 97 | ```js 98 | var options = { 99 | validateResponseSchema: true, 100 | validateParameterSchema: true, 101 | errorOnExtraParameters: true, 102 | errorOnExtraHeaderParameters: false 103 | }; 104 | hippie(app, swagger, options) 105 | ``` 106 | 107 | ```validateResponseSchema``` - Validate the server's response against the swagger json-schema definition (default: ```true```) 108 | 109 | ```validateParameterSchema``` - Validate the request parameters against the swagger json-schema definition (default: ```true```) 110 | 111 | ```validateRequiredParameters``` - Validate that required parameters were provided (default: ```true```) 112 | 113 | ```errorOnExtraParameters``` - Throw an error if a parameter is missing from the swagger file (default: ```true```) 114 | 115 | ```errorOnExtraHeaderParameters``` - Throw an error if a request header is missing from the swagger file. By default this is turned off, because it results in every request needing to specify the "Content-Type" and "Accept" headers, which quickly becomes verbose. (default: ```false```) 116 | 117 | 118 | ## Example 119 | See the [example](example/index.js) folder 120 | 121 | ## Validations 122 | 123 | When hippie-swagger detects it is interacting with the app in ways not specified in the swagger file, it will throw an error and fail the test. The idea is to use hippie's core features to write API tests as per usual, and hippie-swagger will only interject if the swagger contract is violated. 124 | 125 | Below are list of some of the validations that hippie-swagger checks for: 126 | 127 | ### Paths 128 | ```js 129 | hippie(app, swagger) 130 | .get('/pathNotMentionedInSwagger') 131 | .end(fn); 132 | // path does not exist in swagger file; throws: 133 | // Swagger spec does not define path: pathNotMentionedInSwagger 134 | ``` 135 | 136 | ### Parameter format 137 | ```js 138 | hippie(app, swagger) 139 | .get('/users/{userId}') 140 | .pathParams({ 141 | userId: 'string-value', 142 | }) 143 | .end(fn); 144 | // userId provided as a string, but swagger specifies it as an integer; throws: 145 | // Invalid format for parameter {userId} 146 | ``` 147 | 148 | ### Required Parameters 149 | ```js 150 | hippie(app, swagger) 151 | .get('/users/{username}') 152 | .end(fn); 153 | // "username" is marked 'required' in swagger file; throws: 154 | // Missing required parameter in path: username 155 | ``` 156 | 157 | ### Extraneous Parameters 158 | ```js 159 | hippie(app, swagger) 160 | .get('/users') 161 | .qs({ page: 2, limit: 30 }) 162 | .end(fn); 163 | // "page" missing from swagger file; throws: 164 | // Error: query parameter not mentioned in swagger spec: "page", available params: limit 165 | ``` 166 | 167 | ### Response format 168 | ```js 169 | hippie(app, swagger) 170 | .get('/users') 171 | .end(fn); 172 | // body failed to validate against swagger file's "response" schema; throws: 173 | // Response from /users failed validation: [failure description] 174 | ``` 175 | 176 | ### Method validation 177 | ```js 178 | hippie(app, swagger) 179 | .post('/users') 180 | .end(fn); 181 | // "post" method not mentioned in swagger file; throws: 182 | // Swagger spec does not define method: "post" in path /users 183 | ``` 184 | 185 | ### Post body format 186 | ```js 187 | hippie(app, swagger) 188 | .post('/users') 189 | .send({"bogus":"post-body"}) 190 | .end(fn); 191 | 192 | // post body fails to validate against swagger file's "body" parameter; throws: 193 | // Invalid format for parameter {body}, received: {"bogus":"post-body"} 194 | ``` 195 | 196 | ### Form Url-Encoded Parameters 197 | ```js 198 | hippie(app, swagger) 199 | .form() 200 | .post('/users') 201 | .send({}) 202 | .end(fn); 203 | 204 | // "username" is {required: true, in: formData} in swagger; throws: 205 | // Missing required parameter in formData: username 206 | ``` 207 | 208 | ### Multipart Forms 209 | ```js 210 | hippie(app, swagger) 211 | .header('Content-Type','multipart/form-data') 212 | .send() 213 | .post('/users/upload') 214 | .end(fn); 215 | 216 | // "fileUpload" is {required: true, in: formData, type: file} in swagger; throws: 217 | // Missing required parameter in formData: fileUpload 218 | ``` 219 | 220 | ## Troubleshooting 221 | 222 | The most common mistake is forgetting to dereference the swagger file: 223 | 224 | ```js 225 | "'Error: cant resolve reference ...' 226 | ``` 227 | 228 | Dereferencing can be accomplished using [swagger-parser](https://github.com/BigstickCarpet/swagger-parser/blob/master/docs/swagger-parser.md#dereferenceapi-options-callback). The [example](example/index.js) gives a demonstration. 229 | 230 | ## Contributing 231 | 232 | To run the `hippie-swagger` tests: 233 | 234 | ``` 235 | npm test 236 | ``` 237 | 238 | ## License 239 | [ISC](./LICENSE) 240 | -------------------------------------------------------------------------------- /example/api.swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "description": "Example swagger file with $refs", 5 | "version": "1.0.0", 6 | "title": "Hippie Swagger" 7 | }, 8 | "paths": { 9 | "/tags/{tagId}": { 10 | "get": { 11 | "produces": [ 12 | "application/json" 13 | ], 14 | "parameters": [ 15 | { 16 | "name": "tagId", 17 | "in": "path", 18 | "description": "tag identifier", 19 | "required": true, 20 | "type": "integer" 21 | } 22 | ], 23 | "responses": { 24 | "200": { 25 | "description": "successful operation", 26 | "schema": { 27 | "type": "array", 28 | "items": { 29 | "$ref": "http://petstore.swagger.io/v2/swagger.json#/definitions/Tag" 30 | } 31 | } 32 | } 33 | } 34 | } 35 | }, 36 | "/tags/invalidResponse": { 37 | "get": { 38 | "description": "", 39 | "produces": [ 40 | "application/json" 41 | ], 42 | "responses": { 43 | "200": { 44 | "description": "successful operation", 45 | "schema": { 46 | "type": "array", 47 | "items": { 48 | "$ref": "http://petstore.swagger.io/v2/swagger.json#/definitions/Tag" 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Example for demonstrating hippie-swagger usage, including dereferencing 5 | * 6 | * Usage: mocha example/index.js 7 | */ 8 | 9 | var SwaggerParser = require('swagger-parser') 10 | var parser = new SwaggerParser() 11 | var hippie = require('..') 12 | var app = require('./server') 13 | var expect = require('chai').expect 14 | var path = require('path') 15 | var dereferencedSwagger 16 | 17 | describe('Example of', function () { 18 | this.timeout(10000) // very large swagger files may take a few seconds to parse 19 | 20 | before(function (done) { 21 | // if using mocha, dereferencing can be performed prior during initialization via the delay flag: 22 | // https://mochajs.org/#delayed-root-suite 23 | parser.dereference(path.join(__dirname, './api.swagger.json'), function (err, api) { 24 | if (err) return done(err) 25 | dereferencedSwagger = api 26 | done() 27 | }) 28 | }) 29 | 30 | describe('correct usage', function () { 31 | it('works when the request matches the swagger file', function (done) { 32 | hippie(app, dereferencedSwagger) 33 | .get('/tags/{tagId}') 34 | .pathParams({ 35 | tagId: 1 36 | }) 37 | .expectStatus(200) 38 | .expectValue('[0].id', 1) 39 | .expectValue('[0].name', 'user') 40 | .expectValue('[1].id', 2) 41 | .expectValue('[1].name', 'store') 42 | .end(done) 43 | }) 44 | }) 45 | 46 | describe('things hippie-swagger will punish you for:', function () { 47 | it('validates paths', function (done) { 48 | try { 49 | hippie(app, dereferencedSwagger) 50 | .get('/undocumented-endpoint') 51 | .end(done) 52 | } catch (ex) { 53 | expect(ex.message).to.equal('Swagger spec does not define path: /undocumented-endpoint') 54 | done() 55 | } 56 | }) 57 | 58 | it('validates parameters', function (done) { 59 | try { 60 | hippie(app, dereferencedSwagger) 61 | .get('/tags/{tagId}') 62 | .qs({ username: 'not-in-swagger' }) 63 | .end(done) 64 | } catch (ex) { 65 | expect(ex.message).to.equal('query parameter not mentioned in swagger spec: "username", available params: tagId') 66 | done() 67 | } 68 | }) 69 | 70 | it('validates responses', function (done) { 71 | hippie(app, dereferencedSwagger) 72 | .get('/tags/invalidResponse') 73 | .end(function (err) { 74 | expect(err.message).to.match(/Response failed validation/) 75 | done() 76 | }) 77 | }) 78 | 79 | it('validates many other things! See README for the complete list of validations.') 80 | }) 81 | }) 82 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var app = require('express')() 3 | 4 | /* 5 | * pet by id endpoint w/valid responses 6 | */ 7 | 8 | app.get('/tags/invalidResponse', function (req, res) { 9 | res.send({ invalid: 'tag' }) 10 | }) 11 | 12 | app.get('/tags/:tagId', function (req, res) { 13 | res.send([ 14 | { id: 1, name: 'user' }, 15 | { id: 2, name: 'store' } 16 | ]) 17 | }) 18 | 19 | /** 20 | * Primary export. 21 | */ 22 | 23 | module.exports = app 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/hippie-swagger.js') 2 | -------------------------------------------------------------------------------- /lib/extra-parameters.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var settings = require('./settings') 4 | var throwParamNotFound 5 | 6 | /** 7 | * Body/FormData/MultipartFormData handling 8 | * 9 | * "body", the parameter name is superfluous since there can only be one. 10 | * "body" is mutually exclusive with formData/multipartFormData 11 | * 12 | * @param {Object} requestBody 13 | * @param {Object} pathSpecParamHash - subset of swagger file for this path 14 | * @return {Boolean} found - true when variable mentioned in file, false if unmentioned 15 | */ 16 | function handleBody (requestBody, specBody) { 17 | if (requestBody && specBody === undefined) { 18 | throw new Error('Request "body" present, but Swagger spec has no body parameter mentioned') 19 | } 20 | } 21 | 22 | /** 23 | * Asserts that request form data variables are in Swagger. Throws if not found. 24 | * @param {Object} requestFormData - request form data(query string format) as a hash 25 | * @param {Object} requestParams - hash of request parameters 26 | */ 27 | function handleFormData (requestFormData, specFormData) { 28 | for (var qsKey in requestFormData) { 29 | var found = false 30 | specFormData.forEach(function (formSpecParameter) { 31 | if (formSpecParameter.name === qsKey && formSpecParameter.type !== 'file') { 32 | found = true 33 | } 34 | }) 35 | if (!found) { 36 | throwParamNotFound(qsKey, 'formData') 37 | } 38 | } 39 | } 40 | 41 | /** 42 | * Asserts that multipart file uploads are in swagger. Throws if not found. 43 | * @param {String} multipartFormData - request body 44 | * @param {Object} specParamsByType - swagger parameters hashed by location 45 | */ 46 | function handleMultipartFormData (multipartFormData, specFormData) { 47 | var found = false 48 | specFormData.forEach(function (formSpecParameter) { 49 | if (formSpecParameter.type === 'file') { 50 | var regex = new RegExp('name=\\\\"' + formSpecParameter.name + '\\\\"') 51 | if (!multipartFormData || multipartFormData.match(regex)) { 52 | found = true 53 | } 54 | } 55 | }) 56 | if (!found) { 57 | throwParamNotFound(multipartFormData, 'formData (expected type "file")') 58 | } 59 | } 60 | 61 | /** 62 | * Traverses the parameters in the request and asserts that all were mentioned 63 | * in the swagger file. 64 | * 65 | * Pass the {errorOnExtraParameters:false) option to disable 66 | * 67 | * @param {Object} pathSpec - swagger path definition 68 | * @param {Object} requestParams - hash of request parameters 69 | */ 70 | module.exports = function validateNoExtraParams (pathSpec, requestParams) { 71 | if (settings.get('errorOnExtraParameters') === false) return 72 | 73 | throwParamNotFound = function (param, location) { 74 | throw new Error(location + ' parameter not mentioned in swagger spec: "' + param + '", available params: ' + 75 | pathSpec.parameters.map(function (p) { 76 | return p.name 77 | }).join(',') 78 | ) 79 | } 80 | 81 | var specParamsByName = {} 82 | var specParamsByType = {} 83 | pathSpec.parameters.forEach(function (param) { 84 | if (!specParamsByType[param.in]) specParamsByType[param.in] = [] 85 | specParamsByType[param.in].push(param) 86 | specParamsByName[param.name] = param 87 | }, {}) 88 | 89 | for (var type in requestParams) { 90 | if (type === 'header' && settings.get('errorOnExtraHeaderParameters') === false) continue 91 | switch (type) { 92 | case 'body': 93 | handleBody(requestParams.body, specParamsByType.body) 94 | break 95 | case 'formData': 96 | handleFormData(requestParams.formData, specParamsByType.formData) 97 | break 98 | case 'multipartFormData': 99 | handleMultipartFormData(requestParams.multipartFormData, specParamsByType.formData) 100 | break 101 | default: 102 | for (var param in requestParams[type]) { 103 | if (specParamsByName[param] === undefined) { 104 | throwParamNotFound(param, type) 105 | } 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/hippie-swagger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var hippie = require('hippie') 4 | var middleware = require('./middleware') 5 | var settings = require('./settings') 6 | var validateSwagger = require('./validate-swagger').validateSwagger 7 | var isSwagger = require('./validate-swagger').isSwagger 8 | 9 | /** 10 | * Stores parameters in the url path for later substitution 11 | * @param {Object} parameters - { pathVariableName: value } 12 | * @return {Object } - current hippie instance 13 | */ 14 | hippie.prototype.pathParams = function (parameters) { 15 | var self = this 16 | this.swaggerParams = this.swaggerParams || {} 17 | this.swaggerParams.path = this.swaggerParams.path || {} 18 | 19 | Object.keys(parameters).forEach(function (key) { 20 | self.swaggerParams.path[key] = parameters[key] 21 | }) 22 | return this 23 | } 24 | 25 | module.exports = function (app, swaggerDef, overrides) { 26 | if (isSwagger(app)) { 27 | overrides = swaggerDef 28 | swaggerDef = app 29 | app = null 30 | } 31 | settings.store(overrides) 32 | 33 | validateSwagger(swaggerDef) 34 | var api = hippie(app) 35 | api.json() 36 | api.use(middleware.bind(api, swaggerDef)) 37 | 38 | return api 39 | } 40 | -------------------------------------------------------------------------------- /lib/middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var parameterize = require('./parameters.js') 4 | var URL = require('url').URL 5 | var response = require('./response.js') 6 | require('string.prototype.startswith') 7 | 8 | function middleware (swaggerDef, options, next) { 9 | var parsedUrl = new URL(this._url, 'http://example.com/') 10 | var path = decodeURI(parsedUrl.pathname) 11 | 12 | // handle basePath if set and different from / 13 | if (swaggerDef.basePath && swaggerDef.basePath !== '/') { 14 | if (!path.startsWith(swaggerDef.basePath)) { 15 | throw new Error('Swagger spec does not define path: ' + this._url) 16 | } 17 | 18 | // remove basePath before searching path into swagger paths 19 | path = path.replace(swaggerDef.basePath, '') 20 | path = path === '' ? '/' : path 21 | } 22 | 23 | if (!swaggerDef.paths[path]) { 24 | throw new Error('Swagger spec does not define path: ' + this._url) 25 | } 26 | if (!options.method) { 27 | throw new Error('No request method provided(get, post, delete, etc)') 28 | } 29 | var method = options.method.toLowerCase() 30 | var pathSpec = swaggerDef.paths[path][method] 31 | if (!pathSpec) { 32 | throw new Error('Swagger spec does not define method: "' + method + '" in path ' + path + '. Available methods: ' + Object.keys(swaggerDef.paths[path]).join(',')) 33 | } 34 | 35 | pathSpec.parameters = pathSpec.parameters ? pathSpec.parameters.slice() : [] 36 | var pathParameters = swaggerDef.paths[path].parameters || [] 37 | 38 | // iterate over the path parameters (the defaults), and append any that were not 39 | // overridden within the method definition 40 | pathParameters.forEach(function (pathLevelParam) { 41 | var addDefaultParam = true 42 | for (var i = 0; i < pathSpec.parameters.length; i++) { 43 | var methodLevelParam = pathSpec.parameters[i] 44 | if (methodLevelParam.name === pathLevelParam.name) { 45 | addDefaultParam = false 46 | break 47 | } 48 | } 49 | if (addDefaultParam) pathSpec.parameters.push(pathLevelParam) 50 | }) 51 | 52 | // add expect callback to validate response against json-schema 53 | this.expect(response.bind(this, pathSpec)) 54 | 55 | // replace {variables} in the url with whatever was specified by params() 56 | options = parameterize(pathSpec, options, this.swaggerParams) 57 | next(options) 58 | } 59 | 60 | module.exports = middleware 61 | -------------------------------------------------------------------------------- /lib/parameters.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var unknownFormats = require('./unknown-formats') 4 | var ajv = require('ajv')({ coerceTypes: ['number', 'boolean'], unknownFormats: unknownFormats }) 5 | var settings = require('./settings') 6 | var qs = require('qs') 7 | var validateNoExtraParams = require('./extra-parameters') 8 | var objectAssign = require('object-assign') 9 | 10 | /** 11 | * Traverses hippie options, building out a hash of swagger parameters 12 | * organized by parameter location(body, header, query, etc) 13 | * 14 | * @param {Hash} params - parameters specified 15 | * @param {Hash} opts - hippie options 16 | * @return {Hash} parameters by location 17 | */ 18 | function paramHashFromHippie (pathParams, opts) { 19 | var params = objectAssign({}, pathParams) 20 | if (opts.headers) { 21 | params.header = objectAssign({}, opts.headers) 22 | } 23 | 24 | // manage body 25 | if (opts.headers && opts.headers['Content-Type']) { 26 | if (opts.headers['Content-Type'].match(/application\/x-www-form-urlencoded/)) { 27 | params.formData = qs.parse(opts.body) 28 | } else if (opts.headers['Content-Type'].match(/multipart\/form-data/)) { 29 | params.multipartFormData = opts.body 30 | } 31 | } 32 | if (opts.body && !params.formData && !params.multipartFormData) { 33 | params.body = opts.body 34 | } 35 | 36 | if (opts.qs) { 37 | params.query = objectAssign({}, opts.qs) 38 | } 39 | return params 40 | } 41 | function throwMissingRequiredParameter (param) { 42 | if (settings.get('validateRequiredParameters') === false) return 43 | throw new Error('Missing required parameter in ' + param.in + ': ' + param.name) 44 | } 45 | 46 | /** 47 | * Hippie middleware that validates parameters prior to request 48 | * @param {Object} pathSpec - swagger path definition 49 | * @param {Object} options - hippie-provided hash describing the request about to be made 50 | * @param {Object} parameters - hash of path parameters, added via pathParams() 51 | */ 52 | module.exports = function parameters (pathSpec, options, pathParams) { 53 | /** 54 | * Replaces variables mentioned in the request uri with what was specified by pathParams() 55 | * @param {Any} param - parameter to inject into the uri 56 | */ 57 | function replacePathParams (param, requestParams) { 58 | if (param.in === 'path') { 59 | var regex = new RegExp('{' + param.name + '}', 'g') 60 | options.url = options.url.replace(regex, requestParams[param.in][param.name]) 61 | } 62 | } 63 | 64 | /** 65 | * Validates that any parameter marked as 'required' in the swagger spec was present 66 | * in the request 67 | * @param {Any} param - parameter to assert 68 | */ 69 | function validateRequiredParams (param, requestParams) { 70 | if (param.required) { 71 | if (param.in === 'body') { // There can only be one body parameter, and naming is irrelevant 72 | if (!requestParams[param.in]) throwMissingRequiredParameter(param) 73 | } else if (requestParams[param.in] === undefined || 74 | requestParams[param.in][param.name] === undefined) { 75 | throwMissingRequiredParameter(param) 76 | } 77 | } 78 | } 79 | 80 | /** 81 | * Validates each parameter against the swagger definition 82 | * @param {Any} param - request parameter to be validated 83 | */ 84 | function validateParamSchema (param, requestParams) { 85 | if (settings.get('validateParameterSchema') === false) return 86 | if (!requestParams[param.in]) return 87 | 88 | var paramSchema = objectAssign({}, param) 89 | var paramValue 90 | 91 | if (paramSchema.in === 'body') { 92 | paramSchema = paramSchema.schema // "body" params are nested under schema 93 | paramValue = JSON.parse(requestParams[param.in]) 94 | } else { 95 | paramValue = requestParams[param.in][param.name] 96 | if (!paramValue) return 97 | 98 | // delete all the non-json-schema(swagger specific) properties, which will cause an invalid schema 99 | delete paramSchema.in 100 | delete paramSchema.description 101 | delete paramSchema.required 102 | delete paramSchema.name 103 | } 104 | 105 | if (!ajv.validate(paramSchema, paramValue)) { 106 | throw new Error('Invalid format for parameter {' + param.name + '}, received: ' + requestParams[param.in][param.name] + '. errors:' + ajv.errorsText()) 107 | } 108 | } 109 | var requestParams = paramHashFromHippie(pathParams, options) 110 | pathSpec.parameters = pathSpec.parameters || [] 111 | 112 | validateNoExtraParams(pathSpec, requestParams) 113 | 114 | pathSpec.parameters.forEach(function (param) { 115 | validateRequiredParams(param, requestParams) 116 | validateParamSchema(param, requestParams) 117 | replacePathParams(param, requestParams) 118 | }) 119 | return options 120 | } 121 | -------------------------------------------------------------------------------- /lib/response.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var unknownFormats = require('./unknown-formats') 4 | var ajv = require('ajv')({ unknownFormats: unknownFormats }) 5 | var settings = require('./settings') 6 | 7 | /** 8 | * Performs json-schema validation using the schema defined in the swagger response 9 | * against the response body returned from the server. 10 | * 11 | * Can be disabled by passing the {validateResponseSchema: false} option 12 | * 13 | * @param {Object} res - response object returned 14 | * @param {Object} body - response body 15 | * @param {Object} schema - json schema 16 | */ 17 | function validateBody (res, body, schema) { 18 | if (!body || settings.get('validateResponseSchema') === false) return 19 | 20 | if (schema) { 21 | if (!ajv.validate(schema, body)) { 22 | throw new Error('Response from ' + res.request.path + ' failed validation: [' + ajv.errorsText() + ']\n Response:' + JSON.stringify(body)) 23 | } 24 | } else if (body.length) { 25 | // From swagger specifications: "If this field(statusCode.schema) does not exist, 26 | // it means no content is returned as part of the response" 27 | throw new Error('Received non-empty response from ' + res.request.path + '. Expected empty response body because no "schema" property was specified in swagger path.') 28 | } 29 | } 30 | 31 | /** 32 | * Throws an exception if the status code received from the 33 | * server was not specified in the swagger response 34 | * @param {Number} statusCode - status code received from response 35 | */ 36 | function validateStatusCode (res, statusCode) { 37 | if (statusCode === undefined) { 38 | throw new Error('No mention of statusCode: ' + res.statusCode + ' in ' + res.request.path) 39 | } 40 | } 41 | 42 | module.exports = function response (pathSpec, res, body, next) { 43 | var statusCode = pathSpec.responses[res.statusCode] 44 | try { 45 | validateStatusCode(res, statusCode) 46 | validateBody(res, body, statusCode.schema) 47 | } catch (err) { 48 | return next(err) 49 | } 50 | next() 51 | } 52 | -------------------------------------------------------------------------------- /lib/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var objectAssign = require('object-assign') 4 | 5 | var settings = {} 6 | var defaults = { 7 | validateResponseSchema: true, 8 | validateParameterSchema: true, 9 | validateRequiredParameters: true, 10 | errorOnExtraParameters: true, 11 | errorOnExtraHeaderParameters: false 12 | } 13 | 14 | /** 15 | * Retrieves the default hippie-swagger settings, honoring overrides 16 | * @param {Object} overrides - user specified overrides 17 | * @return {Object} settings w/applied defaults 18 | */ 19 | function settingsWithDefaults (overrides) { 20 | var opts = overrides || {} 21 | return objectAssign({}, defaults, opts) 22 | } 23 | 24 | module.exports = { 25 | get: function (key) { 26 | return settings[key] 27 | }, 28 | store: function (data) { 29 | settings = settingsWithDefaults(data) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/unknown-formats.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Provides a list of swagger formats that are not part of the json-schema specification 3 | * Allows json-schema parses like ajv to explicitly ignore formats 4 | */ 5 | module.exports = [ 6 | 'int32', 7 | 'int64', 8 | 'float', 9 | 'double', 10 | 'byte', 11 | 'binary', 12 | 'date', 13 | 'password' 14 | ] 15 | -------------------------------------------------------------------------------- /lib/validate-swagger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * Determines if object is a swagger definition, raising errors if invalid 5 | * @param {Object} swaggerDef 6 | */ 7 | function validateSwagger (swaggerDef) { 8 | if (!swaggerDef) { 9 | throw new Error('Swagger schema required') 10 | } 11 | if (!isSwagger(swaggerDef)) { 12 | throw new Error('Swagger schema invalid') 13 | } 14 | } 15 | 16 | /** 17 | * Poor man's swagger validator. Does some basic validation on the existence of the schema itself and 18 | * root level keys. Provides basic validations, meant to catch wrong params types. 19 | * Not meant as a substitute for swagger-parser (https://github.com/BigstickCarpet/swagger-parser) 20 | * 21 | * @param {Object} swaggerDef - dereferrenced swagger object 22 | */ 23 | function isSwagger (obj) { 24 | var requiredKeys = ['swagger', 'info', 'paths'] 25 | return requiredKeys.every(function (key) { 26 | return Object.prototype.hasOwnProperty.call(obj, key) 27 | }) 28 | } 29 | 30 | module.exports = { 31 | validateSwagger: validateSwagger, 32 | isSwagger: isSwagger 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hippie-swagger", 3 | "version": "3.3.2", 4 | "description": "Hippie wrapper that provides end to end API testing with swagger validation", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./node_modules/mocha/bin/mocha --exit && npm run lint", 8 | "lint": "node ./node_modules/standard/bin/cmd.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/CacheControl/hippie-swagger.git" 13 | }, 14 | "keywords": [ 15 | "hippie", 16 | "swagger" 17 | ], 18 | "author": "Cache Hamm", 19 | "license": "ISC", 20 | "bugs": { 21 | "url": "https://github.com/CacheControl/hippie-swagger/issues" 22 | }, 23 | "standard": { 24 | "globals": [ 25 | "beforeEach", 26 | "describe", 27 | "it", 28 | "before", 29 | "expect", 30 | "data", 31 | "cloneSwagger", 32 | "app", 33 | "hippie", 34 | "swaggerSchema", 35 | "run" 36 | ] 37 | }, 38 | "homepage": "https://github.com/CacheControl/hippie-swagger#readme", 39 | "dependencies": { 40 | "ajv": "^6.12.3", 41 | "hippie": "^0.6.0", 42 | "object-assign": "^4.1.1", 43 | "qs": "^6.9.4", 44 | "string.prototype.startswith": "^0.2.0" 45 | }, 46 | "devDependencies": { 47 | "chai": "^3.2.0", 48 | "chai-as-promised": "^6.0.0", 49 | "dirty-chai": "^2.0.1", 50 | "express": "^4.17.1", 51 | "lodash": "^4.17.15", 52 | "mocha": "^4.1.0", 53 | "nock": "^13.0.2", 54 | "standard": "^14.3.4", 55 | "swagger-parser": "^3.3.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/delete.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | describe('DELETE requests', function () { 4 | it('works when valid', function (done) { 5 | hippie(app, swaggerSchema) 6 | .del('/foos/{fooId}') 7 | .pathParams({ fooId: data.firstFoo.id }) 8 | .end(function (err, res) { 9 | expect(err).to.be.undefined() 10 | expect(res.body).to.eql('') 11 | done() 12 | }) 13 | }) 14 | 15 | describe('when path is missing a response schema', function () { 16 | it('errors if a response is actually returned', function (done) { 17 | hippie(app, swaggerSchema) 18 | .del('/invalid-foos') 19 | .end(function (err, res) { 20 | expect(err.message).to.match(/Received non-empty response from \/invalid-foos/) 21 | done() 22 | }) 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /test/extra-parameters.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | describe('extra parameters', function () { 4 | it('errors on path parameters not mentioned in the swagger spec', function () { 5 | expect(hippie(app, swaggerSchema) 6 | .get('/foos') 7 | .pathParams({ unmentionedParam: 50 }) 8 | .end() 9 | ).to.be.rejectedWith(/path parameter not mentioned in swagger spec: "unmentionedParam"/) 10 | }) 11 | 12 | describe('formData', function () { 13 | it('errors on formData parameters not mentioned in the swagger spec', function () { 14 | expect(hippie(app, swaggerSchema) 15 | .get('/foos') 16 | .form() 17 | .send({ unmentionedParam1: 'nothing', unmentionedParam2: 'nothing' }) 18 | .end() 19 | ).to.be.rejectedWith(/formData parameter not mentioned in swagger spec: "unmentionedParam1"/) 20 | }) 21 | 22 | it('errors on formData file parameters not mentioned in the swagger spec', function () { 23 | var file = 'Content-Disposition: form-data; name="uploadedFile"' 24 | 25 | expect(hippie(app, swaggerSchema) 26 | .header('Content-Type', 'multipart/form-data') 27 | .send(file) 28 | .get('/foos') 29 | .end() 30 | ).to.be.rejectedWith(/formData \(expected type "file"\) parameter not mentioned in swagger spec: ""Content-Disposition/) 31 | }) 32 | }) 33 | 34 | it('errors on body parameters not mentioned in the swagger spec', function () { 35 | expect(hippie(app, swaggerSchema) 36 | .get('/foos') 37 | .send({ unmentionedParam1: 'nothing' }) 38 | .end() 39 | ).to.be.rejectedWith(/Request "body" present, but Swagger spec has no body parameter mentioned/) 40 | }) 41 | 42 | describe('header parameters', function () { 43 | it('does not error if the header was not mentioned in swagger', function () { 44 | expect(function () { 45 | hippie(app, swaggerSchema) 46 | .header('X-New-Header', 1) 47 | .get('/foos') 48 | .end() 49 | }).to.not.throw() 50 | }) 51 | 52 | it('errors if the header was not mentioned in swagger, and errorOnExtraHeaderParameters is true', function () { 53 | expect(hippie(app, swaggerSchema, { errorOnExtraHeaderParameters: true }) 54 | .header('X-New-Header', 1) 55 | .get('/foos') 56 | .end() 57 | ).to.be.rejectedWith(/header parameter not mentioned in swagger spec:/) 58 | }) 59 | }) 60 | 61 | describe('settings', function () { 62 | it('when validateParameterSchema is off, extra parameters are allowed', function () { 63 | expect(function () { 64 | hippie(app, swaggerSchema, { errorOnExtraParameters: false }) 65 | .get('/foos/{fooId}') 66 | .pathParams({ fooId: data.firstFoo.id, asdf: 50 }) 67 | .header('X-Foo', 12) 68 | .end() 69 | }).to.not.throw() 70 | }) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /test/get.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | describe('GET requests', function () { 4 | it('works when valid', function (done) { 5 | hippie(app, swaggerSchema) 6 | .get('/foos/{fooId}') 7 | .pathParams({ fooId: data.firstFoo.id }) 8 | .end(function (err, res) { 9 | expect(err).to.be.undefined() 10 | done() 11 | }) 12 | }) 13 | 14 | it('errors when the response is invalid', function (done) { 15 | hippie(app, swaggerSchema) 16 | .get('/invalid-foos') 17 | .end(function (err) { 18 | expect(err.message).to.match(/Response from \/invalid-foos failed validation/) 19 | done() 20 | }) 21 | }) 22 | 23 | it('does not error if the validateResponseSchema is off', function (done) { 24 | hippie(app, swaggerSchema, { validateResponseSchema: false }) 25 | .get('/invalid-foos') 26 | .end(function (err) { 27 | expect(err).be.undefined() 28 | done() 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /test/hippie-swagger.test.js: -------------------------------------------------------------------------------- 1 | var nock = require('nock') 2 | 3 | describe('hippie-swagger', function () { 4 | var options = { validateResponseSchema: false } 5 | 6 | it('works with 3 arguments', function (done) { 7 | hippie(app, swaggerSchema, options) 8 | .get('/foos/{fooId}') 9 | .pathParams({ fooId: data.firstFoo.id }) 10 | .end(function (err, res) { 11 | expect(err).to.be.undefined() 12 | done() 13 | }) 14 | }) 15 | 16 | describe('2 arguments', function () { 17 | it('app and swagger', function (done) { 18 | hippie(app, swaggerSchema) 19 | .get('/foos/{fooId}') 20 | .pathParams({ fooId: data.firstFoo.id }) 21 | .end(function (err, res) { 22 | expect(err).to.be.undefined() 23 | done() 24 | }) 25 | }) 26 | 27 | it('swagger and options', function (done) { 28 | var HOST = 'http://localhost:3000' 29 | var PATH = '/foos/' + data.firstFoo.id 30 | nock(HOST) 31 | .get(PATH) 32 | .reply(200) 33 | 34 | hippie(swaggerSchema, options) 35 | .get(HOST + '/foos/{fooId}') 36 | .pathParams({ fooId: data.firstFoo.id }) 37 | .end(function (err, res) { 38 | expect(err).to.be.undefined() 39 | done() 40 | }) 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /test/middleware.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var middleware = require('../lib/middleware') 4 | var next = function () {} 5 | var options = { method: 'get', url: '/foos' } 6 | 7 | function hippieStub (options) { 8 | options = options || { qs: { limit: 0, offset: 0 } } 9 | return new function () { 10 | this._url = options.url || '/foos' 11 | this.expect = function () {} 12 | }() 13 | } 14 | var ctx = hippieStub() 15 | 16 | describe('middleware', function () { 17 | it('calls next with options', function () { 18 | middleware.call(ctx, swaggerSchema, options, function (opts) { 19 | expect(opts).to.eql(options) 20 | }) 21 | }) 22 | 23 | it('accepts mixed-case request methods', function () { 24 | middleware.call(ctx, swaggerSchema, { method: 'GET', url: '/foos' }, function (opts) { 25 | expect(opts).to.exist() 26 | }) 27 | }) 28 | 29 | it('url decodes the path', function () { 30 | var pathCtx = hippieStub({ url: '/foos/%7BfooId%7D' }) 31 | pathCtx.swaggerParams = { path: { fooId: '6e9b25c2-7c22-44c5-8890-15613aa1fb6a' } } 32 | middleware.call(pathCtx, swaggerSchema, { method: 'GET', url: '/foos/{fooId}' }, function (opts) { 33 | expect(opts).to.exist() 34 | }) 35 | }) 36 | 37 | it('merges operation and path parameters', function () { 38 | var pathCtx = hippieStub({ url: '/foos/%7BfooId%7D' }) 39 | pathCtx.swaggerParams = { path: { fooId: '6e9b25c2-7c22-44c5-8890-15613aa1fb6a' } } 40 | middleware.call(pathCtx, swaggerSchema, { method: 'DELETE', url: '/foos/{fooId}' }, function (opts) { 41 | expect(opts.url).to.eql('/foos/6e9b25c2-7c22-44c5-8890-15613aa1fb6a') 42 | }) 43 | }) 44 | 45 | describe('throws an error', function () { 46 | it('when the path is not defined in the swagger schema', function () { 47 | var pathCtx = hippieStub({ url: 'pathNotMentionedInSwagger' }) 48 | expect(middleware.bind(pathCtx, swaggerSchema, options, next)) 49 | .to.throw(/Swagger spec does not define path: pathNotMentionedInSwagger/) 50 | }) 51 | 52 | it('when the options is missing a method', function () { 53 | expect(middleware.bind(ctx, swaggerSchema, {}, next)) 54 | .to.throw(/No request method provided/) 55 | }) 56 | 57 | it('when the request method is not defined in the swagger schema', function () { 58 | expect(middleware.bind(ctx, swaggerSchema, { method: 'put' }, next)) 59 | .to.throw(/Swagger spec does not define method: "put"/) 60 | }) 61 | }) 62 | 63 | describe('basePath support', function () { 64 | it('basePath is set', function () { 65 | swaggerSchema.basePath = '/base' 66 | ctx._url = '/base/foos' 67 | middleware.call(ctx, swaggerSchema, options, function (opts) { 68 | expect(opts).to.eql(options) 69 | }) 70 | }) 71 | 72 | it('basePath is set and subpath is /', function () { 73 | var originalSwaggerSchema = swaggerSchema.basePath 74 | var basePathOptions = { method: 'GET', url: '/' } 75 | swaggerSchema.basePath = '/base' 76 | ctx._url = '/base' 77 | 78 | middleware.call(ctx, swaggerSchema, basePathOptions, function (opts) { 79 | expect(opts).to.eql(basePathOptions) 80 | }) 81 | swaggerSchema.basePath = originalSwaggerSchema 82 | }) 83 | 84 | it('basePath is set but equal to /', function () { 85 | swaggerSchema.basePath = '/' 86 | ctx._url = '/foos' 87 | middleware.call(ctx, swaggerSchema, options, function (opts) { 88 | expect(opts).to.eql(options) 89 | }) 90 | }) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ./test/support/bootstrap.js 2 | --check-leaks 3 | --delay 4 | --globals expect,data,swaggerSchema,hippie,app -------------------------------------------------------------------------------- /test/parameters.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var _ = require('lodash') 4 | 5 | describe('parameters', function () { 6 | it('ignores optional parameters that are missing', function (done) { 7 | hippie(app, swaggerSchema) 8 | .get('/foos') 9 | .end(done) 10 | }) 11 | 12 | it('replaces path parameters with provided variables', function (done) { 13 | hippie(app, swaggerSchema) 14 | .get('/foos/{fooId}') 15 | .pathParams({ fooId: data.firstFoo.id }) 16 | .end(function (err, res) { 17 | expect(err).to.be.undefined() 18 | expect(res.req.path).to.equal('/foos/' + data.firstFoo.id) 19 | done() 20 | }) 21 | }) 22 | 23 | it('replaces query string variables', function (done) { 24 | var limit = 10 25 | var offset = 2 26 | 27 | hippie(app, swaggerSchema) 28 | .get('/foos') 29 | .qs({ limit: limit, offset: offset }) 30 | .end(function (err, res) { 31 | expect(err).to.be.undefined() 32 | expect(res.req.path).to.equal('/foos?limit=' + limit + '&offset=' + offset) 33 | done() 34 | }) 35 | }) 36 | 37 | describe('header variables', function () { 38 | it('errors if the header is required', function () { 39 | var headerSchema = cloneSwagger(swaggerSchema) 40 | 41 | // set X-Total-Count to be required for this test 42 | headerSchema.paths['/foos'].get.parameters.filter(function (param) { 43 | return param.name === 'X-Total-Count' 44 | })[0].required = true 45 | 46 | expect(hippie(app, headerSchema) 47 | .get('/foos') 48 | .end() 49 | ).to.be.rejectedWith(/Missing required parameter in header: X-Total-Count/) 50 | }) 51 | 52 | it('allows multiple headers to be given', function (done) { 53 | hippie(app, swaggerSchema) 54 | .get('/foos') 55 | .headers({ 'X-Total-Count': 1, 'New-Header': 0 }) 56 | .end(function (err, res) { 57 | expect(err).to.be.undefined() 58 | expect(res.request.headers['X-Total-Count']).to.exist() 59 | expect(res.request.headers['New-Header']).to.exist() 60 | done() 61 | }) 62 | }) 63 | 64 | it('replaces header variables', function (done) { 65 | hippie(app, swaggerSchema) 66 | .get('/foos') 67 | .header('X-Total-Count', 1) 68 | .end(function (err, res) { 69 | expect(err).to.be.undefined() 70 | expect(res.request.headers['X-Total-Count']).to.exist() 71 | expect(res.request.headers['X-Total-Count']).to.equal(1) 72 | done() 73 | }) 74 | }) 75 | }) 76 | 77 | describe('forms', function () { 78 | describe('form-data', function () { 79 | it('works if formData is set to type "file"', function (done) { 80 | var file = 'Content-Disposition: form-data; name="uploadedFile"' 81 | 82 | hippie(app, swaggerSchema) 83 | .header('Content-Type', 'multipart/form-data') 84 | .send(file) 85 | .post('/foos/{fooId}') 86 | .pathParams({ fooId: data.firstFoo.id }) 87 | .end(done) 88 | }) 89 | 90 | describe('required file', function () { 91 | var formSchema 92 | before(function () { 93 | formSchema = cloneSwagger(swaggerSchema) 94 | // set parameter to be required for this test 95 | formSchema.paths['/foos/{fooId}'].post.parameters.filter(function (param) { 96 | return param.name === 'uploadedFile' 97 | })[0].required = true 98 | }) 99 | 100 | it('errors if file is required & missing header and body', function () { 101 | expect(hippie(app, formSchema) 102 | .post('/foos/{fooId}') 103 | .pathParams({ fooId: data.firstFoo.id }) 104 | .end() 105 | ).to.be.rejectedWith(/Missing required parameter in formData: uploadedFile/) 106 | }) 107 | 108 | it('errors if file is required & missing body', function () { 109 | expect(hippie(app, formSchema) 110 | .header('Content-Type', 'multipart/form-data') 111 | .post('/foos/{fooId}') 112 | .pathParams({ fooId: data.firstFoo.id }) 113 | .end() 114 | ).to.be.rejectedWith(/Missing required parameter in formData: uploadedFile/) 115 | }) 116 | }) 117 | }) 118 | 119 | describe('urlencoded', function () { 120 | it('works if formData is optional & not provided', function (done) { 121 | hippie(app, swaggerSchema) 122 | .form() 123 | .get('/foos') 124 | .end(done) 125 | }) 126 | 127 | it('works if formData is required & present', function (done) { 128 | // set formMetadata to be required for this test 129 | var formSchema = cloneSwagger(swaggerSchema) 130 | formSchema.paths['/foos'].get.parameters.filter(function (param) { 131 | return param.name === 'formMetadata' 132 | })[0].required = true 133 | 134 | hippie(app, formSchema) 135 | .form() 136 | .send({ formMetadata: 'formMetadataValue' }) 137 | .get('/foos') 138 | .end(done) 139 | }) 140 | 141 | it('errors if formData is required & missing', function () { 142 | var formSchema = cloneSwagger(swaggerSchema) 143 | 144 | // set parameter to be required for this test 145 | formSchema.paths['/foos'].get.parameters.filter(function (param) { 146 | return param.name === 'formMetadata' 147 | })[0].required = true 148 | 149 | expect( 150 | hippie(app, formSchema) 151 | .form() 152 | .get('/foos') 153 | .end() 154 | ).to.be.rejectedWith(/Missing required parameter in formData: formMetadata/) 155 | }) 156 | }) 157 | }) 158 | 159 | describe('integers', function () { 160 | describe('when using get', function () { 161 | it('when requesting with valid integers, validation is ok', function (done) { 162 | hippie(app, swaggerSchema) 163 | .get('/integerTest/{fooId}') 164 | .pathParams({ fooId: 137 }) 165 | .end(done) 166 | }) 167 | 168 | describe('int32', function () { 169 | it('when requesting with valid integer, validation is ok', function (done) { 170 | hippie(app, swaggerSchema) 171 | .get('/foos') 172 | .qs({ int32: 1 }) 173 | .end(done) 174 | }) 175 | }) 176 | 177 | describe('int64', function () { 178 | it('when requesting with valid integer, validation is ok', function (done) { 179 | hippie(app, swaggerSchema) 180 | .get('/foos') 181 | .qs({ int64: 2147483647 + 1 }) 182 | .end(done) 183 | }) 184 | }) 185 | 186 | describe('floats', function () { 187 | it('when requesting with valid float, validation is ok', function (done) { 188 | hippie(app, swaggerSchema) 189 | .get('/foos') 190 | .qs({ float: 1.1211 }) 191 | .end(done) 192 | }) 193 | }) 194 | 195 | it('when requesting with non-integer values, validation is rejected', function (done) { 196 | try { 197 | hippie(app, swaggerSchema) 198 | .get('/integerTest/{fooId}') 199 | .pathParams({ fooId: '137' }) 200 | .end(done) 201 | } catch (e) { 202 | expect(e).to.match(/Invalid format for parameter {fooId}/) 203 | done() 204 | } 205 | }) 206 | }) 207 | 208 | describe('when using delete', function () { 209 | it('when requesting with valid integers, validation is ok', function (done) { 210 | hippie(app, swaggerSchema) 211 | .del('/integerTest/{fooId}') 212 | .pathParams({ fooId: 137 }) 213 | .end(done) 214 | }) 215 | 216 | it('when requesting with non-integer values, validation is rejected', function (done) { 217 | hippie(app, swaggerSchema) 218 | .del('/integerTest/{fooId}') 219 | .pathParams({ fooId: 'c' }) 220 | .end() 221 | .catch(function (err) { 222 | expect(err).to.match(/Invalid format for parameter {fooId}/) 223 | done() 224 | }) 225 | }) 226 | }) 227 | 228 | describe('when using post', function () { 229 | it('when sending valid integers, validation is ok', function (done) { 230 | hippie(app, swaggerSchema) 231 | .form() 232 | .send({ 233 | barId: 1 234 | }) 235 | .post('/integerTest/{fooId}') 236 | .pathParams({ fooId: 137 }) 237 | .end(done) 238 | }) 239 | 240 | it('when sending non-integer values in path, validation is rejected', function (done) { 241 | hippie(app, swaggerSchema) 242 | .form() 243 | .send({ 244 | barId: 1 245 | }) 246 | .post('/integerTest/{fooId}') 247 | .pathParams({ fooId: 'c' }) 248 | .end() 249 | .catch(function (err) { 250 | expect(err).to.match(/Invalid format for parameter {fooId}/) 251 | done() 252 | }) 253 | }) 254 | 255 | it('when sending non-integer values in formData, validation is rejected', function (done) { 256 | hippie(app, swaggerSchema) 257 | .form() 258 | .send({ 259 | barId: 'c' 260 | }) 261 | .post('/integerTest/{fooId}') 262 | .pathParams({ fooId: 137 }) 263 | .end() 264 | .catch(function (err) { 265 | expect(err).to.match(/Invalid format for parameter {barId}/) 266 | done() 267 | }) 268 | }) 269 | }) 270 | 271 | describe('when using patch', function () { 272 | it('when sending valid integers, validation is ok', function (done) { 273 | hippie(app, swaggerSchema) 274 | .form() 275 | .send({ 276 | barId: 1 277 | }) 278 | .patch('/integerTest/{fooId}') 279 | .pathParams({ fooId: 137 }) 280 | .end(done) 281 | }) 282 | 283 | it('when sending non-integer values in path, validation is rejected', function (done) { 284 | hippie(app, swaggerSchema) 285 | .form() 286 | .send({ 287 | barId: 1 288 | }) 289 | .patch('/integerTest/{fooId}') 290 | .pathParams({ fooId: 'c' }) 291 | .end() 292 | .catch(function (err) { 293 | expect(err).to.match(/Invalid format for parameter {fooId}/) 294 | done() 295 | }) 296 | }) 297 | 298 | it('when sending non-integer values in formData, validation is rejected', function (done) { 299 | hippie(app, swaggerSchema) 300 | .form() 301 | .send({ 302 | barId: 'c' 303 | }) 304 | .patch('/integerTest/{fooId}') 305 | .pathParams({ fooId: 137 }) 306 | .end() 307 | .catch(function (err) { 308 | expect(err).to.match(/Invalid format for parameter {barId}/) 309 | done() 310 | }) 311 | }) 312 | }) 313 | }) 314 | 315 | describe('errors', function () { 316 | it('when a required parameter is missing', function () { 317 | expect(hippie(app, swaggerSchema) 318 | .get('/foos/{fooId}') 319 | .end() 320 | ).to.be.rejectedWith(/Missing required parameter in path: fooId/) 321 | }) 322 | 323 | it('when a parameter fails json-schema validation', function () { 324 | expect( 325 | hippie(app, swaggerSchema) 326 | .get('/foos/{fooId}') 327 | .pathParams({ fooId: 45 }) 328 | .end() 329 | ).to.be.rejectedWith(/Invalid format for parameter {fooId}/) 330 | }) 331 | }) 332 | 333 | describe('settings', function () { 334 | it('when validateParameterSchema is off, it does not error if parameter fails json-schema validation', function () { 335 | return expect(hippie(app, swaggerSchema, { validateParameterSchema: false }) 336 | .get('/foos/{fooId}') 337 | .pathParams({ fooId: 45 }) 338 | .end() 339 | ).to.be.fulfilled() 340 | }) 341 | 342 | it('when validateRequiredParameters is off, it does not error if required parameter is not provided', function (done) { 343 | var schema = _.cloneDeep(swaggerSchema) 344 | schema.paths['/foos'].get.parameters.forEach(function (p) { 345 | if (p.name === 'limit') p.required = true 346 | }) 347 | hippie(app, schema, { validateRequiredParameters: false }) 348 | .get('/foos') 349 | .end(done) 350 | }) 351 | }) 352 | 353 | describe('methods parameters override path parameters', function () { 354 | it('rejects when parameter fails json-schema validation', function () { 355 | expect(hippie(app, swaggerSchema) 356 | .patch('/foos/{fooId}') 357 | .pathParams({ fooId: data.firstFoo.id }) // uuid valid at path level, but overriden to integer at method level 358 | .end() 359 | ).to.be.rejectedWith(/Invalid format for parameter {fooId}/) 360 | }) 361 | 362 | it('accepts valid method parameters', function (done) { 363 | hippie(app, swaggerSchema) 364 | .patch('/foos/{fooId}') 365 | .pathParams({ fooId: 45 }) 366 | .end(function (err, res) { 367 | expect(err).to.be.undefined() 368 | expect(res.req.path).to.equal('/foos/' + 45) 369 | done() 370 | }) 371 | }) 372 | }) 373 | }) 374 | -------------------------------------------------------------------------------- /test/post.test.js: -------------------------------------------------------------------------------- 1 | describe('POST requests', function () { 2 | var validPostBody = { 3 | id: '241a4d44-5b90-41fa-9454-5aa6568087e4', 4 | description: 'third foo', 5 | orderNumber: 3 6 | } 7 | 8 | it('works when valid', function (done) { 9 | hippie(app, swaggerSchema) 10 | .post('/foos') 11 | .send(validPostBody) 12 | .end(function (err, res) { 13 | expect(err).to.be.undefined() 14 | done() 15 | }) 16 | }) 17 | 18 | it('errors when the post body is invalid', function () { 19 | expect(hippie(app, swaggerSchema) 20 | .post('/foos') 21 | .send({ 22 | bogus: 'post-body' 23 | }) 24 | .end() 25 | ).to.be.rejectedWith(/Invalid format for parameter/) 26 | }) 27 | 28 | it('errors when the post response is invalid', function (done) { 29 | hippie(app, swaggerSchema) 30 | .post('/invalid-foos') 31 | .send(validPostBody) 32 | .end(function (err) { 33 | expect(err.message).to.match(/Response from \/invalid-foos failed validation/) 34 | done() 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /test/responses.test.js: -------------------------------------------------------------------------------- 1 | describe('responses', function () { 2 | it('errors when status code is not specified in the swagger file', function (done) { 3 | hippie(app, swaggerSchema) 4 | .put('/invalid-foos') 5 | .end(function (err) { 6 | expect(err.message).to.match(/No mention of statusCode: 200/) 7 | done() 8 | }) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /test/support/bootstrap.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var swaggerParser = require('swagger-parser') 4 | var swaggerSpec = require('./swagger.js') 5 | var chai = require('chai') 6 | var chaiAsPromised = require('chai-as-promised') 7 | var dirtyChai = require('dirty-chai') 8 | 9 | chai.use(dirtyChai) 10 | chai.use(chaiAsPromised) 11 | 12 | global.expect = chai.expect 13 | global.data = require('./data') 14 | global.app = require('./server') 15 | global.hippie = require('../../index') 16 | global.cloneSwagger = function (swagger) { 17 | return JSON.parse(JSON.stringify(swagger)) 18 | } 19 | 20 | swaggerParser.dereference(swaggerSpec, function (err, api, metadata) { 21 | if (err) { 22 | console.error(err) 23 | process.exit(1) 24 | } 25 | global.swaggerSchema = api 26 | run(err) 27 | }) 28 | -------------------------------------------------------------------------------- /test/support/data.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var data = { 4 | firstFoo: { 5 | id: 'cfad73a3-d8d1-46a6-97d2-169ae7561aa6', 6 | description: 'first foo', 7 | orderNumber: 1 8 | }, 9 | secondFoo: { 10 | id: '3d1750e8-f8ef-4f6b-9c9b-b5c44be99d39', 11 | description: 'second foo', 12 | orderNumber: 2 13 | } 14 | } 15 | data.foos = [ 16 | data.firstFoo, 17 | data.secondFoo 18 | ] 19 | 20 | module.exports = data 21 | -------------------------------------------------------------------------------- /test/support/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies. 3 | */ 4 | 5 | var express = require('express') 6 | 7 | /** 8 | * Server. 9 | */ 10 | 11 | var app = express() 12 | 13 | /* 14 | * endpoints w/valid responses 15 | */ 16 | /* /foos/:id */ 17 | app.get('/foos/:fooId', function (req, res) { 18 | res.send(data.firstFoo) 19 | }) 20 | app.delete('/foos/:fooId', function (req, res) { 21 | res.status(204).end() 22 | }) 23 | app.patch('/foos/:fooId', function (req, res) { 24 | res.status(200).end() 25 | }) 26 | app.post('/foos/:fooId', function (req, res) { 27 | res.status(201).end() 28 | }) 29 | 30 | /* /foos */ 31 | app.post('/foos', function (req, res) { 32 | res.status(201) 33 | res.send(req.body) 34 | }) 35 | app.get('/foos', function (req, res) { 36 | res.send(data.foos) 37 | }) 38 | 39 | /* /integerTest */ 40 | app.get('/integerTest/:fooId', function (req, res) { 41 | res.send({ status: 'ok' }) 42 | }) 43 | app.patch('/integerTest/:fooId', function (req, res) { 44 | res.send({ status: 'ok' }) 45 | }) 46 | app.post('/integerTest/:fooId', function (req, res) { 47 | res.status(204).end() 48 | }) 49 | app.delete('/integerTest/:fooId', function (req, res) { 50 | res.status(204).end() 51 | }) 52 | 53 | /* 54 | * endpoints with invalid response 55 | */ 56 | app.all('/invalid-foos', function (req, res) { 57 | res.send([{ 'invalid-foo': true }]) 58 | }) 59 | 60 | /** 61 | * Primary export. 62 | */ 63 | 64 | module.exports = app 65 | 66 | /** 67 | * Export the configured port. 68 | */ 69 | 70 | module.exports.PORT = process.env.HIPPIE_PORT || 7891 71 | -------------------------------------------------------------------------------- /test/support/swagger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | swagger: '2.0', 5 | info: { 6 | version: '1.0.0', 7 | title: 'Test App' 8 | }, 9 | paths: { 10 | '/': { 11 | get: { 12 | description: 'List foos from base path', 13 | responses: { 14 | 200: { 15 | description: 'Successful response', 16 | schema: { 17 | type: 'array', 18 | items: { 19 | $ref: '#/definitions/foo' 20 | } 21 | } 22 | } 23 | } 24 | } 25 | }, 26 | '/foos': { 27 | get: { 28 | description: 'List all foos', 29 | parameters: [{ 30 | name: 'int32', 31 | in: 'query', 32 | description: '32 bit integer', 33 | required: false, 34 | type: 'integer', 35 | format: 'int32' 36 | }, { 37 | name: 'int64', 38 | in: 'query', 39 | description: '64 bit integer', 40 | required: false, 41 | type: 'integer', 42 | format: 'int64' 43 | }, { 44 | name: 'float', 45 | in: 'query', 46 | description: 'float number', 47 | required: false, 48 | type: 'number', 49 | format: 'float' 50 | }, { 51 | name: 'limit', 52 | in: 'query', 53 | description: 'resultset limiter for pagination', 54 | required: false, 55 | type: 'number' 56 | }, { 57 | name: 'offset', 58 | in: 'query', 59 | description: 'resultset offset for pagination', 60 | required: false, 61 | type: 'number' 62 | }, { 63 | name: 'X-Total-Count', 64 | in: 'header', 65 | description: 'header example', 66 | required: false, 67 | type: 'number' 68 | }, { 69 | name: 'formMetadata', 70 | in: 'formData', 71 | description: 'Additional data to pass to server', 72 | required: false, 73 | type: 'string' 74 | }], 75 | responses: { 76 | 200: { 77 | description: 'Successful response', 78 | schema: { 79 | type: 'array', 80 | items: { 81 | $ref: '#/definitions/foo' 82 | } 83 | } 84 | } 85 | } 86 | }, 87 | post: { 88 | description: 'Create a foo', 89 | parameters: [{ 90 | in: 'body', 91 | // Swagger spec: "The name of the body parameter has no effect on the parameter itself" 92 | name: 'name-irrelevant', 93 | description: 'foo object to be added', 94 | required: true, 95 | schema: { 96 | $ref: '#/definitions/foo' 97 | } 98 | }], 99 | responses: { 100 | 201: { 101 | description: 'Successful response', 102 | schema: { 103 | $ref: '#/definitions/foo' 104 | } 105 | } 106 | } 107 | } 108 | }, 109 | '/invalid-foos': { 110 | get: { 111 | description: 'Server returns data that does not validate', 112 | responses: { 113 | 200: { 114 | description: 'Successful response', 115 | schema: { 116 | type: 'array', 117 | items: { 118 | $ref: '#/definitions/foo' 119 | } 120 | } 121 | } 122 | } 123 | }, 124 | post: { 125 | description: 'Server returns data that does not validate', 126 | responses: { 127 | 200: { 128 | description: 'Server returns data that does not validate', 129 | schema: { 130 | $ref: '#/definitions/foo' 131 | } 132 | } 133 | }, 134 | parameters: [{ 135 | in: 'body', 136 | name: 'body', 137 | description: 'foo object to be added', 138 | required: true, 139 | schema: { 140 | $ref: '#/definitions/foo' 141 | } 142 | }] 143 | }, 144 | delete: { 145 | description: 'Server returns data that does not validate', 146 | responses: { 147 | 200: { 148 | description: 'Deleted. No content' 149 | } 150 | } 151 | }, 152 | put: { 153 | description: 'Server returns data that does not validate', 154 | responses: { 155 | 204: { 156 | description: 'Updated. No content.' 157 | } 158 | } 159 | } 160 | }, 161 | '/foos/{fooId}': { 162 | parameters: [{ 163 | name: 'fooId', 164 | in: 'path', 165 | description: 'foo identifier', 166 | required: true, 167 | type: 'string', 168 | pattern: '^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$' 169 | }], 170 | get: { 171 | description: 'Retrieving a foo', 172 | responses: { 173 | 200: { 174 | description: 'Successful response', 175 | schema: { 176 | $ref: '#/definitions/foo' 177 | } 178 | } 179 | } 180 | }, 181 | patch: { 182 | description: 'Patching a foo', 183 | parameters: [{ 184 | name: 'fooId', 185 | in: 'path', 186 | description: 'foo identifier (integer override)', 187 | required: true, 188 | type: 'integer' 189 | }], 190 | responses: { 191 | 200: { 192 | description: 'Successful response', 193 | schema: { 194 | $ref: '#/definitions/foo' 195 | } 196 | } 197 | } 198 | }, 199 | post: { 200 | description: 'Upload file example', 201 | consumes: [ 202 | 'multipart/form-data' 203 | ], 204 | responses: { 205 | 201: { 206 | description: 'noop' 207 | } 208 | }, 209 | parameters: [{ 210 | in: 'formData', 211 | name: 'uploadedFile', 212 | type: 'file', 213 | description: 'file upload', 214 | required: false 215 | }] 216 | }, 217 | delete: { 218 | description: 'Deleting a foo', 219 | parameters: [], 220 | responses: { 221 | 204: { 222 | description: 'Deleted. No content' 223 | } 224 | } 225 | } 226 | }, 227 | '/integerTest/{fooId}': { 228 | parameters: [{ 229 | name: 'fooId', 230 | in: 'path', 231 | description: 'foo identifier', 232 | required: true, 233 | type: 'integer' 234 | }], 235 | get: { 236 | description: 'get via integer', 237 | responses: { 238 | 200: { 239 | description: 'Successful response', 240 | schema: { 241 | $ref: '#/definitions/simple' 242 | } 243 | } 244 | } 245 | }, 246 | patch: { 247 | description: 'patch via integer', 248 | consumes: 'application/x-www-form-urlencoded', 249 | parameters: [{ 250 | name: 'barId', 251 | in: 'formData', 252 | description: 'bar identifier', 253 | required: true, 254 | type: 'integer' 255 | }], 256 | responses: { 257 | 200: { 258 | description: 'Successful response', 259 | schema: { 260 | $ref: '#/definitions/simple' 261 | } 262 | } 263 | } 264 | }, 265 | post: { 266 | description: 'post with integers', 267 | consumes: 'application/x-www-form-urlencoded', 268 | responses: { 269 | 204: { 270 | description: 'OK' 271 | } 272 | }, 273 | parameters: [{ 274 | name: 'barId', 275 | in: 'formData', 276 | description: 'bar identifier', 277 | required: true, 278 | type: 'integer' 279 | }] 280 | }, 281 | delete: { 282 | description: 'delete with integers', 283 | parameters: [], 284 | responses: { 285 | 204: { 286 | description: 'Deleted. No content' 287 | } 288 | } 289 | } 290 | } 291 | }, 292 | definitions: { 293 | foo: { 294 | title: 'Foo object definition', 295 | description: 'Schema for a foo object.', 296 | type: 'object', 297 | required: ['id', 'description', 'orderNumber'], 298 | properties: { 299 | id: { 300 | description: 'primary identifier', 301 | $ref: '#/definitions/uuid' 302 | }, 303 | description: { 304 | description: 'an explanation of the foo', 305 | type: 'string' 306 | }, 307 | orderNumber: { 308 | description: 'the order index of the foo', 309 | type: 'integer' 310 | } 311 | } 312 | }, 313 | simple: { 314 | type: 'object', 315 | required: ['status'], 316 | properties: { 317 | status: { 318 | type: 'string' 319 | } 320 | } 321 | }, 322 | uuid: { 323 | title: 'UUID', 324 | description: 'Schema defining structure of a string representation of a uuid.', 325 | type: 'string', 326 | pattern: '^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$' 327 | } 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /test/swagger-validator.test.js: -------------------------------------------------------------------------------- 1 | var schema = { 2 | swagger: '2.0', 3 | info: { 4 | version: '1.0.0', 5 | title: 'Valid Application' 6 | }, 7 | paths: {} 8 | } 9 | var objectAssign = require('object-assign') 10 | 11 | describe('swagger-validator', function () { 12 | it('passes valid schemas', function () { 13 | expect(function () { 14 | hippie(app, schema) 15 | }).to.not.throw() 16 | }) 17 | 18 | describe('errors', function () { 19 | it('if a swagger definition is not provided', function () { 20 | expect(function () { 21 | hippie(app) 22 | }).to.throw(/Swagger schema required/) 23 | }) 24 | 25 | it('if the swagger definition does not include required properties', function () { 26 | Object.keys(schema).forEach(function (key) { 27 | var invalidSchema = objectAssign({}, schema) 28 | delete invalidSchema[key] 29 | var regex = new RegExp('Swagger schema invalid', 'g') 30 | 31 | expect(function () { 32 | hippie(app, invalidSchema) 33 | }).to.throw(regex) 34 | }) 35 | }) 36 | }) 37 | }) 38 | --------------------------------------------------------------------------------