diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bfa66f4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,35 @@ +### +### .editorconfig +### + + +## Topmost EditorConfig file +root = true + +## UTF8, Unix-style newlines, newline ends every file, trim trailing whitespace +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +## Source code: 2 space indentation +[*.coffee,*.js] +indent_style = space +indent_size = 2 + +## RAML: 2 space indentation +[*.raml] +indent_style = space +indent_size = 2 + +## Config files: 2 space indentation +[*.json,*.yml,*.yaml] +indent_style = space +indent_size = 2 + +## Matches the exact files either package.json or .travis.yml +[{package.json,.travis.yml}] +indent_style = space +indent_size = 2 + diff --git a/.gitignore b/.gitignore index a41115d..3c8b9fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,33 +1,37 @@ -# Mac shit +### +### .gitignore +### + +## Mac shit .DS_Store -# Logfiles -logs -*.log +## Editor-related tempfiles +.*.sw[op] +*~ -# Runtime data -pids -*.pid -*.seed +## Logfiles +*.log +logs/ -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov +## Build tool configuration files and intermediate storage +.grunt/ -# Coverage directory used by tools like istanbul -coverage +# Node/npm +.node_repl_history +.npm/ -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt +## Package dependencies +node_modules/ -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release +## Coverage reports and instrumented source +.nyc_output/ +coverage/ +lib-cov/ +lib/*.js -# Dependency directory -# Deployed apps should consider commenting this line out: -# see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git -node_modules +## `npm pack` artifact +abao-[0-9]*.[0-9]*.[0-9]*.tgz -# Generated from CoffeeScript source -lib/*.js -tests/unit/*.js +## dotenv environment variables file +.env diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..dcb905f --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,18 @@ +{ + "default": true, + "header-style": { "style": "atx" }, + "ul-style": { "style": "asterisk" }, + "ul-indent": { "indent": 2 }, + "no-multiple-blanks": { "maximum": 2 }, + "line-length": { "line_length": 120 }, + "commands-show-output": false, + "ol-prefix": { "style": "one" }, + "hr-style": { "style": "---" }, + "proper-names": { + "names": [ + "CoffeeScript", + "JavaScript" + ], + "code_blocks": false + } +} diff --git a/.npmignore b/.npmignore index 20b513f..091eb02 100644 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,39 @@ -test/ -lib/*.js +### +### .npmignore +### + +## Git +.gitignore + +## GitHub +.github/ + +## CI configuration files +.codeclimate.yml .travis.yml +wercker.yml + +## Build tool configuration files and intermediate storage +.editorconfig +.grunt/ +.markdownlint.json Gruntfile.coffee +coffeelint.json + +## Node/npm +.node_repl_history +.npm/ + +## Coverage reports and instrumented source +coverage/ +lib/*.js + +## Test code and fixtures +test/ + +## `npm pack` artifact +abao-[0-9]*.[0-9]*.[0-9]*.tgz + +# dotenv environment variables file +.env + diff --git a/.travis.yml b/.travis.yml index d3c2098..6bf4916 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ node_js: - '6' - '4' env: -- NODE_ENV=development + - NODE_ENV=development notifications: slack: secure: fsCX0/TDE9TAJR0S91dboOZ4expmCc8o6joVzsHNJYTJfDtSJehdKjTzYuO/vsRigOOoQZ0dJEPl+D4fysBDV+jkOT5sTjp/uKtcfwHwPi03K8GauwvyW0x4N1M+mY+5jN2ZyBZXqVM5dc0wbgldP9QOg5UpB80hfWUZ+0F1MTM= @@ -26,11 +26,18 @@ deploy: repo: cybertk/abao after_success: # - grunt coveralls:upload - - BUILD_DIR="$HOME/build" - - GITHUB_REPO="cybertk/abao" - - PROJ_BUILD_DIR="$BUILD_DIR/$GITHUB_REPO" - - PROJ_COVER_DIR="$PROJ_BUILD_DIR/coverage" - - COVERAGE_FILE="$PROJ_COVER_DIR/coverage.lcov" - - COVERALLS_BIN="./node_modules/coveralls/bin/coveralls.js" + - COVERAGE_FILE="$TRAVIS_BUILD_DIR/coverage/coverage.lcov" + - COVERALLS_BIN="./node_modules/.bin/coveralls" - $COVERALLS_BIN lib < $COVERAGE_FILE; echo "exit=$?" + - echo + - echo + - echo "===== COMMIT =====" + - echo "TRAVIS_REPO_SLUG=$TRAVIS_REPO_SLUG" + - echo "TRAVIS_COMMIT=$TRAVIS_COMMIT" + - echo "TRAVIS_COMMIT_MESSAGE=$TRAVIS_COMMIT_MESSAGE" + - echo "TRAVIS_TAG=$TRAVIS_TAG" + - echo "TRAVIS_BRANCH=$TRAVIS_BRANCH" + - echo "===== BUILD =====" + - echo "TRAVIS_BUILD_NUMBER=$TRAVIS_BUILD_NUMBER" + - echo "TRAVIS_BUILD_DIR=$TRAVIS_BUILD_DIR" diff --git a/Gruntfile.coffee b/Gruntfile.coffee index d53dbf5..222f759 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -1,5 +1,6 @@ module.exports = (grunt) -> + 'use strict' require('time-grunt') grunt # Dynamically load npm tasks @@ -10,6 +11,7 @@ module.exports = (grunt) -> # Load in the module information pkg: grunt.file.readJSON 'package.json' + readme: 'README.md' gruntfile: 'Gruntfile.coffee' clean: @@ -42,6 +44,8 @@ module.exports = (grunt) -> ] coffeelint: + options: + configFile: 'coffeelint.json' default: src: [ 'lib/*.coffee' @@ -49,8 +53,14 @@ module.exports = (grunt) -> ] gruntfile: src: '<%= gruntfile %>' + + markdownlint: options: - configFile: 'coffeelint.json' + config: require './.markdownlint.json' + default: + src: [ + '<%= readme %>' + ] coffeecov: transpile: @@ -84,7 +94,10 @@ module.exports = (grunt) -> ] grunt.registerTask 'instrument', [ 'coffeecov' ] - grunt.registerTask 'lint', [ 'coffeelint' ] + grunt.registerTask 'lint', [ + 'coffeelint', + 'markdownlint' + ] grunt.registerTask 'test', [ 'lint' diff --git a/README.md b/README.md index cdfd9af..aa988cb 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -Automated testing tool based on RAML-0.8 - # Abao +RAML-based automated testing tool + [![Build Status][Travis-Abao-badge]][Travis-Abao] -[![Dependency Status][David-AbaoDep-badge]][David-AbaoDep] -[![devDependency Status][David-AbaoDevDep-badge]][David-AbaoDevDep] +[![Dependency Status][DavidDM-AbaoDep-badge]][DavidDM-AbaoDep] +[![devDependency Status][DavidDM-AbaoDevDep-badge]][DavidDM-AbaoDevDep] [![Coverage Status][Coveralls-Abao-badge]][Coveralls-Abao] [![Gitter][Gitter-Abao-badge]][Gitter-Abao] [![CII Best Practices][BestPractices-Abao-badge]][BestPractices-Abao] @@ -20,13 +20,13 @@ is valid or not. ## Features -- Verify that each endpoint defined in RAML exists in service -- Verify that URL params for each endpoint defined in RAML are supported in service -- Verify that the required query parameters defined in RAML are supported in service -- Verify that HTTP request headers for each endpoint defined in RAML are supported in service -- Verify that HTTP request body for each endpoint defined in RAML is supported in service, via [JSONSchema][] validation -- Verify that HTTP response headers for each endpoint defined in RAML are supported in service -- Verify that HTTP response body for each endpoint defined in RAML is supported in service, via [JSONSchema][] validation +* Verify that each endpoint defined in RAML exists in service +* Verify that URL params for each endpoint defined in RAML are supported in service +* Verify that the required query parameters defined in RAML are supported in service +* Verify that HTTP request headers for each endpoint defined in RAML are supported in service +* Verify that HTTP request body for each endpoint defined in RAML is supported in service, via [JSONSchema][] validation +* Verify that HTTP response headers for each endpoint defined in RAML are supported in service +* Verify that HTTP response body for each endpoint defined in RAML is supported in service, via [JSONSchema][] validation ## RAML Support @@ -93,12 +93,12 @@ the RAML. You can print a list of the generated names with the `--names` flag. ### Example -The RAML file used in the examples below can be found [here](../master/test/fixtures/single-get.raml). +The RAML file used in the examples below can be found [here](../master/test/fixtures/machines-single_get.raml). Get Names: ```bash -$ abao single-get.raml --names +$ abao machines-single_get.raml --names GET /machines -> 200 ``` @@ -108,7 +108,7 @@ response code for each path. ```bash $ ABAO_HOME="/path/to/node_modules/abao" $ TEMPLATE="${ABAO_HOME}/templates/hookfile.js" -$ abao single-get.raml --generate-hooks --template="${TEMPLATE}" > test_machines_hooks.js +$ abao machines-single_get.raml --generate-hooks --template="${TEMPLATE}" > test_machines_hooks.js ``` @@ -155,7 +155,7 @@ after 'GET /machines -> 200', (test, done) -> Run validation with *JavaScript* hookfile (from above): ```bash -$ abao single-get.raml --hookfiles=test_machines_hooks.js +$ abao machines-single_get.raml --hookfiles=test_machines_hooks.js ``` You can also specify what tests **Abao** should skip: @@ -208,24 +208,27 @@ test 'GET /machines -> 200', (response, body, done) -> ### test.request -- `server` - Server address, provided from command line. -- `path` - API endpoint path, parsed from RAML. -- `method` - HTTP method, parsed from RAML request method (e.g., `get`). -- `params` - URI parameters, parsed from RAML request `uriParameters` [default: `{}`]. -- `query` - Object containing querystring values to be appended to the `path`,parsed from RAML `queryParameters` section [default: `{}`]. -- `headers` - HTTP headers, parsed from RAML `headers` [default: `{}`]. -- `body` - Entity body for POST, PUT, and PATCH requests. Must be a JSON-serializable object. Parsed from RAML `example` [default: `{}`]. +* `server` - Server address, provided by command line option or parsed from + RAML `baseUri`. +* `path` - API endpoint path, parsed from RAML. +* `method` - HTTP method, parsed from RAML request method (e.g., `get`). +* `params` - URI parameters, parsed from RAML request `uriParameters` [default: `{}`]. +* `query` - Object containing querystring values to be appended to the `path`. + Parsed from RAML `queryParameters` section [default: `{}`]. +* `headers` - HTTP headers, parsed from RAML `headers` [default: `{}`]. +* `body` - Entity body for POST, PUT, and PATCH requests. Must be a + JSON-serializable object. Parsed from RAML `example` [default: `{}`]. ### test.response -- `status` - Expected HTTP response code, parsed from RAML response status. -- `schema` - Expected schema of HTTP response body, parsed from RAML response `schema`. -- `headers` - Object containing HTTP response headers from server [default: `{}`]. -- `body` - HTTP response body (JSON-format) from server [default: `null`]. +* `status` - Expected HTTP response code, parsed from RAML response status. +* `schema` - Expected schema of HTTP response body, parsed from RAML response `schema`. +* `headers` - Object containing HTTP response headers from server [default: `{}`]. +* `body` - HTTP response body (JSON-format) from server [default: `null`]. ## Command Line Options -``` +```console Usage: abao [OPTIONS] @@ -273,6 +276,11 @@ $ npm test **Abao** is always looking for new ideas to make the codebase useful. If you think of something that would make life easier, please submit an issue. +```bash +$ npm issues abao +``` + + [//]: # (Cross reference section) [RAML]: https://raml.org/ @@ -285,10 +293,10 @@ If you think of something that would make life easier, please submit an issue. [Travis-Abao]: https://travis-ci.org/cybertk/abao/ [Travis-Abao-badge]: https://img.shields.io/travis/cybertk/abao.svg?style=flat -[David-AbaoDep]: https://david-dm.org/cybertk/abao/ -[David-AbaoDep-badge]: https://david-dm.org/cybertk/abao/status.svg -[David-AbaoDevDep]: https://david-dm.org/cybertk/abao?type=dev -[David-AbaoDevDep-badge]: https://david-dm.org/cybertk/abao/dev-status.svg +[DavidDM-AbaoDep]: https://david-dm.org/cybertk/abao/ +[DavidDM-AbaoDep-badge]: https://david-dm.org/cybertk/abao/status.svg +[DavidDM-AbaoDevDep]: https://david-dm.org/cybertk/abao?type=dev +[DavidDM-AbaoDevDep-badge]: https://david-dm.org/cybertk/abao/dev-status.svg [Coveralls-Abao]: https://coveralls.io/r/cybertk/abao/ [Coveralls-Abao-badge]: https://img.shields.io/coveralls/cybertk/abao.svg [Gitter-Abao]: https://gitter.im/cybertk/abao/ @@ -298,4 +306,3 @@ If you think of something that would make life easier, please submit an issue. [NPM-Abao]: https://npmjs.org/package/abao/ [NPM-Abao-badge]: https://nodei.co/npm/abao.png?downloads=true&downloadRank=true&stars=true - diff --git a/bin/abao b/bin/abao index 9671ac2..e4cc1e1 100755 --- a/bin/abao +++ b/bin/abao @@ -12,6 +12,5 @@ var path = require('path'); var fs = require('fs'); var libpath = path.join(path.dirname(fs.realpathSync(__filename)), '../lib'); - -require(libpath + '/cli'); +require(libpath + '/cli').main(process.argv.slice(2)); diff --git a/coffeelint.json b/coffeelint.json index 362f039..ad370e2 100644 --- a/coffeelint.json +++ b/coffeelint.json @@ -1,7 +1,141 @@ { + "arrow_spacing": { + "level": "warn" + }, + "braces_spacing": { + "level": "warn", + "spaces": 0, + "empty_object_spaces": 0 + }, + "camel_case_classes": { + "level": "error" + }, + "coffeescript_error": { + "level": "error" + }, + "colon_assignment_spacing": { + "level": "warn", + "spacing": { + "left": 0, + "right": 1 + } + }, + "cyclomatic_complexity": { + "level": "warn", + "value": 10 + }, + "duplicate_key": { + "level": "error" + }, + "empty_constructor_needs_parens": { + "level": "warn" + }, + "ensure_comprehensions": { + "level": "warn" + }, + "eol_last": { + "level": "ignore" + }, + "indentation": { + "value": 2, + "level": "error" + }, + "line_endings": { + "level": "error", + "value": "unix" + }, "max_line_length": { "value": 120, "level": "error", "limitComments": true + }, + "missing_fat_arrows": { + "level": "warn", + "is_strict": false + }, + "newlines_after_classes": { + "value": 3, + "level": "warn" + }, + "no_backticks": { + "level": "error" + }, + "no_debugger": { + "level": "warn", + "console": false + }, + "no_empty_functions": { + "level": "warn" + }, + "no_empty_param_list": { + "level": "ignore" + }, + "no_implicit_braces": { + "level": "ignore", + "strict": true + }, + "no_implicit_parens": { + "level": "ignore", + "strict": true + }, + "no_interpolation_in_single_quotes": { + "level": "warn" + }, + "no_nested_string_interpolation": { + "level": "warn" + }, + "no_plusplus": { + "level": "ignore" + }, + "no_private_function_fat_arrows": { + "level": "warn" + }, + "no_stand_alone_at": { + "level": "warn" + }, + "no_tabs": { + "level": "error" + }, + "no_this": { + "level": "warn" + }, + "no_throwing_strings": { + "level": "error" + }, + "no_trailing_semicolons": { + "level": "error" + }, + "no_trailing_whitespace": { + "level": "error", + "allowed_in_comments": false, + "allowed_in_empty_lines": false + }, + "no_unnecessary_double_quotes": { + "level": "warn" + }, + "no_unnecessary_fat_arrows": { + "level": "warn" + }, + "non_empty_constructor_needs_parens": { + "level": "ignore" + }, + "prefer_english_operator": { + "level": "ignore", + "doubleNotLevel": "ignore" + }, + "space_operators": { + "level": "warn" + }, + "spacing_after_comma": { + "level": "error" + }, + "transform_messes_up_line_numbers": { + "level": "warn" + }, + "use_strict": { + "module": "coffeelint-use-strict", + "level": "warn", + "allowGlobal": false, + "requireGlobal": false } } diff --git a/lib/abao.coffee b/lib/abao.coffee index 2941f97..fa03def 100644 --- a/lib/abao.coffee +++ b/lib/abao.coffee @@ -2,59 +2,77 @@ # @file Abao class ### -sms = require("source-map-support").install({handleUncaughtExceptions: false}) -ramlParser = require 'raml-parser' +require('source-map-support').install({handleUncaughtExceptions: false}) async = require 'async' +ramlParser = require 'raml-parser' -options = require './options' addTests = require './add-tests' -TestFactory = require './test' addHooks = require './add-hooks' -Runner = require './test-runner' -applyConfiguration = require './apply-configuration' +asConfiguration = require './configuration' hooks = require './hooks' +Runner = require './test-runner' +TestFactory = require './test' + +defaultArgs = + _: [] + options: + help: true class Abao - constructor: (config) -> - @configuration = applyConfiguration(config) + constructor: (parsedArgs = defaultArgs) -> + 'use strict' + @configuration = asConfiguration parsedArgs @tests = [] @hooks = hooks run: (done) -> + 'use strict' config = @configuration tests = @tests hooks = @hooks - # Inject the JSON refs schemas - factory = new TestFactory(config.options.schemas) + parseHooks = (callback) -> + addHooks hooks, config.options.hookfiles, callback + return # NOTREACHED + + loadRAML = (callback) -> + if !config.ramlPath + nofile = new Error 'unspecified RAML file' + return callback nofile + + ramlParser.loadFile config.ramlPath + .then (raml) -> + return callback null, raml + .catch (err) -> + return callback err + return # NOTREACHED + + parseTestsFromRAML = (raml, callback) -> + if !config.options.server + if raml.baseUri + config.options.server = raml.baseUri + + # Inject the JSON refs schemas + factory = new TestFactory config.options.schemas + + addTests raml, tests, hooks, callback, factory, config.options.sorted + return # NOTREACHED + + runTests = (callback) -> + runner = new Runner config.options, config.ramlPath + runner.run tests, hooks, callback + return # NOTREACHED async.waterfall [ - # Parse hooks - (callback) -> - addHooks hooks, config.options.hookfiles - callback() - , - # Load RAML - (callback) -> - ramlParser.loadFile(config.ramlPath).then (raml) -> - callback(null, raml) - , callback - , - # Parse tests from RAML - (raml, callback) -> - if !config.options.server - if raml.baseUri - config.options.server = raml.baseUri - addTests raml, tests, hooks, callback, factory, config.options.sorted - , - # Run tests - (callback) -> - runner = new Runner config.options, config.ramlPath - runner.run tests, hooks, callback + parseHooks, + loadRAML, + parseTestsFromRAML, + runTests ], done + return + module.exports = Abao -module.exports.options = options diff --git a/lib/add-hooks.coffee b/lib/add-hooks.coffee index 247c1ad..32320cf 100644 --- a/lib/add-hooks.coffee +++ b/lib/add-hooks.coffee @@ -3,32 +3,34 @@ ### require 'coffee-script/register' -proxyquire = require('proxyquire').noCallThru() glob = require 'glob' path = require 'path' +proxyquire = require('proxyquire').noCallThru() -addHooks = (hooks, pattern) -> - +addHooks = (hooks, pattern, callback) -> + 'use strict' if pattern files = glob.sync pattern - console.info 'hook file pattern matches: ' + files + if files.length == 0 + nomatch = new Error "no hook files found matching pattern '#{pattern}'" + return callback nomatch + console.info 'processing hook file(s):' try - for file in files - proxyquire path.resolve(process.cwd(), file), { + files.map (file) -> + absFile = path.resolve process.cwd(), file + console.info ' ' + absFile + proxyquire absFile, { 'hooks': hooks } + console.log() catch error - console.error 'skipping hook loading...' - console.group - console.error 'error resolving absolute paths of hook files (' + files + ')' - console.error 'the "--hookfiles" pattern is probably invalid.' - console.error 'message: ' + error.message if error.message? - console.error 'stack: ' + error.stack if error.stack? - console.groupEnd - return + console.error 'error loading hooks...' + return callback error + + return callback null module.exports = addHooks diff --git a/lib/add-tests.coffee b/lib/add-tests.coffee index 47aa751..460578e 100644 --- a/lib/add-tests.coffee +++ b/lib/add-tests.coffee @@ -3,11 +3,12 @@ ### async = require 'async' -_ = require 'lodash' csonschema = require 'csonschema' +_ = require 'lodash' parseSchema = (source) -> + 'use strict' if source.contains('$schema') #jsonschema # @response.schema = JSON.parse @response.schema @@ -16,14 +17,24 @@ parseSchema = (source) -> csonschema.parse source # @response.schema = csonschema.parse @response.schema + parseHeaders = (raml) -> + 'use strict' headers = {} if raml for key, v of raml headers[key] = v.example headers + +getCompatibleMediaTypes = (bodyObj) -> + 'use strict' + vendorRE = /^application\/(.*\+)?json/i + return (type for type of bodyObj when type.match(vendorRE)) + + addTests = (raml, tests, hooks, parent, callback, testFactory, sortFirst) -> + 'use strict' # Handle 4th optional param if _.isFunction(parent) @@ -120,10 +131,11 @@ addTests = (raml, tests, hooks, parent, callback, testFactory, sortFirst) -> # Update test.request test.request.path = path test.request.method = method - test.request.headers = parseHeaders(api.headers) + test.request.headers = parseHeaders api.headers - # select compatible content-type in request body (to support vendor tree types, i.e. application/vnd.api+json) - contentType = (type for type of api.body when type.match(/^application\/(.*\+)?json/i))?[0] + # Select compatible content-type in request body to support + # vendor tree types (e.g., 'application/vnd.api+json') + contentType = getCompatibleMediaTypes(api.body)?[0] if contentType test.request.headers['Content-Type'] = contentType try @@ -138,13 +150,12 @@ addTests = (raml, tests, hooks, parent, callback, testFactory, sortFirst) -> test.response.schema = null if res?.body - # expect content-type of response body to be identical to request body + # Expect content-type of response body to be identical to request body if contentType && res.body[contentType]?.schema test.response.schema = parseSchema res.body[contentType].schema - # otherwise filter in responses section for compatible content-types - # (vendor tree, i.e. application/vnd.api+json) + # Otherwise, filter in responses section for compatible content-types else - contentType = (type for type of res.body when type.match(/^application\/(.*\+)?json/i))?[0] + contentType = getCompatibleMediaTypes(res.body)?[0] if res.body[contentType]?.schema test.response.schema = parseSchema res.body[contentType].schema diff --git a/lib/apply-configuration.coffee b/lib/apply-configuration.coffee deleted file mode 100644 index 2bbd129..0000000 --- a/lib/apply-configuration.coffee +++ /dev/null @@ -1,58 +0,0 @@ -###* -# @file Stores command line arguments in configuration object -### - -applyConfiguration = (config) -> - - coerceToArray = (value) -> - if typeof value is 'string' - value = [value] - else if !value? - value = [] - else if value instanceof Array - value - else value - return value - - coerceToDict = (value) -> - array = coerceToArray value - dict = {} - - if array.length > 0 - for item in array - [key, value] = item.split(':') - dict[key] = value - - return dict - - configuration = - ramlPath: null - options: - server: null - schemas: null - reporters: false - reporter: null - header: null - names: false - hookfiles: null - grep: '' - invert: false - 'hooks-only': false - sorted: false - - # Normalize options and config - for own key, value of config - configuration[key] = value - - # Coerce some options into an dict - configuration.options.header = coerceToDict(configuration.options.header) - - # TODO(quanlong): OAuth2 Bearer Token - if configuration.options.oauth2Token? - configuration.options.headers['Authorization'] = "Bearer #{configuration.options.oauth2Token}" - - return configuration - - -module.exports = applyConfiguration - diff --git a/lib/cli.coffee b/lib/cli.coffee index 97dca65..3fd9275 100644 --- a/lib/cli.coffee +++ b/lib/cli.coffee @@ -4,90 +4,84 @@ require 'coffee-script/register' +child_process = require 'child_process' path = require 'path' _ = require 'lodash' yargs = require 'yargs' -Abao = require '../lib/abao' + +Abao = require './abao' +abaoOptions = require './options-abao' +mochaOptions = require './options-mocha' pkg = require '../package' EXIT_SUCCESS = 0 EXIT_FAILURE = 1 showReporters = () -> - # Copied from node_modules/mocha/_mocha - console.log() - console.log ' dot - dot matrix' - console.log ' doc - html documentation' - console.log ' spec - hierarchical spec list' - console.log ' json - single json object' - console.log ' progress - progress bar' - console.log ' list - spec-style listing' - console.log ' tap - test-anything-protocol' - console.log ' landing - unicode landing strip' - console.log ' xunit - xunit reporter' - console.log ' min - minimal reporter (great with --watch)' - console.log ' json-stream - newline delimited json events' - console.log ' markdown - markdown documentation (github flavour)' - console.log ' nyan - nyan cat!' - console.log() + 'use strict' + mochaDir = path.dirname require.resolve('mocha') + mochaPkg = require 'mocha/package' + executable = path.join mochaDir, mochaPkg.bin._mocha + executable = path.normalize executable + stdoutBuff = child_process.execFileSync executable, ['--reporters'] + stdout = stdoutBuff.toString() + stdout = stdout.slice 0, stdout.length - 1 # Remove last newline + console.log stdout return -mochaOptionNames = [ - 'grep', - 'invert' - 'reporter', - 'timeout' -] -binary = path.basename pkg.bin - -argv = yargs - .usage('Usage:\n ' + binary + ' [OPTIONS]' + - '\n\nExample:\n ' + binary + ' api.raml --server http://api.example.com') - .options(Abao.options) - .group(mochaOptionNames, 'Options passed to Mocha:') - .implies('template', 'generate-hooks') - .check((argv) -> - if argv.reporters == true - showReporters() - process.exit EXIT_SUCCESS - - # Ensure single positional argument present - if argv._.length < 1 - throw new Error binary + ': must specify path to RAML file' - else if argv._.length > 1 - throw new Error binary + ': accepts single positional command-line argument' - - return true - ) - .wrap(80) - .help('help', 'Show usage information and exit') - .version().describe('version', 'Show version number and exit') - .epilog('Website:\n ' + pkg.homepage) - .argv - -aliases = Object.keys(Abao.options).map (key) -> Abao.options[key].alias - .filter (val) -> val != undefined - -configuration = - 'ramlPath': argv._[0], - 'options': _.omit argv, ['_', '$0', aliases...] - -mochaOptions = _.pick configuration.options, mochaOptionNames -configuration.options = _.omit configuration.options, mochaOptionNames -configuration.options.mocha = mochaOptions - -abao = new Abao configuration - -abao.run (error, nfailures) -> - if error - process.exitCode = EXIT_FAILURE - if error.message - console.error error.message - if error.stack - console.error error.stack - - if nfailures > 0 - process.exitCode = EXIT_FAILURE - - process.exit() +parseArgs = (argv) -> + 'use strict' + allOptions = _.assign {}, abaoOptions, mochaOptions + mochaOptionNames = Object.keys mochaOptions + prog = path.basename pkg.bin + return yargs(argv) + .usage("Usage:\n #{prog} [OPTIONS]" + + "\n\nExample:\n #{prog} api.raml --server http://api.example.com") + .options(allOptions) + .group(mochaOptionNames, 'Options passed to Mocha:') + .implies('template', 'generate-hooks') + .check((argv) -> + if argv.reporters == true + showReporters() + process.exit EXIT_SUCCESS + + # Ensure single positional argument present + if argv._.length < 1 + throw new Error "#{prog}: must specify path to RAML file" + else if argv._.length > 1 + throw new Error "#{prog}: accepts single positional command-line argument" + + return true + ) + .wrap(80) + .help('help', 'Show usage information and exit') + .version('version', 'Show version number and exit', pkg.version) + .epilog("Website:\n #{pkg.homepage}") + .argv + +## +## Main +## +main = (argv) -> + 'use strict' + parsedArgs = parseArgs argv + + abao = new Abao parsedArgs + abao.run (error, nfailures) -> + if error + process.exitCode = EXIT_FAILURE + if error.message + console.error error.message + if error.stack + console.error error.stack + + if nfailures > 0 + process.exitCode = EXIT_FAILURE + + process.exit() + return # NOTREACHED + + +module.exports = + main: main diff --git a/lib/configuration.coffee b/lib/configuration.coffee new file mode 100644 index 0000000..4ac93fc --- /dev/null +++ b/lib/configuration.coffee @@ -0,0 +1,97 @@ +###* +# @file Stores command line arguments in configuration object +### + +_ = require 'lodash' +path = require 'path' + +abaoOptions = require './options-abao' +mochaOptions = require './options-mocha' +allOptions = _.assign {}, abaoOptions, mochaOptions + + +applyConfiguration = (config) -> + 'use strict' + + coerceToArray = (value) -> + if typeof value is 'string' + value = [value] + else if !value? + value = [] + else if value instanceof Array + value + else value + return value + + coerceToDict = (value) -> + array = coerceToArray value + dict = {} + + if array.length > 0 + for item in array + [key, value] = item.split(':') + dict[key] = value + + return dict + + configuration = + ramlPath: null + options: + server: null + schemas: null + 'generate-hooks': false + template: null + timeout: 2000 + reporter: null + header: null + names: false + hookfiles: null + grep: '' + invert: false + 'hooks-only': false + sorted: false + + # Normalize options and config + for own key, value of config + configuration[key] = value + + # Customize + if !configuration.options.template + defaultTemplate = path.join 'templates', 'hookfile.js' + configuration.options.template = defaultTemplate + configuration.options.header = coerceToDict(configuration.options.header) + + # TODO(quanlong): OAuth2 Bearer Token + if configuration.options.oauth2Token? + configuration.options.headers['Authorization'] = "Bearer #{configuration.options.oauth2Token}" + + return configuration + +# Create configuration settings from CLI arguments applied against options +# @param {Object} parsedArgs - yargs .argv() output +# @returns {Object} configuration object +asConfiguration = (parsedArgs) -> + 'use strict' + ## TODO(plroebuck): Do all configuration in one place... + aliases = Object.keys(allOptions).map (key) -> allOptions[key].alias + .filter (val) -> val != undefined + alreadyHandled = [ + 'reporters', + 'help', + 'version' + ] + + configuration = + ramlPath: parsedArgs._[0], + options: _.omit parsedArgs, ['_', '$0', aliases..., alreadyHandled...] + + mochaOptionNames = Object.keys mochaOptions + optionsToReparent = _.pick configuration.options, mochaOptionNames + configuration.options = _.omit configuration.options, mochaOptionNames + configuration.options.mocha = optionsToReparent + + return applyConfiguration configuration + + +module.exports = asConfiguration + diff --git a/lib/generate-hooks.coffee b/lib/generate-hooks.coffee index d16ca0a..f15514a 100644 --- a/lib/generate-hooks.coffee +++ b/lib/generate-hooks.coffee @@ -6,6 +6,7 @@ fs = require 'fs' Mustache = require 'mustache' generateHooks = (names, ramlFile, templateFile, callback) -> + 'use strict' if !names callback new Error 'no names found for which to generate hooks' @@ -19,7 +20,7 @@ generateHooks = (names, ramlFile, templateFile, callback) -> ramlFile: ramlFile timestamp: datetime hooks: - { 'name': name } for name in names + {'name': name} for name in names view.hooks[0].comment = true content = Mustache.render template, view diff --git a/lib/hooks.coffee b/lib/hooks.coffee index 1ad696f..40349fd 100644 --- a/lib/hooks.coffee +++ b/lib/hooks.coffee @@ -8,6 +8,7 @@ _ = require 'lodash' class Hooks constructor: () -> + 'use strict' @beforeHooks = {} @afterHooks = {} @beforeAllHooks = [] @@ -18,43 +19,54 @@ class Hooks @skippedTests = [] before: (name, hook) => + 'use strict' @addHook @beforeHooks, name, hook after: (name, hook) => + 'use strict' @addHook @afterHooks, name, hook beforeAll: (hook) => + 'use strict' @beforeAllHooks.push hook afterAll: (hook) => + 'use strict' @afterAllHooks.push hook beforeEach: (hook) => + 'use strict' @beforeEachHooks.push hook afterEach: (hook) => + 'use strict' @afterEachHooks.push hook addHook: (hooks, name, hook) -> + 'use strict' if hooks[name] hooks[name].push hook else hooks[name] = [hook] test: (name, hook) => + 'use strict' if @contentTests[name]? throw new Error "cannot have more than one test with the name: #{name}" @contentTests[name] = hook runBeforeAll: (callback) => + 'use strict' async.series @beforeAllHooks, (err, results) -> callback(err) runAfterAll: (callback) => + 'use strict' async.series @afterAllHooks, (err, results) -> callback(err) runBefore: (test, callback) => + 'use strict' return callback() unless (@beforeHooks[test.name] or @beforeEachHooks) hooks = @beforeEachHooks.concat(@beforeHooks[test.name] ? []) @@ -63,6 +75,7 @@ class Hooks , callback runAfter: (test, callback) => + 'use strict' return callback() unless (@afterHooks[test.name] or @afterEachHooks) hooks = (@afterHooks[test.name] ? []).concat(@afterEachHooks) @@ -71,14 +84,18 @@ class Hooks , callback skip: (name) => + 'use strict' @skippedTests.push name hasName: (name) => + 'use strict' _.has(@beforeHooks, name) || _.has(@afterHooks, name) skipped: (name) => + 'use strict' @skippedTests.indexOf(name) != -1 + module.exports = new Hooks() diff --git a/lib/options.coffee b/lib/options-abao.coffee similarity index 58% rename from lib/options.coffee rename to lib/options-abao.coffee index 2fe565c..62b51a6 100644 --- a/lib/options.coffee +++ b/lib/options-abao.coffee @@ -1,26 +1,23 @@ ###* -# @file Command line options +# @file Command line options (Abao-related) ### -options = +module.exports = 'generate-hooks': description: 'Output hooks generated from template file and exit' type: 'boolean' - grep: - alias: 'g' - description: 'Only run tests matching ' - type: 'string' - header: alias: 'h' - description: 'Add header to include in each request. Header must be in KEY:VALUE format ' + - '(e.g., "-h Accept:application/json").\nReuse option to add multiple headers' + description: 'Add header to include in each request. Header must be ' + + 'in KEY:VALUE format (e.g., "-h Accept:application/json").' + + '\nReuse option to add multiple headers' type: 'string' hookfiles: alias: 'f' - description: 'Specify pattern to match files with before/after hooks for running tests' + description: 'Specify pattern to match files with before/after hooks ' + + 'for running tests' type: 'string' 'hooks-only': @@ -28,32 +25,19 @@ options = description: 'Run test only if defined either before or after hooks' type: 'boolean' - invert: - alias: 'i' - description: 'Invert --grep matches' - type: 'boolean' - names: alias: 'n' description: 'List names of requests and exit' type: 'boolean' - reporter: - alias: 'R' - description: 'Specify reporter to use' - type: 'string' - default: 'spec' - - reporters: - description: 'Display available reporters and exit' - type: 'boolean' - schemas: - description: 'Specify pattern to match schema files to be loaded for use as JSON refs' + description: 'Specify pattern to match schema files to be loaded for ' + + 'use as JSON $refs' type: 'string' server: - description: 'Specify API endpoint to use. The RAML-specified baseUri value will be used if not provided' + description: 'Specify API endpoint to use. The RAML-specified baseUri ' + + 'value will be used if not provided' type: 'string' sorted: @@ -67,11 +51,3 @@ options = type: 'string' normalize: true - timeout: - alias: 't' - description: 'Set test case timeout in milliseconds' - type: 'number' - default: 2000 - -module.exports = options - diff --git a/lib/options-mocha.coffee b/lib/options-mocha.coffee new file mode 100644 index 0000000..26c417e --- /dev/null +++ b/lib/options-mocha.coffee @@ -0,0 +1,31 @@ +###* +# @file Command line options (Mocha-related) +### + +module.exports = + grep: + alias: 'g' + description: 'Only run tests matching ' + type: 'string' + + invert: + alias: 'i' + description: 'Invert --grep matches' + type: 'boolean' + + reporter: + alias: 'R' + description: 'Specify reporter to use' + type: 'string' + default: 'spec' + + reporters: + description: 'Display available reporters and exit' + type: 'boolean' + + timeout: + alias: 't' + description: 'Set test case timeout in milliseconds' + type: 'number' + default: 2000 + diff --git a/lib/test-runner.coffee b/lib/test-runner.coffee index b1f89fd..7d57e51 100644 --- a/lib/test-runner.coffee +++ b/lib/test-runner.coffee @@ -2,16 +2,18 @@ # @file TestRunner class ### -Mocha = require 'mocha' async = require 'async' +Mocha = require 'mocha' path = require 'path' # TODO(proebuck): Replace underscore module with Lodash; ensure compatibility _ = require 'underscore' + generateHooks = require './generate-hooks' class TestRunner constructor: (options, ramlFile) -> + 'use strict' @server = options.server delete options.server @mocha = new Mocha options.mocha @@ -20,6 +22,7 @@ class TestRunner @ramlFile = ramlFile addTestToMocha: (test, hooks) => + 'use strict' mocha = @mocha options = @options @@ -62,6 +65,7 @@ class TestRunner , {test} run: (tests, hooks, done) -> + 'use strict' server = @server options = @options addTestToMocha = @addTestToMocha @@ -91,11 +95,7 @@ class TestRunner (callback) -> if options['generate-hooks'] # Generate hooks skeleton file - templateFile = if options.template - options.template - else - path.join 'templates', 'hookfile.js' - generateHooks names, ramlFile, templateFile, done + generateHooks names, ramlFile, options.template, done else if options.names # Write names to console console.log name for name in names @@ -116,5 +116,6 @@ class TestRunner ], done + module.exports = TestRunner diff --git a/lib/test.coffee b/lib/test.coffee index 7b58a46..43a2e3a 100644 --- a/lib/test.coffee +++ b/lib/test.coffee @@ -2,39 +2,42 @@ # @file TestFactory/Test classes ### -chai = require 'chai' -request = require 'request' -_ = require 'underscore' async = require 'async' -tv4 = require 'tv4' +chai = require 'chai' fs = require 'fs' glob = require 'glob' +request = require 'request' +tv4 = require 'tv4' +_ = require 'underscore' assert = chai.assert String::contains = (it) -> + 'use strict' @indexOf(it) != -1 class TestFactory constructor: (schemaLocation) -> + 'use strict' if schemaLocation files = glob.sync schemaLocation console.log '\tJSON ref schemas: ' + files.join(', ') - tv4.banUnknown = true - for file in files tv4.addSchema(JSON.parse(fs.readFileSync(file, 'utf8'))) create: (name, contentTest) -> + 'use strict' return new Test(name, contentTest) + class Test constructor: (@name, @contentTest) -> + 'use strict' @name ?= '' @skip = false @@ -57,6 +60,7 @@ class Test done() url: () -> + 'use strict' path = @request.server + @request.path for key, value of @request.params @@ -64,6 +68,7 @@ class Test return path run: (callback) -> + 'use strict' assertResponse = @assertResponse contentTest = @contentTest @@ -86,6 +91,7 @@ class Test ], callback assertResponse: (error, response, body) => + 'use strict' assert.isNull error assert.isNotNull response, 'Response' @@ -112,7 +118,12 @@ class Test """ json = validateJson() - result = tv4.validateResult json, schema + + # Validate object against JSON schema + checkRecursive = false + banUnknown = false + result = tv4.validateResult json, schema, checkRecursive, banUnknown + assert.lengthOf result.missing, 0, """ Missing/unresolved JSON schema $refs (#{result.missing?.join(', ')}) in schema: #{JSON.stringify(schema, null, 4)} diff --git a/package.json b/package.json index d389dba..2a1357f 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,13 @@ { "name": "abao", - "version": "0.5.2", + "version": "0.5.3", "description": "RAML testing tool", "bin": "bin/abao", "main": "lib/index.js", "scripts": { - "commit": "git-cz", + "git-cz": "git-cz", + "precommit": "npm test", + "prepush": "npm test", "test": "grunt test" }, "config": { @@ -13,7 +15,7 @@ "path": "./node_modules/cz-conventional-changelog" }, "yargs": { - "camel-case-expansion": false + "camel-case-expansion": true } }, "repository": { @@ -62,6 +64,8 @@ "yargs": "^11.1.0" }, "devDependencies": { + "coffeelint-use-strict": "^1.0.0", + "commitizen": "^2.9.6", "coveralls": "^2.11.14", "cz-conventional-changelog": "^2.1.0", "express": "^4.12.0", @@ -72,9 +76,12 @@ "grunt-contrib-clean": "^1.1.0", "grunt-contrib-watch": "^1.0.0", "grunt-coveralls": "^1.0.1", + "grunt-markdownlint": "^1.1.1", "grunt-mocha-test": "~0.13.2", "grunt-shell": "^2.0.0", + "husky": "^0.14.3", "load-grunt-config": "^0.19.2", + "markdownlint": "^0.8.0", "mocha-phantom-coverage-reporter": "^0.1.0", "mute": "^1.0.0", "nock": "~9.1.6", diff --git a/test/e2e/cli-test.coffee b/test/e2e/cli-test.coffee index e50cc2c..5b64cc5 100644 --- a/test/e2e/cli-test.coffee +++ b/test/e2e/cli-test.coffee @@ -1,8 +1,10 @@ -{assert} = require 'chai' -{exec} = require 'child_process' +chai = require 'chai' +child_process = require 'child_process' express = require 'express' +_ = require 'lodash' +pkg = require '../../package' -pkg = require '../../package.json' +expect = chai.expect HOSTNAME = 'localhost' PORT = 3333 @@ -19,67 +21,107 @@ CMD_PREFIX = '' ABAO_BIN = './bin/abao' MOCHA_BIN = './node_modules/mocha/bin/mocha' +mochaJsonReportKeys = [ + 'stats', + 'tests', + 'pending', + 'failures', + 'passes' +] + stderr = '' stdout = '' report = '' exitStatus = null -receivedRequest = {} +# +# To dump individual raw test results: +# +# describe('show me the results', () -> +# runTestAsync = (done) -> +# cmd = "#{ABAO_BIN}" +# execCommand cmd, done +# before (done) -> +# debugExecCommand = true +# runTestAsync done +# after () -> +# debugExecCommand = false +# +debugExecCommand = false + execCommand = (cmd, callback) -> + 'use strict' stderr = '' stdout = '' report = '' exitStatus = null - cli = exec CMD_PREFIX + cmd, (error, out, err) -> + cli = child_process.exec CMD_PREFIX + cmd, (error, out, err) -> stdout = out stderr = err try report = JSON.parse out + catch ignore + # Ignore issues with creating report from output if error exitStatus = error.code cli.on 'close', (code) -> exitStatus = code if exitStatus == null and code != undefined + if debugExecCommand + console.log "stdout:\n#{stdout}\n" + console.log "stderr:\n#{stderr}\n" + console.log "report:\n#{report}\n" + console.log "exitStatus = #{exitStatus}\n" callback() + describe 'Command line interface', () -> + 'use strict' describe 'when run without any arguments', (done) -> - before (done) -> + runNoArgTestAsync = (done) -> cmd = "#{ABAO_BIN}" execCommand cmd, done - it 'should exit with status 1', () -> - assert.equal exitStatus, 1 + before (done) -> + runNoArgTestAsync done it 'should print usage to stderr', () -> - assert.equal stderr.split('\n')[0], 'Usage:' + firstLine = stderr.split('\n')[0] + expect(firstLine).to.equal('Usage:') it 'should print error message to stderr', () -> - assert.include stderr, 'must specify path to RAML file' + expect(stderr).to.include('must specify path to RAML file') + + it 'should exit due to error', () -> + expect(exitStatus).to.equal(1) describe 'when run with multiple positional arguments', (done) -> - before (done) -> - ramlFile = "#{RAML_DIR}/single-get.raml" + runTooManyArgTestAsync = (done) -> + ramlFile = "#{RAML_DIR}/machines-single_get.raml" cmd = "#{ABAO_BIN} #{ramlFile} #{ramlFile}" execCommand cmd, done - it 'should exit with status 1', () -> - assert.equal exitStatus, 1 + before (done) -> + runTooManyArgTestAsync done it 'should print usage to stderr', () -> - assert.equal stderr.split('\n')[0], 'Usage:' + firstLine = stderr.split('\n')[0] + expect(firstLine).to.equal('Usage:') it 'should print error message to stderr', () -> - assert.include stderr, 'accepts single positional command-line argument' + expect(stderr).to.include('accepts single positional command-line argument') + + it 'should exit due to error', () -> + expect(exitStatus).to.equal(1) describe 'when run with one-and-done options', (done) -> @@ -87,161 +129,204 @@ describe 'Command line interface', () -> describe 'when RAML argument unnecessary', () -> describe 'when invoked with "--reporters" option', () -> + reporters = '' - before (done) -> + runReportersTestAsync = (done) -> execCommand "#{MOCHA_BIN} --reporters", () -> reporters = stdout execCommand "#{ABAO_BIN} --reporters", done - it 'exit status should be 0', () -> - assert.equal exitStatus, 0 + before (done) -> + runReportersTestAsync done it 'should print same output as `mocha --reporters`', () -> - assert.equal stdout, reporters + expect(stdout).to.equal(reporters) + + it 'should exit normally', () -> + expect(exitStatus).to.equal(0) describe 'when invoked with "--version" option', () -> - before (done) -> + + runVersionTestAsync = (done) -> cmd = "#{ABAO_BIN} --version" execCommand cmd, done - it 'should exit with status 0', () -> - assert.equal exitStatus, 0 + before (done) -> + runVersionTestAsync done it 'should print version number to stdout', () -> - assert.equal stdout.trim(), pkg.version + expect(stdout.trim()).to.equal(pkg.version) + + it 'should exit normally', () -> + expect(exitStatus).to.equal(0) describe 'when invoked with "--help" option', () -> - before (done) -> + + runHelpTestAsync = (done) -> cmd = "#{ABAO_BIN} --help" execCommand cmd, done - it 'should exit with status 0', () -> - assert.equal exitStatus, 0 + before (done) -> + runHelpTestAsync done it 'should print usage to stdout', () -> - assert.equal stdout.split('\n')[0], 'Usage:' + firstLine = stdout.split('\n')[0] + expect(firstLine).to.equal('Usage:') + + it 'should exit normally', () -> + expect(exitStatus).to.equal(0) + describe 'when RAML argument required', () -> describe 'when invoked with "--names" option', () -> - before (done) -> - ramlFile = "#{RAML_DIR}/single-get.raml" + + runNamesTestAsync = (done) -> + ramlFile = "#{RAML_DIR}/machines-single_get.raml" cmd = "#{ABAO_BIN} #{ramlFile} --names" execCommand cmd, done - it 'exit status should be 0', () -> - assert.equal exitStatus, 0 + before (done) -> + runNamesTestAsync done it 'should print names', () -> - assert.include stdout, 'GET /machines -> 200' + expect(stdout).to.include('GET /machines -> 200') it 'should not run tests', () -> - assert.notInclude stdout, '0 passing' + expect(stdout).to.not.include('0 passing') + + it 'should exit normally', () -> + expect(exitStatus).to.equal(0) describe 'when invoked with "--generate-hooks" option', () -> - describe 'by itself', () -> - before (done) -> - ramlFile = "#{RAML_DIR}/single-get.raml" + + describe 'by itself (use package-provided template)', () -> + + runGenHooksTestAsync = (done) -> + ramlFile = "#{RAML_DIR}/machines-single_get.raml" cmd = "#{ABAO_BIN} #{ramlFile} --generate-hooks" execCommand cmd, done - it 'exit status should be 0', () -> - assert.equal exitStatus, 0 + before (done) -> + runGenHooksTestAsync done it 'should print skeleton hookfile', () -> - assert.include stdout, '// ABAO hooks file' + expect(stdout).to.include('// ABAO hooks file') it 'should not run tests', () -> - assert.notInclude stdout, '0 passing' + expect(stdout).to.not.include('0 passing') + + it 'should exit normally', () -> + expect(exitStatus).to.equal(0) describe 'with "--template" option', () -> - before (done) -> + + runGenHookTemplateTestAsync = (done) -> templateFile = "#{TEMPLATE_DIR}/hookfile.js" - ramlFile = "#{RAML_DIR}/single-get.raml" + ramlFile = "#{RAML_DIR}/machines-single_get.raml" cmd = "#{ABAO_BIN} #{ramlFile} --generate-hooks --template #{templateFile}" execCommand cmd, done - it 'exit status should be 0', () -> - assert.equal exitStatus, 0 + before (done) -> + runGenHookTemplateTestAsync done it 'should print skeleton hookfile', () -> - assert.include stdout, '// ABAO hooks file' + expect(stdout).to.include('// ABAO hooks file') it 'should not run tests', () -> - assert.notInclude stdout, '0 passing' + expect(stdout).to.not.include('0 passing') + + it 'should exit normally', () -> + expect(exitStatus).to.equal(0) + describe 'when invoked with "--template" but without "--generate-hooks" option', () -> - before (done) -> + + runTemplateOnlyTestAsync = (done) -> templateFile = "#{TEMPLATE_DIR}/hookfile.js" - ramlFile = "#{RAML_DIR}/single-get.raml" + ramlFile = "#{RAML_DIR}/machines-single_get.raml" cmd = "#{ABAO_BIN} #{ramlFile} --template #{templateFile}" execCommand cmd, done - it 'exit status should be 1', () -> - assert.equal exitStatus, 1 + before (done) -> + runTemplateOnlyTestAsync done it 'should print error message to stderr', () -> - assert.include stderr, 'Implications failed:' - assert.include stderr, 'template -> generate-hooks' + expect(stderr).to.include('Implications failed:') + expect(stderr).to.include('template -> generate-hooks') + + it 'should exit due to error', () -> + expect(exitStatus).to.equal(1) describe 'when RAML file not found', (done) -> - before (done) -> + + runNoRamlTestAsync = (done) -> ramlFile = "#{RAML_DIR}/nonexistent_path.raml" cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER}" execCommand cmd, done - it 'should exit with status 1', () -> - assert.equal exitStatus, 1 + before (done) -> + runNoRamlTestAsync done it 'should print error message to stderr', () -> # See https://travis-ci.org/cybertk/abao/jobs/76656192#L479 # iojs behaviour is different from nodejs - assert.include stderr, 'Error: ENOENT' + expect(stderr).to.include('Error: ENOENT') + + it 'should exit due to error', () -> + expect(exitStatus).to.equal(1) describe 'arguments with existing RAML and responding server', () -> + describe 'when invoked without "--server" option', () -> + describe 'when RAML file does not specify "baseUri"', () -> - before (done) -> - ramlFile = "#{RAML_DIR}/no-base-uri.raml" + + runUnspecifiedServerTestAsync = (done) -> + ramlFile = "#{RAML_DIR}/music-no_base_uri.raml" cmd = "#{ABAO_BIN} #{ramlFile} --reporter json" execCommand cmd, done - it 'should exit with status 1', () -> - assert.equal exitStatus, 1 + before (done) -> + runUnspecifiedServerTestAsync done it 'should print error message to stderr', () -> - assert.include stderr, 'no API endpoint specified' + expect(stderr).to.include('no API endpoint specified') - describe 'when RAML file does specify "baseUri"', () -> + it 'should exit due to error', () -> + expect(exitStatus).to.equal(1) - before (done) -> - ramlFile = "#{RAML_DIR}/single-get.raml" + + describe 'when RAML file specifies "baseUri"', () -> + + resTestTitle = 'GET /machines -> 200 Validate response code and body' + + runBaseUriServerTestAsync = (done) -> + ramlFile = "#{RAML_DIR}/machines-single_get.raml" cmd = "#{ABAO_BIN} #{ramlFile} --reporter json" app = express() app.get '/machines', (req, res) -> - res.setHeader 'Content-Type', 'application/json' machine = type: 'bulldozer' name: 'willy' - response = [machine] - res.status(200).send response + res.status(200).json([machine]) server = app.listen PORT, () -> execCommand cmd, () -> @@ -249,30 +334,92 @@ describe 'Command line interface', () -> server.on 'close', done - it 'exit status should be 0', () -> - assert.equal exitStatus, 0 + before (done) -> + runBaseUriServerTestAsync done it 'should print count of tests run', () -> - assert.equal 1, report.tests.length - assert.equal 1, report.passes.length + expect(report).to.exist + expect(report).to.have.all.keys(mochaJsonReportKeys) + expect(report.stats.tests).to.equal(1) + expect(report.stats.passes).to.equal(1) it 'should print correct title for response', () -> - assert.equal report.tests[0].fullTitle, 'GET /machines -> 200 Validate response code and body' + expect(report.tests).to.have.length(1) + expect(report.tests[0].fullTitle).to.equal(resTestTitle) + + it 'should exit normally', () -> + expect(exitStatus).to.equal(0) + describe 'when executing the command and the server is responding as specified in the RAML', () -> - before (done) -> - ramlFile = "#{RAML_DIR}/single-get.raml" + + responses = {} + getResponse = undefined + headResponse = undefined + optionsResponse = undefined + + getTestTitle = 'GET /machines -> 200 Validate response code and body' + headTestTitle = 'HEAD /machines -> 200 Validate response code only' + optionsTestTitle = 'OPTIONS /machines -> 204 Validate response code only' + + runNormalTestAsync = (done) -> + ramlFile = "#{RAML_DIR}/machines-get_head_options.raml" cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --reporter json" app = express() - app.get '/machines', (req, res) -> - res.setHeader 'Content-Type', 'application/json' + app.use (req, res, next) -> + origResWrite = res.write + origResEnd = res.end + chunks = [] + res.write = (chunk) -> + chunks.push new Buffer(chunk) + origResWrite.apply res, arguments + res.end = (chunk) -> + if (chunk) + chunks.push new Buffer(chunk) + res.body = Buffer.concat(chunks).toString('utf8') + origResEnd.apply res, arguments + next() + + app.options '/machines', (req, res, next) -> + allow = ['OPTIONS', 'HEAD', 'GET'] + directives = ['no-cache', 'no-store', 'must-revalidate'] + res.setHeader 'Allow', allow.join ',' + res.setHeader 'Cache-Control', directives.join ',' + res.setHeader 'Pragma', directives[0] + res.setHeader 'Expires', '0' + res.status(204).end() + next() + + app.get '/machines', (req, res, next) -> machine = type: 'bulldozer' name: 'willy' - response = [machine] - res.status(200).send response + res.status(200).json([machine]) + next() + + app.use (req, res, next) -> + response = + headers: {}, + body: res.body + headerNames = do () -> + if req.method == 'OPTIONS' + return [ + 'Allow', + 'Cache-Control', + 'Expires', + 'Pragma' + ] + else + return [ + 'Content-Type', + 'Content-Length', + 'ETag' + ] + headerNames.forEach (headerName) -> + response.headers[headerName] = res.get headerName + responses[req.method] = _.cloneDeep(response) server = app.listen PORT, () -> execCommand cmd, () -> @@ -280,30 +427,64 @@ describe 'Command line interface', () -> server.on 'close', done - it 'exit status should be 0', () -> - assert.equal exitStatus, 0 + before (done) -> + runNormalTestAsync done + + before () -> + getResponse = responses['GET'] + headResponse = responses['HEAD'] + optionsResponse = responses['OPTIONS'] + + it 'should provide count of tests run', () -> + expect(report).to.exist + expect(report).to.have.all.keys(mochaJsonReportKeys) + expect(report.stats.tests).to.equal(3) + + it 'should provide count of tests passing', () -> + expect(report.stats.passes).to.equal(3) + + it 'should print correct title for each response', () -> + expect(report.tests).to.have.length(3) + expect(report.tests[0].fullTitle).to.equal(getTestTitle) + expect(report.tests[1].fullTitle).to.equal(headTestTitle) + expect(report.tests[2].fullTitle).to.equal(optionsTestTitle) + + it 'OPTIONS response should allow GET and HEAD requests', () -> + allow = optionsResponse.headers['Allow'] + expect(allow).to.equal('OPTIONS,HEAD,GET') - it 'should print count of tests run', () -> - assert.equal 1, report.tests.length - assert.equal 1, report.passes.length + it 'OPTIONS response should disable caching of it', () -> + cacheControl = optionsResponse.headers['Cache-Control'] + expect(cacheControl).to.equal('no-cache,no-store,must-revalidate') + pragma = optionsResponse.headers['Pragma'] + expect(pragma).to.equal('no-cache') + expires = optionsResponse.headers['Expires'] + expect(expires).to.equal('0') + + it 'OPTIONS and HEAD responses should not have bodies', () -> + expect(optionsResponse.body).to.be.empty + expect(headResponse.body).to.be.empty + + it 'GET and HEAD responses should have equivalent headers', () -> + expect(getResponse.headers).to.deep.equal(headResponse.headers) + + it 'should exit normally', () -> + expect(exitStatus).to.equal(0) - it 'should print correct title for response', () -> - assert.equal report.tests[0].fullTitle, 'GET /machines -> 200 Validate response code and body' describe 'when executing the command and RAML includes other RAML files', () -> - before (done) -> - ramlFile = "#{RAML_DIR}/include_other_raml.raml" + + runRamlIncludesTestAsync = (done) -> + ramlFile = "#{RAML_DIR}/machines-include_other_raml.raml" cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER}" app = express() app.get '/machines', (req, res) -> - res.setHeader 'Content-Type', 'application/json' machine = type: 'bulldozer' name: 'willy' - response = [machine] - res.status(200).send response + res.status(200).json([machine]) server = app.listen PORT, () -> execCommand cmd, () -> @@ -311,28 +492,31 @@ describe 'Command line interface', () -> server.on 'close', done - it 'exit status should be 0', () -> - assert.equal exitStatus, 0 + before (done) -> + runRamlIncludesTestAsync done + + it 'should print count of passing tests run', () -> + expect(stdout).to.have.string('1 passing') + + it 'should exit normally', () -> + expect(exitStatus).to.equal(0) - it 'should print count of tests run', () -> - assert.include stdout, '1 passing' describe 'when called with arguments', () -> describe 'when invoked with "--reporter" option', () -> - before (done) -> - ramlFile = "#{RAML_DIR}/single-get.raml" + + runReporterTestAsync = (done) -> + ramlFile = "#{RAML_DIR}/machines-single_get.raml" cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --reporter spec" app = express() app.get '/machines', (req, res) -> - res.setHeader 'Content-Type', 'application/json' machine = type: 'bulldozer' name: 'willy' - response = [machine] - res.status(200).send response + res.status(200).json([machine]) server = app.listen PORT, () -> execCommand cmd, () -> @@ -340,62 +524,121 @@ describe 'Command line interface', () -> server.on 'close', done + before (done) -> + runReporterTestAsync done + it 'should print using the specified reporter', () -> - assert.include stdout, '1 passing' + expect(stdout).to.have.string('1 passing') + + it 'should exit normally', () -> + expect(exitStatus).to.equal(0) + describe 'when invoked with "--header" option', () -> receivedRequest = {} + producedMediaType = 'application/vnd.api+json' + reqMediaType = undefined + extraHeader = undefined - before (done) -> - ramlFile = "#{RAML_DIR}/single-get.raml" - cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --header Accept:application/json" + describe 'with "Accept" header', () -> - app = express() + runAcceptHeaderTestAsync = (done) -> + extraHeader = "Accept:#{reqMediaType}" + ramlFile = "#{RAML_DIR}/machines-single_get.raml" + cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --header #{extraHeader}" - app.get '/machines', (req, res) -> - receivedRequest = req - res.setHeader 'Content-Type', 'application/json' - machine = - type: 'bulldozer' - name: 'willy' - response = [machine] - res.status(200).send response + app = express() - server = app.listen PORT, () -> - execCommand cmd, () -> - server.close() + app.use (req, res, next) -> + receivedRequest = req + next() - server.on 'close', done + app.use (req, res, next) -> + err = null + if !req.accepts ["#{producedMediaType}"] + err = new Error('Not Acceptable') + err.status = 406 + next(err) - it 'should have an additional header in the request', () -> - assert.equal receivedRequest.headers.accept, 'application/json' + app.get '/machines', (req, res) -> + machine = + type: 'bulldozer' + name: 'willy' + res.type "#{producedMediaType}" + res.status(200).send([machine]) - it 'exit status should be 0', () -> - assert.equal exitStatus, 0 + app.use (err, req, res, next) -> + res.status(err.status || 500) + .json({ + message: err.message, + stack: err.stack + }) + return - it 'should print count of tests run', () -> - assert.include stdout, '1 passing' + server = app.listen PORT, () -> + execCommand cmd, () -> + server.close() + + server.on 'close', done + + context 'when expecting success', () -> + + before (done) -> + reqMediaType = "#{producedMediaType}" + runAcceptHeaderTestAsync done + + it 'should have the additional header in the request', () -> + expect(receivedRequest.headers.accept).to.equal("#{reqMediaType}") + + it 'should print count of passing tests run', () -> + expect(stdout).to.have.string('1 passing') + + it 'should exit normally', () -> + expect(exitStatus).to.equal(0) + + + context 'when expecting failure', () -> + + before (done) -> + reqMediaType = 'application/json' + runAcceptHeaderTestAsync done + + it 'should have the additional header in the request', () -> + expect(receivedRequest.headers.accept).to.equal("#{reqMediaType}") + + # Errors thrown by Mocha show up in stdout; those by Abao in stderr. + it 'Mocha should throw an error', () -> + detail = "Error: expected 406 to equal '200'" + expect(stdout).to.have.string(detail) + + it 'should run test but not complete', () -> + expect(stdout).to.have.string('1 failing') + + it 'should exit due to error', () -> + expect(exitStatus).to.equal(1) describe 'when invoked with "--hookfiles" option', () -> receivedRequest = {} - before (done) -> - ramlFile = "#{RAML_DIR}/single-get.raml" - cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --hookfiles=#{HOOK_DIR}/*_hooks.*" + runHookfilesTestAsync = (done) -> + pattern = "#{HOOK_DIR}/*_hooks.*" + ramlFile = "#{RAML_DIR}/machines-single_get.raml" + cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --hookfiles=#{pattern}" app = express() - app.get '/machines', (req, res) -> + app.use (req, res, next) -> receivedRequest = req - res.setHeader 'Content-Type', 'application/json' + next() + + app.get '/machines', (req, res) -> machine = type: 'bulldozer' name: 'willy' - response = [machine] - res.status(200).send response + res.status(200).json([machine]) server = app.listen PORT, () -> execCommand cmd, () -> @@ -403,29 +646,34 @@ describe 'Command line interface', () -> server.on 'close', done + before (done) -> + runHookfilesTestAsync done + it 'should modify the transaction with hooks', () -> - assert.equal receivedRequest.headers['header'], '123232323' - assert.equal receivedRequest.query['key'], 'value' + expect(receivedRequest.headers['header']).to.equal('123232323') + expect(receivedRequest.query['key']).to.equal('value') it 'should print message to stdout and stderr', () -> - assert.include stdout, 'before-hook-GET-machines' - assert.include stderr, 'after-hook-GET-machines' + expect(stdout).to.include('before-hook-GET-machines') + expect(stderr).to.include('after-hook-GET-machines') + + it 'should exit normally', () -> + expect(exitStatus).to.equal(0) describe 'when invoked with "--hooks-only" option', () -> - before (done) -> - ramlFile = "#{RAML_DIR}/single-get.raml" + + runHooksOnlyTestAsync = (done) -> + ramlFile = "#{RAML_DIR}/machines-single_get.raml" cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --hooks-only" app = express() app.get '/machines', (req, res) -> - res.setHeader 'Content-Type', 'application/json' machine = type: 'bulldozer' name: 'willy' - response = [machine] - res.status(200).send response + res.status(200).json([machine]) server = app.listen PORT, () -> execCommand cmd, () -> @@ -433,55 +681,97 @@ describe 'Command line interface', () -> server.on 'close', done - it 'exit status should be 0', () -> - assert.equal exitStatus, 0 + before (done) -> + runHooksOnlyTestAsync done it 'should not run test without hooks', () -> - assert.include stdout, '1 pending' + expect(stdout).to.have.string('1 pending') + + it 'should exit normally', () -> + expect(exitStatus).to.equal(0) + describe 'when invoked with "--timeout" option', () -> - cost = '' - before (done) -> - ramlFile = "#{RAML_DIR}/single-get.raml" - cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --timeout 100" + timeout = undefined + elapsed = -1 + finished = undefined + + runTimeoutTestAsync = (done) -> + ramlFile = "#{RAML_DIR}/machines-single_get.raml" + cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --timeout #{timeout}" + + beginTime = undefined + finished = false app = express() - t0 = '' + app.use (req, res, next) -> + beginTime = new Date() + res.on 'finish', () -> + finished = true + next() + + app.use (req, res, next) -> + delay = timeout * 2 + setTimeout next, delay + app.get '/machines', (req, res) -> - t0 = new Date + machine = + type: 'bulldozer' + name: 'willy' + res.status(200).json([machine]) server = app.listen PORT, () -> execCommand cmd, () -> - cost = new Date - t0 + endTime = new Date() + if finished + elapsed = endTime - beginTime + console.log "elapsed = #{elapsed} msecs (req/res)" server.close() server.on 'close', done - it 'exit status should be 1', () -> - assert.equal exitStatus, 1 - it 'should exit before timeout', () -> - assert.ok cost < 200 + context 'given insufficient time to complete', () -> + + before (done) -> + timeout = 20 + console.log "timeout = #{timeout} msecs" + runTimeoutTestAsync done + + after () -> + finished = undefined + + it 'should not finish before timeout occurs', () -> + expect(finished).to.be.false + + # Errors thrown by Mocha show up in stdout; those by Abao in stderr. + it 'Mocha should throw an error', () -> + detail = "Error: Timeout of #{timeout}ms exceeded." + expect(stdout).to.have.string(detail) + + it 'should run test but not complete', () -> + expect(stdout).to.have.string('1 failing') + + it 'should exit due to error', () -> + expect(exitStatus).to.equal(1) - it 'should not run test without hooks', () -> - assert.include stdout, '0 passing' describe 'when invoked with "--schema" option', () -> - before (done) -> - ramlFile = "#{RAML_DIR}/with-json-refs.raml" - cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --schemas=#{SCHEMA_DIR}/*.json" + + runSchemaTestAsync = (done) -> + pattern = "#{SCHEMA_DIR}/*.json" + ramlFile = "#{RAML_DIR}/machines-with_json_refs.raml" + cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --schemas=#{pattern}" app = express() app.get '/machines', (req, res) -> - res.setHeader 'Content-Type', 'application/json' machine = type: 'bulldozer' name: 'willy' - response = [machine] - res.status(200).send response + res.status(200).json([machine]) server = app.listen PORT, () -> execCommand cmd, () -> @@ -489,30 +779,37 @@ describe 'Command line interface', () -> server.on 'close', done - it 'exit status should be 0', () -> - assert.equal exitStatus, 0 - - describe 'when invoked with "--schema" option and expecting error', () -> before (done) -> - ramlFile = "#{RAML_DIR}/with-json-refs.raml" - cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --schemas=#{SCHEMA_DIR}/*.json" + runSchemaTestAsync done - app = express() + it 'should exit normally', () -> + expect(exitStatus).to.equal(0) - app.get '/machines', (req, res) -> - res.setHeader 'Content-Type', 'application/json' - machine = - typO: 'bulldozer' - name: 'willy' - response = [machine] - res.status(200).send response - server = app.listen PORT, () -> - execCommand cmd, () -> - server.close() + describe 'when expecting validation to fail', () -> - server.on 'close', done + runSchemaFailTestAsync = (done) -> + pattern = "#{SCHEMA_DIR}/*.json" + ramlFile = "#{RAML_DIR}/machines-with_json_refs.raml" + cmd = "#{ABAO_BIN} #{ramlFile} --server #{SERVER} --schemas=#{pattern}" + + app = express() + + app.get '/machines', (req, res) -> + machine = + typO: 'bulldozer' # 'type' != 'typO' + name: 'willy' + res.status(200).json([machine]) + + server = app.listen PORT, () -> + execCommand cmd, () -> + server.close() + + server.on 'close', done + + before (done) -> + runSchemaFailTestAsync done - it 'exit status should be 1', () -> - assert.equal exitStatus, 1 + it 'should exit due to error', () -> + expect(exitStatus).to.equal(1) diff --git a/test/fixtures/contacts.raml b/test/fixtures/contacts.raml index 9231afa..bd796a4 100644 --- a/test/fixtures/contacts.raml +++ b/test/fixtures/contacts.raml @@ -6,6 +6,7 @@ version: v1 /contacts: post: + description: Creates a new contact body: application/json: schema: | @@ -15,6 +16,10 @@ version: v1 { "type": "Kulu", "name": "Mike" } responses: 201: + headers: + location: + description: URI of the newly created contact + example: /contacts/{contact_id} body: application/json: schema: | @@ -22,11 +27,13 @@ version: v1 name: 'string' example: | { "type": "Kulu", "name": "Mike" } - /contacts/{id} + /contacts/{contact_id} delete: + description: Deletes an existing contact by `contact_id` responses: 204: put: + description: Replaces an existing contact by `contact_id` body: application/json: schema: | @@ -44,6 +51,7 @@ version: v1 example: | { "type": "Kulu", "name": "Mike" } get: + description: Gets an existing contact by `contact_id` responses: 200: body: diff --git a/test/fixtures/1-get-1-post.raml b/test/fixtures/machines-1_get_1_post.raml similarity index 77% rename from test/fixtures/1-get-1-post.raml rename to test/fixtures/machines-1_get_1_post.raml index b656a52..d2d2e2b 100644 --- a/test/fixtures/1-get-1-post.raml +++ b/test/fixtures/machines-1_get_1_post.raml @@ -6,6 +6,7 @@ version: v1 /machines: get: + description: Get a list of existing machines responses: 200: body: @@ -18,6 +19,7 @@ version: v1 example: | { "type": "Kulu", "name": "Mike" } post: + description: Creates a new machine body: application/json: schema: | @@ -27,6 +29,10 @@ version: v1 { "type": "Kulu", "name": "Mike" } responses: 201: + headers: + location: + description: URI of the newly created machine + example: /machines/{id} body: application/json: schema: | diff --git a/test/fixtures/machines-get_head_options.raml b/test/fixtures/machines-get_head_options.raml new file mode 100644 index 0000000..0ee5c84 --- /dev/null +++ b/test/fixtures/machines-get_head_options.raml @@ -0,0 +1,59 @@ +#%RAML 0.8 + +title: Machines API +baseUri: http://localhost:3333 + +/machines: + get: + description: Gets a list of existing machines + responses: + 200: + body: + application/json: + schema: | + [ + type: 'string' + name: 'string' + ] + example: | + { "type": "Kulu", "name": "Mike" } + + head: + description: Requests the headers that are returned from HTTP GET method + responses: + 200: + headers: + Content-Type: + description: Media type of response body + type: string + required: true + example: application/json; charset=utf-8 + Content-Length: + description: Length of response body + type: string + required: true + example: 37 + ETag: + description: Identifier for this version of the resource + type: string + required: true + example: W/"25-QoLpNeXVKDaodKGK5d2ua9ZMNAc" + body: null + + options: + description: Describes the communication options for this resource. + responses: + 204: + headers: + Allow: + description: Which HTTP methods can be used with `machines` + type: string + required: true + example: OPTIONS, HEAD, GET + Cache-Control: + description: Defines caching policy for OPTIONS requests + type: string + required: true + example: no-cache, no-store, must-revalidate + body: null + diff --git a/test/fixtures/include_other_raml.raml b/test/fixtures/machines-include_other_raml.raml similarity index 89% rename from test/fixtures/include_other_raml.raml rename to test/fixtures/machines-include_other_raml.raml index 895add6..1141953 100644 --- a/test/fixtures/include_other_raml.raml +++ b/test/fixtures/machines-include_other_raml.raml @@ -9,6 +9,7 @@ securitySchemes: /machines: get: + description: Gets a list of existing machines responses: 200: body: diff --git a/test/fixtures/inline_and_included_schemas.raml b/test/fixtures/machines-inline_and_included_schemas.raml similarity index 91% rename from test/fixtures/inline_and_included_schemas.raml rename to test/fixtures/machines-inline_and_included_schemas.raml index 0357800..e3ab190 100644 --- a/test/fixtures/inline_and_included_schemas.raml +++ b/test/fixtures/machines-inline_and_included_schemas.raml @@ -10,6 +10,7 @@ schemas: /machines: get: + description: Gets a list of existing machines responses: 200: body: diff --git a/test/fixtures/no-method.raml b/test/fixtures/machines-no_method.raml similarity index 88% rename from test/fixtures/no-method.raml rename to test/fixtures/machines-no_method.raml index b9ae3a2..e145269 100644 --- a/test/fixtures/no-method.raml +++ b/test/fixtures/machines-no_method.raml @@ -7,6 +7,7 @@ version: v1 /root: /machines: get: + description: Gets a list of existing machines responses: 200: body: diff --git a/test/fixtures/non_required_query_parameter.raml b/test/fixtures/machines-non_required_query_parameter.raml similarity index 90% rename from test/fixtures/non_required_query_parameter.raml rename to test/fixtures/machines-non_required_query_parameter.raml index 56264cf..5466f0b 100644 --- a/test/fixtures/non_required_query_parameter.raml +++ b/test/fixtures/machines-non_required_query_parameter.raml @@ -6,6 +6,7 @@ version: v1 /machines: get: + description: Gets a list of existing machines queryParameters: quux: type: string diff --git a/test/fixtures/ref_other_schemas.raml b/test/fixtures/machines-ref_other_schemas.raml similarity index 85% rename from test/fixtures/ref_other_schemas.raml rename to test/fixtures/machines-ref_other_schemas.raml index 211e7df..728867c 100644 --- a/test/fixtures/ref_other_schemas.raml +++ b/test/fixtures/machines-ref_other_schemas.raml @@ -10,6 +10,7 @@ schemas: /machines: get: + description: Gets a list of existing machines responses: 200: body: diff --git a/test/fixtures/required_query_parameter.raml b/test/fixtures/machines-required_query_parameter.raml similarity index 90% rename from test/fixtures/required_query_parameter.raml rename to test/fixtures/machines-required_query_parameter.raml index 0fb5a23..4e84652 100644 --- a/test/fixtures/required_query_parameter.raml +++ b/test/fixtures/machines-required_query_parameter.raml @@ -6,6 +6,7 @@ version: v1 /machines: get: + description: Gets a list of existing machines queryParameters: quux: type: string diff --git a/test/fixtures/single-get.raml b/test/fixtures/machines-single_get.raml similarity index 89% rename from test/fixtures/single-get.raml rename to test/fixtures/machines-single_get.raml index f0e7316..6af46da 100644 --- a/test/fixtures/single-get.raml +++ b/test/fixtures/machines-single_get.raml @@ -5,6 +5,7 @@ baseUri: http://localhost:3333 /machines: get: + description: Gets a list of existing machines headers: Abao-API-Key: type: string diff --git a/test/fixtures/three-levels.raml b/test/fixtures/machines-three_levels.raml similarity index 80% rename from test/fixtures/three-levels.raml rename to test/fixtures/machines-three_levels.raml index 9b03f99..30ec089 100644 --- a/test/fixtures/three-levels.raml +++ b/test/fixtures/machines-three_levels.raml @@ -6,6 +6,7 @@ version: v1 /machines: get: + description: Gets a list of existing machines responses: 200: body: @@ -23,10 +24,12 @@ version: v1 type: string example: '1' delete: + description: Delete a machine by `machine_id` responses: 204: /parts: get: + description: Gets a list of machine `machine_id`'s parts responses: 200: body: diff --git a/test/fixtures/with-json-refs.raml b/test/fixtures/machines-with_json_refs.raml similarity index 100% rename from test/fixtures/with-json-refs.raml rename to test/fixtures/machines-with_json_refs.raml index 640369a..cb1bbbb 100644 --- a/test/fixtures/with-json-refs.raml +++ b/test/fixtures/machines-with_json_refs.raml @@ -5,11 +5,11 @@ version: v1 resourceTypes: - resource: get: + description: Get <> by Identifier headers: Abao-API-Key: type: string example: abcdef - description: Get <> by Identifier responses: 200: body: diff --git a/test/fixtures/no-base-uri.raml b/test/fixtures/music-no_base_uri.raml similarity index 83% rename from test/fixtures/no-base-uri.raml rename to test/fixtures/music-no_base_uri.raml index 32aa268..cf7c133 100644 --- a/test/fixtures/no-base-uri.raml +++ b/test/fixtures/music-no_base_uri.raml @@ -11,12 +11,15 @@ traits: /songs: is: [ paged ] get: + description: Gets a list of existing songs queryParameters: genre: description: filter the songs by genre post: + description: Adds a new song /{songId}: get: + description: Gets an existing song by `songId` responses: 200: body: @@ -35,6 +38,5 @@ traits: { "title": "A Beautiful Day", "artist": "Mike" } application/xml: delete: - description: | - This method will *delete* an **individual song** + description: Deletes an existing song by `songId` diff --git a/test/fixtures/simple.raml b/test/fixtures/music-simple.raml similarity index 83% rename from test/fixtures/simple.raml rename to test/fixtures/music-simple.raml index 4086a7e..d134fcc 100644 --- a/test/fixtures/simple.raml +++ b/test/fixtures/music-simple.raml @@ -12,12 +12,15 @@ traits: /songs: is: [ paged ] get: + description: Gets a list of existing songs queryParameters: genre: description: filter the songs by genre post: + description: Adds a new song /{songId}: get: + description: Gets an existing song by `songId` responses: 200: body: @@ -36,6 +39,5 @@ traits: { "title": "A Beautiful Day", "artist": "Mike" } application/xml: delete: - description: | - This method will *delete* an **individual song** + description: Deletes an existing song by `songId` diff --git a/test/fixtures/vendor-content-type.raml b/test/fixtures/music-vendor_content_type.raml similarity index 95% rename from test/fixtures/vendor-content-type.raml rename to test/fixtures/music-vendor_content_type.raml index 199c17f..ce76a2e 100644 --- a/test/fixtures/vendor-content-type.raml +++ b/test/fixtures/music-vendor_content_type.raml @@ -9,6 +9,7 @@ version: v1 songId: example: "mike-a-beautiful-day" patch: + description: Edits an existing song by `songId` body: application/vnd.api+json: schema: | diff --git a/test/fixtures/test_hooks.coffee b/test/fixtures/test_hooks.coffee index 10e4245..3ac38c6 100644 --- a/test/fixtures/test_hooks.coffee +++ b/test/fixtures/test_hooks.coffee @@ -1,5 +1,7 @@ {after} = require 'hooks' -after "GET /machines -> 200", (test, done) -> - console.error "after-hook-GET-machines" +after 'GET /machines -> 200', (test, done) -> + 'use strict' + console.error 'after-hook-GET-machines' done() + diff --git a/test/stub/server.coffee b/test/stub/server.coffee index 9770482..da69919 100644 --- a/test/stub/server.coffee +++ b/test/stub/server.coffee @@ -1,18 +1,44 @@ -express = require 'express' +###* +# @file Express server stub +# +# Start: +# $ ../../node_modules/coffee-script/bin/coffee server.coffee +### +require 'coffee-script/register' -PORT = '3333' +express = require 'express' app = express() +app.set 'port', process.env.PORT || 3333 + +app.options '/machines', (req, res) -> + 'use strict' + allow = ['OPTIONS', 'HEAD', 'GET'] + directives = ['no-cache', 'no-store', 'must-revalidate'] + res.setHeader 'Allow', allow.join ',' + res.setHeader 'Cache-Control', directives.join ',' + res.setHeader 'Pragma', directives[0] + res.setHeader 'Expires', '0' + res.status(204).end() app.get '/machines', (req, res) -> - res.setHeader 'Content-Type', 'application/json' + 'use strict' machine = type: 'bulldozer' name: 'willy' - response = [machine] - res.status(200).send response + res.status(200).json [machine] + +app.use (err, req, res, next) -> + 'use strict' + res.status(err.status || 500) + .json({ + message: err.message, + stack: err.stack + }) + return -server = app.listen PORT, () -> - console.log 'server started' +server = app.listen app.get('port'), () -> + 'use strict' + console.log 'server listening on port', server.address().port diff --git a/test/unit/abao-test.coffee b/test/unit/abao-test.coffee index 0954f0e..69fb024 100644 --- a/test/unit/abao-test.coffee +++ b/test/unit/abao-test.coffee @@ -8,7 +8,6 @@ ramlParserStub = require 'raml-parser' addTestsStub = require '../../lib/add-tests' addHooksStub = require '../../lib/add-hooks' runnerStub = require '../../lib/test-runner' -applyConfigurationStub = require '../../lib/apply-configuration' hooksStub = require '../../lib/hooks' Abao = proxyquire '../../', { @@ -16,7 +15,6 @@ Abao = proxyquire '../../', { './add-tests': addTestsStub, './add-hooks': addHooksStub, './test-runner': runnerStub, - './apply-configuration': applyConfigurationStub, './hooks': hooksStub } @@ -25,6 +23,7 @@ chai.use(sinonChai) describe 'Abao', () -> + 'use strict' describe '#constructor', () -> diff --git a/test/unit/add-hooks-test.coffee b/test/unit/add-hooks-test.coffee index 9379a12..4c11f74 100644 --- a/test/unit/add-hooks-test.coffee +++ b/test/unit/add-hooks-test.coffee @@ -1,10 +1,15 @@ require 'coffee-errors' -{assert} = require 'chai' +chai = require 'chai' +chai.use require('sinon-chai') {EventEmitter} = require 'events' +mute = require 'mute' nock = require 'nock' proxyquire = require 'proxyquire' sinon = require 'sinon' -mute = require 'mute' + +assert = chai.assert +expect = chai.expect +should = chai.should() globStub = require 'glob' pathStub = require 'path' @@ -15,96 +20,175 @@ addHooks = proxyquire '../../lib/add-hooks', { 'path': pathStub } -describe 'addHooks(hooks, pattern)', () -> +describe 'addHooks(hooks, pattern, callback)', () -> + 'use strict' + callback = undefined + globSyncSpy = undefined + addHookSpy = undefined + pathResolveSpy = undefined + consoleErrorSpy = undefined transactions = {} describe 'with no pattern', () -> before () -> - sinon.spy globStub, 'sync' + callback = sinon.spy() + globSyncSpy = sinon.spy globStub, 'sync' - after () -> - globStub.sync.restore() + it 'should return immediately', (done) -> + addHooks hooksStub, '', callback + globSyncSpy.should.not.have.been.called + done() - it 'should return immediately', () -> - addHooks(hooksStub, '') - assert.ok globStub.sync.notCalled + it 'should return successful continuation', () -> + callback.should.have.been.calledOnce + callback.should.have.been.calledWith( + sinon.match.typeOf('null')) - describe 'with valid pattern', () -> + after () -> + globStub.sync.restore() - pattern = './test/**/*_hooks.*' - it 'should return files', (done)-> - mute (unmute) -> - sinon.spy globStub, 'sync' - addHooks(hooksStub, pattern) - assert.ok globStub.sync.called - globStub.sync.restore() + describe 'with pattern', () -> - unmute() - done() + context 'not matching any files', () -> - describe 'when files are valid js/coffeescript', () -> + pattern = '/path/to/directory/without/hooks/*' beforeEach () -> - sinon.spy globStub, 'sync' - sinon.spy pathStub, 'resolve' - sinon.spy hooksStub, 'addHook' - - afterEach () -> - globStub.sync.restore() - pathStub.resolve.restore() - hooksStub.addHook.restore() + callback = sinon.spy() + addHookSpy = sinon.spy hooksStub, 'addHook' + globSyncSpy = sinon.stub globStub, 'sync' + .callsFake (pattern) -> + [] + pathResolveSpy = sinon.spy pathStub, 'resolve' - it 'should load the files', (done) -> + it 'should not return any file names', (done) -> mute (unmute) -> - addHooks(hooksStub, pattern) - assert.ok pathStub.resolve.called - + addHooks hooksStub, pattern, callback + globSyncSpy.should.have.returned [] unmute() done() - it 'should attach the hooks', (done) -> + it 'should not attempt to load files', (done) -> mute (unmute) -> - addHooks(hooksStub, pattern) - assert.ok hooksStub.addHook.called + addHooks hooksStub, pattern, callback + pathResolveSpy.should.not.have.been.called + unmute() + done() + it 'should propagate the error condition', (done) -> + mute (unmute) -> + addHooks hooksStub, pattern, callback + callback.should.have.been.calledOnce + detail = "no hook files found matching pattern '#{pattern}'" + callback.should.have.been.calledWith( + sinon.match.instanceOf(Error).and( + sinon.match.has('message', detail))) unmute() done() + afterEach () -> + hooksStub.addHook.restore() + globStub.sync.restore() + pathStub.resolve.restore() - describe 'when there is an error reading the hook files', () -> - beforeEach () -> - sinon.stub pathStub, 'resolve' - .callsFake (path, rel) -> - throw new Error() - sinon.spy console, 'error' - sinon.stub globStub, 'sync' - .callsFake (pattern) -> - ['invalid.xml', 'unexist.md'] - sinon.spy hooksStub, 'addHook' + context 'matching files', () -> - afterEach () -> - pathStub.resolve.restore() - console.error.restore() - globStub.sync.restore() - hooksStub.addHook.restore() + pattern = './test/**/*_hooks.*' - it 'should log a warning', (done) -> + it 'should return file names', (done) -> mute (unmute) -> - addHooks(hooksStub, pattern) - assert.ok console.error.called - + globSyncSpy = sinon.spy globStub, 'sync' + addHooks hooksStub, pattern, callback + globSyncSpy.should.have.been.called + globStub.sync.restore() unmute() done() - it 'should not attach the hooks', (done) -> - mute (unmute) -> - addHooks(hooksStub, pattern) - assert.ok hooksStub.addHook.notCalled - unmute() - done() + context 'when files are valid javascript/coffeescript', () -> + + beforeEach () -> + callback = sinon.spy() + globSyncSpy = sinon.spy globStub, 'sync' + pathResolveSpy = sinon.spy pathStub, 'resolve' + addHookSpy = sinon.spy hooksStub, 'addHook' + + it 'should load the files', (done) -> + mute (unmute) -> + addHooks hooksStub, pattern, callback + pathResolveSpy.should.have.been.called + unmute() + done() + + it 'should attach the hooks', (done) -> + mute (unmute) -> + addHooks hooksStub, pattern, callback + addHookSpy.should.have.been.called + unmute() + done() + + it 'should return successful continuation', (done) -> + mute (unmute) -> + addHooks hooksStub, pattern, callback + callback.should.have.been.calledOnce + callback.should.have.been.calledWith( + sinon.match.typeOf('null')) + unmute() + done() + + afterEach () -> + globStub.sync.restore() + pathStub.resolve.restore() + hooksStub.addHook.restore() + + + context 'when error occurs reading the hook files', () -> + + addHookSpy = undefined + consoleErrorSpy = undefined + + beforeEach () -> + callback = sinon.spy() + pathResolveSpy = sinon.stub pathStub, 'resolve' + .callsFake (path, rel) -> + throw new Error 'resolve' + consoleErrorSpy = sinon.spy console, 'error' + globSyncSpy = sinon.stub globStub, 'sync' + .callsFake (pattern) -> + ['invalid.xml', 'unexist.md'] + addHookSpy = sinon.spy hooksStub, 'addHook' + + it 'should log an error', (done) -> + mute (unmute) -> + addHooks hooksStub, pattern, callback + consoleErrorSpy.should.have.been.called + unmute() + done() + + it 'should not attach the hooks', (done) -> + mute (unmute) -> + addHooks hooksStub, pattern, callback + addHookSpy.should.not.have.been.called + unmute() + done() + + it 'should propagate the error condition', (done) -> + mute (unmute) -> + addHooks hooksStub, pattern, callback + callback.should.have.been.calledOnce + callback.should.have.been.calledWith( + sinon.match.instanceOf(Error).and( + sinon.match.has('message', 'resolve'))) + unmute() + done() + + afterEach () -> + pathStub.resolve.restore() + console.error.restore() + globStub.sync.restore() + hooksStub.addHook.restore() diff --git a/test/unit/add-tests-test.coffee b/test/unit/add-tests-test.coffee index 00b32c1..878b85a 100644 --- a/test/unit/add-tests-test.coffee +++ b/test/unit/add-tests-test.coffee @@ -17,6 +17,7 @@ RAML_DIR = "#{FIXTURE_DIR}" describe '#addTests', () -> + 'use strict' describe '#run', () -> @@ -24,17 +25,21 @@ describe '#addTests', () -> tests = [] testFactory = new TestFactory() - callback = '' + callback = undefined before (done) -> - ramlFile = "#{RAML_DIR}/single-get.raml" + ramlFile = "#{RAML_DIR}/machines-single_get.raml" ramlParser.loadFile(ramlFile) - .then (raml) -> - callback = sinon.stub() - callback.returns(done()) + .then (raml) -> + callback = sinon.stub() + callback.returns(done()) - addTests raml, tests, hooks, callback, testFactory, false + addTests raml, tests, hooks, callback, testFactory, false + .catch (err) -> + console.error err + done(err) return + after () -> tests = [] @@ -68,23 +73,28 @@ describe '#addTests', () -> assert.isNull res.headers assert.isNull res.body + describe 'when endpoint has multiple methods', () -> + describe 'when processed in order specified in RAML', () -> tests = [] testFactory = new TestFactory() - callback = '' + callback = undefined before (done) -> - - ramlFile = "#{RAML_DIR}/1-get-1-post.raml" + ramlFile = "#{RAML_DIR}/machines-1_get_1_post.raml" ramlParser.loadFile(ramlFile) - .then (raml) -> - callback = sinon.stub() - callback.returns(done()) - - addTests raml, tests, hooks, callback, testFactory, false + .then (raml) -> + callback = sinon.stub() + callback.returns(done()) + + addTests raml, tests, hooks, callback, testFactory, false + .catch (err) -> + console.error err + done(err) return + after () -> tests = [] @@ -123,22 +133,26 @@ describe '#addTests', () -> assert.isNull res.headers assert.isNull res.body + describe 'when processed in order specified by "--sorted" option', () -> tests = [] testFactory = new TestFactory() - callback = '' + callback = undefined before (done) -> - - ramlFile = "#{RAML_DIR}/1-get-1-post.raml" + ramlFile = "#{RAML_DIR}/machines-1_get_1_post.raml" ramlParser.loadFile(ramlFile) - .then (raml) -> - callback = sinon.stub() - callback.returns(done()) - - addTests raml, tests, hooks, null, callback, testFactory, true + .then (raml) -> + callback = sinon.stub() + callback.returns(done()) + + addTests raml, tests, hooks, null, callback, testFactory, true + .catch (err) -> + console.error err + done(err) return + after () -> tests = [] @@ -177,22 +191,26 @@ describe '#addTests', () -> assert.isNull res.headers assert.isNull res.body + describe 'when RAML includes multiple referencing schemas', () -> tests = [] - testFactory = new TestFactory - callback = '' + testFactory = new TestFactory() + callback = undefined before (done) -> - - ramlFile = "#{RAML_DIR}/ref_other_schemas.raml" + ramlFile = "#{RAML_DIR}/machines-ref_other_schemas.raml" ramlParser.loadFile(ramlFile) - .then (raml) -> - callback = sinon.stub() - callback.returns(done()) + .then (raml) -> + callback = sinon.stub() + callback.returns(done()) - addTests raml, tests, hooks, callback, testFactory, false + addTests raml, tests, hooks, callback, testFactory, false + .catch (err) -> + console.error err + done(err) return + after () -> tests = [] @@ -218,26 +236,30 @@ describe '#addTests', () -> res = tests[0].response assert.equal res.status, 200 - assert.equal res.schema?.properties?.chick?.type, "string" + assert.equal res.schema?.properties?.chick?.type, 'string' assert.isNull res.headers assert.isNull res.body + describe 'when RAML has inline and included schemas', () -> tests = [] - testFactory = new TestFactory - callback = '' + testFactory = new TestFactory() + callback = undefined before (done) -> - - ramlFile = "#{RAML_DIR}/inline_and_included_schemas.raml" + ramlFile = "#{RAML_DIR}/machines-inline_and_included_schemas.raml" ramlParser.loadFile(ramlFile) - .then (raml) -> - callback = sinon.stub() - callback.returns(done()) + .then (raml) -> + callback = sinon.stub() + callback.returns(done()) - addTests raml, tests, hooks, callback, testFactory, false + addTests raml, tests, hooks, callback, testFactory, false + .catch (err) -> + console.error err + done(err) return + after () -> tests = [] @@ -263,25 +285,28 @@ describe '#addTests', () -> res = tests[0].response assert.equal res.status, 200 - assert.equal res.schema?.properties?.type["$ref"], "type2" + assert.equal res.schema?.properties?.type['$ref'], 'type2' assert.isNull res.headers assert.isNull res.body + describe 'when RAML contains three-levels endpoints', () -> tests = [] testFactory = new TestFactory() - callback = '' + callback = undefined before (done) -> - - ramlFile = "#{RAML_DIR}/three-levels.raml" + ramlFile = "#{RAML_DIR}/machines-three_levels.raml" ramlParser.loadFile(ramlFile) - .then (raml) -> - callback = sinon.stub() - callback.returns(done()) + .then (raml) -> + callback = sinon.stub() + callback.returns(done()) - addTests raml, tests, hooks, callback, testFactory, false + addTests raml, tests, hooks, callback, testFactory, false + .catch (err) -> + console.error err + done(err) return after () -> @@ -308,21 +333,24 @@ describe '#addTests', () -> assert.deepEqual test.request.params, machine_id: '1' + describe 'when RAML has resource not defined method', () -> tests = [] testFactory = new TestFactory() - callback = '' + callback = undefined before (done) -> - - ramlFile = "#{RAML_DIR}/no-method.raml" + ramlFile = "#{RAML_DIR}/machines-no_method.raml" ramlParser.loadFile(ramlFile) - .then (raml) -> - callback = sinon.stub() - callback.returns(done()) + .then (raml) -> + callback = sinon.stub() + callback.returns(done()) - addTests raml, tests, hooks, callback, testFactory, false + addTests raml, tests, hooks, callback, testFactory, false + .catch (err) -> + console.error err + done(err) return after () -> @@ -337,14 +365,14 @@ describe '#addTests', () -> it 'should set test.name', () -> assert.equal tests[0].name, 'GET /root/machines -> 200' + describe 'when RAML has invalid request body example', () -> tests = [] testFactory = new TestFactory() - callback = '' + callback = undefined before (done) -> - raml = """ #%RAML 0.8 @@ -361,12 +389,15 @@ describe '#addTests', () -> 204: """ ramlParser.load(raml) - .then (raml) -> - callback = sinon.stub() - callback.returns(done()) + .then (raml) -> + callback = sinon.stub() + callback.returns(done()) - sinon.stub console, 'warn' - addTests raml, tests, hooks, callback, testFactory, false + sinon.stub console, 'warn' + addTests raml, tests, hooks, callback, testFactory, false + .catch (err) -> + console.error err + done(err) return after () -> @@ -383,20 +414,26 @@ describe '#addTests', () -> assert.lengthOf tests, 1 assert.equal tests[0].name, 'POST /machines -> 204' + describe 'when RAML media type uses a JSON-suffixed vendor tree subtype', () -> + tests = [] testFactory = new TestFactory() - callback = '' + callback = undefined before (done) -> - ramlFile = "#{RAML_DIR}/vendor-content-type.raml" + ramlFile = "#{RAML_DIR}/music-vendor_content_type.raml" ramlParser.loadFile(ramlFile) - .then (raml) -> - callback = sinon.stub() - callback.returns(done()) + .then (raml) -> + callback = sinon.stub() + callback.returns(done()) - addTests raml, tests, hooks, callback, testFactory, false + addTests raml, tests, hooks, callback, testFactory, false + .catch (err) -> + console.error err + done(err) return + after () -> tests = [] @@ -430,41 +467,65 @@ describe '#addTests', () -> describe 'when there is required query parameter with example value', () -> + tests = [] testFactory = new TestFactory() - callback = '' + callback = undefined before (done) -> + ramlFile = "#{RAML_DIR}/machines-required_query_parameter.raml" + ramlParser.loadFile(ramlFile) + .then (raml) -> + callback = sinon.stub() + callback.returns(done()) - ramlParser.loadFile("#{RAML_DIR}/required_query_parameter.raml") - .then (raml) -> - callback = sinon.stub() - callback.returns(done()) - - addTests raml, tests, hooks, callback, testFactory, false + addTests raml, tests, hooks, callback, testFactory, false + .catch (err) -> + console.error err + done(err) return after () -> tests = [] + it 'should run callback', () -> + assert.ok callback.called + + it 'should add 1 test', () -> + assert.lengthOf tests, 1 + it 'should append query parameters with example value', () -> assert.equal tests[0].request.query['quux'], 'foo' + describe 'when there is no required query parameter', () -> + tests = [] testFactory = new TestFactory() - callback = '' + callback = undefined before (done) -> - ramlParser.loadFile("#{RAML_DIR}/non_required_query_parameter.raml") - .then (raml) -> - callback = sinon.stub() - callback.returns(done()) + ramlFile = "#{RAML_DIR}/machines-non_required_query_parameter.raml" + ramlParser.loadFile(ramlFile) + .then (raml) -> + callback = sinon.stub() + callback.returns(done()) - addTests raml, tests, hooks, callback, testFactory, false + addTests raml, tests, hooks, callback, testFactory, false + .catch (err) -> + console.error err + done(err) return + after () -> tests = [] + it 'should run callback', () -> + assert.ok callback.called + + it 'should add 1 test', () -> + assert.lengthOf tests, 1 + it 'should not append query parameters', () -> assert.deepEqual tests[0].request.query, {} + diff --git a/test/unit/hooks-test.coffee b/test/unit/hooks-test.coffee index 0988757..f23de6f 100644 --- a/test/unit/hooks-test.coffee +++ b/test/unit/hooks-test.coffee @@ -9,12 +9,15 @@ hooks = require '../../lib/hooks' ABAO_IO_SERVER = 'http://abao.io' describe 'Hooks', () -> + 'use strict' + + noop = () -> {} describe 'when adding before hook', () -> before () -> - hooks.before 'beforeHook', () -> - "" + hooks.before 'beforeHook', noop + after () -> hooks.beforeHooks = {} @@ -25,8 +28,8 @@ describe 'Hooks', () -> describe 'when adding after hook', () -> before () -> - hooks.after 'afterHook', () -> - "" + hooks.after 'afterHook', noop + after () -> hooks.afterHooks = {} @@ -74,42 +77,42 @@ describe 'Hooks', () -> hooks.beforeHooks = {} it 'should add to hook list', () -> - hooks.beforeEach () -> + hooks.beforeEach noop assert.lengthOf hooks.beforeEachHooks, 1 it 'should invoke registered callbacks', (testDone) -> before_called = false before_each_called = false - test_name = "before_test" + test_name = 'before_test' hooks.before test_name, (test, done) -> assert.equal test.name, test_name before_called = true assert.isTrue before_each_called, - "before_hook should be called after before_each" + 'before_hook should be called after before_each' done() hooks.beforeEach (test, done) -> assert.equal test.name, test_name before_each_called = true assert.isFalse before_called, - "before_each should be called before before_hook" + 'before_each should be called before before_hook' done() hooks.runBefore {name: test_name}, () -> - assert.isTrue before_each_called, "before_each should have been called" - assert.isTrue before_called, "before_hook should have been called" + assert.isTrue before_each_called, 'before_each should have been called' + assert.isTrue before_called, 'before_hook should have been called' testDone() it 'should work without test-specific before', (testDone) -> before_each_called = false - test_name = "before_test" + test_name = 'before_test' hooks.beforeEach (test, done) -> assert.equal test.name, test_name before_each_called = true done() hooks.runBefore {name: test_name}, () -> - assert.isTrue before_each_called, "before_each should have been called" + assert.isTrue before_each_called, 'before_each should have been called' testDone() describe 'when adding afterEach hooks', () -> @@ -119,42 +122,42 @@ describe 'Hooks', () -> hooks.afterHooks = {} it 'should add to hook list', () -> - hooks.afterEach () -> + hooks.afterEach noop assert.lengthOf hooks.afterEachHooks, 1 it 'should invoke registered callbacks', (testDone) -> after_called = false after_each_called = false - test_name = "after_test" + test_name = 'after_test' hooks.after test_name, (test, done) -> assert.equal test.name, test_name after_called = true assert.isFalse after_each_called, - "after_hook should be called before after_each" + 'after_hook should be called before after_each' done() hooks.afterEach (test, done) -> assert.equal test.name, test_name after_each_called = true assert.isTrue after_called, - "after_each should be called after after_hook" + 'after_each should be called after after_hook' done() hooks.runAfter {name: test_name}, () -> - assert.isTrue after_each_called, "after_each should have been called" - assert.isTrue after_called, "after_hook should have been called" + assert.isTrue after_each_called, 'after_each should have been called' + assert.isTrue after_called, 'after_hook should have been called' testDone() it 'should work without test-specific after', (testDone) -> after_each_called = false - test_name = "after_test" + test_name = 'after_test' hooks.afterEach (test, done) -> assert.equal test.name, test_name after_each_called = true done() hooks.runAfter {name: test_name}, () -> - assert.isTrue after_each_called, "after_each should have been called" + assert.isTrue after_each_called, 'after_each should have been called' testDone() describe 'when check has name', () -> @@ -348,21 +351,21 @@ describe 'Hooks', () -> afterEach () -> hooks.contentTests = {} - test_name = "content_test_test" + test_name = 'content_test_test' it 'should get added to the set of hooks', () -> - hooks.test(test_name, () ->) - assert.isDefined(hooks.contentTests[test_name]) + hooks.test test_name, noop + assert.isDefined hooks.contentTests[test_name] describe 'adding two content tests fails', () -> afterEach () -> hooks.contentTests = {} - test_name = "content_test_test" + test_name = 'content_test_test' - it 'should assert when adding a second content test', () -> + it 'should assert when attempting to add a second content test', () -> f = () -> - hooks.test(test_name, () ->) + hooks.test test_name, noop f() assert.throw f, "cannot have more than one test with the name: #{test_name}" @@ -386,7 +389,7 @@ describe 'Hooks', () -> afterEach () -> hooks.skippedTests = [] - test_name = "content_test_test" + test_name = 'content_test_test' it 'should get added to the set of hooks', () -> hooks.skip test_name diff --git a/test/unit/test-runner-test.coffee b/test/unit/test-runner-test.coffee index fc60cb4..efc3d05 100644 --- a/test/unit/test-runner-test.coffee +++ b/test/unit/test-runner-test.coffee @@ -24,31 +24,38 @@ should = chai.should() chai.use(sinonChai) describe 'Test Runner', () -> + 'use strict' runner = undefined + test = undefined + + createStdTest = () -> + testname = 'GET /machines -> 200' + testFactory = new TestFactory() + stdTest = testFactory.create testname, undefined + stdTest.request.path = '/machines' + stdTest.request.method = 'GET' + return stdTest + describe '#run', () -> describe 'when test is valid', () -> - runner = '' - beforeAllHook = '' - afterAllHook = '' - beforeHook = '' - afterHook = '' - runCallback = '' - testFactory = new TestFactory() - test = testFactory.create() - test.name = 'GET /machines -> 200' - test.request.path = '/machines' - test.request.method = 'GET' - test.response.status = 200 - test.response.schema = """[ - type: 'string' - name: 'string' - ]""" + beforeAllHook = undefined + afterAllHook = undefined + beforeHook = undefined + afterHook = undefined + runCallback = undefined before (done) -> + test = createStdTest() + test.response.status = 200 + test.response.schema = """[ + type: 'string' + name: 'string' + ]""" + options = server: "#{ABAO_IO_SERVER}" @@ -117,7 +124,9 @@ describe 'Test Runner', () -> hooksStub.runBefore.restore() hooksStub.runAfter.restore() - runCallback = '' + runCallback = undefined + runner = undefined + test = undefined it 'should generate beforeAll hooks', () -> mochaStub = runner.mocha @@ -157,15 +166,8 @@ describe 'Test Runner', () -> describe 'Interact with #test', () -> - test = '' - runner = '' - before (done) -> - testFactory = new TestFactory() - test = testFactory.create() - test.name = 'GET /machines -> 200' - test.request.path = '/machines' - test.request.method = 'GET' + test = createStdTest() test.response.status = 200 test.response.schema = """[ type: 'string' @@ -188,6 +190,8 @@ describe 'Test Runner', () -> after () -> test.run.restore() + runner = undefined + test = undefined it 'should call #test.run', () -> assert.ok test.run.calledOnce @@ -217,6 +221,8 @@ describe 'Test Runner', () -> after () -> runner.mocha.run.restore() + runner = undefined + test = undefined it 'should run mocha', () -> assert.ok runner.mocha.run.called @@ -235,11 +241,7 @@ describe 'Test Runner', () -> describe 'when test skipped in hooks', () -> before (done) -> - testFactory = new TestFactory() - test = testFactory.create() - test.name = 'GET /machines -> 200' - test.request.path = '/machines' - test.request.method = 'GET' + test = createStdTest() test.response.status = 200 test.response.schema = """[ type: 'string' @@ -262,6 +264,8 @@ describe 'Test Runner', () -> after () -> hooksStub.skippedTests = [] runner.mocha.run.restore() + runner = undefined + test = undefined it 'should run mocha', () -> assert.ok runner.mocha.run.called @@ -280,11 +284,7 @@ describe 'Test Runner', () -> describe 'when test has no response schema', () -> before (done) -> - testFactory = new TestFactory() - test = testFactory.create() - test.name = 'GET /machines -> 200' - test.request.path = '/machines' - test.request.method = 'GET' + test = createStdTest() test.response.status = 200 options = @@ -302,6 +302,8 @@ describe 'Test Runner', () -> after () -> runner.mocha.run.restore() + runner = undefined + test = undefined it 'should run mocha', () -> assert.ok runner.mocha.run.called @@ -319,14 +321,10 @@ describe 'Test Runner', () -> describe 'when test throws AssertionError', () -> - afterAllHook = '' + afterAllHook = undefined before (done) -> - testFactory = new TestFactory() - test = testFactory.create() - test.name = 'GET /machines -> 200' - test.request.path = '/machines' - test.request.method = 'GET' + test = createStdTest() test.response.status = 200 afterAllHook = sinon.stub() @@ -349,7 +347,9 @@ describe 'Test Runner', () -> done() after () -> - afterAllHook = '' + afterAllHook = undefined + runner = undefined + test = undefined it 'should call afterAll hook', () -> afterAllHook.should.have.been.called @@ -357,15 +357,11 @@ describe 'Test Runner', () -> describe 'when beforeAllHooks throws UncaughtError', () -> - beforeAllHook = '' - afterAllHook = '' + beforeAllHook = undefined + afterAllHook = undefined before (done) -> - testFactory = new TestFactory() - test = testFactory.create() - test.name = 'GET /machines -> 200' - test.request.path = '/machines' - test.request.method = 'GET' + test = createStdTest() test.response.status = 200 beforeAllHook = sinon.stub() @@ -391,8 +387,10 @@ describe 'Test Runner', () -> done() after () -> - beforeAllHook = '' - afterAllHook = '' + beforeAllHook = undefined + afterAllHook = undefined + runner = undefined + test = undefined it 'should call afterAll hook', () -> afterAllHook.should.have.been.called @@ -403,11 +401,7 @@ describe 'Test Runner', () -> describe 'list all tests with `names`', () -> before (done) -> - testFactory = new TestFactory() - test = testFactory.create() - test.name = 'GET /machines -> 200' - test.request.path = '/machines' - test.request.method = 'GET' + test = createStdTest() test.response.status = 200 test.response.schema = """[ type: 'string' @@ -430,8 +424,10 @@ describe 'Test Runner', () -> done() after () -> - runner.mocha.run.restore() console.log.restore() + runner.mocha.run.restore() + runner = undefined + test = undefined it 'should not run mocha', () -> assert.notOk runner.mocha.run.called @@ -446,11 +442,7 @@ describe 'Test Runner', () -> headers = undefined before (done) -> - testFactory = new TestFactory() - test = testFactory.create() - test.name = 'GET /machines -> 200' - test.request.path = '/machines' - test.request.method = 'GET' + test = createStdTest() test.response.status = 200 test.response.schema = {} @@ -465,13 +457,15 @@ describe 'Test Runner', () -> runner = new TestRunner options, '' sinon.stub runner.mocha, 'run' .callsFake (callback) -> - receivedTest = _.cloneDeep(test) + receivedTest = _.cloneDeep test callback() runner.run [test], hooksStub, done after () -> runner.mocha.run.restore() + runner = undefined + test = undefined it 'should run mocha', () -> assert.ok runner.mocha.run.called @@ -482,17 +476,13 @@ describe 'Test Runner', () -> describe 'run test with hooks only indicated by `hooks-only`', () -> - testFactory = new TestFactory() - test = testFactory.create() - test.name = 'GET /machines -> 200' - test.request.path = '/machines' - test.request.method = 'GET' - test.response.status = 200 - test.response.schema = {} - - suiteStub = '' + suiteStub = undefined before (done) -> + test = createStdTest() + test.response.status = 200 + test.response.schema = {} + options = server: "#{SERVER}" 'hooks-only': true @@ -519,11 +509,13 @@ describe 'Test Runner', () -> runner.run [test], hooksStub, done after () -> - runner.mocha.run.restore() - mocha.Suite.create.restore() suiteStub.addTest.restore() suiteStub.beforeAll.restore() suiteStub.afterAll.restore() + mocha.Suite.create.restore() + runner.mocha.run.restore() + runner = undefined + test = undefined it 'should run mocha', () -> assert.ok runner.mocha.run.called diff --git a/test/unit/test-test.coffee b/test/unit/test-test.coffee index 59ba739..2f891f5 100644 --- a/test/unit/test-test.coffee +++ b/test/unit/test-test.coffee @@ -10,6 +10,7 @@ chai.use(sinonChai) requestStub = sinon.stub() requestStub.restore = () -> + 'use strict' this.callsArgWith(1, null, {statusCode: 200}, '') TestFactory = proxyquire '../../lib/test', { @@ -20,6 +21,7 @@ ABAO_IO_SERVER = 'http://abao.io' describe 'Test', () -> + 'use strict' describe '#run', () -> @@ -48,7 +50,12 @@ describe 'Test', () -> test.request.body = body: 'value' test.response.status = 201 - test.response.schema = [{ type: 'object', properties: { type: 'string', name: 'string'}}] + test.response.schema = [ + type: 'object' + properties: + type: 'string' + name: 'string' + ] machine = type: 'foo' @@ -124,7 +131,12 @@ describe 'Test', () -> test.request.body = body: 'value' test.response.status = 200 - test.response.schema = [{ type: 'object', properties: { type: 'string', name: 'string'}}] + test.response.schema = [ + type: 'object' + properties: + type: 'string' + name: 'string' + ] machine = type: 'foo' @@ -178,7 +190,6 @@ describe 'Test', () -> ) tv4Stub = {} - tv4Stub.banUnknown = false tv4Stub.addSchema = sinon.spy() TestTestFactory = proxyquire '../../lib/test', { @@ -191,23 +202,20 @@ describe 'Test', () -> new TestTestFactory('') assert.isFalse globStub.sync.called assert.isFalse fsStub.readFileSync.called - assert.isFalse tv4Stub.banUnknown assert.isFalse tv4Stub.addSchema.called it 'test TestFactory with name 1', () -> new TestTestFactory('thisisaword') - assert.isTrue globStub.sync.calledWith('thisisaword') + assert.isTrue globStub.sync.calledWith 'thisisaword' assert.isTrue fsStub.readFileSync.calledOnce - assert.isTrue fsStub.readFileSync.calledWith('thisisaword','utf8') - assert.isTrue tv4Stub.banUnknown + assert.isTrue fsStub.readFileSync.calledWith 'thisisaword', 'utf8' assert.isTrue tv4Stub.addSchema.calledWith(JSON.parse('{ "text": "example" }')) it 'test TestFactory with name 2', () -> new TestTestFactory('thisIsAnotherWord') - assert.isTrue globStub.sync.calledWith('thisIsAnotherWord') + assert.isTrue globStub.sync.calledWith 'thisIsAnotherWord' assert.isTrue fsStub.readFileSync.calledTwice - assert.isTrue fsStub.readFileSync.calledWith('thisIsAnotherWord','utf8') - assert.isTrue tv4Stub.banUnknown + assert.isTrue fsStub.readFileSync.calledWith 'thisIsAnotherWord', 'utf8' assert.isTrue tv4Stub.addSchema.calledWith(JSON.parse('{ "text": "example" }')) @@ -261,7 +269,7 @@ describe 'Test', () -> errorStub = null responseStub = - statusCode : 201 + statusCode: 201 bodyStub = JSON.stringify type: 'foo' name: 'bar' @@ -275,7 +283,7 @@ describe 'Test', () -> errorStub = null responseStub = - statusCode : 201 + statusCode: 201 bodyStub = null fn = _.partial test.assertResponse, errorStub, responseStub, bodyStub assert.throw fn, chai.AssertionError @@ -286,7 +294,7 @@ describe 'Test', () -> errorStub = null responseStub = - statusCode : 201 + statusCode: 201 bodyStub = 'Im invalid' fn = _.partial test.assertResponse, errorStub, responseStub, bodyStub assert.throw fn, chai.AssertionError