├── .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 | [](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 | [](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 |
--------------------------------------------------------------------------------