├── .editorconfig ├── .gitignore ├── .markdownlint.json ├── .npmignore ├── .travis.yml ├── Gruntfile.coffee ├── LICENSE ├── README.md ├── bin └── abao ├── coffeelint.json ├── lib ├── abao.coffee ├── add-hooks.coffee ├── add-tests.coffee ├── cli.coffee ├── configuration.coffee ├── generate-hooks.coffee ├── hooks.coffee ├── index.coffee ├── options-abao.coffee ├── options-mocha.coffee ├── test-runner.coffee └── test.coffee ├── package.json ├── templates └── hookfile.js ├── test ├── e2e │ └── cli-test.coffee ├── fixtures │ ├── contacts.raml │ ├── machines-1_get_1_post.raml │ ├── machines-get_head_options.raml │ ├── machines-include_other_raml.raml │ ├── machines-inline_and_included_schemas.raml │ ├── machines-no_method.raml │ ├── machines-non_required_query_parameter.raml │ ├── machines-ref_other_schemas.raml │ ├── machines-required_query_parameter.raml │ ├── machines-single_get.raml │ ├── machines-three_levels.raml │ ├── machines-with_json_refs.raml │ ├── music-no_base_uri.raml │ ├── music-simple.raml │ ├── music-vendor_content_type.raml │ ├── oauth_2_0.yml │ ├── schemas │ │ ├── definitions.json │ │ ├── type1.json │ │ ├── type2.json │ │ └── with-json-refs.json │ ├── test2_hooks.js │ └── test_hooks.coffee ├── stub │ └── server.coffee └── unit │ ├── abao-test.coffee │ ├── add-hooks-test.coffee │ ├── add-tests-test.coffee │ ├── hooks-test.coffee │ ├── test-runner-test.coffee │ └── test-test.coffee └── wercker.yml /.editorconfig: -------------------------------------------------------------------------------- 1 | ### 2 | ### .editorconfig 3 | ### 4 | 5 | 6 | ## Topmost EditorConfig file 7 | root = true 8 | 9 | ## UTF8, Unix-style newlines, newline ends every file, trim trailing whitespace 10 | [*] 11 | charset = utf-8 12 | end_of_line = lf 13 | insert_final_newline = true 14 | trim_trailing_whitespace = true 15 | 16 | ## Source code: 2 space indentation 17 | [*.coffee,*.js] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | ## RAML: 2 space indentation 22 | [*.raml] 23 | indent_style = space 24 | indent_size = 2 25 | 26 | ## Config files: 2 space indentation 27 | [*.json,*.yml,*.yaml] 28 | indent_style = space 29 | indent_size = 2 30 | 31 | ## Matches the exact files either package.json or .travis.yml 32 | [{package.json,.travis.yml}] 33 | indent_style = space 34 | indent_size = 2 35 | 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### 2 | ### .gitignore 3 | ### 4 | 5 | ## Mac shit 6 | .DS_Store 7 | 8 | ## Editor-related tempfiles 9 | .*.sw[op] 10 | *~ 11 | 12 | ## Logfiles 13 | *.log 14 | logs/ 15 | 16 | ## Build tool configuration files and intermediate storage 17 | .grunt/ 18 | 19 | # Node/npm 20 | .node_repl_history 21 | .npm/ 22 | 23 | ## Package dependencies 24 | node_modules/ 25 | 26 | ## Coverage reports and instrumented source 27 | .nyc_output/ 28 | coverage/ 29 | lib-cov/ 30 | lib/*.js 31 | 32 | ## `npm pack` artifact 33 | abao-[0-9]*.[0-9]*.[0-9]*.tgz 34 | 35 | ## dotenv environment variables file 36 | .env 37 | 38 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "header-style": { "style": "atx" }, 4 | "ul-style": { "style": "asterisk" }, 5 | "ul-indent": { "indent": 2 }, 6 | "no-multiple-blanks": { "maximum": 2 }, 7 | "line-length": { "line_length": 120 }, 8 | "commands-show-output": false, 9 | "ol-prefix": { "style": "one" }, 10 | "hr-style": { "style": "---" }, 11 | "proper-names": { 12 | "names": [ 13 | "CoffeeScript", 14 | "JavaScript" 15 | ], 16 | "code_blocks": false 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | ### 2 | ### .npmignore 3 | ### 4 | 5 | ## Git 6 | .gitignore 7 | 8 | ## GitHub 9 | .github/ 10 | 11 | ## CI configuration files 12 | .codeclimate.yml 13 | .travis.yml 14 | wercker.yml 15 | 16 | ## Build tool configuration files and intermediate storage 17 | .editorconfig 18 | .grunt/ 19 | .markdownlint.json 20 | Gruntfile.coffee 21 | coffeelint.json 22 | 23 | ## Node/npm 24 | .node_repl_history 25 | .npm/ 26 | 27 | ## Coverage reports and instrumented source 28 | coverage/ 29 | lib/*.js 30 | 31 | ## Test code and fixtures 32 | test/ 33 | 34 | ## `npm pack` artifact 35 | abao-[0-9]*.[0-9]*.[0-9]*.tgz 36 | 37 | # dotenv environment variables file 38 | .env 39 | 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | ### 2 | ### .travis.yml 3 | ### 4 | 5 | sudo: false 6 | os: linux 7 | dist: trusty 8 | language: node_js 9 | node_js: 10 | - 'node' 11 | - '8' 12 | - '6' 13 | - '4' 14 | env: 15 | - NODE_ENV=development 16 | notifications: 17 | slack: 18 | secure: fsCX0/TDE9TAJR0S91dboOZ4expmCc8o6joVzsHNJYTJfDtSJehdKjTzYuO/vsRigOOoQZ0dJEPl+D4fysBDV+jkOT5sTjp/uKtcfwHwPi03K8GauwvyW0x4N1M+mY+5jN2ZyBZXqVM5dc0wbgldP9QOg5UpB80hfWUZ+0F1MTM= 19 | deploy: 20 | provider: npm 21 | email: kyan.ql.he@gmail.com 22 | api_key: 23 | secure: G58hf18DK3OzBUnSflTj9z4HPImAVxa9v/VKCvnG9gqaRyDtjoHweZWjzEu2K+ThtMOTbDCJx86KEOkHxKnjYPoXPbhHwK6LlfzRqv2rwsqkJLG0EirPecZA2aeTkxZBqf4camLIJY8GL9v0FiwB7CZ5QHlxhluhnZj+N6kPkaU= 24 | on: 25 | tags: true 26 | repo: cybertk/abao 27 | after_success: 28 | # - grunt coveralls:upload 29 | - COVERAGE_FILE="$TRAVIS_BUILD_DIR/coverage/coverage.lcov" 30 | - COVERALLS_BIN="./node_modules/.bin/coveralls" 31 | - $COVERALLS_BIN lib < $COVERAGE_FILE; echo "exit=$?" 32 | - echo 33 | - echo 34 | - echo "===== COMMIT =====" 35 | - echo "TRAVIS_REPO_SLUG=$TRAVIS_REPO_SLUG" 36 | - echo "TRAVIS_COMMIT=$TRAVIS_COMMIT" 37 | - echo "TRAVIS_COMMIT_MESSAGE=$TRAVIS_COMMIT_MESSAGE" 38 | - echo "TRAVIS_TAG=$TRAVIS_TAG" 39 | - echo "TRAVIS_BRANCH=$TRAVIS_BRANCH" 40 | - echo "===== BUILD =====" 41 | - echo "TRAVIS_BUILD_NUMBER=$TRAVIS_BUILD_NUMBER" 42 | - echo "TRAVIS_BUILD_DIR=$TRAVIS_BUILD_DIR" 43 | 44 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | 'use strict' 4 | require('time-grunt') grunt 5 | 6 | # Dynamically load npm tasks 7 | require('load-grunt-config') grunt 8 | 9 | # Initialize configuration object 10 | grunt.initConfig 11 | # Load in the module information 12 | pkg: grunt.file.readJSON 'package.json' 13 | 14 | readme: 'README.md' 15 | gruntfile: 'Gruntfile.coffee' 16 | 17 | clean: 18 | cover: [ 19 | 'coverage' 20 | ], 21 | instrumented: [ 22 | 'lib/*.js' 23 | ] 24 | 25 | watch: 26 | options: 27 | spawn: false 28 | lib: 29 | files: 'lib/*.coffee' 30 | tasks: [ 31 | 'instrument' 32 | 'mochaTest' 33 | ] 34 | test: 35 | files: 'test/**/*.coffee' 36 | tasks: [ 37 | 'instrument' 38 | 'mochaTest' 39 | ] 40 | gruntfile: 41 | files: '<%= gruntfile %>' 42 | tasks: [ 43 | 'coffeelint:gruntfile' 44 | ] 45 | 46 | coffeelint: 47 | options: 48 | configFile: 'coffeelint.json' 49 | default: 50 | src: [ 51 | 'lib/*.coffee' 52 | 'test/**/*.coffee' 53 | ] 54 | gruntfile: 55 | src: '<%= gruntfile %>' 56 | 57 | markdownlint: 58 | options: 59 | config: require './.markdownlint.json' 60 | default: 61 | src: [ 62 | '<%= readme %>' 63 | ] 64 | 65 | coffeecov: 66 | transpile: 67 | src: 'lib' 68 | dest: 'lib' 69 | 70 | mochaTest: 71 | test: 72 | options: 73 | reporter: 'mocha-phantom-coverage-reporter' 74 | require: 'coffee-script/register' 75 | src: [ 76 | 'test/unit/*-test.coffee' 77 | 'test/e2e/cli-test.coffee' 78 | ] 79 | 80 | coveralls: 81 | upload: 82 | src: 'coverage/coverage.lcov' 83 | 84 | # Register alias tasks 85 | grunt.registerTask 'cover', [ 86 | 'clean', 87 | 'instrument', 88 | 'mochaTest' 89 | ] 90 | 91 | grunt.registerTask 'default', [ 92 | 'watch' 93 | 'mochaTest' 94 | ] 95 | 96 | grunt.registerTask 'instrument', [ 'coffeecov' ] 97 | grunt.registerTask 'lint', [ 98 | 'coffeelint', 99 | 'markdownlint' 100 | ] 101 | 102 | grunt.registerTask 'test', [ 103 | 'lint' 104 | 'cover' 105 | ] 106 | 107 | return 108 | 109 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Abao Contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Abao 2 | 3 | RAML-based automated testing tool 4 | 5 | [![Build Status][Travis-Abao-badge]][Travis-Abao] 6 | [![Dependency Status][DavidDM-AbaoDep-badge]][DavidDM-AbaoDep] 7 | [![devDependency Status][DavidDM-AbaoDevDep-badge]][DavidDM-AbaoDevDep] 8 | [![Coverage Status][Coveralls-Abao-badge]][Coveralls-Abao] 9 | [![Gitter][Gitter-Abao-badge]][Gitter-Abao] 10 | [![CII Best Practices][BestPractices-Abao-badge]][BestPractices-Abao] 11 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fcybertk%2Fabao.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fcybertk%2Fabao?ref=badge_shield) 12 | 13 | **Abao** is a command-line tool for testing API documentation written in 14 | [RAML][] format against its back-end implementation. With **Abao**, you can 15 | easily plug your API documentation into a Continuous Integration (CI) system 16 | (e.g., [Travis][], [Jenkins][]) and have API documentation up-to-date, all 17 | the time. **Abao** uses [Mocha][] for judging if a particular API response 18 | is valid or not. 19 | 20 | [![NPM][NPM-Abao-badge]][NPM-Abao] 21 | 22 | ## Features 23 | 24 | * Verify that each endpoint defined in RAML exists in service 25 | * Verify that URL params for each endpoint defined in RAML are supported in service 26 | * Verify that the required query parameters defined in RAML are supported in service 27 | * Verify that HTTP request headers for each endpoint defined in RAML are supported in service 28 | * Verify that HTTP request body for each endpoint defined in RAML is supported in service, via [JSONSchema][] validation 29 | * Verify that HTTP response headers for each endpoint defined in RAML are supported in service 30 | * Verify that HTTP response body for each endpoint defined in RAML is supported in service, via [JSONSchema][] validation 31 | 32 | ## RAML Support 33 | 34 | This version of the software **only** supports the [RAML-0.8][] specification. 35 | 36 | ## Installation 37 | 38 | Install stable version of full package globally. 39 | 40 | ```bash 41 | $ npm install -g abao 42 | ``` 43 | 44 | A trimmed down version (without developer dependencies) can be installed for 45 | production usage. 46 | 47 | ```bash 48 | $ npm install --only=prod -g abao 49 | ``` 50 | 51 | Install latest development version in GitHub branch 52 | 53 | ```bash 54 | $ npm install -g github:cybertk/abao 55 | ``` 56 | 57 | If you get an `EACCES` error, see 58 | [this](https://docs.npmjs.com/getting-started/fixing-npm-permissions) 59 | NPM documentation. 60 | 61 | ## Get Started Testing Your API 62 | 63 | For general usage, an API endpoint (i.e., web service to be tested) **must** 64 | be specified; this can be done implicitly or explicitly, with the latter 65 | having priority. If the RAML file to be tested provides a [baseUri][] property, 66 | the API endpoint is implicitly set to that value. 67 | 68 | ```bash 69 | $ abao api.raml 70 | ``` 71 | 72 | To explicitly specify the API endpoint, use the `--server` argument. 73 | 74 | ```bash 75 | $ abao api.raml --server http://localhost:8080 76 | ``` 77 | 78 | ## Writing testable RAML 79 | 80 | **Abao** validates the HTTP response body against `schema` defined in [RAML][]. 81 | **No response body will be returned if the corresponding [RAML][] `schema` is missing.** 82 | However, the response status code can **always** be verified, regardless. 83 | 84 | ## Hooks 85 | 86 | **Abao** can be configured to use hookfiles to do basic setup/teardown between 87 | each validation (specified with the `--hookfiles` flag). Hookfiles can be 88 | written in either JavaScript or CoffeeScript, and must import the hook methods. 89 | 90 | **NOTE**: CoffeeScript files **must** use file extension `.coffee`. 91 | 92 | Requests are identified by their name, which is derived from the structure of 93 | the RAML. You can print a list of the generated names with the `--names` flag. 94 | 95 | ### Example 96 | 97 | The RAML file used in the examples below can be found [here](../master/test/fixtures/machines-single_get.raml). 98 | 99 | Get Names: 100 | 101 | ```bash 102 | $ abao machines-single_get.raml --names 103 | GET /machines -> 200 104 | ``` 105 | 106 | **Abao** can generate a hookfile to help validate more than just the 107 | response code for each path. 108 | 109 | ```bash 110 | $ ABAO_HOME="/path/to/node_modules/abao" 111 | $ TEMPLATE="${ABAO_HOME}/templates/hookfile.js" 112 | $ abao machines-single_get.raml --generate-hooks --template="${TEMPLATE}" > test_machines_hooks.js 113 | 114 | ``` 115 | 116 | Then edit the *JavaScript* hookfile `test_machines_hooks.js` created in the 117 | previous step to add request parameters and response validation logic. 118 | 119 | ```javascript 120 | var 121 | hooks = require('hooks'), 122 | assert = require('chai').assert; 123 | 124 | hooks.before('GET /machines -> 200', function (test, done) { 125 | test.request.query = { 126 | color: 'red' 127 | }; 128 | done(); 129 | }); 130 | 131 | hooks.after('GET /machines -> 200', function (test, done) { 132 | machine = test.response.body[0]; 133 | console.log(machine.name); 134 | done(); 135 | }); 136 | ``` 137 | 138 | Alternately, write the same hookfile in *CoffeeScript* named 139 | `test_machines_hooks.coffee`: 140 | 141 | ```coffeescript 142 | {before, after} = require 'hooks' 143 | {assert} = require 'chai' 144 | 145 | before 'GET /machines -> 200', (test, done) -> 146 | test.request.query = 147 | color: 'red' 148 | done() 149 | 150 | after 'GET /machines -> 200', (test, done) -> 151 | machine = test.response.body[0] 152 | console.log machine.name 153 | done() 154 | ``` 155 | 156 | Run validation with *JavaScript* hookfile (from above): 157 | 158 | ```bash 159 | $ abao machines-single_get.raml --hookfiles=test_machines_hooks.js 160 | ``` 161 | 162 | You can also specify what tests **Abao** should skip: 163 | 164 | ```javascript 165 | var 166 | hooks = require('hooks'); 167 | 168 | hooks.skip('DELETE /machines/{machineId} -> 204'); 169 | ``` 170 | 171 | **Abao** supports callbacks for intro and outro (coda) of all tests, 172 | as well as before/after each test: 173 | 174 | ```coffeescript 175 | {beforeAll, beforeEach, afterEach, afterAll} = require 'hooks' 176 | 177 | beforeAll (done) -> 178 | # runs one-time setup before all tests (intro) 179 | done() 180 | 181 | beforeEach (done) -> 182 | # runs generic setup before any test-specific 'before()` 183 | done() 184 | 185 | afterEach (done) -> 186 | # runs generic teardown after any test-specific 'after()' 187 | done() 188 | 189 | afterAll (done) -> 190 | # do one-time teardown after all tests (coda) 191 | done() 192 | ``` 193 | 194 | If `beforeEach`, `afterEach`, `before` and `after` are called multiple times, 195 | the callbacks are executed serially in the order they were called. 196 | 197 | **Abao** provides hook to allow the content of the response to be checked 198 | within the test: 199 | 200 | ```coffeescript 201 | {test} = require 'hooks' 202 | {assert} = require 'chai' 203 | 204 | test 'GET /machines -> 200', (response, body, done) -> 205 | assert.deepEqual JSON.parse(body), ['machine1', 'machine2'] 206 | assert.equal headers['content-type'], 'application/json; charset=utf-8' 207 | return done() 208 | ``` 209 | 210 | ### test.request 211 | 212 | * `server` - Server address, provided by command line option or parsed from 213 | RAML `baseUri`. 214 | * `path` - API endpoint path, parsed from RAML. 215 | * `method` - HTTP method, parsed from RAML request method (e.g., `get`). 216 | * `params` - URI parameters, parsed from RAML request `uriParameters` [default: `{}`]. 217 | * `query` - Object containing querystring values to be appended to the `path`. 218 | Parsed from RAML `queryParameters` section [default: `{}`]. 219 | * `headers` - HTTP headers, parsed from RAML `headers` [default: `{}`]. 220 | * `body` - Entity body for POST, PUT, and PATCH requests. Must be a 221 | JSON-serializable object. Parsed from RAML `example` [default: `{}`]. 222 | 223 | ### test.response 224 | 225 | * `status` - Expected HTTP response code, parsed from RAML response status. 226 | * `schema` - Expected schema of HTTP response body, parsed from RAML response `schema`. 227 | * `headers` - Object containing HTTP response headers from server [default: `{}`]. 228 | * `body` - HTTP response body (JSON-format) from server [default: `null`]. 229 | 230 | ## Command Line Options 231 | 232 | ```console 233 | Usage: 234 | abao [OPTIONS] 235 | 236 | Example: 237 | abao api.raml --server http://api.example.com 238 | 239 | Options passed to Mocha: 240 | --grep, -g Only run tests matching [string] 241 | --invert, -i Invert --grep matches [boolean] 242 | --reporter, -R Specify reporter to use [string] [default: "spec"] 243 | --timeout, -t Set test case timeout in milliseconds [number] [default: 2000] 244 | 245 | Options: 246 | --generate-hooks Output hooks generated from template file and exit [boolean] 247 | --header, -h Add header to include in each request. Header must be in 248 | KEY:VALUE format (e.g., "-h Accept:application/json"). 249 | Reuse option to add multiple headers [string] 250 | --hookfiles, -f Specify pattern to match files with before/after hooks for 251 | running tests [string] 252 | --hooks-only, -H Run test only if defined either before or after hooks 253 | [boolean] 254 | --names, -n List names of requests and exit [boolean] 255 | --reporters Display available reporters and exit [boolean] 256 | --schemas Specify pattern to match schema files to be loaded for use 257 | as JSON refs [string] 258 | --server Specify API endpoint to use. The RAML-specified baseUri 259 | value will be used if not provided [string] 260 | --sorted Sorts requests in a sensible way so that objects are not 261 | modified before they are created. 262 | Order: CONNECT, OPTIONS, POST, GET, HEAD, PUT, PATCH, 263 | DELETE, TRACE. [boolean] 264 | --template Specify template file to use for generating hooks [string] 265 | --help Show usage information and exit [boolean] 266 | --version Show version number and exit [boolean] 267 | ``` 268 | 269 | ## Run Tests 270 | 271 | ```bash 272 | $ npm test 273 | ``` 274 | 275 | ## Contribution 276 | 277 | **Abao** is always looking for new ideas to make the codebase useful. 278 | If you think of something that would make life easier, please submit an issue. 279 | 280 | ```bash 281 | $ npm issues abao 282 | ``` 283 | 284 | 285 | [//]: # (Cross reference section) 286 | 287 | [RAML]: https://raml.org/ 288 | [Mocha]: https://mochajs.org/ 289 | [JSONSchema]: http://json-schema.org/ 290 | [Travis]: https://travis-ci.org/ 291 | [Jenkins]: https://jenkins-ci.org/ 292 | [RAML-0.8]: https://github.com/raml-org/raml-spec/blob/master/versions/raml-08/raml-08.md 293 | [baseUri]: https://github.com/raml-org/raml-spec/blob/master/versions/raml-08/raml-08.md#base-uri-and-baseuriparameters 294 | 295 | [Travis-Abao]: https://travis-ci.org/cybertk/abao/ 296 | [Travis-Abao-badge]: https://img.shields.io/travis/cybertk/abao.svg?style=flat 297 | [DavidDM-AbaoDep]: https://david-dm.org/cybertk/abao/ 298 | [DavidDM-AbaoDep-badge]: https://david-dm.org/cybertk/abao/status.svg 299 | [DavidDM-AbaoDevDep]: https://david-dm.org/cybertk/abao?type=dev 300 | [DavidDM-AbaoDevDep-badge]: https://david-dm.org/cybertk/abao/dev-status.svg 301 | [Coveralls-Abao]: https://coveralls.io/r/cybertk/abao/ 302 | [Coveralls-Abao-badge]: https://img.shields.io/coveralls/cybertk/abao.svg 303 | [Gitter-Abao]: https://gitter.im/cybertk/abao/ 304 | [Gitter-Abao-badge]: https://badges.gitter.im/cybertk/abao.svg 305 | [BestPractices-Abao]: https://bestpractices.coreinfrastructure.org/projects/388 306 | [BestPractices-Abao-badge]: https://bestpractices.coreinfrastructure.org/projects/388/badge 307 | [NPM-Abao]: https://npmjs.org/package/abao/ 308 | [NPM-Abao-badge]: https://nodei.co/npm/abao.png?downloads=true&downloadRank=true&stars=true 309 | 310 | 311 | ## License 312 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fcybertk%2Fabao.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fcybertk%2Fabao?ref=badge_large) 313 | -------------------------------------------------------------------------------- /bin/abao: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // 3 | // abao 4 | // REST API automated testing tool based on RAML 5 | // 6 | 7 | 'use strict'; 8 | 9 | require('coffee-script/register'); 10 | 11 | var path = require('path'); 12 | var fs = require('fs'); 13 | 14 | var libpath = path.join(path.dirname(fs.realpathSync(__filename)), '../lib'); 15 | require(libpath + '/cli').main(process.argv.slice(2)); 16 | 17 | -------------------------------------------------------------------------------- /coffeelint.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrow_spacing": { 3 | "level": "warn" 4 | }, 5 | "braces_spacing": { 6 | "level": "warn", 7 | "spaces": 0, 8 | "empty_object_spaces": 0 9 | }, 10 | "camel_case_classes": { 11 | "level": "error" 12 | }, 13 | "coffeescript_error": { 14 | "level": "error" 15 | }, 16 | "colon_assignment_spacing": { 17 | "level": "warn", 18 | "spacing": { 19 | "left": 0, 20 | "right": 1 21 | } 22 | }, 23 | "cyclomatic_complexity": { 24 | "level": "warn", 25 | "value": 10 26 | }, 27 | "duplicate_key": { 28 | "level": "error" 29 | }, 30 | "empty_constructor_needs_parens": { 31 | "level": "warn" 32 | }, 33 | "ensure_comprehensions": { 34 | "level": "warn" 35 | }, 36 | "eol_last": { 37 | "level": "ignore" 38 | }, 39 | "indentation": { 40 | "value": 2, 41 | "level": "error" 42 | }, 43 | "line_endings": { 44 | "level": "error", 45 | "value": "unix" 46 | }, 47 | "max_line_length": { 48 | "value": 120, 49 | "level": "error", 50 | "limitComments": true 51 | }, 52 | "missing_fat_arrows": { 53 | "level": "warn", 54 | "is_strict": false 55 | }, 56 | "newlines_after_classes": { 57 | "value": 3, 58 | "level": "warn" 59 | }, 60 | "no_backticks": { 61 | "level": "error" 62 | }, 63 | "no_debugger": { 64 | "level": "warn", 65 | "console": false 66 | }, 67 | "no_empty_functions": { 68 | "level": "warn" 69 | }, 70 | "no_empty_param_list": { 71 | "level": "ignore" 72 | }, 73 | "no_implicit_braces": { 74 | "level": "ignore", 75 | "strict": true 76 | }, 77 | "no_implicit_parens": { 78 | "level": "ignore", 79 | "strict": true 80 | }, 81 | "no_interpolation_in_single_quotes": { 82 | "level": "warn" 83 | }, 84 | "no_nested_string_interpolation": { 85 | "level": "warn" 86 | }, 87 | "no_plusplus": { 88 | "level": "ignore" 89 | }, 90 | "no_private_function_fat_arrows": { 91 | "level": "warn" 92 | }, 93 | "no_stand_alone_at": { 94 | "level": "warn" 95 | }, 96 | "no_tabs": { 97 | "level": "error" 98 | }, 99 | "no_this": { 100 | "level": "warn" 101 | }, 102 | "no_throwing_strings": { 103 | "level": "error" 104 | }, 105 | "no_trailing_semicolons": { 106 | "level": "error" 107 | }, 108 | "no_trailing_whitespace": { 109 | "level": "error", 110 | "allowed_in_comments": false, 111 | "allowed_in_empty_lines": false 112 | }, 113 | "no_unnecessary_double_quotes": { 114 | "level": "warn" 115 | }, 116 | "no_unnecessary_fat_arrows": { 117 | "level": "warn" 118 | }, 119 | "non_empty_constructor_needs_parens": { 120 | "level": "ignore" 121 | }, 122 | "prefer_english_operator": { 123 | "level": "ignore", 124 | "doubleNotLevel": "ignore" 125 | }, 126 | "space_operators": { 127 | "level": "warn" 128 | }, 129 | "spacing_after_comma": { 130 | "level": "error" 131 | }, 132 | "transform_messes_up_line_numbers": { 133 | "level": "warn" 134 | }, 135 | "use_strict": { 136 | "module": "coffeelint-use-strict", 137 | "level": "warn", 138 | "allowGlobal": false, 139 | "requireGlobal": false 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /lib/abao.coffee: -------------------------------------------------------------------------------- 1 | ###* 2 | # @file Abao class 3 | ### 4 | 5 | require('source-map-support').install({handleUncaughtExceptions: false}) 6 | async = require 'async' 7 | ramlParser = require 'raml-parser' 8 | 9 | addTests = require './add-tests' 10 | addHooks = require './add-hooks' 11 | asConfiguration = require './configuration' 12 | hooks = require './hooks' 13 | Runner = require './test-runner' 14 | TestFactory = require './test' 15 | 16 | defaultArgs = 17 | _: [] 18 | options: 19 | help: true 20 | 21 | 22 | class Abao 23 | constructor: (parsedArgs = defaultArgs) -> 24 | 'use strict' 25 | @configuration = asConfiguration parsedArgs 26 | @tests = [] 27 | @hooks = hooks 28 | 29 | run: (done) -> 30 | 'use strict' 31 | config = @configuration 32 | tests = @tests 33 | hooks = @hooks 34 | 35 | parseHooks = (callback) -> 36 | addHooks hooks, config.options.hookfiles, callback 37 | return # NOTREACHED 38 | 39 | loadRAML = (callback) -> 40 | if !config.ramlPath 41 | nofile = new Error 'unspecified RAML file' 42 | return callback nofile 43 | 44 | ramlParser.loadFile config.ramlPath 45 | .then (raml) -> 46 | return callback null, raml 47 | .catch (err) -> 48 | return callback err 49 | return # NOTREACHED 50 | 51 | parseTestsFromRAML = (raml, callback) -> 52 | if !config.options.server 53 | if raml.baseUri 54 | config.options.server = raml.baseUri 55 | 56 | # Inject the JSON refs schemas 57 | factory = new TestFactory config.options.schemas 58 | 59 | addTests raml, tests, hooks, callback, factory, config.options.sorted 60 | return # NOTREACHED 61 | 62 | runTests = (callback) -> 63 | runner = new Runner config.options, config.ramlPath 64 | runner.run tests, hooks, callback 65 | return # NOTREACHED 66 | 67 | async.waterfall [ 68 | parseHooks, 69 | loadRAML, 70 | parseTestsFromRAML, 71 | runTests 72 | ], done 73 | return 74 | 75 | 76 | 77 | module.exports = Abao 78 | 79 | -------------------------------------------------------------------------------- /lib/add-hooks.coffee: -------------------------------------------------------------------------------- 1 | ###* 2 | # @file Load user hooks 3 | ### 4 | 5 | require 'coffee-script/register' 6 | glob = require 'glob' 7 | path = require 'path' 8 | proxyquire = require('proxyquire').noCallThru() 9 | 10 | 11 | addHooks = (hooks, pattern, callback) -> 12 | 'use strict' 13 | if pattern 14 | files = glob.sync pattern 15 | 16 | if files.length == 0 17 | nomatch = new Error "no hook files found matching pattern '#{pattern}'" 18 | return callback nomatch 19 | 20 | console.info 'processing hook file(s):' 21 | try 22 | files.map (file) -> 23 | absFile = path.resolve process.cwd(), file 24 | console.info ' ' + absFile 25 | proxyquire absFile, { 26 | 'hooks': hooks 27 | } 28 | console.log() 29 | catch error 30 | console.error 'error loading hooks...' 31 | return callback error 32 | 33 | return callback null 34 | 35 | 36 | module.exports = addHooks 37 | 38 | -------------------------------------------------------------------------------- /lib/add-tests.coffee: -------------------------------------------------------------------------------- 1 | ###* 2 | # @file Add tests 3 | ### 4 | 5 | async = require 'async' 6 | csonschema = require 'csonschema' 7 | _ = require 'lodash' 8 | 9 | 10 | parseSchema = (source) -> 11 | 'use strict' 12 | if source.contains('$schema') 13 | #jsonschema 14 | # @response.schema = JSON.parse @response.schema 15 | JSON.parse source 16 | else 17 | csonschema.parse source 18 | # @response.schema = csonschema.parse @response.schema 19 | 20 | 21 | parseHeaders = (raml) -> 22 | 'use strict' 23 | headers = {} 24 | if raml 25 | for key, v of raml 26 | headers[key] = v.example 27 | headers 28 | 29 | 30 | getCompatibleMediaTypes = (bodyObj) -> 31 | 'use strict' 32 | vendorRE = /^application\/(.*\+)?json/i 33 | return (type for type of bodyObj when type.match(vendorRE)) 34 | 35 | 36 | addTests = (raml, tests, hooks, parent, callback, testFactory, sortFirst) -> 37 | 'use strict' 38 | 39 | # Handle 4th optional param 40 | if _.isFunction(parent) 41 | sortFirst = testFactory 42 | testFactory = callback 43 | callback = parent 44 | parent = null 45 | 46 | return callback() unless raml.resources 47 | 48 | # Iterate endpoint 49 | async.each raml.resources, (resource, callback) -> 50 | path = resource.relativeUri 51 | params = {} 52 | query = {} 53 | 54 | # Apply parent properties 55 | if parent 56 | path = parent.path + path 57 | params = _.clone parent.params # shallow copy 58 | 59 | # Setup param 60 | if resource.uriParameters 61 | for key, param of resource.uriParameters 62 | params[key] = param.example 63 | 64 | 65 | # In case of issue #8, resource does not define methods 66 | resource.methods ?= [] 67 | 68 | if sortFirst && resource.methods.length > 1 69 | methodTests = [ 70 | method: 'CONNECT', tests: [] 71 | , 72 | method: 'OPTIONS', tests: [] 73 | , 74 | method: 'POST', tests: [] 75 | , 76 | method: 'GET', tests: [] 77 | , 78 | method: 'HEAD', tests: [] 79 | , 80 | method: 'PUT', tests: [] 81 | , 82 | method: 'PATCH', tests: [] 83 | , 84 | method: 'DELETE', tests: [] 85 | , 86 | method: 'TRACE', tests: [] 87 | ] 88 | 89 | # Group endpoint tests by method name 90 | _.each methodTests, (methodTest) -> 91 | isSameMethod = (test) -> 92 | return methodTest.method == test.method.toUpperCase() 93 | 94 | ans = _.partition resource.methods, isSameMethod 95 | if ans[0].length != 0 96 | _.each ans[0], (test) -> methodTest.tests.push test 97 | resource.methods = ans[1] 98 | 99 | # Shouldn't happen unless new HTTP method introduced... 100 | leftovers = resource.methods 101 | if leftovers.length > 1 102 | console.error 'unknown method calls present!', leftovers 103 | 104 | # Now put them back, but in order of methods listed above 105 | sortedTests = _.map methodTests, (methodTest) -> return methodTest.tests 106 | leftoverTests = _.map leftovers, (leftover) -> return leftover 107 | reassembled = _.flattenDeep [_.reject sortedTests, _.isEmpty, 108 | _.reject leftoverTests, _.isEmpty] 109 | resource.methods = reassembled 110 | 111 | # Iterate response method 112 | async.each resource.methods, (api, callback) -> 113 | method = api.method.toUpperCase() 114 | 115 | # Setup query 116 | if api.queryParameters 117 | for qkey, qvalue of api.queryParameters 118 | if (!!qvalue.required) 119 | query[qkey] = qvalue.example 120 | 121 | 122 | # Iterate response status 123 | for status, res of api.responses 124 | 125 | testName = "#{method} #{path} -> #{status}" 126 | 127 | # Append new test to tests 128 | test = testFactory.create(testName, hooks.contentTests[testName]) 129 | tests.push test 130 | 131 | # Update test.request 132 | test.request.path = path 133 | test.request.method = method 134 | test.request.headers = parseHeaders api.headers 135 | 136 | # Select compatible content-type in request body to support 137 | # vendor tree types (e.g., 'application/vnd.api+json') 138 | contentType = getCompatibleMediaTypes(api.body)?[0] 139 | if contentType 140 | test.request.headers['Content-Type'] = contentType 141 | try 142 | test.request.body = JSON.parse api.body[contentType]?.example 143 | catch 144 | console.warn "cannot parse JSON example request body for #{test.name}" 145 | test.request.params = params 146 | test.request.query = query 147 | 148 | # Update test.response 149 | test.response.status = status 150 | test.response.schema = null 151 | 152 | if res?.body 153 | # Expect content-type of response body to be identical to request body 154 | if contentType && res.body[contentType]?.schema 155 | test.response.schema = parseSchema res.body[contentType].schema 156 | # Otherwise, filter in responses section for compatible content-types 157 | else 158 | contentType = getCompatibleMediaTypes(res.body)?[0] 159 | if res.body[contentType]?.schema 160 | test.response.schema = parseSchema res.body[contentType].schema 161 | 162 | callback() 163 | , (err) -> 164 | return callback(err) if err 165 | 166 | # Recursive 167 | addTests resource, tests, hooks, {path, params}, callback, testFactory, sortFirst 168 | , callback 169 | 170 | 171 | module.exports = addTests 172 | 173 | -------------------------------------------------------------------------------- /lib/cli.coffee: -------------------------------------------------------------------------------- 1 | ###* 2 | # @file Command line interface 3 | ### 4 | 5 | require 'coffee-script/register' 6 | 7 | child_process = require 'child_process' 8 | path = require 'path' 9 | _ = require 'lodash' 10 | yargs = require 'yargs' 11 | 12 | Abao = require './abao' 13 | abaoOptions = require './options-abao' 14 | mochaOptions = require './options-mocha' 15 | pkg = require '../package' 16 | 17 | EXIT_SUCCESS = 0 18 | EXIT_FAILURE = 1 19 | 20 | showReporters = () -> 21 | 'use strict' 22 | mochaDir = path.dirname require.resolve('mocha') 23 | mochaPkg = require 'mocha/package' 24 | executable = path.join mochaDir, mochaPkg.bin._mocha 25 | executable = path.normalize executable 26 | stdoutBuff = child_process.execFileSync executable, ['--reporters'] 27 | stdout = stdoutBuff.toString() 28 | stdout = stdout.slice 0, stdout.length - 1 # Remove last newline 29 | console.log stdout 30 | return 31 | 32 | parseArgs = (argv) -> 33 | 'use strict' 34 | allOptions = _.assign {}, abaoOptions, mochaOptions 35 | mochaOptionNames = Object.keys mochaOptions 36 | prog = path.basename pkg.bin 37 | return yargs(argv) 38 | .usage("Usage:\n #{prog} [OPTIONS]" + 39 | "\n\nExample:\n #{prog} api.raml --server http://api.example.com") 40 | .options(allOptions) 41 | .group(mochaOptionNames, 'Options passed to Mocha:') 42 | .implies('template', 'generate-hooks') 43 | .check((argv) -> 44 | if argv.reporters == true 45 | showReporters() 46 | process.exit EXIT_SUCCESS 47 | 48 | # Ensure single positional argument present 49 | if argv._.length < 1 50 | throw new Error "#{prog}: must specify path to RAML file" 51 | else if argv._.length > 1 52 | throw new Error "#{prog}: accepts single positional command-line argument" 53 | 54 | return true 55 | ) 56 | .wrap(80) 57 | .help('help', 'Show usage information and exit') 58 | .version('version', 'Show version number and exit', pkg.version) 59 | .epilog("Website:\n #{pkg.homepage}") 60 | .argv 61 | 62 | ## 63 | ## Main 64 | ## 65 | main = (argv) -> 66 | 'use strict' 67 | parsedArgs = parseArgs argv 68 | 69 | abao = new Abao parsedArgs 70 | abao.run (error, nfailures) -> 71 | if error 72 | process.exitCode = EXIT_FAILURE 73 | if error.message 74 | console.error error.message 75 | if error.stack 76 | console.error error.stack 77 | 78 | if nfailures > 0 79 | process.exitCode = EXIT_FAILURE 80 | 81 | process.exit() 82 | return # NOTREACHED 83 | 84 | 85 | module.exports = 86 | main: main 87 | 88 | -------------------------------------------------------------------------------- /lib/configuration.coffee: -------------------------------------------------------------------------------- 1 | ###* 2 | # @file Stores command line arguments in configuration object 3 | ### 4 | 5 | _ = require 'lodash' 6 | path = require 'path' 7 | 8 | abaoOptions = require './options-abao' 9 | mochaOptions = require './options-mocha' 10 | allOptions = _.assign {}, abaoOptions, mochaOptions 11 | 12 | 13 | applyConfiguration = (config) -> 14 | 'use strict' 15 | 16 | coerceToArray = (value) -> 17 | if typeof value is 'string' 18 | value = [value] 19 | else if !value? 20 | value = [] 21 | else if value instanceof Array 22 | value 23 | else value 24 | return value 25 | 26 | coerceToDict = (value) -> 27 | array = coerceToArray value 28 | dict = {} 29 | 30 | if array.length > 0 31 | for item in array 32 | [key, value] = item.split(':') 33 | dict[key] = value 34 | 35 | return dict 36 | 37 | configuration = 38 | ramlPath: null 39 | options: 40 | server: null 41 | schemas: null 42 | 'generate-hooks': false 43 | template: null 44 | timeout: 2000 45 | reporter: null 46 | header: null 47 | names: false 48 | hookfiles: null 49 | grep: '' 50 | invert: false 51 | 'hooks-only': false 52 | sorted: false 53 | 54 | # Normalize options and config 55 | for own key, value of config 56 | configuration[key] = value 57 | 58 | # Customize 59 | if !configuration.options.template 60 | defaultTemplate = path.join 'templates', 'hookfile.js' 61 | configuration.options.template = defaultTemplate 62 | configuration.options.header = coerceToDict(configuration.options.header) 63 | 64 | # TODO(quanlong): OAuth2 Bearer Token 65 | if configuration.options.oauth2Token? 66 | configuration.options.headers['Authorization'] = "Bearer #{configuration.options.oauth2Token}" 67 | 68 | return configuration 69 | 70 | # Create configuration settings from CLI arguments applied against options 71 | # @param {Object} parsedArgs - yargs .argv() output 72 | # @returns {Object} configuration object 73 | asConfiguration = (parsedArgs) -> 74 | 'use strict' 75 | ## TODO(plroebuck): Do all configuration in one place... 76 | aliases = Object.keys(allOptions).map (key) -> allOptions[key].alias 77 | .filter (val) -> val != undefined 78 | alreadyHandled = [ 79 | 'reporters', 80 | 'help', 81 | 'version' 82 | ] 83 | 84 | configuration = 85 | ramlPath: parsedArgs._[0], 86 | options: _.omit parsedArgs, ['_', '$0', aliases..., alreadyHandled...] 87 | 88 | mochaOptionNames = Object.keys mochaOptions 89 | optionsToReparent = _.pick configuration.options, mochaOptionNames 90 | configuration.options = _.omit configuration.options, mochaOptionNames 91 | configuration.options.mocha = optionsToReparent 92 | 93 | return applyConfiguration configuration 94 | 95 | 96 | module.exports = asConfiguration 97 | 98 | -------------------------------------------------------------------------------- /lib/generate-hooks.coffee: -------------------------------------------------------------------------------- 1 | ###* 2 | # @file Generates hooks stub file 3 | ### 4 | 5 | fs = require 'fs' 6 | Mustache = require 'mustache' 7 | 8 | generateHooks = (names, ramlFile, templateFile, callback) -> 9 | 'use strict' 10 | if !names 11 | callback new Error 'no names found for which to generate hooks' 12 | 13 | if !templateFile 14 | callback new Error 'missing template file' 15 | 16 | try 17 | template = fs.readFileSync templateFile, 'utf8' 18 | datetime = new Date().toISOString().replace('T', ' ').substr(0, 19) 19 | view = 20 | ramlFile: ramlFile 21 | timestamp: datetime 22 | hooks: 23 | {'name': name} for name in names 24 | view.hooks[0].comment = true 25 | 26 | content = Mustache.render template, view 27 | console.log content 28 | catch error 29 | console.error 'failed to generate skeleton hooks' 30 | callback error 31 | 32 | callback 33 | 34 | module.exports = generateHooks 35 | 36 | -------------------------------------------------------------------------------- /lib/hooks.coffee: -------------------------------------------------------------------------------- 1 | ###* 2 | # @file Hooks class 3 | ### 4 | 5 | async = require 'async' 6 | _ = require 'lodash' 7 | 8 | 9 | class Hooks 10 | constructor: () -> 11 | 'use strict' 12 | @beforeHooks = {} 13 | @afterHooks = {} 14 | @beforeAllHooks = [] 15 | @afterAllHooks = [] 16 | @beforeEachHooks = [] 17 | @afterEachHooks = [] 18 | @contentTests = {} 19 | @skippedTests = [] 20 | 21 | before: (name, hook) => 22 | 'use strict' 23 | @addHook @beforeHooks, name, hook 24 | 25 | after: (name, hook) => 26 | 'use strict' 27 | @addHook @afterHooks, name, hook 28 | 29 | beforeAll: (hook) => 30 | 'use strict' 31 | @beforeAllHooks.push hook 32 | 33 | afterAll: (hook) => 34 | 'use strict' 35 | @afterAllHooks.push hook 36 | 37 | beforeEach: (hook) => 38 | 'use strict' 39 | @beforeEachHooks.push hook 40 | 41 | afterEach: (hook) => 42 | 'use strict' 43 | @afterEachHooks.push hook 44 | 45 | addHook: (hooks, name, hook) -> 46 | 'use strict' 47 | if hooks[name] 48 | hooks[name].push hook 49 | else 50 | hooks[name] = [hook] 51 | 52 | test: (name, hook) => 53 | 'use strict' 54 | if @contentTests[name]? 55 | throw new Error "cannot have more than one test with the name: #{name}" 56 | @contentTests[name] = hook 57 | 58 | runBeforeAll: (callback) => 59 | 'use strict' 60 | async.series @beforeAllHooks, (err, results) -> 61 | callback(err) 62 | 63 | runAfterAll: (callback) => 64 | 'use strict' 65 | async.series @afterAllHooks, (err, results) -> 66 | callback(err) 67 | 68 | runBefore: (test, callback) => 69 | 'use strict' 70 | return callback() unless (@beforeHooks[test.name] or @beforeEachHooks) 71 | 72 | hooks = @beforeEachHooks.concat(@beforeHooks[test.name] ? []) 73 | async.eachSeries hooks, (hook, callback) -> 74 | hook test, callback 75 | , callback 76 | 77 | runAfter: (test, callback) => 78 | 'use strict' 79 | return callback() unless (@afterHooks[test.name] or @afterEachHooks) 80 | 81 | hooks = (@afterHooks[test.name] ? []).concat(@afterEachHooks) 82 | async.eachSeries hooks, (hook, callback) -> 83 | hook test, callback 84 | , callback 85 | 86 | skip: (name) => 87 | 'use strict' 88 | @skippedTests.push name 89 | 90 | hasName: (name) => 91 | 'use strict' 92 | _.has(@beforeHooks, name) || _.has(@afterHooks, name) 93 | 94 | skipped: (name) => 95 | 'use strict' 96 | @skippedTests.indexOf(name) != -1 97 | 98 | 99 | 100 | module.exports = new Hooks() 101 | 102 | -------------------------------------------------------------------------------- /lib/index.coffee: -------------------------------------------------------------------------------- 1 | ###* 2 | # @file Description 3 | ### 4 | 5 | abao = require './abao' 6 | 7 | module.exports = abao 8 | 9 | -------------------------------------------------------------------------------- /lib/options-abao.coffee: -------------------------------------------------------------------------------- 1 | ###* 2 | # @file Command line options (Abao-related) 3 | ### 4 | 5 | module.exports = 6 | 'generate-hooks': 7 | description: 'Output hooks generated from template file and exit' 8 | type: 'boolean' 9 | 10 | header: 11 | alias: 'h' 12 | description: 'Add header to include in each request. Header must be ' + 13 | 'in KEY:VALUE format (e.g., "-h Accept:application/json").' + 14 | '\nReuse option to add multiple headers' 15 | type: 'string' 16 | 17 | hookfiles: 18 | alias: 'f' 19 | description: 'Specify pattern to match files with before/after hooks ' + 20 | 'for running tests' 21 | type: 'string' 22 | 23 | 'hooks-only': 24 | alias: 'H' 25 | description: 'Run test only if defined either before or after hooks' 26 | type: 'boolean' 27 | 28 | names: 29 | alias: 'n' 30 | description: 'List names of requests and exit' 31 | type: 'boolean' 32 | 33 | schemas: 34 | description: 'Specify pattern to match schema files to be loaded for ' + 35 | 'use as JSON $refs' 36 | type: 'string' 37 | 38 | server: 39 | description: 'Specify API endpoint to use. The RAML-specified baseUri ' + 40 | 'value will be used if not provided' 41 | type: 'string' 42 | 43 | sorted: 44 | description: 'Sorts requests in a sensible way so that objects are not ' + 45 | 'modified before they are created.\nOrder: ' + 46 | 'CONNECT, OPTIONS, POST, GET, HEAD, PUT, PATCH, DELETE, TRACE.' 47 | type: 'boolean' 48 | 49 | template: 50 | description: 'Specify template file to use for generating hooks' 51 | type: 'string' 52 | normalize: true 53 | 54 | -------------------------------------------------------------------------------- /lib/options-mocha.coffee: -------------------------------------------------------------------------------- 1 | ###* 2 | # @file Command line options (Mocha-related) 3 | ### 4 | 5 | module.exports = 6 | grep: 7 | alias: 'g' 8 | description: 'Only run tests matching ' 9 | type: 'string' 10 | 11 | invert: 12 | alias: 'i' 13 | description: 'Invert --grep matches' 14 | type: 'boolean' 15 | 16 | reporter: 17 | alias: 'R' 18 | description: 'Specify reporter to use' 19 | type: 'string' 20 | default: 'spec' 21 | 22 | reporters: 23 | description: 'Display available reporters and exit' 24 | type: 'boolean' 25 | 26 | timeout: 27 | alias: 't' 28 | description: 'Set test case timeout in milliseconds' 29 | type: 'number' 30 | default: 2000 31 | 32 | -------------------------------------------------------------------------------- /lib/test-runner.coffee: -------------------------------------------------------------------------------- 1 | ###* 2 | # @file TestRunner class 3 | ### 4 | 5 | async = require 'async' 6 | Mocha = require 'mocha' 7 | path = require 'path' 8 | # TODO(proebuck): Replace underscore module with Lodash; ensure compatibility 9 | _ = require 'underscore' 10 | 11 | generateHooks = require './generate-hooks' 12 | 13 | 14 | class TestRunner 15 | constructor: (options, ramlFile) -> 16 | 'use strict' 17 | @server = options.server 18 | delete options.server 19 | @mocha = new Mocha options.mocha 20 | delete options.mocha 21 | @options = options 22 | @ramlFile = ramlFile 23 | 24 | addTestToMocha: (test, hooks) => 25 | 'use strict' 26 | mocha = @mocha 27 | options = @options 28 | 29 | # Generate Test Suite 30 | suite = Mocha.Suite.create mocha.suite, test.name 31 | 32 | # No Response defined 33 | if !test.response.status 34 | suite.addTest new Mocha.Test 'Skip as no response code defined' 35 | return 36 | 37 | # No Hooks for this test 38 | if not hooks.hasName(test.name) and options['hooks-only'] 39 | suite.addTest new Mocha.Test 'Skip as no hooks defined' 40 | return 41 | 42 | # Test skipped in hook file 43 | if hooks.skipped(test.name) 44 | suite.addTest new Mocha.Test 'Skipped in hooks' 45 | return 46 | 47 | # Setup hooks 48 | if hooks 49 | suite.beforeAll _.bind (done) -> 50 | @hooks.runBefore @test, done 51 | , {hooks, test} 52 | 53 | suite.afterAll _.bind (done) -> 54 | @hooks.runAfter @test, done 55 | , {hooks, test} 56 | 57 | # Setup test 58 | # Vote test name 59 | title = if test.response.schema 60 | 'Validate response code and body' 61 | else 62 | 'Validate response code only' 63 | suite.addTest new Mocha.Test title, _.bind (done) -> 64 | @test.run done 65 | , {test} 66 | 67 | run: (tests, hooks, done) -> 68 | 'use strict' 69 | server = @server 70 | options = @options 71 | addTestToMocha = @addTestToMocha 72 | mocha = @mocha 73 | ramlFile = path.basename @ramlFile 74 | names = [] 75 | 76 | async.waterfall [ 77 | (callback) -> 78 | async.each tests, (test, cb) -> 79 | if options.names || options['generate-hooks'] 80 | # Save test names for use by next step 81 | names.push test.name 82 | return cb() 83 | 84 | # None shall pass without... 85 | return callback(new Error 'no API endpoint specified') if !server 86 | 87 | # Update test.request 88 | test.request.server = server 89 | _.extend(test.request.headers, options.header) 90 | 91 | addTestToMocha test, hooks 92 | cb() 93 | , callback 94 | , # Handle options that don't run tests 95 | (callback) -> 96 | if options['generate-hooks'] 97 | # Generate hooks skeleton file 98 | generateHooks names, ramlFile, options.template, done 99 | else if options.names 100 | # Write names to console 101 | console.log name for name in names 102 | return done(null, 0) 103 | else 104 | return callback() 105 | , # Run mocha 106 | (callback) -> 107 | mocha.suite.beforeAll _.bind (done) -> 108 | @hooks.runBeforeAll done 109 | , {hooks} 110 | mocha.suite.afterAll _.bind (done) -> 111 | @hooks.runAfterAll done 112 | , {hooks} 113 | 114 | mocha.run (failures) -> 115 | return callback(null, failures) 116 | ], done 117 | 118 | 119 | 120 | module.exports = TestRunner 121 | 122 | -------------------------------------------------------------------------------- /lib/test.coffee: -------------------------------------------------------------------------------- 1 | ###* 2 | # @file TestFactory/Test classes 3 | ### 4 | 5 | async = require 'async' 6 | chai = require 'chai' 7 | fs = require 'fs' 8 | glob = require 'glob' 9 | request = require 'request' 10 | tv4 = require 'tv4' 11 | _ = require 'underscore' 12 | 13 | assert = chai.assert 14 | 15 | 16 | String::contains = (it) -> 17 | 'use strict' 18 | @indexOf(it) != -1 19 | 20 | 21 | class TestFactory 22 | constructor: (schemaLocation) -> 23 | 'use strict' 24 | if schemaLocation 25 | 26 | files = glob.sync schemaLocation 27 | console.log '\tJSON ref schemas: ' + files.join(', ') 28 | 29 | for file in files 30 | tv4.addSchema(JSON.parse(fs.readFileSync(file, 'utf8'))) 31 | 32 | create: (name, contentTest) -> 33 | 'use strict' 34 | return new Test(name, contentTest) 35 | 36 | 37 | 38 | class Test 39 | constructor: (@name, @contentTest) -> 40 | 'use strict' 41 | @name ?= '' 42 | @skip = false 43 | 44 | @request = 45 | server: '' 46 | path: '' 47 | method: 'GET' 48 | params: {} 49 | query: {} 50 | headers: {} 51 | body: '' 52 | 53 | @response = 54 | status: '' 55 | schema: null 56 | headers: null 57 | body: null 58 | 59 | @contentTest ?= (response, body, done) -> 60 | done() 61 | 62 | url: () -> 63 | 'use strict' 64 | path = @request.server + @request.path 65 | 66 | for key, value of @request.params 67 | path = path.replace "{#{key}}", value 68 | return path 69 | 70 | run: (callback) -> 71 | 'use strict' 72 | assertResponse = @assertResponse 73 | contentTest = @contentTest 74 | 75 | options = _.pick @request, 'headers', 'method' 76 | options['url'] = @url() 77 | if typeof @request.body is 'string' 78 | options['body'] = @request.body 79 | else 80 | options['body'] = JSON.stringify @request.body 81 | options['qs'] = @request.query 82 | 83 | async.waterfall [ 84 | (callback) -> 85 | request options, (error, response, body) -> 86 | callback null, error, response, body 87 | , 88 | (error, response, body, callback) -> 89 | assertResponse(error, response, body) 90 | contentTest(response, body, callback) 91 | ], callback 92 | 93 | assertResponse: (error, response, body) => 94 | 'use strict' 95 | assert.isNull error 96 | assert.isNotNull response, 'Response' 97 | 98 | # Headers 99 | @response.headers = response.headers 100 | 101 | # Status code 102 | assert.equal response.statusCode, @response.status, """ 103 | Got unexpected response code: 104 | #{body} 105 | Error 106 | """ 107 | response.status = response.statusCode 108 | 109 | # Body 110 | if @response.schema 111 | schema = @response.schema 112 | validateJson = _.partial JSON.parse, body 113 | body = '[empty]' if body is '' 114 | assert.doesNotThrow validateJson, JSON.SyntaxError, """ 115 | Invalid JSON: 116 | #{body} 117 | Error 118 | """ 119 | 120 | json = validateJson() 121 | 122 | # Validate object against JSON schema 123 | checkRecursive = false 124 | banUnknown = false 125 | result = tv4.validateResult json, schema, checkRecursive, banUnknown 126 | 127 | assert.lengthOf result.missing, 0, """ 128 | Missing/unresolved JSON schema $refs (#{result.missing?.join(', ')}) in schema: 129 | #{JSON.stringify(schema, null, 4)} 130 | Error 131 | """ 132 | assert.ok result.valid, """ 133 | Got unexpected response body: #{result.error?.message} 134 | #{JSON.stringify(json, null, 4)} 135 | Error 136 | """ 137 | 138 | # Update @response 139 | @response.body = json 140 | 141 | 142 | module.exports = TestFactory 143 | 144 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "abao", 3 | "version": "0.5.3", 4 | "description": "RAML testing tool", 5 | "bin": "bin/abao", 6 | "main": "lib/index.js", 7 | "scripts": { 8 | "git-cz": "git-cz", 9 | "precommit": "npm test", 10 | "prepush": "npm test", 11 | "test": "grunt test" 12 | }, 13 | "config": { 14 | "commitizen": { 15 | "path": "./node_modules/cz-conventional-changelog" 16 | }, 17 | "yargs": { 18 | "camel-case-expansion": true 19 | } 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/cybertk/abao.git" 24 | }, 25 | "keywords": [ 26 | "api", 27 | "test", 28 | "testing", 29 | "documentation", 30 | "integration", 31 | "acceptance", 32 | "RAML", 33 | "automated" 34 | ], 35 | "author": "Quanlong", 36 | "contributors": [ 37 | "P. Roebuck (https://github.com/plroebuck/)" 38 | ], 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/cybertk/abao/issues/" 42 | }, 43 | "homepage": "https://github.com/cybertk/abao/", 44 | "directories": { 45 | "lib": "./lib", 46 | "test": "./test" 47 | }, 48 | "dependencies": { 49 | "async": "^2.0.1", 50 | "chai": "~3.5.0", 51 | "coffee-errors": "^0.8.6", 52 | "coffee-script": "1.12.7", 53 | "csonschema": "^0.5.1", 54 | "glob": "^7.0.6", 55 | "lodash": "^4.16.4", 56 | "mocha": "~5.0.4", 57 | "mustache": "~2.3.0", 58 | "proxyquire": "^2.0.0", 59 | "raml-parser": "^0.8.18", 60 | "request": "^2.85.0", 61 | "source-map-support": "^0.5.4", 62 | "tv4": "^1.2.7", 63 | "underscore": "^1.8.3", 64 | "yargs": "^16.2.0" 65 | }, 66 | "devDependencies": { 67 | "coffeelint-use-strict": "^1.0.0", 68 | "commitizen": "^2.9.6", 69 | "coveralls": "^2.11.14", 70 | "cz-conventional-changelog": "^2.1.0", 71 | "express": "^4.12.0", 72 | "grunt": "^0.4.5", 73 | "grunt-cli": "~1.2.0", 74 | "grunt-coffeecov": "git+https://github.com/plroebuck/grunt-coffeecov.git", 75 | "grunt-coffeelint": "^0.0.16", 76 | "grunt-contrib-clean": "^1.1.0", 77 | "grunt-contrib-watch": "^1.0.0", 78 | "grunt-coveralls": "^1.0.1", 79 | "grunt-markdownlint": "^1.1.1", 80 | "grunt-mocha-test": "~0.13.2", 81 | "grunt-shell": "^2.0.0", 82 | "husky": "^0.14.3", 83 | "load-grunt-config": "^0.19.2", 84 | "markdownlint": "^0.8.0", 85 | "mocha-phantom-coverage-reporter": "^0.1.0", 86 | "mute": "^1.0.0", 87 | "nock": "~9.1.6", 88 | "sinon": "~4.1.6", 89 | "sinon-chai": "^2.14.0", 90 | "time-grunt": "~1.4.0" 91 | }, 92 | "engines": { 93 | "node": ">= 4.8.7", 94 | "npm": ">= 2.15.11" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /templates/hookfile.js: -------------------------------------------------------------------------------- 1 | // 2 | // ABAO hooks file {{! Mustache template }} 3 | // Generated from RAML specification 4 | // RAML: {{ramlFile}} 5 | // Date: {{timestamp}} 6 | // 7 | // 8 | 9 | var 10 | hooks = require('hooks'), 11 | assert = require('chai').assert; 12 | 13 | // 14 | // Setup/Teardown 15 | // 16 | 17 | hooks.beforeAll(function (done) { 18 | done(); 19 | }); 20 | 21 | hooks.afterAll(function (done) { 22 | done(); 23 | }); 24 | 25 | 26 | // 27 | // Hooks 28 | // 29 | 30 | {{#hooks}} 31 | //----------------------------------------------------------------------------- 32 | hooks.before('{{{name}}}', function (test, done) { 33 | {{#comment}} 34 | // Modify 'test.request' properties here to modify the inbound request 35 | {{/comment}} 36 | done(); 37 | }); 38 | 39 | hooks.after('{{{name}}}', function (test, done) { 40 | {{#comment}} 41 | // Assert against 'test.response' properties here to verify expected results 42 | {{/comment}} 43 | done(); 44 | }); 45 | 46 | {{/hooks}} 47 | 48 | -------------------------------------------------------------------------------- /test/e2e/cli-test.coffee: -------------------------------------------------------------------------------- 1 | chai = require 'chai' 2 | child_process = require 'child_process' 3 | express = require 'express' 4 | _ = require 'lodash' 5 | pkg = require '../../package' 6 | 7 | expect = chai.expect 8 | 9 | HOSTNAME = 'localhost' 10 | PORT = 3333 11 | SERVER = "http://#{HOSTNAME}:#{PORT}" 12 | 13 | TEMPLATE_DIR = './templates' 14 | DFLT_TEMPLATE_FILE = "#{TEMPLATE_DIR}/hooks.js" 15 | FIXTURE_DIR = './test/fixtures' 16 | RAML_DIR = "#{FIXTURE_DIR}" 17 | HOOK_DIR = "#{FIXTURE_DIR}" 18 | SCHEMA_DIR = "#{FIXTURE_DIR}/schemas" 19 | 20 | CMD_PREFIX = '' 21 | ABAO_BIN = './bin/abao' 22 | MOCHA_BIN = './node_modules/mocha/bin/mocha' 23 | 24 | mochaJsonReportKeys = [ 25 | 'stats', 26 | 'tests', 27 | 'pending', 28 | 'failures', 29 | 'passes' 30 | ] 31 | 32 | stderr = '' 33 | stdout = '' 34 | report = '' 35 | exitStatus = null 36 | 37 | # 38 | # To dump individual raw test results: 39 | # 40 | # describe('show me the results', () -> 41 | # runTestAsync = (done) -> 42 | # cmd = "#{ABAO_BIN}" 43 | # execCommand cmd, done 44 | # before (done) -> 45 | # debugExecCommand = true 46 | # runTestAsync done 47 | # after () -> 48 | # debugExecCommand = false 49 | # 50 | debugExecCommand = false 51 | 52 | 53 | execCommand = (cmd, callback) -> 54 | 'use strict' 55 | stderr = '' 56 | stdout = '' 57 | report = '' 58 | exitStatus = null 59 | 60 | cli = child_process.exec CMD_PREFIX + cmd, (error, out, err) -> 61 | stdout = out 62 | stderr = err 63 | try 64 | report = JSON.parse out 65 | catch ignore 66 | # Ignore issues with creating report from output 67 | 68 | if error 69 | exitStatus = error.code 70 | 71 | cli.on 'close', (code) -> 72 | exitStatus = code if exitStatus == null and code != undefined 73 | if debugExecCommand 74 | console.log "stdout:\n#{stdout}\n" 75 | console.log "stderr:\n#{stderr}\n" 76 | console.log "report:\n#{report}\n" 77 | console.log "exitStatus = #{exitStatus}\n" 78 | callback() 79 | 80 | 81 | describe 'Command line interface', () -> 82 | 'use strict' 83 | 84 | describe 'when run without any arguments', (done) -> 85 | 86 | runNoArgTestAsync = (done) -> 87 | cmd = "#{ABAO_BIN}" 88 | 89 | execCommand cmd, done 90 | 91 | before (done) -> 92 | runNoArgTestAsync done 93 | 94 | it 'should print usage to stderr', () -> 95 | firstLine = stderr.split('\n')[0] 96 | expect(firstLine).to.equal('Usage:') 97 | 98 | it 'should print error message to stderr', () -> 99 | expect(stderr).to.include('must specify path to RAML file') 100 | 101 | it 'should exit due to error', () -> 102 | expect(exitStatus).to.equal(1) 103 | 104 | 105 | describe 'when run with multiple positional arguments', (done) -> 106 | 107 | runTooManyArgTestAsync = (done) -> 108 | ramlFile = "#{RAML_DIR}/machines-single_get.raml" 109 | cmd = "#{ABAO_BIN} #{ramlFile} #{ramlFile}" 110 | 111 | execCommand cmd, done 112 | 113 | before (done) -> 114 | runTooManyArgTestAsync done 115 | 116 | it 'should print usage to stderr', () -> 117 | firstLine = stderr.split('\n')[0] 118 | expect(firstLine).to.equal('Usage:') 119 | 120 | it 'should print error message to stderr', () -> 121 | expect(stderr).to.include('accepts single positional command-line argument') 122 | 123 | it 'should exit due to error', () -> 124 | expect(exitStatus).to.equal(1) 125 | 126 | 127 | describe 'when run with one-and-done options', (done) -> 128 | 129 | describe 'when RAML argument unnecessary', () -> 130 | 131 | describe 'when invoked with "--reporters" option', () -> 132 | 133 | reporters = '' 134 | 135 | runReportersTestAsync = (done) -> 136 | execCommand "#{MOCHA_BIN} --reporters", () -> 137 | reporters = stdout 138 | execCommand "#{ABAO_BIN} --reporters", done 139 | 140 | before (done) -> 141 | runReportersTestAsync done 142 | 143 | it 'should print same output as `mocha --reporters`', () -> 144 | expect(stdout).to.equal(reporters) 145 | 146 | it 'should exit normally', () -> 147 | expect(exitStatus).to.equal(0) 148 | 149 | 150 | describe 'when invoked with "--version" option', () -> 151 | 152 | runVersionTestAsync = (done) -> 153 | cmd = "#{ABAO_BIN} --version" 154 | 155 | execCommand cmd, done 156 | 157 | before (done) -> 158 | runVersionTestAsync done 159 | 160 | it 'should print version number to stdout', () -> 161 | expect(stdout.trim()).to.equal(pkg.version) 162 | 163 | it 'should exit normally', () -> 164 | expect(exitStatus).to.equal(0) 165 | 166 | 167 | describe 'when invoked with "--help" option', () -> 168 | 169 | runHelpTestAsync = (done) -> 170 | cmd = "#{ABAO_BIN} --help" 171 | 172 | execCommand cmd, done 173 | 174 | before (done) -> 175 | runHelpTestAsync done 176 | 177 | it 'should print usage to stdout', () -> 178 | firstLine = stdout.split('\n')[0] 179 | expect(firstLine).to.equal('Usage:') 180 | 181 | it 'should exit normally', () -> 182 | expect(exitStatus).to.equal(0) 183 | 184 | 185 | describe 'when RAML argument required', () -> 186 | 187 | describe 'when invoked with "--names" option', () -> 188 | 189 | runNamesTestAsync = (done) -> 190 | ramlFile = "#{RAML_DIR}/machines-single_get.raml" 191 | cmd = "#{ABAO_BIN} #{ramlFile} --names" 192 | 193 | execCommand cmd, done 194 | 195 | before (done) -> 196 | runNamesTestAsync done 197 | 198 | it 'should print names', () -> 199 | expect(stdout).to.include('GET /machines -> 200') 200 | 201 | it 'should not run tests', () -> 202 | expect(stdout).to.not.include('0 passing') 203 | 204 | it 'should exit normally', () -> 205 | expect(exitStatus).to.equal(0) 206 | 207 | 208 | describe 'when invoked with "--generate-hooks" option', () -> 209 | 210 | describe 'by itself (use package-provided template)', () -> 211 | 212 | runGenHooksTestAsync = (done) -> 213 | ramlFile = "#{RAML_DIR}/machines-single_get.raml" 214 | cmd = "#{ABAO_BIN} #{ramlFile} --generate-hooks" 215 | 216 | execCommand cmd, done 217 | 218 | before (done) -> 219 | runGenHooksTestAsync done 220 | 221 | it 'should print skeleton hookfile', () -> 222 | expect(stdout).to.include('// ABAO hooks file') 223 | 224 | it 'should not run tests', () -> 225 | expect(stdout).to.not.include('0 passing') 226 | 227 | it 'should exit normally', () -> 228 | expect(exitStatus).to.equal(0) 229 | 230 | 231 | describe 'with "--template" option', () -> 232 | 233 | runGenHookTemplateTestAsync = (done) -> 234 | templateFile = "#{TEMPLATE_DIR}/hookfile.js" 235 | ramlFile = "#{RAML_DIR}/machines-single_get.raml" 236 | cmd = "#{ABAO_BIN} #{ramlFile} --generate-hooks --template #{templateFile}" 237 | 238 | execCommand cmd, done 239 | 240 | before (done) -> 241 | runGenHookTemplateTestAsync done 242 | 243 | it 'should print skeleton hookfile', () -> 244 | expect(stdout).to.include('// ABAO hooks file') 245 | 246 | it 'should not run tests', () -> 247 | expect(stdout).to.not.include('0 passing') 248 | 249 | it 'should exit normally', () -> 250 | expect(exitStatus).to.equal(0) 251 | 252 | 253 | describe 'when invoked with "--template" but without "--generate-hooks" option', () -> 254 | 255 | runTemplateOnlyTestAsync = (done) -> 256 | templateFile = "#{TEMPLATE_DIR}/hookfile.js" 257 | ramlFile = "#{RAML_DIR}/machines-single_get.raml" 258 | cmd = "#{ABAO_BIN} #{ramlFile} --template #{templateFile}" 259 | 260 | execCommand cmd, done 261 | 262 | before (done) -> 263 | runTemplateOnlyTestAsync done 264 | 265 | it 'should print error message to stderr', () -> 266 | expect(stderr).to.include('Implications failed:') 267 | expect(stderr).to.include('template -> generate-hooks') 268 | 269 | it 'should exit due to error', () -> 270 | expect(exitStatus).to.equal(1) 271 | 272 | 273 | describe 'when RAML file not found', (done) -> 274 | 275 | runNoRamlTestAsync = (done) -> 276 | ramlFile = "#{RAML_DIR}/nonexistent_path.raml" 277 | cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER}" 278 | 279 | execCommand cmd, done 280 | 281 | before (done) -> 282 | runNoRamlTestAsync done 283 | 284 | it 'should print error message to stderr', () -> 285 | # See https://travis-ci.org/cybertk/abao/jobs/76656192#L479 286 | # iojs behaviour is different from nodejs 287 | expect(stderr).to.include('Error: ENOENT') 288 | 289 | it 'should exit due to error', () -> 290 | expect(exitStatus).to.equal(1) 291 | 292 | 293 | describe 'arguments with existing RAML and responding server', () -> 294 | 295 | describe 'when invoked without "--server" option', () -> 296 | 297 | describe 'when RAML file does not specify "baseUri"', () -> 298 | 299 | runUnspecifiedServerTestAsync = (done) -> 300 | ramlFile = "#{RAML_DIR}/music-no_base_uri.raml" 301 | cmd = "#{ABAO_BIN} #{ramlFile} --reporter json" 302 | 303 | execCommand cmd, done 304 | 305 | before (done) -> 306 | runUnspecifiedServerTestAsync done 307 | 308 | it 'should print error message to stderr', () -> 309 | expect(stderr).to.include('no API endpoint specified') 310 | 311 | it 'should exit due to error', () -> 312 | expect(exitStatus).to.equal(1) 313 | 314 | 315 | describe 'when RAML file specifies "baseUri"', () -> 316 | 317 | resTestTitle = 'GET /machines -> 200 Validate response code and body' 318 | 319 | runBaseUriServerTestAsync = (done) -> 320 | ramlFile = "#{RAML_DIR}/machines-single_get.raml" 321 | cmd = "#{ABAO_BIN} #{ramlFile} --reporter json" 322 | 323 | app = express() 324 | 325 | app.get '/machines', (req, res) -> 326 | machine = 327 | type: 'bulldozer' 328 | name: 'willy' 329 | res.status(200).json([machine]) 330 | 331 | server = app.listen PORT, () -> 332 | execCommand cmd, () -> 333 | server.close() 334 | 335 | server.on 'close', done 336 | 337 | before (done) -> 338 | runBaseUriServerTestAsync done 339 | 340 | it 'should print count of tests run', () -> 341 | expect(report).to.exist 342 | expect(report).to.have.all.keys(mochaJsonReportKeys) 343 | expect(report.stats.tests).to.equal(1) 344 | expect(report.stats.passes).to.equal(1) 345 | 346 | it 'should print correct title for response', () -> 347 | expect(report.tests).to.have.length(1) 348 | expect(report.tests[0].fullTitle).to.equal(resTestTitle) 349 | 350 | it 'should exit normally', () -> 351 | expect(exitStatus).to.equal(0) 352 | 353 | 354 | describe 'when executing the command and the server is responding as specified in the RAML', () -> 355 | 356 | responses = {} 357 | getResponse = undefined 358 | headResponse = undefined 359 | optionsResponse = undefined 360 | 361 | getTestTitle = 'GET /machines -> 200 Validate response code and body' 362 | headTestTitle = 'HEAD /machines -> 200 Validate response code only' 363 | optionsTestTitle = 'OPTIONS /machines -> 204 Validate response code only' 364 | 365 | runNormalTestAsync = (done) -> 366 | ramlFile = "#{RAML_DIR}/machines-get_head_options.raml" 367 | cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --reporter json" 368 | 369 | app = express() 370 | 371 | app.use (req, res, next) -> 372 | origResWrite = res.write 373 | origResEnd = res.end 374 | chunks = [] 375 | res.write = (chunk) -> 376 | chunks.push new Buffer(chunk) 377 | origResWrite.apply res, arguments 378 | res.end = (chunk) -> 379 | if (chunk) 380 | chunks.push new Buffer(chunk) 381 | res.body = Buffer.concat(chunks).toString('utf8') 382 | origResEnd.apply res, arguments 383 | next() 384 | 385 | app.options '/machines', (req, res, next) -> 386 | allow = ['OPTIONS', 'HEAD', 'GET'] 387 | directives = ['no-cache', 'no-store', 'must-revalidate'] 388 | res.setHeader 'Allow', allow.join ',' 389 | res.setHeader 'Cache-Control', directives.join ',' 390 | res.setHeader 'Pragma', directives[0] 391 | res.setHeader 'Expires', '0' 392 | res.status(204).end() 393 | next() 394 | 395 | app.get '/machines', (req, res, next) -> 396 | machine = 397 | type: 'bulldozer' 398 | name: 'willy' 399 | res.status(200).json([machine]) 400 | next() 401 | 402 | app.use (req, res, next) -> 403 | response = 404 | headers: {}, 405 | body: res.body 406 | headerNames = do () -> 407 | if req.method == 'OPTIONS' 408 | return [ 409 | 'Allow', 410 | 'Cache-Control', 411 | 'Expires', 412 | 'Pragma' 413 | ] 414 | else 415 | return [ 416 | 'Content-Type', 417 | 'Content-Length', 418 | 'ETag' 419 | ] 420 | headerNames.forEach (headerName) -> 421 | response.headers[headerName] = res.get headerName 422 | responses[req.method] = _.cloneDeep(response) 423 | 424 | server = app.listen PORT, () -> 425 | execCommand cmd, () -> 426 | server.close() 427 | 428 | server.on 'close', done 429 | 430 | before (done) -> 431 | runNormalTestAsync done 432 | 433 | before () -> 434 | getResponse = responses['GET'] 435 | headResponse = responses['HEAD'] 436 | optionsResponse = responses['OPTIONS'] 437 | 438 | it 'should provide count of tests run', () -> 439 | expect(report).to.exist 440 | expect(report).to.have.all.keys(mochaJsonReportKeys) 441 | expect(report.stats.tests).to.equal(3) 442 | 443 | it 'should provide count of tests passing', () -> 444 | expect(report.stats.passes).to.equal(3) 445 | 446 | it 'should print correct title for each response', () -> 447 | expect(report.tests).to.have.length(3) 448 | expect(report.tests[0].fullTitle).to.equal(getTestTitle) 449 | expect(report.tests[1].fullTitle).to.equal(headTestTitle) 450 | expect(report.tests[2].fullTitle).to.equal(optionsTestTitle) 451 | 452 | it 'OPTIONS response should allow GET and HEAD requests', () -> 453 | allow = optionsResponse.headers['Allow'] 454 | expect(allow).to.equal('OPTIONS,HEAD,GET') 455 | 456 | it 'OPTIONS response should disable caching of it', () -> 457 | cacheControl = optionsResponse.headers['Cache-Control'] 458 | expect(cacheControl).to.equal('no-cache,no-store,must-revalidate') 459 | pragma = optionsResponse.headers['Pragma'] 460 | expect(pragma).to.equal('no-cache') 461 | expires = optionsResponse.headers['Expires'] 462 | expect(expires).to.equal('0') 463 | 464 | it 'OPTIONS and HEAD responses should not have bodies', () -> 465 | expect(optionsResponse.body).to.be.empty 466 | expect(headResponse.body).to.be.empty 467 | 468 | it 'GET and HEAD responses should have equivalent headers', () -> 469 | expect(getResponse.headers).to.deep.equal(headResponse.headers) 470 | 471 | it 'should exit normally', () -> 472 | expect(exitStatus).to.equal(0) 473 | 474 | 475 | describe 'when executing the command and RAML includes other RAML files', () -> 476 | 477 | runRamlIncludesTestAsync = (done) -> 478 | ramlFile = "#{RAML_DIR}/machines-include_other_raml.raml" 479 | cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER}" 480 | 481 | app = express() 482 | 483 | app.get '/machines', (req, res) -> 484 | machine = 485 | type: 'bulldozer' 486 | name: 'willy' 487 | res.status(200).json([machine]) 488 | 489 | server = app.listen PORT, () -> 490 | execCommand cmd, () -> 491 | server.close() 492 | 493 | server.on 'close', done 494 | 495 | before (done) -> 496 | runRamlIncludesTestAsync done 497 | 498 | it 'should print count of passing tests run', () -> 499 | expect(stdout).to.have.string('1 passing') 500 | 501 | it 'should exit normally', () -> 502 | expect(exitStatus).to.equal(0) 503 | 504 | 505 | describe 'when called with arguments', () -> 506 | 507 | describe 'when invoked with "--reporter" option', () -> 508 | 509 | runReporterTestAsync = (done) -> 510 | ramlFile = "#{RAML_DIR}/machines-single_get.raml" 511 | cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --reporter spec" 512 | 513 | app = express() 514 | 515 | app.get '/machines', (req, res) -> 516 | machine = 517 | type: 'bulldozer' 518 | name: 'willy' 519 | res.status(200).json([machine]) 520 | 521 | server = app.listen PORT, () -> 522 | execCommand cmd, () -> 523 | server.close() 524 | 525 | server.on 'close', done 526 | 527 | before (done) -> 528 | runReporterTestAsync done 529 | 530 | it 'should print using the specified reporter', () -> 531 | expect(stdout).to.have.string('1 passing') 532 | 533 | it 'should exit normally', () -> 534 | expect(exitStatus).to.equal(0) 535 | 536 | 537 | describe 'when invoked with "--header" option', () -> 538 | 539 | receivedRequest = {} 540 | producedMediaType = 'application/vnd.api+json' 541 | reqMediaType = undefined 542 | extraHeader = undefined 543 | 544 | describe 'with "Accept" header', () -> 545 | 546 | runAcceptHeaderTestAsync = (done) -> 547 | extraHeader = "Accept:#{reqMediaType}" 548 | ramlFile = "#{RAML_DIR}/machines-single_get.raml" 549 | cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --header #{extraHeader}" 550 | 551 | app = express() 552 | 553 | app.use (req, res, next) -> 554 | receivedRequest = req 555 | next() 556 | 557 | app.use (req, res, next) -> 558 | err = null 559 | if !req.accepts ["#{producedMediaType}"] 560 | err = new Error('Not Acceptable') 561 | err.status = 406 562 | next(err) 563 | 564 | app.get '/machines', (req, res) -> 565 | machine = 566 | type: 'bulldozer' 567 | name: 'willy' 568 | res.type "#{producedMediaType}" 569 | res.status(200).send([machine]) 570 | 571 | app.use (err, req, res, next) -> 572 | res.status(err.status || 500) 573 | .json({ 574 | message: err.message, 575 | stack: err.stack 576 | }) 577 | return 578 | 579 | server = app.listen PORT, () -> 580 | execCommand cmd, () -> 581 | server.close() 582 | 583 | server.on 'close', done 584 | 585 | context 'when expecting success', () -> 586 | 587 | before (done) -> 588 | reqMediaType = "#{producedMediaType}" 589 | runAcceptHeaderTestAsync done 590 | 591 | it 'should have the additional header in the request', () -> 592 | expect(receivedRequest.headers.accept).to.equal("#{reqMediaType}") 593 | 594 | it 'should print count of passing tests run', () -> 595 | expect(stdout).to.have.string('1 passing') 596 | 597 | it 'should exit normally', () -> 598 | expect(exitStatus).to.equal(0) 599 | 600 | 601 | context 'when expecting failure', () -> 602 | 603 | before (done) -> 604 | reqMediaType = 'application/json' 605 | runAcceptHeaderTestAsync done 606 | 607 | it 'should have the additional header in the request', () -> 608 | expect(receivedRequest.headers.accept).to.equal("#{reqMediaType}") 609 | 610 | # Errors thrown by Mocha show up in stdout; those by Abao in stderr. 611 | it 'Mocha should throw an error', () -> 612 | detail = "Error: expected 406 to equal '200'" 613 | expect(stdout).to.have.string(detail) 614 | 615 | it 'should run test but not complete', () -> 616 | expect(stdout).to.have.string('1 failing') 617 | 618 | it 'should exit due to error', () -> 619 | expect(exitStatus).to.equal(1) 620 | 621 | 622 | describe 'when invoked with "--hookfiles" option', () -> 623 | 624 | receivedRequest = {} 625 | 626 | runHookfilesTestAsync = (done) -> 627 | pattern = "#{HOOK_DIR}/*_hooks.*" 628 | ramlFile = "#{RAML_DIR}/machines-single_get.raml" 629 | cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --hookfiles=#{pattern}" 630 | 631 | app = express() 632 | 633 | app.use (req, res, next) -> 634 | receivedRequest = req 635 | next() 636 | 637 | app.get '/machines', (req, res) -> 638 | machine = 639 | type: 'bulldozer' 640 | name: 'willy' 641 | res.status(200).json([machine]) 642 | 643 | server = app.listen PORT, () -> 644 | execCommand cmd, () -> 645 | server.close() 646 | 647 | server.on 'close', done 648 | 649 | before (done) -> 650 | runHookfilesTestAsync done 651 | 652 | it 'should modify the transaction with hooks', () -> 653 | expect(receivedRequest.headers['header']).to.equal('123232323') 654 | expect(receivedRequest.query['key']).to.equal('value') 655 | 656 | it 'should print message to stdout and stderr', () -> 657 | expect(stdout).to.include('before-hook-GET-machines') 658 | expect(stderr).to.include('after-hook-GET-machines') 659 | 660 | it 'should exit normally', () -> 661 | expect(exitStatus).to.equal(0) 662 | 663 | 664 | describe 'when invoked with "--hooks-only" option', () -> 665 | 666 | runHooksOnlyTestAsync = (done) -> 667 | ramlFile = "#{RAML_DIR}/machines-single_get.raml" 668 | cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --hooks-only" 669 | 670 | app = express() 671 | 672 | app.get '/machines', (req, res) -> 673 | machine = 674 | type: 'bulldozer' 675 | name: 'willy' 676 | res.status(200).json([machine]) 677 | 678 | server = app.listen PORT, () -> 679 | execCommand cmd, () -> 680 | server.close() 681 | 682 | server.on 'close', done 683 | 684 | before (done) -> 685 | runHooksOnlyTestAsync done 686 | 687 | it 'should not run test without hooks', () -> 688 | expect(stdout).to.have.string('1 pending') 689 | 690 | it 'should exit normally', () -> 691 | expect(exitStatus).to.equal(0) 692 | 693 | 694 | describe 'when invoked with "--timeout" option', () -> 695 | 696 | timeout = undefined 697 | elapsed = -1 698 | finished = undefined 699 | 700 | runTimeoutTestAsync = (done) -> 701 | ramlFile = "#{RAML_DIR}/machines-single_get.raml" 702 | cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --timeout #{timeout}" 703 | 704 | beginTime = undefined 705 | finished = false 706 | 707 | app = express() 708 | 709 | app.use (req, res, next) -> 710 | beginTime = new Date() 711 | res.on 'finish', () -> 712 | finished = true 713 | next() 714 | 715 | app.use (req, res, next) -> 716 | delay = timeout * 2 717 | setTimeout next, delay 718 | 719 | app.get '/machines', (req, res) -> 720 | machine = 721 | type: 'bulldozer' 722 | name: 'willy' 723 | res.status(200).json([machine]) 724 | 725 | server = app.listen PORT, () -> 726 | execCommand cmd, () -> 727 | endTime = new Date() 728 | if finished 729 | elapsed = endTime - beginTime 730 | console.log "elapsed = #{elapsed} msecs (req/res)" 731 | server.close() 732 | 733 | server.on 'close', done 734 | 735 | 736 | context 'given insufficient time to complete', () -> 737 | 738 | before (done) -> 739 | timeout = 20 740 | console.log "timeout = #{timeout} msecs" 741 | runTimeoutTestAsync done 742 | 743 | after () -> 744 | finished = undefined 745 | 746 | it 'should not finish before timeout occurs', () -> 747 | expect(finished).to.be.false 748 | 749 | # Errors thrown by Mocha show up in stdout; those by Abao in stderr. 750 | it 'Mocha should throw an error', () -> 751 | detail = "Error: Timeout of #{timeout}ms exceeded." 752 | expect(stdout).to.have.string(detail) 753 | 754 | it 'should run test but not complete', () -> 755 | expect(stdout).to.have.string('1 failing') 756 | 757 | it 'should exit due to error', () -> 758 | expect(exitStatus).to.equal(1) 759 | 760 | 761 | describe 'when invoked with "--schema" option', () -> 762 | 763 | runSchemaTestAsync = (done) -> 764 | pattern = "#{SCHEMA_DIR}/*.json" 765 | ramlFile = "#{RAML_DIR}/machines-with_json_refs.raml" 766 | cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --schemas=#{pattern}" 767 | 768 | app = express() 769 | 770 | app.get '/machines', (req, res) -> 771 | machine = 772 | type: 'bulldozer' 773 | name: 'willy' 774 | res.status(200).json([machine]) 775 | 776 | server = app.listen PORT, () -> 777 | execCommand cmd, () -> 778 | server.close() 779 | 780 | server.on 'close', done 781 | 782 | before (done) -> 783 | runSchemaTestAsync done 784 | 785 | it 'should exit normally', () -> 786 | expect(exitStatus).to.equal(0) 787 | 788 | 789 | describe 'when expecting validation to fail', () -> 790 | 791 | runSchemaFailTestAsync = (done) -> 792 | pattern = "#{SCHEMA_DIR}/*.json" 793 | ramlFile = "#{RAML_DIR}/machines-with_json_refs.raml" 794 | cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --schemas=#{pattern}" 795 | 796 | app = express() 797 | 798 | app.get '/machines', (req, res) -> 799 | machine = 800 | typO: 'bulldozer' # 'type' != 'typO' 801 | name: 'willy' 802 | res.status(200).json([machine]) 803 | 804 | server = app.listen PORT, () -> 805 | execCommand cmd, () -> 806 | server.close() 807 | 808 | server.on 'close', done 809 | 810 | before (done) -> 811 | runSchemaFailTestAsync done 812 | 813 | it 'should exit due to error', () -> 814 | expect(exitStatus).to.equal(1) 815 | 816 | -------------------------------------------------------------------------------- /test/fixtures/contacts.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | 3 | title: Address Book API 4 | baseUri: http://example.api.com/{version} 5 | version: v1 6 | 7 | /contacts: 8 | post: 9 | description: Creates a new contact 10 | body: 11 | application/json: 12 | schema: | 13 | type: 'string' 14 | name: 'string' 15 | example: | 16 | { "type": "Kulu", "name": "Mike" } 17 | responses: 18 | 201: 19 | headers: 20 | location: 21 | description: URI of the newly created contact 22 | example: /contacts/{contact_id} 23 | body: 24 | application/json: 25 | schema: | 26 | type: 'string' 27 | name: 'string' 28 | example: | 29 | { "type": "Kulu", "name": "Mike" } 30 | /contacts/{contact_id} 31 | delete: 32 | description: Deletes an existing contact by `contact_id` 33 | responses: 34 | 204: 35 | put: 36 | description: Replaces an existing contact by `contact_id` 37 | body: 38 | application/json: 39 | schema: | 40 | type: 'string' 41 | name: 'string' 42 | example: | 43 | { "type": "Kulu", "name": "Mike" } 44 | responses: 45 | 201: 46 | body: 47 | application/json: 48 | schema: | 49 | type: 'string' 50 | name: 'string' 51 | example: | 52 | { "type": "Kulu", "name": "Mike" } 53 | get: 54 | description: Gets an existing contact by `contact_id` 55 | responses: 56 | 200: 57 | body: 58 | application/json: 59 | schema: | 60 | [ 61 | type: 'string' 62 | name: 'string' 63 | phone: 'string' 64 | ] 65 | example: | 66 | { 67 | "type": "contact", 68 | "name": "Jenny", 69 | "phone": "867-5309" 70 | } 71 | 72 | -------------------------------------------------------------------------------- /test/fixtures/machines-1_get_1_post.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | 3 | title: Machines API 4 | baseUri: http://example.api.com/{version} 5 | version: v1 6 | 7 | /machines: 8 | get: 9 | description: Get a list of existing machines 10 | responses: 11 | 200: 12 | body: 13 | application/json: 14 | schema: | 15 | [ 16 | type: 'string' 17 | name: 'string' 18 | ] 19 | example: | 20 | { "type": "Kulu", "name": "Mike" } 21 | post: 22 | description: Creates a new machine 23 | body: 24 | application/json: 25 | schema: | 26 | type: 'string' 27 | name: 'string' 28 | example: | 29 | { "type": "Kulu", "name": "Mike" } 30 | responses: 31 | 201: 32 | headers: 33 | location: 34 | description: URI of the newly created machine 35 | example: /machines/{id} 36 | body: 37 | application/json: 38 | schema: | 39 | type: 'string' 40 | name: 'string' 41 | example: | 42 | { "type": "Kulu", "name": "Mike" } 43 | 44 | -------------------------------------------------------------------------------- /test/fixtures/machines-get_head_options.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | 3 | title: Machines API 4 | baseUri: http://localhost:3333 5 | 6 | /machines: 7 | get: 8 | description: Gets a list of existing machines 9 | responses: 10 | 200: 11 | body: 12 | application/json: 13 | schema: | 14 | [ 15 | type: 'string' 16 | name: 'string' 17 | ] 18 | example: | 19 | { "type": "Kulu", "name": "Mike" } 20 | 21 | head: 22 | description: Requests the headers that are returned from HTTP GET method 23 | responses: 24 | 200: 25 | headers: 26 | Content-Type: 27 | description: Media type of response body 28 | type: string 29 | required: true 30 | example: application/json; charset=utf-8 31 | Content-Length: 32 | description: Length of response body 33 | type: string 34 | required: true 35 | example: 37 36 | ETag: 37 | description: Identifier for this version of the resource 38 | type: string 39 | required: true 40 | example: W/"25-QoLpNeXVKDaodKGK5d2ua9ZMNAc" 41 | body: null 42 | 43 | options: 44 | description: Describes the communication options for this resource. 45 | responses: 46 | 204: 47 | headers: 48 | Allow: 49 | description: Which HTTP methods can be used with `machines` 50 | type: string 51 | required: true 52 | example: OPTIONS, HEAD, GET 53 | Cache-Control: 54 | description: Defines caching policy for OPTIONS requests 55 | type: string 56 | required: true 57 | example: no-cache, no-store, must-revalidate 58 | body: null 59 | 60 | -------------------------------------------------------------------------------- /test/fixtures/machines-include_other_raml.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | 3 | title: Machines API 4 | baseUri: http://example.api.com/{version} 5 | version: v1 6 | 7 | securitySchemes: 8 | - oauth_2_0: !include ./oauth_2_0.yml 9 | 10 | /machines: 11 | get: 12 | description: Gets a list of existing machines 13 | responses: 14 | 200: 15 | body: 16 | application/json: 17 | schema: | 18 | [ 19 | type: 'string' 20 | name: 'string' 21 | ] 22 | example: | 23 | { "type": "Kulu", "name": "Mike" } 24 | 25 | -------------------------------------------------------------------------------- /test/fixtures/machines-inline_and_included_schemas.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | 3 | title: Machines API 4 | baseUri: http://example.api.com/{version} 5 | version: v1 6 | 7 | schemas: 8 | - type1: !include schemas/type1.json 9 | - type2: !include schemas/type2.json 10 | 11 | /machines: 12 | get: 13 | description: Gets a list of existing machines 14 | responses: 15 | 200: 16 | body: 17 | application/json: 18 | schema: | 19 | { 20 | "type": "object", 21 | "$schema": "http://json-schema.org/draft-03/schema", 22 | "properties": { 23 | "type": {"$ref": "type2"}, 24 | "name": "string" 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /test/fixtures/machines-no_method.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | 3 | title: Machines API 4 | baseUri: http://example.api.com/{version} 5 | version: v1 6 | 7 | /root: 8 | /machines: 9 | get: 10 | description: Gets a list of existing machines 11 | responses: 12 | 200: 13 | body: 14 | application/json: 15 | schema: | 16 | [ 17 | type: 'string' 18 | name: 'string' 19 | ] 20 | example: | 21 | { "type": "Kulu", "name": "Mike" } 22 | 23 | -------------------------------------------------------------------------------- /test/fixtures/machines-non_required_query_parameter.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | 3 | title: Machines API 4 | baseUri: http://example.api.com/{version} 5 | version: v1 6 | 7 | /machines: 8 | get: 9 | description: Gets a list of existing machines 10 | queryParameters: 11 | quux: 12 | type: string 13 | required: false 14 | example: foo 15 | responses: 16 | 200: 17 | body: 18 | application/json: 19 | schema: | 20 | [ 21 | type: 'string' 22 | name: 'string' 23 | ] 24 | example: | 25 | { "type": "Kulu", "name": "Mike" } 26 | 27 | -------------------------------------------------------------------------------- /test/fixtures/machines-ref_other_schemas.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | 3 | title: Machines API 4 | baseUri: http://example.api.com/{version} 5 | version: v1 6 | 7 | schemas: 8 | - type1: !include schemas/type1.json 9 | - type2: !include schemas/type2.json 10 | 11 | /machines: 12 | get: 13 | description: Gets a list of existing machines 14 | responses: 15 | 200: 16 | body: 17 | application/json: 18 | schema: type2 19 | 20 | -------------------------------------------------------------------------------- /test/fixtures/machines-required_query_parameter.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | 3 | title: Machines API 4 | baseUri: http://example.api.com/{version} 5 | version: v1 6 | 7 | /machines: 8 | get: 9 | description: Gets a list of existing machines 10 | queryParameters: 11 | quux: 12 | type: string 13 | required: true 14 | example: foo 15 | responses: 16 | 200: 17 | body: 18 | application/json: 19 | schema: | 20 | [ 21 | type: 'string' 22 | name: 'string' 23 | ] 24 | example: | 25 | { "type": "Kulu", "name": "Mike" } 26 | 27 | -------------------------------------------------------------------------------- /test/fixtures/machines-single_get.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | 3 | title: Machines API 4 | baseUri: http://localhost:3333 5 | 6 | /machines: 7 | get: 8 | description: Gets a list of existing machines 9 | headers: 10 | Abao-API-Key: 11 | type: string 12 | example: abcdef 13 | responses: 14 | 200: 15 | body: 16 | application/json: 17 | schema: | 18 | [ 19 | type: 'string' 20 | name: 'string' 21 | ] 22 | example: | 23 | { "type": "Kulu", "name": "Mike" } 24 | 25 | -------------------------------------------------------------------------------- /test/fixtures/machines-three_levels.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | 3 | title: Machines API 4 | baseUri: http://example.api.com/{version} 5 | version: v1 6 | 7 | /machines: 8 | get: 9 | description: Gets a list of existing machines 10 | responses: 11 | 200: 12 | body: 13 | application/json: 14 | schema: | 15 | [ 16 | type: 'string' 17 | name: 'string' 18 | ] 19 | /{machine_id}: 20 | uriParameters: 21 | machine_id: 22 | description: | 23 | The ID of the Machine 24 | type: string 25 | example: '1' 26 | delete: 27 | description: Delete a machine by `machine_id` 28 | responses: 29 | 204: 30 | /parts: 31 | get: 32 | description: Gets a list of machine `machine_id`'s parts 33 | responses: 34 | 200: 35 | body: 36 | application/json: 37 | schema: | 38 | type: 'string' 39 | name: 'string' 40 | 41 | -------------------------------------------------------------------------------- /test/fixtures/machines-with_json_refs.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | 3 | title: Machines API 4 | version: v1 5 | resourceTypes: 6 | - resource: 7 | get: 8 | description: Get <> by Identifier 9 | headers: 10 | Abao-API-Key: 11 | type: string 12 | example: abcdef 13 | responses: 14 | 200: 15 | body: 16 | application/json: 17 | schema: <> 18 | /machines: 19 | type: 20 | resource: 21 | resourceSchema: !include schemas/with-json-refs.json 22 | 23 | -------------------------------------------------------------------------------- /test/fixtures/music-no_base_uri.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | 3 | title: World Music API 4 | version: v1 5 | traits: 6 | - paged: 7 | queryParameters: 8 | pages: 9 | description: The number of pages to return 10 | type: number 11 | /songs: 12 | is: [ paged ] 13 | get: 14 | description: Gets a list of existing songs 15 | queryParameters: 16 | genre: 17 | description: filter the songs by genre 18 | post: 19 | description: Adds a new song 20 | /{songId}: 21 | get: 22 | description: Gets an existing song by `songId` 23 | responses: 24 | 200: 25 | body: 26 | application/json: 27 | schema: | 28 | { "$schema": "http://json-schema.org/schema", 29 | "type": "object", 30 | "description": "A canonical song", 31 | "properties": { 32 | "title": { "type": "string" }, 33 | "artist": { "type": "string" } 34 | }, 35 | "required": [ "title", "artist" ] 36 | } 37 | example: | 38 | { "title": "A Beautiful Day", "artist": "Mike" } 39 | application/xml: 40 | delete: 41 | description: Deletes an existing song by `songId` 42 | 43 | -------------------------------------------------------------------------------- /test/fixtures/music-simple.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | 3 | title: World Music API 4 | baseUri: http://example.api.com/{version} 5 | version: v1 6 | traits: 7 | - paged: 8 | queryParameters: 9 | pages: 10 | description: The number of pages to return 11 | type: number 12 | /songs: 13 | is: [ paged ] 14 | get: 15 | description: Gets a list of existing songs 16 | queryParameters: 17 | genre: 18 | description: filter the songs by genre 19 | post: 20 | description: Adds a new song 21 | /{songId}: 22 | get: 23 | description: Gets an existing song by `songId` 24 | responses: 25 | 200: 26 | body: 27 | application/json: 28 | schema: | 29 | { "$schema": "http://json-schema.org/schema", 30 | "type": "object", 31 | "description": "A canonical song", 32 | "properties": { 33 | "title": { "type": "string" }, 34 | "artist": { "type": "string" } 35 | }, 36 | "required": [ "title", "artist" ] 37 | } 38 | example: | 39 | { "title": "A Beautiful Day", "artist": "Mike" } 40 | application/xml: 41 | delete: 42 | description: Deletes an existing song by `songId` 43 | 44 | -------------------------------------------------------------------------------- /test/fixtures/music-vendor_content_type.raml: -------------------------------------------------------------------------------- 1 | #%RAML 0.8 2 | 3 | title: World Music API 4 | baseUri: http://example.api.com/{version} 5 | version: v1 6 | 7 | /{songId}: 8 | uriParameters: 9 | songId: 10 | example: "mike-a-beautiful-day" 11 | patch: 12 | description: Edits an existing song by `songId` 13 | body: 14 | application/vnd.api+json: 15 | schema: | 16 | { "$schema": "http://json-schema.org/schema", 17 | "type": "object", 18 | "description": "A canonical song", 19 | "properties": { 20 | "title": { "type": "string" }, 21 | "artist": { "type": "string" } 22 | }, 23 | "required": [ "title", "artist" ] 24 | } 25 | example: | 26 | { "title": "A Beautiful Day", "artist": "Mike" } 27 | text/plain: 28 | responses: 29 | 200: 30 | body: 31 | application/vnd.api+json: 32 | schema: | 33 | { "$schema": "http://json-schema.org/schema", 34 | "type": "object", 35 | "description": "A canonical song", 36 | "properties": { 37 | "title": { "type": "string" }, 38 | "artist": { "type": "string" } 39 | }, 40 | "required": [ "title", "artist" ] 41 | } 42 | example: | 43 | { "title": "A Beautiful Day", "artist": "Mike" } 44 | text/plain: 45 | 46 | -------------------------------------------------------------------------------- /test/fixtures/oauth_2_0.yml: -------------------------------------------------------------------------------- 1 | # http://raml.link/securitySchemas/oauth_2_0.yml 2 | 3 | description: | 4 | OAuth 2.0 for authenticating all API requests. 5 | 6 | type: OAuth 2.0 7 | describedBy: 8 | headers: 9 | Authorization: 10 | description: | 11 | Used to send a valid OAuth 2 access token. Do not use with the "access_token" query string parameter. 12 | type: string 13 | queryParameters: 14 | access_token: 15 | description: | 16 | Used to send a valid OAuth 2 access token. Do not use together with the "Authorization" header 17 | type: string 18 | responses: 19 | 401: 20 | description: | 21 | Bad or expired token. This can happen if the user or Platform revoked or expired an access token. To fix, you should re-authenticate the user. 22 | 403: 23 | description: | 24 | Bad OAuth request (wrong consumer key, bad nonce, expired timestamp...). Unfortunately, re-authenticating the user won't help here. 25 | 26 | settings: 27 | authorizationUri: http://raml.link/oauth2/authorize 28 | accessTokenUri: http://raml.link/oauth2/token 29 | authorizationGrants: [ code, token ] 30 | -------------------------------------------------------------------------------- /test/fixtures/schemas/definitions.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "http://example.api.com/jsonrefs/definitions#", 4 | "title": "Machine", 5 | "definitions": { 6 | "machine": { 7 | "title": "Machine object", 8 | "description": "Used to describe a machine", 9 | "type": "object", 10 | "properties": { 11 | "type": { "type": "string" }, 12 | "name": { "type": "string" } 13 | }, 14 | "additionalProperties": false 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/fixtures/schemas/type1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-03/schema", 3 | "type": "object", 4 | "description": "A type", 5 | "properties": { 6 | "type": "string", 7 | "name": "string", 8 | "chick": { "type": "string"} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/schemas/type2.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-03/schema", 3 | "type": "object", 4 | "description": "Another type", 5 | "properties": { 6 | "chick": { "type": "string"} 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /test/fixtures/schemas/with-json-refs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "http://example.api.com/jsonrefs/machine#", 4 | "title": "Machine-Array", 5 | "oneOf": [ 6 | { 7 | "type": "array", 8 | "items": { 9 | "$ref": "http://example.api.com/jsonrefs/definitions#/definitions/machine" 10 | } 11 | } 12 | ], 13 | "additionalProperties": false 14 | } -------------------------------------------------------------------------------- /test/fixtures/test2_hooks.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | var hooks; 3 | 4 | hooks = require('hooks'); 5 | 6 | hooks.before('GET /machines -> 200', function(test, done) { 7 | test.request.query['key'] = 'value'; 8 | test.request.headers['header'] = '123232323'; 9 | console.log('before-hook-GET-machines'); 10 | return done(); 11 | }); 12 | -------------------------------------------------------------------------------- /test/fixtures/test_hooks.coffee: -------------------------------------------------------------------------------- 1 | {after} = require 'hooks' 2 | 3 | after 'GET /machines -> 200', (test, done) -> 4 | 'use strict' 5 | console.error 'after-hook-GET-machines' 6 | done() 7 | 8 | -------------------------------------------------------------------------------- /test/stub/server.coffee: -------------------------------------------------------------------------------- 1 | ###* 2 | # @file Express server stub 3 | # 4 | # Start: 5 | # $ ../../node_modules/coffee-script/bin/coffee server.coffee 6 | ### 7 | 8 | require 'coffee-script/register' 9 | 10 | express = require 'express' 11 | 12 | app = express() 13 | app.set 'port', process.env.PORT || 3333 14 | 15 | app.options '/machines', (req, res) -> 16 | 'use strict' 17 | allow = ['OPTIONS', 'HEAD', 'GET'] 18 | directives = ['no-cache', 'no-store', 'must-revalidate'] 19 | res.setHeader 'Allow', allow.join ',' 20 | res.setHeader 'Cache-Control', directives.join ',' 21 | res.setHeader 'Pragma', directives[0] 22 | res.setHeader 'Expires', '0' 23 | res.status(204).end() 24 | 25 | app.get '/machines', (req, res) -> 26 | 'use strict' 27 | machine = 28 | type: 'bulldozer' 29 | name: 'willy' 30 | res.status(200).json [machine] 31 | 32 | app.use (err, req, res, next) -> 33 | 'use strict' 34 | res.status(err.status || 500) 35 | .json({ 36 | message: err.message, 37 | stack: err.stack 38 | }) 39 | return 40 | 41 | server = app.listen app.get('port'), () -> 42 | 'use strict' 43 | console.log 'server listening on port', server.address().port 44 | 45 | -------------------------------------------------------------------------------- /test/unit/abao-test.coffee: -------------------------------------------------------------------------------- 1 | chai = require 'chai' 2 | sinon = require 'sinon' 3 | sinonChai = require 'sinon-chai' 4 | proxyquire = require('proxyquire').noCallThru() 5 | 6 | Test = require '../../lib/test' 7 | ramlParserStub = require 'raml-parser' 8 | addTestsStub = require '../../lib/add-tests' 9 | addHooksStub = require '../../lib/add-hooks' 10 | runnerStub = require '../../lib/test-runner' 11 | hooksStub = require '../../lib/hooks' 12 | 13 | Abao = proxyquire '../../', { 14 | 'raml-parser': ramlParserStub, 15 | './add-tests': addTestsStub, 16 | './add-hooks': addHooksStub, 17 | './test-runner': runnerStub, 18 | './hooks': hooksStub 19 | } 20 | 21 | should = chai.should() 22 | chai.use(sinonChai) 23 | 24 | 25 | describe 'Abao', () -> 26 | 'use strict' 27 | 28 | describe '#constructor', () -> 29 | 30 | describe 'with valid config', () -> 31 | 32 | it 'should create a new instance', () -> 33 | abao = new Abao() 34 | abao.should.not.be.null 35 | 36 | 37 | describe '#run', () -> 38 | 39 | abao = '' 40 | callback = undefined 41 | before (done) -> 42 | abao = new Abao() 43 | callback = sinon.stub() 44 | callback.returns(done()) 45 | abao.run callback 46 | 47 | it 'should invoke callback', () -> 48 | callback.should.be.called 49 | 50 | -------------------------------------------------------------------------------- /test/unit/add-hooks-test.coffee: -------------------------------------------------------------------------------- 1 | require 'coffee-errors' 2 | chai = require 'chai' 3 | chai.use require('sinon-chai') 4 | {EventEmitter} = require 'events' 5 | mute = require 'mute' 6 | nock = require 'nock' 7 | proxyquire = require 'proxyquire' 8 | sinon = require 'sinon' 9 | 10 | assert = chai.assert 11 | expect = chai.expect 12 | should = chai.should() 13 | 14 | globStub = require 'glob' 15 | pathStub = require 'path' 16 | hooksStub = require '../../lib/hooks' 17 | 18 | addHooks = proxyquire '../../lib/add-hooks', { 19 | 'glob': globStub, 20 | 'path': pathStub 21 | } 22 | 23 | describe 'addHooks(hooks, pattern, callback)', () -> 24 | 'use strict' 25 | 26 | callback = undefined 27 | globSyncSpy = undefined 28 | addHookSpy = undefined 29 | pathResolveSpy = undefined 30 | consoleErrorSpy = undefined 31 | transactions = {} 32 | 33 | describe 'with no pattern', () -> 34 | 35 | before () -> 36 | callback = sinon.spy() 37 | globSyncSpy = sinon.spy globStub, 'sync' 38 | 39 | it 'should return immediately', (done) -> 40 | addHooks hooksStub, '', callback 41 | globSyncSpy.should.not.have.been.called 42 | done() 43 | 44 | it 'should return successful continuation', () -> 45 | callback.should.have.been.calledOnce 46 | callback.should.have.been.calledWith( 47 | sinon.match.typeOf('null')) 48 | 49 | after () -> 50 | globStub.sync.restore() 51 | 52 | 53 | describe 'with pattern', () -> 54 | 55 | context 'not matching any files', () -> 56 | 57 | pattern = '/path/to/directory/without/hooks/*' 58 | 59 | beforeEach () -> 60 | callback = sinon.spy() 61 | addHookSpy = sinon.spy hooksStub, 'addHook' 62 | globSyncSpy = sinon.stub globStub, 'sync' 63 | .callsFake (pattern) -> 64 | [] 65 | pathResolveSpy = sinon.spy pathStub, 'resolve' 66 | 67 | it 'should not return any file names', (done) -> 68 | mute (unmute) -> 69 | addHooks hooksStub, pattern, callback 70 | globSyncSpy.should.have.returned [] 71 | unmute() 72 | done() 73 | 74 | it 'should not attempt to load files', (done) -> 75 | mute (unmute) -> 76 | addHooks hooksStub, pattern, callback 77 | pathResolveSpy.should.not.have.been.called 78 | unmute() 79 | done() 80 | 81 | it 'should propagate the error condition', (done) -> 82 | mute (unmute) -> 83 | addHooks hooksStub, pattern, callback 84 | callback.should.have.been.calledOnce 85 | detail = "no hook files found matching pattern '#{pattern}'" 86 | callback.should.have.been.calledWith( 87 | sinon.match.instanceOf(Error).and( 88 | sinon.match.has('message', detail))) 89 | unmute() 90 | done() 91 | 92 | afterEach () -> 93 | hooksStub.addHook.restore() 94 | globStub.sync.restore() 95 | pathStub.resolve.restore() 96 | 97 | 98 | context 'matching files', () -> 99 | 100 | pattern = './test/**/*_hooks.*' 101 | 102 | it 'should return file names', (done) -> 103 | mute (unmute) -> 104 | globSyncSpy = sinon.spy globStub, 'sync' 105 | addHooks hooksStub, pattern, callback 106 | globSyncSpy.should.have.been.called 107 | globStub.sync.restore() 108 | unmute() 109 | done() 110 | 111 | 112 | context 'when files are valid javascript/coffeescript', () -> 113 | 114 | beforeEach () -> 115 | callback = sinon.spy() 116 | globSyncSpy = sinon.spy globStub, 'sync' 117 | pathResolveSpy = sinon.spy pathStub, 'resolve' 118 | addHookSpy = sinon.spy hooksStub, 'addHook' 119 | 120 | it 'should load the files', (done) -> 121 | mute (unmute) -> 122 | addHooks hooksStub, pattern, callback 123 | pathResolveSpy.should.have.been.called 124 | unmute() 125 | done() 126 | 127 | it 'should attach the hooks', (done) -> 128 | mute (unmute) -> 129 | addHooks hooksStub, pattern, callback 130 | addHookSpy.should.have.been.called 131 | unmute() 132 | done() 133 | 134 | it 'should return successful continuation', (done) -> 135 | mute (unmute) -> 136 | addHooks hooksStub, pattern, callback 137 | callback.should.have.been.calledOnce 138 | callback.should.have.been.calledWith( 139 | sinon.match.typeOf('null')) 140 | unmute() 141 | done() 142 | 143 | afterEach () -> 144 | globStub.sync.restore() 145 | pathStub.resolve.restore() 146 | hooksStub.addHook.restore() 147 | 148 | 149 | context 'when error occurs reading the hook files', () -> 150 | 151 | addHookSpy = undefined 152 | consoleErrorSpy = undefined 153 | 154 | beforeEach () -> 155 | callback = sinon.spy() 156 | pathResolveSpy = sinon.stub pathStub, 'resolve' 157 | .callsFake (path, rel) -> 158 | throw new Error 'resolve' 159 | consoleErrorSpy = sinon.spy console, 'error' 160 | globSyncSpy = sinon.stub globStub, 'sync' 161 | .callsFake (pattern) -> 162 | ['invalid.xml', 'unexist.md'] 163 | addHookSpy = sinon.spy hooksStub, 'addHook' 164 | 165 | it 'should log an error', (done) -> 166 | mute (unmute) -> 167 | addHooks hooksStub, pattern, callback 168 | consoleErrorSpy.should.have.been.called 169 | unmute() 170 | done() 171 | 172 | it 'should not attach the hooks', (done) -> 173 | mute (unmute) -> 174 | addHooks hooksStub, pattern, callback 175 | addHookSpy.should.not.have.been.called 176 | unmute() 177 | done() 178 | 179 | it 'should propagate the error condition', (done) -> 180 | mute (unmute) -> 181 | addHooks hooksStub, pattern, callback 182 | callback.should.have.been.calledOnce 183 | callback.should.have.been.calledWith( 184 | sinon.match.instanceOf(Error).and( 185 | sinon.match.has('message', 'resolve'))) 186 | unmute() 187 | done() 188 | 189 | afterEach () -> 190 | pathStub.resolve.restore() 191 | console.error.restore() 192 | globStub.sync.restore() 193 | hooksStub.addHook.restore() 194 | 195 | -------------------------------------------------------------------------------- /test/unit/add-tests-test.coffee: -------------------------------------------------------------------------------- 1 | {assert} = require 'chai' 2 | sinon = require 'sinon' 3 | ramlParser = require 'raml-parser' 4 | 5 | proxyquire = require('proxyquire').noCallThru() 6 | 7 | mochaStub = require 'mocha' 8 | 9 | TestFactory = require '../../lib/test' 10 | hooks = require '../../lib/hooks' 11 | addTests = proxyquire '../../lib/add-tests', { 12 | 'mocha': mochaStub 13 | } 14 | 15 | FIXTURE_DIR = "#{__dirname}/../fixtures" 16 | RAML_DIR = "#{FIXTURE_DIR}" 17 | 18 | 19 | describe '#addTests', () -> 20 | 'use strict' 21 | 22 | describe '#run', () -> 23 | 24 | describe 'when endpoint specifies a single method', () -> 25 | 26 | tests = [] 27 | testFactory = new TestFactory() 28 | callback = undefined 29 | 30 | before (done) -> 31 | ramlFile = "#{RAML_DIR}/machines-single_get.raml" 32 | ramlParser.loadFile(ramlFile) 33 | .then (raml) -> 34 | callback = sinon.stub() 35 | callback.returns(done()) 36 | 37 | addTests raml, tests, hooks, callback, testFactory, false 38 | .catch (err) -> 39 | console.error err 40 | done(err) 41 | return 42 | 43 | after () -> 44 | tests = [] 45 | 46 | it 'should run callback', () -> 47 | assert.ok callback.called 48 | 49 | it 'should add 1 test', () -> 50 | assert.lengthOf tests, 1 51 | 52 | it 'should set test.name', () -> 53 | assert.equal tests[0].name, 'GET /machines -> 200' 54 | 55 | it 'should setup test.request', () -> 56 | req = tests[0].request 57 | 58 | assert.equal req.path, '/machines' 59 | assert.deepEqual req.params, {} 60 | assert.deepEqual req.query, {} 61 | assert.deepEqual req.headers, 62 | 'Abao-API-Key': 'abcdef' 63 | req.body.should.be.empty 64 | assert.equal req.method, 'GET' 65 | 66 | it 'should setup test.response', () -> 67 | res = tests[0].response 68 | 69 | assert.equal res.status, 200 70 | schema = res.schema 71 | assert.equal schema.items.properties.type.type, 'string' 72 | assert.equal schema.items.properties.name.type, 'string' 73 | assert.isNull res.headers 74 | assert.isNull res.body 75 | 76 | 77 | describe 'when endpoint has multiple methods', () -> 78 | 79 | describe 'when processed in order specified in RAML', () -> 80 | 81 | tests = [] 82 | testFactory = new TestFactory() 83 | callback = undefined 84 | 85 | before (done) -> 86 | ramlFile = "#{RAML_DIR}/machines-1_get_1_post.raml" 87 | ramlParser.loadFile(ramlFile) 88 | .then (raml) -> 89 | callback = sinon.stub() 90 | callback.returns(done()) 91 | 92 | addTests raml, tests, hooks, callback, testFactory, false 93 | .catch (err) -> 94 | console.error err 95 | done(err) 96 | return 97 | 98 | after () -> 99 | tests = [] 100 | 101 | it 'should run callback', () -> 102 | assert.ok callback.called 103 | 104 | it 'should add 2 tests', () -> 105 | assert.lengthOf tests, 2 106 | 107 | it 'should process GET request before POST request', () -> 108 | req = tests[0].request 109 | assert.equal req.method, 'GET' 110 | req = tests[1].request 111 | assert.equal req.method, 'POST' 112 | 113 | it 'should setup test.request of POST', () -> 114 | req = tests[1].request 115 | 116 | assert.equal req.path, '/machines' 117 | assert.deepEqual req.params, {} 118 | assert.deepEqual req.query, {} 119 | assert.deepEqual req.headers, 120 | 'Content-Type': 'application/json' 121 | assert.deepEqual req.body, 122 | type: 'Kulu' 123 | name: 'Mike' 124 | assert.equal req.method, 'POST' 125 | 126 | it 'should setup test.response of POST', () -> 127 | res = tests[1].response 128 | 129 | assert.equal res.status, 201 130 | schema = res.schema 131 | assert.equal schema.properties.type.type, 'string' 132 | assert.equal schema.properties.name.type, 'string' 133 | assert.isNull res.headers 134 | assert.isNull res.body 135 | 136 | 137 | describe 'when processed in order specified by "--sorted" option', () -> 138 | 139 | tests = [] 140 | testFactory = new TestFactory() 141 | callback = undefined 142 | 143 | before (done) -> 144 | ramlFile = "#{RAML_DIR}/machines-1_get_1_post.raml" 145 | ramlParser.loadFile(ramlFile) 146 | .then (raml) -> 147 | callback = sinon.stub() 148 | callback.returns(done()) 149 | 150 | addTests raml, tests, hooks, null, callback, testFactory, true 151 | .catch (err) -> 152 | console.error err 153 | done(err) 154 | return 155 | 156 | after () -> 157 | tests = [] 158 | 159 | it 'should run callback', () -> 160 | assert.ok callback.called 161 | 162 | it 'should add 2 tests', () -> 163 | assert.lengthOf tests, 2 164 | 165 | it 'should process GET request after POST request', () -> 166 | req = tests[0].request 167 | assert.equal req.method, 'POST' 168 | req = tests[1].request 169 | assert.equal req.method, 'GET' 170 | 171 | it 'should setup test.request of POST', () -> 172 | req = tests[0].request 173 | 174 | assert.equal req.path, '/machines' 175 | assert.deepEqual req.params, {} 176 | assert.deepEqual req.query, {} 177 | assert.deepEqual req.headers, 178 | 'Content-Type': 'application/json' 179 | assert.deepEqual req.body, 180 | type: 'Kulu' 181 | name: 'Mike' 182 | assert.equal req.method, 'POST' 183 | 184 | it 'should setup test.response of POST', () -> 185 | res = tests[0].response 186 | 187 | assert.equal res.status, 201 188 | schema = res.schema 189 | assert.equal schema.properties.type.type, 'string' 190 | assert.equal schema.properties.name.type, 'string' 191 | assert.isNull res.headers 192 | assert.isNull res.body 193 | 194 | 195 | describe 'when RAML includes multiple referencing schemas', () -> 196 | 197 | tests = [] 198 | testFactory = new TestFactory() 199 | callback = undefined 200 | 201 | before (done) -> 202 | ramlFile = "#{RAML_DIR}/machines-ref_other_schemas.raml" 203 | ramlParser.loadFile(ramlFile) 204 | .then (raml) -> 205 | callback = sinon.stub() 206 | callback.returns(done()) 207 | 208 | addTests raml, tests, hooks, callback, testFactory, false 209 | .catch (err) -> 210 | console.error err 211 | done(err) 212 | return 213 | 214 | after () -> 215 | tests = [] 216 | 217 | it 'should run callback', () -> 218 | assert.ok callback.called 219 | 220 | it 'should add 1 test', () -> 221 | assert.lengthOf tests, 1 222 | 223 | it 'should set test.name', () -> 224 | assert.equal tests[0].name, 'GET /machines -> 200' 225 | 226 | it 'should setup test.request', () -> 227 | req = tests[0].request 228 | 229 | assert.equal req.path, '/machines' 230 | assert.deepEqual req.params, {} 231 | assert.deepEqual req.query, {} 232 | req.body.should.be.empty 233 | assert.equal req.method, 'GET' 234 | 235 | it 'should setup test.response', () -> 236 | res = tests[0].response 237 | 238 | assert.equal res.status, 200 239 | assert.equal res.schema?.properties?.chick?.type, 'string' 240 | assert.isNull res.headers 241 | assert.isNull res.body 242 | 243 | 244 | describe 'when RAML has inline and included schemas', () -> 245 | 246 | tests = [] 247 | testFactory = new TestFactory() 248 | callback = undefined 249 | 250 | before (done) -> 251 | ramlFile = "#{RAML_DIR}/machines-inline_and_included_schemas.raml" 252 | ramlParser.loadFile(ramlFile) 253 | .then (raml) -> 254 | callback = sinon.stub() 255 | callback.returns(done()) 256 | 257 | addTests raml, tests, hooks, callback, testFactory, false 258 | .catch (err) -> 259 | console.error err 260 | done(err) 261 | return 262 | 263 | after () -> 264 | tests = [] 265 | 266 | it 'should run callback', () -> 267 | assert.ok callback.called 268 | 269 | it 'should add 1 test', () -> 270 | assert.lengthOf tests, 1 271 | 272 | it 'should set test.name', () -> 273 | assert.equal tests[0].name, 'GET /machines -> 200' 274 | 275 | it 'should setup test.request', () -> 276 | req = tests[0].request 277 | 278 | assert.equal req.path, '/machines' 279 | assert.deepEqual req.params, {} 280 | assert.deepEqual req.query, {} 281 | req.body.should.be.empty 282 | assert.equal req.method, 'GET' 283 | 284 | it 'should setup test.response', () -> 285 | res = tests[0].response 286 | 287 | assert.equal res.status, 200 288 | assert.equal res.schema?.properties?.type['$ref'], 'type2' 289 | assert.isNull res.headers 290 | assert.isNull res.body 291 | 292 | 293 | describe 'when RAML contains three-levels endpoints', () -> 294 | 295 | tests = [] 296 | testFactory = new TestFactory() 297 | callback = undefined 298 | 299 | before (done) -> 300 | ramlFile = "#{RAML_DIR}/machines-three_levels.raml" 301 | ramlParser.loadFile(ramlFile) 302 | .then (raml) -> 303 | callback = sinon.stub() 304 | callback.returns(done()) 305 | 306 | addTests raml, tests, hooks, callback, testFactory, false 307 | .catch (err) -> 308 | console.error err 309 | done(err) 310 | return 311 | 312 | after () -> 313 | tests = [] 314 | 315 | it 'should run callback', () -> 316 | assert.ok callback.called 317 | 318 | it 'should add 3 tests', () -> 319 | assert.lengthOf tests, 3 320 | 321 | it 'should set test.name', () -> 322 | assert.equal tests[0].name, 'GET /machines -> 200' 323 | assert.equal tests[1].name, 'DELETE /machines/{machine_id} -> 204' 324 | assert.equal tests[2].name, 'GET /machines/{machine_id}/parts -> 200' 325 | 326 | it 'should set request.param of test 1', () -> 327 | test = tests[1] 328 | assert.deepEqual test.request.params, 329 | machine_id: '1' 330 | 331 | it 'should set request.param of test 2', () -> 332 | test = tests[2] 333 | assert.deepEqual test.request.params, 334 | machine_id: '1' 335 | 336 | 337 | describe 'when RAML has resource not defined method', () -> 338 | 339 | tests = [] 340 | testFactory = new TestFactory() 341 | callback = undefined 342 | 343 | before (done) -> 344 | ramlFile = "#{RAML_DIR}/machines-no_method.raml" 345 | ramlParser.loadFile(ramlFile) 346 | .then (raml) -> 347 | callback = sinon.stub() 348 | callback.returns(done()) 349 | 350 | addTests raml, tests, hooks, callback, testFactory, false 351 | .catch (err) -> 352 | console.error err 353 | done(err) 354 | return 355 | 356 | after () -> 357 | tests = [] 358 | 359 | it 'should run callback', () -> 360 | assert.ok callback.called 361 | 362 | it 'should add 1 test', () -> 363 | assert.lengthOf tests, 1 364 | 365 | it 'should set test.name', () -> 366 | assert.equal tests[0].name, 'GET /root/machines -> 200' 367 | 368 | 369 | describe 'when RAML has invalid request body example', () -> 370 | 371 | tests = [] 372 | testFactory = new TestFactory() 373 | callback = undefined 374 | 375 | before (done) -> 376 | raml = """ 377 | #%RAML 0.8 378 | 379 | title: World Music API 380 | baseUri: http://example.api.com/{version} 381 | version: v1 382 | mediaType: application/json 383 | 384 | /machines: 385 | post: 386 | body: 387 | example: 'invalid-json' 388 | responses: 389 | 204: 390 | """ 391 | ramlParser.load(raml) 392 | .then (raml) -> 393 | callback = sinon.stub() 394 | callback.returns(done()) 395 | 396 | sinon.stub console, 'warn' 397 | addTests raml, tests, hooks, callback, testFactory, false 398 | .catch (err) -> 399 | console.error err 400 | done(err) 401 | return 402 | 403 | after () -> 404 | tests = [] 405 | console.warn.restore() 406 | 407 | it 'should run callback', () -> 408 | assert.ok callback.called 409 | 410 | it 'should give a warning', () -> 411 | assert.ok console.warn.called 412 | 413 | it 'should add 1 test', () -> 414 | assert.lengthOf tests, 1 415 | assert.equal tests[0].name, 'POST /machines -> 204' 416 | 417 | 418 | describe 'when RAML media type uses a JSON-suffixed vendor tree subtype', () -> 419 | 420 | tests = [] 421 | testFactory = new TestFactory() 422 | callback = undefined 423 | 424 | before (done) -> 425 | ramlFile = "#{RAML_DIR}/music-vendor_content_type.raml" 426 | ramlParser.loadFile(ramlFile) 427 | .then (raml) -> 428 | callback = sinon.stub() 429 | callback.returns(done()) 430 | 431 | addTests raml, tests, hooks, callback, testFactory, false 432 | .catch (err) -> 433 | console.error err 434 | done(err) 435 | return 436 | 437 | after () -> 438 | tests = [] 439 | 440 | it 'should run callback', () -> 441 | assert.ok callback.called 442 | 443 | it 'should add 1 test', () -> 444 | assert.lengthOf tests, 1 445 | 446 | it 'should setup test.request of PATCH', () -> 447 | req = tests[0].request 448 | 449 | assert.equal req.path, '/{songId}' 450 | assert.deepEqual req.params, 451 | songId: 'mike-a-beautiful-day' 452 | assert.deepEqual req.query, {} 453 | assert.deepEqual req.headers, 454 | 'Content-Type': 'application/vnd.api+json' 455 | assert.deepEqual req.body, 456 | title: 'A Beautiful Day' 457 | artist: 'Mike' 458 | assert.equal req.method, 'PATCH' 459 | 460 | it 'should setup test.response of PATCH', () -> 461 | res = tests[0].response 462 | 463 | assert.equal res.status, 200 464 | schema = res.schema 465 | assert.equal schema.properties.title.type, 'string' 466 | assert.equal schema.properties.artist.type, 'string' 467 | 468 | 469 | describe 'when there is required query parameter with example value', () -> 470 | 471 | tests = [] 472 | testFactory = new TestFactory() 473 | callback = undefined 474 | 475 | before (done) -> 476 | ramlFile = "#{RAML_DIR}/machines-required_query_parameter.raml" 477 | ramlParser.loadFile(ramlFile) 478 | .then (raml) -> 479 | callback = sinon.stub() 480 | callback.returns(done()) 481 | 482 | addTests raml, tests, hooks, callback, testFactory, false 483 | .catch (err) -> 484 | console.error err 485 | done(err) 486 | return 487 | 488 | after () -> 489 | tests = [] 490 | 491 | it 'should run callback', () -> 492 | assert.ok callback.called 493 | 494 | it 'should add 1 test', () -> 495 | assert.lengthOf tests, 1 496 | 497 | it 'should append query parameters with example value', () -> 498 | assert.equal tests[0].request.query['quux'], 'foo' 499 | 500 | 501 | describe 'when there is no required query parameter', () -> 502 | 503 | tests = [] 504 | testFactory = new TestFactory() 505 | callback = undefined 506 | 507 | before (done) -> 508 | ramlFile = "#{RAML_DIR}/machines-non_required_query_parameter.raml" 509 | ramlParser.loadFile(ramlFile) 510 | .then (raml) -> 511 | callback = sinon.stub() 512 | callback.returns(done()) 513 | 514 | addTests raml, tests, hooks, callback, testFactory, false 515 | .catch (err) -> 516 | console.error err 517 | done(err) 518 | return 519 | 520 | after () -> 521 | tests = [] 522 | 523 | it 'should run callback', () -> 524 | assert.ok callback.called 525 | 526 | it 'should add 1 test', () -> 527 | assert.lengthOf tests, 1 528 | 529 | it 'should not append query parameters', () -> 530 | assert.deepEqual tests[0].request.query, {} 531 | 532 | -------------------------------------------------------------------------------- /test/unit/hooks-test.coffee: -------------------------------------------------------------------------------- 1 | require 'coffee-errors' 2 | sinon = require 'sinon' 3 | {assert} = require 'chai' 4 | 5 | TestFactoryStub = require '../../lib/test' 6 | 7 | hooks = require '../../lib/hooks' 8 | 9 | ABAO_IO_SERVER = 'http://abao.io' 10 | 11 | describe 'Hooks', () -> 12 | 'use strict' 13 | 14 | noop = () -> {} 15 | 16 | describe 'when adding before hook', () -> 17 | 18 | before () -> 19 | hooks.before 'beforeHook', noop 20 | 21 | after () -> 22 | hooks.beforeHooks = {} 23 | 24 | it 'should add to hook collection', () -> 25 | assert.property hooks.beforeHooks, 'beforeHook' 26 | assert.lengthOf hooks.beforeHooks['beforeHook'], 1 27 | 28 | describe 'when adding after hook', () -> 29 | 30 | before () -> 31 | hooks.after 'afterHook', noop 32 | 33 | after () -> 34 | hooks.afterHooks = {} 35 | 36 | it 'should add to hook collection', () -> 37 | assert.property hooks.afterHooks, 'afterHook' 38 | 39 | describe 'when adding beforeAll hooks', () -> 40 | 41 | afterEach () -> 42 | hooks.beforeAllHooks = [] 43 | 44 | it 'should invoke registered callbacks', (testDone) -> 45 | callback = sinon.stub() 46 | callback.callsArg(0) 47 | 48 | hooks.beforeAll callback 49 | hooks.beforeAll (done) -> 50 | assert.ok typeof done is 'function' 51 | assert.ok callback.called 52 | done() 53 | hooks.runBeforeAll (done) -> 54 | testDone() 55 | 56 | describe 'when adding afterAll hooks', () -> 57 | 58 | afterEach () -> 59 | hooks.afterAllHooks = [] 60 | 61 | it 'should callback if registered', (testDone) -> 62 | callback = sinon.stub() 63 | callback.callsArg(0) 64 | 65 | hooks.afterAll callback 66 | hooks.afterAll (done) -> 67 | assert.ok(typeof done is 'function') 68 | assert.ok callback.called 69 | done() 70 | hooks.runAfterAll (done) -> 71 | testDone() 72 | 73 | describe 'when adding beforeEach hooks', () -> 74 | 75 | afterEach () -> 76 | hooks.beforeEachHooks = [] 77 | hooks.beforeHooks = {} 78 | 79 | it 'should add to hook list', () -> 80 | hooks.beforeEach noop 81 | assert.lengthOf hooks.beforeEachHooks, 1 82 | 83 | it 'should invoke registered callbacks', (testDone) -> 84 | before_called = false 85 | before_each_called = false 86 | test_name = 'before_test' 87 | hooks.before test_name, (test, done) -> 88 | assert.equal test.name, test_name 89 | before_called = true 90 | assert.isTrue before_each_called, 91 | 'before_hook should be called after before_each' 92 | done() 93 | 94 | hooks.beforeEach (test, done) -> 95 | assert.equal test.name, test_name 96 | before_each_called = true 97 | assert.isFalse before_called, 98 | 'before_each should be called before before_hook' 99 | done() 100 | 101 | hooks.runBefore {name: test_name}, () -> 102 | assert.isTrue before_each_called, 'before_each should have been called' 103 | assert.isTrue before_called, 'before_hook should have been called' 104 | testDone() 105 | 106 | it 'should work without test-specific before', (testDone) -> 107 | before_each_called = false 108 | test_name = 'before_test' 109 | hooks.beforeEach (test, done) -> 110 | assert.equal test.name, test_name 111 | before_each_called = true 112 | done() 113 | 114 | hooks.runBefore {name: test_name}, () -> 115 | assert.isTrue before_each_called, 'before_each should have been called' 116 | testDone() 117 | 118 | describe 'when adding afterEach hooks', () -> 119 | 120 | afterEach () -> 121 | hooks.afterEachHooks = [] 122 | hooks.afterHooks = {} 123 | 124 | it 'should add to hook list', () -> 125 | hooks.afterEach noop 126 | assert.lengthOf hooks.afterEachHooks, 1 127 | 128 | it 'should invoke registered callbacks', (testDone) -> 129 | after_called = false 130 | after_each_called = false 131 | test_name = 'after_test' 132 | hooks.after test_name, (test, done) -> 133 | assert.equal test.name, test_name 134 | after_called = true 135 | assert.isFalse after_each_called, 136 | 'after_hook should be called before after_each' 137 | done() 138 | 139 | hooks.afterEach (test, done) -> 140 | assert.equal test.name, test_name 141 | after_each_called = true 142 | assert.isTrue after_called, 143 | 'after_each should be called after after_hook' 144 | done() 145 | 146 | hooks.runAfter {name: test_name}, () -> 147 | assert.isTrue after_each_called, 'after_each should have been called' 148 | assert.isTrue after_called, 'after_hook should have been called' 149 | testDone() 150 | 151 | it 'should work without test-specific after', (testDone) -> 152 | after_each_called = false 153 | test_name = 'after_test' 154 | hooks.afterEach (test, done) -> 155 | assert.equal test.name, test_name 156 | after_each_called = true 157 | done() 158 | 159 | hooks.runAfter {name: test_name}, () -> 160 | assert.isTrue after_each_called, 'after_each should have been called' 161 | testDone() 162 | 163 | describe 'when check has name', () -> 164 | 165 | it 'should return true if in before hooks', () -> 166 | hooks.beforeHooks = 167 | foo: (test, done) -> 168 | done() 169 | 170 | assert.ok hooks.hasName 'foo' 171 | 172 | hooks.beforeHooks = {} 173 | 174 | it 'should return true if in after hooks', () -> 175 | hooks.afterHooks = 176 | foo: (test, done) -> 177 | done() 178 | 179 | assert.ok hooks.hasName 'foo' 180 | 181 | hooks.afterHooks = {} 182 | 183 | it 'should return true if in both before and after hooks', () -> 184 | hooks.beforeHooks = 185 | foo: (test, done) -> 186 | done() 187 | hooks.afterHooks = 188 | foo: (test, done) -> 189 | done() 190 | 191 | assert.ok hooks.hasName 'foo' 192 | 193 | hooks.beforeHooks = {} 194 | hooks.afterHooks = {} 195 | 196 | it 'should return false if in neither before nor after hooks', () -> 197 | assert.notOk hooks.hasName 'foo' 198 | 199 | 200 | describe 'when running hooks', () -> 201 | 202 | beforeHook = '' 203 | afterHook = '' 204 | 205 | beforeEach () -> 206 | beforeHook = sinon.stub() 207 | beforeHook.callsArg(1) 208 | 209 | afterHook = sinon.stub() 210 | afterHook.callsArg(1) 211 | 212 | hooks.beforeHooks = 213 | 'GET /machines -> 200': [beforeHook] 214 | hooks.afterHooks = 215 | 'GET /machines -> 200': [afterHook] 216 | 217 | afterEach () -> 218 | hooks.beforeHooks = {} 219 | hooks.afterHooks = {} 220 | beforeHook = '' 221 | afterHook = '' 222 | 223 | describe 'with corresponding GET test', () -> 224 | 225 | testFactory = new TestFactoryStub() 226 | test = testFactory.create() 227 | test.name = 'GET /machines -> 200' 228 | test.request.server = "#{ABAO_IO_SERVER}" 229 | test.request.path = '/machines' 230 | test.request.method = 'GET' 231 | test.request.params = 232 | param: 'value' 233 | test.request.query = 234 | q: 'value' 235 | test.request.headers = 236 | key: 'value' 237 | test.response.status = 200 238 | test.response.schema = """ 239 | [ 240 | type: 'string' 241 | name: 'string' 242 | ] 243 | """ 244 | 245 | describe 'on before hook', () -> 246 | beforeEach (done) -> 247 | hooks.runBefore test, done 248 | 249 | it 'should run hook', () -> 250 | assert.ok beforeHook.called 251 | 252 | it 'should pass #test to hook', () -> 253 | assert.ok beforeHook.calledWith(test) 254 | 255 | describe 'on after hook', () -> 256 | beforeEach (done) -> 257 | hooks.runAfter test, done 258 | 259 | it 'should run hook', () -> 260 | assert.ok afterHook.called 261 | 262 | it 'should pass #test to hook', () -> 263 | assert.ok afterHook.calledWith(test) 264 | 265 | describe 'with corresponding POST test', () -> 266 | 267 | testFactory = new TestFactoryStub() 268 | test = testFactory.create() 269 | test.name = 'POST /machines -> 201' 270 | test.request.server = "#{ABAO_IO_SERVER}" 271 | test.request.path = '/machines' 272 | test.request.method = 'POST' 273 | test.request.params = 274 | param: 'value' 275 | test.request.query = 276 | q: 'value' 277 | test.request.headers = 278 | key: 'value' 279 | test.response.status = 201 280 | test.response.schema = """ 281 | type: 'string' 282 | name: 'string' 283 | """ 284 | 285 | describe 'on before hook', () -> 286 | beforeEach (done) -> 287 | hooks.runBefore test, done 288 | 289 | it 'should not run hook', () -> 290 | assert.ok beforeHook.notCalled 291 | 292 | describe 'on after hook', () -> 293 | beforeEach (done) -> 294 | hooks.runAfter test, done 295 | 296 | it 'should not run hook', () -> 297 | assert.ok afterHook.notCalled 298 | 299 | describe 'when running beforeAll/afterAll', () -> 300 | 301 | funcs = [] 302 | 303 | before () -> 304 | for i in [1..4] 305 | hook = sinon.stub() 306 | hook.callsArg(0) 307 | funcs.push hook 308 | 309 | hooks.beforeAllHooks = [funcs[0], funcs[1]] 310 | hooks.afterAllHooks = [funcs[2], funcs[3]] 311 | 312 | after () -> 313 | hooks.beforeAllHooks = [] 314 | hooks.afterAllHooks = [] 315 | funcs = [] 316 | 317 | describe 'on beforeAll hook', () -> 318 | callback = '' 319 | 320 | before (done) -> 321 | callback = sinon.stub() 322 | callback.returns(done()) 323 | 324 | hooks.runBeforeAll callback 325 | 326 | it 'should invoke callback', () -> 327 | assert.ok callback.calledWithExactly(null), callback.printf('%C') 328 | 329 | it 'should run hook', () -> 330 | assert.ok funcs[0].called 331 | assert.ok funcs[1].called 332 | 333 | describe 'on afterAll hook', () -> 334 | callback = '' 335 | 336 | before (done) -> 337 | callback = sinon.stub() 338 | callback.returns(done()) 339 | 340 | hooks.runAfterAll callback 341 | 342 | it 'should invoke callback', () -> 343 | assert.ok callback.calledWithExactly(null), callback.printf('%C') 344 | 345 | it 'should run hook', () -> 346 | assert.ok funcs[2].called 347 | assert.ok funcs[3].called 348 | 349 | describe 'when successfully adding test hook', () -> 350 | 351 | afterEach () -> 352 | hooks.contentTests = {} 353 | 354 | test_name = 'content_test_test' 355 | 356 | it 'should get added to the set of hooks', () -> 357 | hooks.test test_name, noop 358 | assert.isDefined hooks.contentTests[test_name] 359 | 360 | describe 'adding two content tests fails', () -> 361 | afterEach () -> 362 | hooks.contentTests = {} 363 | 364 | test_name = 'content_test_test' 365 | 366 | it 'should assert when attempting to add a second content test', () -> 367 | f = () -> 368 | hooks.test test_name, noop 369 | f() 370 | assert.throw f, 371 | "cannot have more than one test with the name: #{test_name}" 372 | 373 | describe 'when check skipped', () -> 374 | 375 | beforeEach () -> 376 | hooks.skippedTests = ['foo'] 377 | 378 | afterEach () -> 379 | hooks.skippedTests = [] 380 | 381 | it 'should return true if in skippedTests', () -> 382 | assert.ok hooks.skipped 'foo' 383 | 384 | it 'should return false if not in skippedTests', () -> 385 | assert.notOk hooks.skipped 'buz' 386 | 387 | describe 'when successfully skip test', () -> 388 | 389 | afterEach () -> 390 | hooks.skippedTests = [] 391 | 392 | test_name = 'content_test_test' 393 | 394 | it 'should get added to the set of hooks', () -> 395 | hooks.skip test_name 396 | assert.include(hooks.skippedTests, test_name) 397 | 398 | -------------------------------------------------------------------------------- /test/unit/test-runner-test.coffee: -------------------------------------------------------------------------------- 1 | chai = require 'chai' 2 | sinon = require 'sinon' 3 | sinonChai = require 'sinon-chai' 4 | _ = require 'lodash' 5 | mocha = require 'mocha' 6 | mute = require 'mute' 7 | proxyquire = require('proxyquire').noCallThru() 8 | 9 | pkg = require '../../package' 10 | TestFactory = require '../../lib/test' 11 | hooksStub = require '../../lib/hooks' 12 | suiteStub = undefined 13 | 14 | TestRunner = proxyquire '../../lib/test-runner', { 15 | 'mocha': mocha, 16 | 'hooks': hooksStub 17 | } 18 | 19 | ABAO_IO_SERVER = 'http://abao.io' 20 | SERVER = 'http://localhost:3000' 21 | 22 | assert = chai.assert 23 | should = chai.should() 24 | chai.use(sinonChai) 25 | 26 | describe 'Test Runner', () -> 27 | 'use strict' 28 | 29 | runner = undefined 30 | test = undefined 31 | 32 | createStdTest = () -> 33 | testname = 'GET /machines -> 200' 34 | testFactory = new TestFactory() 35 | stdTest = testFactory.create testname, undefined 36 | stdTest.request.path = '/machines' 37 | stdTest.request.method = 'GET' 38 | return stdTest 39 | 40 | 41 | describe '#run', () -> 42 | 43 | describe 'when test is valid', () -> 44 | 45 | beforeAllHook = undefined 46 | afterAllHook = undefined 47 | beforeHook = undefined 48 | afterHook = undefined 49 | runCallback = undefined 50 | 51 | before (done) -> 52 | test = createStdTest() 53 | test.response.status = 200 54 | test.response.schema = """[ 55 | type: 'string' 56 | name: 'string' 57 | ]""" 58 | 59 | options = 60 | server: "#{ABAO_IO_SERVER}" 61 | 62 | runner = new TestRunner options, '' 63 | 64 | runCallback = sinon.stub() 65 | runCallback(done) 66 | runCallback.yield() 67 | 68 | beforeAllHook = sinon.stub() 69 | beforeAllHook.callsArg(0) 70 | afterAllHook = sinon.stub() 71 | afterAllHook.callsArg(0) 72 | 73 | hooksStub.beforeAllHooks = [beforeAllHook] 74 | hooksStub.afterAllHooks = [afterAllHook] 75 | 76 | beforeHook = sinon.stub() 77 | beforeHook.callsArg(1) 78 | hooksStub.beforeHooks[test.name] = beforeHook 79 | 80 | mochaStub = runner.mocha 81 | originSuiteCreate = mocha.Suite.create 82 | sinon.stub mocha.Suite, 'create' 83 | .callsFake (parent, title) -> 84 | suiteStub = originSuiteCreate.call(mocha.Suite, parent, title) 85 | 86 | # Stub suite 87 | originSuiteBeforeAll = suiteStub.beforeAll 88 | originSuiteAfterAll = suiteStub.afterAll 89 | sinon.stub suiteStub, 'beforeAll' 90 | .callsFake (title, fn) -> 91 | beforeHook = fn 92 | originSuiteBeforeAll.call(suiteStub, title, fn) 93 | sinon.stub suiteStub, 'afterAll' 94 | .callsFake (title, fn) -> 95 | afterHook = fn 96 | originSuiteAfterAll.call(suiteStub, title, fn) 97 | 98 | suiteStub 99 | 100 | sinon.stub mochaStub, 'run' 101 | .callsFake (callback) -> 102 | callback(0) 103 | 104 | sinon.spy mochaStub.suite, 'beforeAll' 105 | sinon.spy mochaStub.suite, 'afterAll' 106 | 107 | sinon.stub hooksStub, 'runBefore' 108 | .callsFake (test, callback) -> 109 | callback() 110 | sinon.stub hooksStub, 'runAfter' 111 | .callsFake (test, callback) -> 112 | callback() 113 | 114 | runner.run [test], hooksStub, runCallback 115 | 116 | after () -> 117 | hooksStub.beforeAllHooks = [beforeAllHook] 118 | hooksStub.afterAllHooks = [afterAllHook] 119 | 120 | mochaStub = runner.mocha 121 | mochaStub.run.restore() 122 | mocha.Suite.create.restore() 123 | 124 | hooksStub.runBefore.restore() 125 | hooksStub.runAfter.restore() 126 | 127 | runCallback = undefined 128 | runner = undefined 129 | test = undefined 130 | 131 | it 'should generate beforeAll hooks', () -> 132 | mochaStub = runner.mocha 133 | assert.ok mochaStub.suite.beforeAll.called 134 | assert.ok mochaStub.suite.afterAll.called 135 | 136 | it 'should run mocha', () -> 137 | assert.ok runner.mocha.run.calledOnce 138 | 139 | it 'should invoke callback with failures', () -> 140 | runCallback.should.be.calledWith null, 0 141 | 142 | it 'should generate mocha suite', () -> 143 | suites = runner.mocha.suite.suites 144 | assert.equal suites.length, 1 145 | assert.equal suites[0].title, 'GET /machines -> 200' 146 | 147 | it 'should generate mocha test', () -> 148 | tests = runner.mocha.suite.suites[0].tests 149 | assert.equal tests.length, 1 150 | assert.notOk tests[0].pending 151 | 152 | it 'should generate hook of suite', () -> 153 | assert.ok suiteStub.beforeAll.called 154 | assert.ok suiteStub.afterAll.called 155 | 156 | # describe 'when executed hooks', () -> 157 | # before (done) -> 158 | # 159 | # it 'should execute hooks', () -> 160 | # # it 'should generate before hook', () -> 161 | # assert.ok hooksStub.runBefore.calledWith(test) 162 | # 163 | # it 'should call after hook', () -> 164 | # assert.ok hooksStub.runAfter.calledWith(test) 165 | 166 | 167 | describe 'Interact with #test', () -> 168 | 169 | before (done) -> 170 | test = createStdTest() 171 | test.response.status = 200 172 | test.response.schema = """[ 173 | type: 'string' 174 | name: 'string' 175 | ]""" 176 | 177 | options = 178 | server: "#{ABAO_IO_SERVER}" 179 | 180 | runner = new TestRunner options, '' 181 | sinon.stub test, 'run' 182 | .callsFake (callback) -> 183 | callback() 184 | 185 | # Mute stdout/stderr 186 | mute (unmute) -> 187 | runner.run [test], hooksStub, () -> 188 | unmute() 189 | done() 190 | 191 | after () -> 192 | test.run.restore() 193 | runner = undefined 194 | test = undefined 195 | 196 | it 'should call #test.run', () -> 197 | assert.ok test.run.calledOnce 198 | 199 | 200 | describe 'when test has no response code', () -> 201 | 202 | before (done) -> 203 | testFactory = new TestFactory() 204 | test = testFactory.create() 205 | test.name = 'GET /machines -> 200' 206 | test.request.path = '/machines' 207 | test.request.method = 'GET' 208 | 209 | options = 210 | server: "#{SERVER}" 211 | 212 | runner = new TestRunner options, '' 213 | sinon.stub runner.mocha, 'run' 214 | .callsFake (callback) -> 215 | callback() 216 | sinon.stub test, 'run' 217 | .callsFake (callback) -> 218 | callback() 219 | 220 | runner.run [test], hooksStub, done 221 | 222 | after () -> 223 | runner.mocha.run.restore() 224 | runner = undefined 225 | test = undefined 226 | 227 | it 'should run mocha', () -> 228 | assert.ok runner.mocha.run.called 229 | 230 | it 'should generate mocha suite', () -> 231 | suites = runner.mocha.suite.suites 232 | assert.equal suites.length, 1 233 | assert.equal suites[0].title, 'GET /machines -> 200' 234 | 235 | it 'should generate pending mocha test', () -> 236 | tests = runner.mocha.suite.suites[0].tests 237 | assert.equal tests.length, 1 238 | assert.ok tests[0].pending 239 | 240 | 241 | describe 'when test skipped in hooks', () -> 242 | 243 | before (done) -> 244 | test = createStdTest() 245 | test.response.status = 200 246 | test.response.schema = """[ 247 | type: 'string' 248 | name: 'string' 249 | ]""" 250 | 251 | options = 252 | server: "#{SERVER}" 253 | 254 | runner = new TestRunner options, '' 255 | sinon.stub runner.mocha, 'run' 256 | .callsFake (callback) -> 257 | callback() 258 | sinon.stub test, 'run' 259 | .callsFake (callback) -> 260 | callback() 261 | hooksStub.skippedTests = [test.name] 262 | runner.run [test], hooksStub, done 263 | 264 | after () -> 265 | hooksStub.skippedTests = [] 266 | runner.mocha.run.restore() 267 | runner = undefined 268 | test = undefined 269 | 270 | it 'should run mocha', () -> 271 | assert.ok runner.mocha.run.called 272 | 273 | it 'should generate mocha suite', () -> 274 | suites = runner.mocha.suite.suites 275 | assert.equal suites.length, 1 276 | assert.equal suites[0].title, 'GET /machines -> 200' 277 | 278 | it 'should generate pending mocha test', () -> 279 | tests = runner.mocha.suite.suites[0].tests 280 | assert.equal tests.length, 1 281 | assert.ok tests[0].pending 282 | 283 | 284 | describe 'when test has no response schema', () -> 285 | 286 | before (done) -> 287 | test = createStdTest() 288 | test.response.status = 200 289 | 290 | options = 291 | server: "#{SERVER}" 292 | 293 | runner = new TestRunner options, '' 294 | sinon.stub runner.mocha, 'run' 295 | .callsFake (callback) -> 296 | callback() 297 | sinon.stub test, 'run' 298 | .callsFake (callback) -> 299 | callback() 300 | 301 | runner.run [test], hooksStub, done 302 | 303 | after () -> 304 | runner.mocha.run.restore() 305 | runner = undefined 306 | test = undefined 307 | 308 | it 'should run mocha', () -> 309 | assert.ok runner.mocha.run.called 310 | 311 | it 'should generate mocha suite', () -> 312 | suites = runner.mocha.suite.suites 313 | assert.equal suites.length, 1 314 | assert.equal suites[0].title, 'GET /machines -> 200' 315 | 316 | it 'should not generate pending mocha test', () -> 317 | tests = runner.mocha.suite.suites[0].tests 318 | assert.equal tests.length, 1 319 | assert.notOk tests[0].pending 320 | 321 | 322 | describe 'when test throws AssertionError', () -> 323 | 324 | afterAllHook = undefined 325 | 326 | before (done) -> 327 | test = createStdTest() 328 | test.response.status = 200 329 | 330 | afterAllHook = sinon.stub() 331 | afterAllHook.callsArg(0) 332 | 333 | hooksStub.afterAllHooks = [afterAllHook] 334 | 335 | options = 336 | server: "#{SERVER}" 337 | 338 | runner = new TestRunner options, '' 339 | # sinon.stub runner.mocha, 'run', (callback) -> callback() 340 | testStub = sinon.stub test, 'run' 341 | testStub.throws('AssertionError') 342 | 343 | # Mute stdout/stderr 344 | mute (unmute) -> 345 | runner.run [test], hooksStub, () -> 346 | unmute() 347 | done() 348 | 349 | after () -> 350 | afterAllHook = undefined 351 | runner = undefined 352 | test = undefined 353 | 354 | it 'should call afterAll hook', () -> 355 | afterAllHook.should.have.been.called 356 | 357 | 358 | describe 'when beforeAllHooks throws UncaughtError', () -> 359 | 360 | beforeAllHook = undefined 361 | afterAllHook = undefined 362 | 363 | before (done) -> 364 | test = createStdTest() 365 | test.response.status = 200 366 | 367 | beforeAllHook = sinon.stub() 368 | beforeAllHook.throws('Error') 369 | afterAllHook = sinon.stub() 370 | afterAllHook.callsArg(0) 371 | 372 | hooksStub.beforeAllHooks = [beforeAllHook] 373 | hooksStub.afterAllHooks = [afterAllHook] 374 | 375 | options = 376 | server: "#{SERVER}" 377 | 378 | runner = new TestRunner options, '' 379 | sinon.stub test, 'run' 380 | .callsFake (callback) -> 381 | callback() 382 | 383 | # Mute stdout/stderr 384 | mute (unmute) -> 385 | runner.run [test], hooksStub, () -> 386 | unmute() 387 | done() 388 | 389 | after () -> 390 | beforeAllHook = undefined 391 | afterAllHook = undefined 392 | runner = undefined 393 | test = undefined 394 | 395 | it 'should call afterAll hook', () -> 396 | afterAllHook.should.have.been.called 397 | 398 | 399 | describe '#run with options', () -> 400 | 401 | describe 'list all tests with `names`', () -> 402 | 403 | before (done) -> 404 | test = createStdTest() 405 | test.response.status = 200 406 | test.response.schema = """[ 407 | type: 'string' 408 | name: 'string' 409 | ]""" 410 | 411 | options = 412 | names: true 413 | 414 | runner = new TestRunner options, '' 415 | sinon.stub runner.mocha, 'run' 416 | .callsFake (callback) -> 417 | callback() 418 | sinon.spy console, 'log' 419 | 420 | # Mute stdout/stderr 421 | mute (unmute) -> 422 | runner.run [test], hooksStub, () -> 423 | unmute() 424 | done() 425 | 426 | after () -> 427 | console.log.restore() 428 | runner.mocha.run.restore() 429 | runner = undefined 430 | test = undefined 431 | 432 | it 'should not run mocha', () -> 433 | assert.notOk runner.mocha.run.called 434 | 435 | it 'should print tests', () -> 436 | assert.ok console.log.calledWith('GET /machines -> 200') 437 | 438 | 439 | describe 'add additional headers with `headers`', () -> 440 | 441 | receivedTest = undefined 442 | headers = undefined 443 | 444 | before (done) -> 445 | test = createStdTest() 446 | test.response.status = 200 447 | test.response.schema = {} 448 | 449 | headers = 450 | key: 'value' 451 | 'X-Abao-Version': pkg.version 452 | 453 | options = 454 | server: "#{SERVER}" 455 | header: headers 456 | 457 | runner = new TestRunner options, '' 458 | sinon.stub runner.mocha, 'run' 459 | .callsFake (callback) -> 460 | receivedTest = _.cloneDeep test 461 | callback() 462 | 463 | runner.run [test], hooksStub, done 464 | 465 | after () -> 466 | runner.mocha.run.restore() 467 | runner = undefined 468 | test = undefined 469 | 470 | it 'should run mocha', () -> 471 | assert.ok runner.mocha.run.called 472 | 473 | it 'should add headers into test', () -> 474 | assert.deepEqual receivedTest.request.headers, headers 475 | 476 | 477 | describe 'run test with hooks only indicated by `hooks-only`', () -> 478 | 479 | suiteStub = undefined 480 | 481 | before (done) -> 482 | test = createStdTest() 483 | test.response.status = 200 484 | test.response.schema = {} 485 | 486 | options = 487 | server: "#{SERVER}" 488 | 'hooks-only': true 489 | 490 | runner = new TestRunner options, '' 491 | 492 | mochaStub = runner.mocha 493 | originSuiteCreate = mocha.Suite.create 494 | sinon.stub mocha.Suite, 'create' 495 | .callsFake (parent, title) -> 496 | suiteStub = originSuiteCreate.call(mocha.Suite, parent, title) 497 | 498 | # Stub suite 499 | sinon.spy suiteStub, 'addTest' 500 | sinon.spy suiteStub, 'beforeAll' 501 | sinon.spy suiteStub, 'afterAll' 502 | 503 | suiteStub 504 | 505 | sinon.stub mochaStub, 'run' 506 | .callsFake (callback) -> 507 | callback() 508 | 509 | runner.run [test], hooksStub, done 510 | 511 | after () -> 512 | suiteStub.addTest.restore() 513 | suiteStub.beforeAll.restore() 514 | suiteStub.afterAll.restore() 515 | mocha.Suite.create.restore() 516 | runner.mocha.run.restore() 517 | runner = undefined 518 | test = undefined 519 | 520 | it 'should run mocha', () -> 521 | assert.ok runner.mocha.run.called 522 | 523 | it 'should add a pending test' 524 | # TODO(quanlong): Implement this test 525 | # console.log suiteStub.addTest.printf('%n-%c-%C') 526 | # assert.ok suiteStub.addTest.calledWithExactly('GET /machines -> 200') 527 | 528 | -------------------------------------------------------------------------------- /test/unit/test-test.coffee: -------------------------------------------------------------------------------- 1 | chai = require 'chai' 2 | sinon = require 'sinon' 3 | sinonChai = require 'sinon-chai' 4 | _ = require 'underscore' 5 | proxyquire = require('proxyquire').noCallThru() 6 | 7 | assert = chai.assert 8 | should = chai.should() 9 | chai.use(sinonChai) 10 | 11 | requestStub = sinon.stub() 12 | requestStub.restore = () -> 13 | 'use strict' 14 | this.callsArgWith(1, null, {statusCode: 200}, '') 15 | 16 | TestFactory = proxyquire '../../lib/test', { 17 | 'request': requestStub 18 | } 19 | 20 | ABAO_IO_SERVER = 'http://abao.io' 21 | 22 | 23 | describe 'Test', () -> 24 | 'use strict' 25 | 26 | describe '#run', () -> 27 | 28 | describe 'of simple test', () -> 29 | 30 | testFact = '' 31 | test = '' 32 | machine = '' 33 | contentTestCalled = null 34 | 35 | before (done) -> 36 | 37 | testFact = new TestFactory() 38 | test = testFact.create() 39 | contentTestCalled = false 40 | test.name = 'POST /machines -> 201' 41 | test.request.server = "#{ABAO_IO_SERVER}" 42 | test.request.path = '/machines' 43 | test.request.method = 'POST' 44 | test.request.params = 45 | param: 'value' 46 | test.request.query = 47 | q: 'value' 48 | test.request.headers = 49 | key: 'value' 50 | test.request.body = 51 | body: 'value' 52 | test.response.status = 201 53 | test.response.schema = [ 54 | type: 'object' 55 | properties: 56 | type: 'string' 57 | name: 'string' 58 | ] 59 | 60 | machine = 61 | type: 'foo' 62 | name: 'bar' 63 | 64 | test.contentTest = (response, body, done) -> 65 | contentTestCalled = true 66 | assert.equal(response.status, 201) 67 | assert.deepEqual(JSON.parse(body), machine) 68 | return done() 69 | 70 | requestStub.callsArgWith(1, null, {statusCode: 201}, JSON.stringify(machine)) 71 | test.run done 72 | 73 | after () -> 74 | requestStub.restore() 75 | 76 | it 'should call #request', () -> 77 | requestStub.should.be.calledWith 78 | url: "#{ABAO_IO_SERVER}/machines" 79 | method: 'POST' 80 | headers: 81 | key: 'value' 82 | qs: 83 | q: 'value' 84 | body: JSON.stringify 85 | body: 'value' 86 | 87 | it 'should not modify @name', () -> 88 | assert.equal test.name, 'POST /machines -> 201' 89 | 90 | it 'should not modify @request', () -> 91 | request = test.request 92 | assert.equal request.server, "#{ABAO_IO_SERVER}" 93 | assert.equal request.path, '/machines' 94 | assert.equal request.method, 'POST' 95 | assert.deepEqual request.params, {param: 'value'} 96 | assert.deepEqual request.query, {q: 'value'} 97 | assert.deepEqual request.headers, {key: 'value'} 98 | 99 | it 'should update @response', () -> 100 | response = test.response 101 | # Unchanged properties 102 | assert.equal response.status, 201 103 | 104 | # changed properties 105 | # assert.equal response.headers, 201 106 | assert.deepEqual response.body, machine 107 | 108 | it 'should call contentTest', () -> 109 | assert.isTrue contentTestCalled 110 | 111 | 112 | describe 'of test that contains params', () -> 113 | 114 | test = '' 115 | machine = '' 116 | 117 | before (done) -> 118 | 119 | testFact = new TestFactory() 120 | test = testFact.create() 121 | test.name = 'PUT /machines/{machine_id} -> 200' 122 | test.request.server = "#{ABAO_IO_SERVER}" 123 | test.request.path = '/machines/{machine_id}' 124 | test.request.method = 'PUT' 125 | test.request.params = 126 | machine_id: '1' 127 | test.request.query = 128 | q: 'value' 129 | test.request.headers = 130 | key: 'value' 131 | test.request.body = 132 | body: 'value' 133 | test.response.status = 200 134 | test.response.schema = [ 135 | type: 'object' 136 | properties: 137 | type: 'string' 138 | name: 'string' 139 | ] 140 | 141 | machine = 142 | type: 'foo' 143 | name: 'bar' 144 | 145 | requestStub.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(machine)) 146 | test.run done 147 | 148 | after () -> 149 | requestStub.restore() 150 | 151 | it 'should call #request', () -> 152 | requestStub.should.be.calledWith 153 | url: "#{ABAO_IO_SERVER}/machines/1" 154 | method: 'PUT' 155 | headers: 156 | key: 'value' 157 | qs: 158 | q: 'value' 159 | body: JSON.stringify 160 | body: 'value' 161 | 162 | it 'should not modify @name', () -> 163 | assert.equal test.name, 'PUT /machines/{machine_id} -> 200' 164 | 165 | it 'should not modify @request', () -> 166 | request = test.request 167 | assert.equal request.server, "#{ABAO_IO_SERVER}" 168 | assert.equal request.path, '/machines/{machine_id}' 169 | assert.equal request.method, 'PUT' 170 | assert.deepEqual request.params, {machine_id: '1'} 171 | assert.deepEqual request.query, {q: 'value'} 172 | assert.deepEqual request.headers, {key: 'value'} 173 | 174 | it 'should update @response', () -> 175 | response = test.response 176 | # Unchanged properties 177 | assert.equal response.status, 200 178 | assert.deepEqual response.body, machine 179 | 180 | describe 'construct a TestFactory', () -> 181 | 182 | globStub = {} 183 | globStub.sync = sinon.spy((location) -> 184 | return [location] 185 | ) 186 | 187 | fsStub = {} 188 | fsStub.readFileSync = sinon.spy(() -> 189 | return '{ "text": "example" }' 190 | ) 191 | 192 | tv4Stub = {} 193 | tv4Stub.addSchema = sinon.spy() 194 | 195 | TestTestFactory = proxyquire '../../lib/test', { 196 | 'fs': fsStub, 197 | 'glob': globStub, 198 | 'tv4': tv4Stub 199 | } 200 | 201 | it 'test TestFactory without parameter', () -> 202 | new TestTestFactory('') 203 | assert.isFalse globStub.sync.called 204 | assert.isFalse fsStub.readFileSync.called 205 | assert.isFalse tv4Stub.addSchema.called 206 | 207 | it 'test TestFactory with name 1', () -> 208 | new TestTestFactory('thisisaword') 209 | assert.isTrue globStub.sync.calledWith 'thisisaword' 210 | assert.isTrue fsStub.readFileSync.calledOnce 211 | assert.isTrue fsStub.readFileSync.calledWith 'thisisaword', 'utf8' 212 | assert.isTrue tv4Stub.addSchema.calledWith(JSON.parse('{ "text": "example" }')) 213 | 214 | it 'test TestFactory with name 2', () -> 215 | new TestTestFactory('thisIsAnotherWord') 216 | assert.isTrue globStub.sync.calledWith 'thisIsAnotherWord' 217 | assert.isTrue fsStub.readFileSync.calledTwice 218 | assert.isTrue fsStub.readFileSync.calledWith 'thisIsAnotherWord', 'utf8' 219 | assert.isTrue tv4Stub.addSchema.calledWith(JSON.parse('{ "text": "example" }')) 220 | 221 | 222 | describe '#url', () -> 223 | 224 | describe 'when called with path that does not contain param', () -> 225 | testFact = new TestFactory() 226 | test = testFact.create() 227 | test.request.path = '/machines' 228 | 229 | it 'should return origin path', () -> 230 | assert.equal test.url(), '/machines' 231 | 232 | describe 'when called with path that contains param', () -> 233 | testFact = new TestFactory() 234 | test = testFact.create() 235 | test.request.path = '/machines/{machine_id}/parts/{part_id}' 236 | test.request.params = 237 | machine_id: 'tianmao' 238 | part_id: 2 239 | 240 | it 'should replace all params', () -> 241 | assert.equal test.url(), '/machines/tianmao/parts/2' 242 | 243 | it 'should not touch origin request.path', () -> 244 | assert.equal test.request.path, '/machines/{machine_id}/parts/{part_id}' 245 | 246 | 247 | describe '#assertResponse', () -> 248 | 249 | errorStub = '' 250 | responseStub = '' 251 | bodyStub = '' 252 | 253 | testFact = new TestFactory() 254 | test = testFact.create() 255 | test.response.status = 201 256 | test.response.schema = { 257 | $schema: 'http://json-schema.org/draft-04/schema#' 258 | type: 'object' 259 | properties: 260 | type: 261 | type: 'string' 262 | name: 263 | type: 'string' 264 | } 265 | 266 | describe 'when given valid response', () -> 267 | 268 | it 'should pass all asserts', () -> 269 | 270 | errorStub = null 271 | responseStub = 272 | statusCode: 201 273 | bodyStub = JSON.stringify 274 | type: 'foo' 275 | name: 'bar' 276 | # assert.doesNotThrow 277 | test.assertResponse(errorStub, responseStub, bodyStub) 278 | 279 | describe 'when given invalid response', () -> 280 | describe 'when response body is null', () -> 281 | 282 | it 'should throw AssertionError', () -> 283 | 284 | errorStub = null 285 | responseStub = 286 | statusCode: 201 287 | bodyStub = null 288 | fn = _.partial test.assertResponse, errorStub, responseStub, bodyStub 289 | assert.throw fn, chai.AssertionError 290 | 291 | describe 'when response body is invalid JSON', () -> 292 | 293 | it 'should throw AssertionError', () -> 294 | 295 | errorStub = null 296 | responseStub = 297 | statusCode: 201 298 | bodyStub = 'Im invalid' 299 | fn = _.partial test.assertResponse, errorStub, responseStub, bodyStub 300 | assert.throw fn, chai.AssertionError 301 | 302 | -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | ### 2 | ### wercker.yml 3 | ### 4 | 5 | box: node:latest 6 | 7 | ## Build definition 8 | build: 9 | steps: 10 | # A custom script step, name value is used in the UI 11 | # and the code value contains the command that get executed 12 | - script: 13 | name: NodeJS information 14 | code: | 15 | echo "node version $(node -v) running" 16 | echo "npm version $(npm -v) running" 17 | echo "USER: $USER" 18 | # A step that executes `npm install` command 19 | - npm-install 20 | # A custom script step 21 | - script: 22 | code: export NODE_ENV='testing' 23 | # A step that executes `npm test` command 24 | - npm-test 25 | 26 | --------------------------------------------------------------------------------