Coverage

94%
280
264
16

abao.coffee

86%
30
26
4
LineHitsSource
11sms = require("source-map-support").install({handleUncaughtExceptions: false})
21raml = require 'raml-parser'
31async = require 'async'
41chai = require 'chai'
5
61options = require './options'
71addTests = require './add-tests'
81TestFactory = require './test'
91addHooks = require './add-hooks'
101Runner = require './test-runner'
111applyConfiguration = require './apply-configuration'
121hooks = require './hooks'
13
14
151class Abao
161 constructor: (config) ->
172 @configuration = applyConfiguration(config)
182 @tests = []
192 @hooks = hooks
20
21 run: (done) ->
221 config = @configuration
231 tests = @tests
241 hooks = @hooks
25
26 # init the test factory to inject the json refs schemas
271 factory = new TestFactory(config.options.schemas)
28
291 async.waterfall [
30 # Parse hooks
31 (callback) ->
321 addHooks hooks, config.options.hookfiles
331 callback()
34 ,
35 # Load RAML
36 (callback) ->
371 raml.loadFile(config.ramlPath).then (raml) ->
380 callback(null, raml)
39 , callback
40 ,
41 # Parse tests from RAML
42 (raml, callback) ->
430 addTests raml, tests, hooks, callback, factory
44 ,
45 # Run tests
46 (callback) ->
470 runner = new Runner config.server, config.options
480 runner.run tests, hooks, callback
49 ], done
50
51
521module.exports = Abao
531module.exports.options = options
54
55

add-hooks.coffee

100%
18
18
0
LineHitsSource
12path = require 'path'
2
32require 'coffee-script/register'
42proxyquire = require('proxyquire').noCallThru()
52glob = require 'glob'
6
7
82addHooks = (hooks, pattern) ->
9
107 return unless pattern
11
125 files = glob.sync pattern
13
145 console.error 'Found Hookfiles: ' + files
15
165 try
175 for file in files
188 proxyquire path.resolve(process.cwd(), file), {
19 'hooks': hooks
20 }
21 catch error
222 console.error 'Skipping hook loading...'
232 console.error 'Error reading hook files (' + files + ')'
242 console.error 'This probably means one or more of your hookfiles is invalid.'
252 console.error 'Message: ' + error.message if error.message?
262 console.error 'Stack: ' + error.stack if error.stack?
272 return
28
29
302module.exports = addHooks
31
32

add-tests.coffee

100%
57
57
0
LineHitsSource
12async = require 'async'
22_ = require 'underscore'
32csonschema = require 'csonschema'
4
52parseSchema = (source) ->
69 if source.contains('$schema')
7 #jsonschema
8 # @response.schema = JSON.parse @response.schema
93 JSON.parse source
10 else
116 csonschema.parse source
12 # @response.schema = csonschema.parse @response.schema
13
142parseHeaders = (raml) ->
1511 return {} unless raml
16
171 headers = {}
181 for key, v of raml
191 headers[key] = v.example
20
211 headers
22
23# addTests(raml, tests, [parent], callback, config)
242addTests = (raml, tests, hooks, parent, callback, testFactory) ->
25
26 # Handle 4th optional param
2719 if _.isFunction(parent)
288 testFactory = callback
298 callback = parent
308 parent = null
31
3219 return callback() unless raml.resources
33
34 # Iterate endpoint
3511 async.each raml.resources, (resource, callback) ->
3611 path = resource.relativeUri
3711 params = {}
38
39 # Apply parent properties
4011 if parent
413 path = parent.path + path
423 params = _.clone parent.params
43
44 # Setup param
4511 if resource.uriParameters
462 for key, param of resource.uriParameters
472 params[key] = param.example
48
49 # In case of issue #8, resource does not define methods
5011 resource.methods ?= []
51
52 # Iterate response method
5311 async.each resource.methods, (api, callback) ->
5411 method = api.method.toUpperCase()
55
56 # Iterate response status
5711 for status, res of api.responses
58
5911 testName = "#{method} #{path} -> #{status}"
60
61 # Append new test to tests
6211 test = testFactory.create(testName, hooks.contentTests[testName])
6311 tests.push test
64
65 # Update test.request
6611 test.request.path = path
6711 test.request.method = method
6811 test.request.headers = parseHeaders(api.headers)
69
70 # select compatible content-type in request body (to support vendor tree types, i.e. application/vnd.api+json)
7111 contentType = (type for type of api.body when type.match(/^application\/(.*\+)?json/i))?[0]
7211 if contentType
733 test.request.headers['Content-Type'] = contentType
743 try
753 test.request.body = JSON.parse api.body[contentType]?.example
76 catch
771 console.warn "cannot parse JSON example request body for #{test.name}"
7811 test.request.params = params
79
80 # Update test.response
8111 test.response.status = status
8211 test.response.schema = null
83
8411 if res?.body
85 # expect content-type of response body to be identical to request body
869 if contentType && res.body[contentType]?.schema
872 test.response.schema = parseSchema res.body[contentType].schema
88 # otherwise filter in responses section for compatible content-types (vendor tree, i.e. application/vnd.api+json)
89 else
907 contentType = (type for type of res.body when type.match(/^application\/(.*\+)?json/i))?[0]
917 if res.body[contentType]?.schema
927 test.response.schema = parseSchema res.body[contentType].schema
93
9411 callback()
95 , (err) ->
9611 return callback(err) if err
97
98 # Recursive
9911 addTests resource, tests, hooks, {path, params}, callback, testFactory
100 , callback
101
102
1032module.exports = addTests
104
105

apply-configuration.coffee

64%
25
16
9
LineHitsSource
1
21applyConfiguration = (config) ->
3
42 coerceToArray = (value) ->
52 if typeof value is 'string'
60 value = [value]
72 else if !value?
82 value = []
90 else if value instanceof Array
100 value
110 else value
12
132 coerceToDict = (value) ->
142 array = coerceToArray value
152 @dict = {}
16
172 if array.length > 0
180 for item in array
190 splitItem = item.split(':')
200 @dict[splitItem[0]] = splitItem[1]
21
222 return @dict
23
242 configuration =
25 ramlPath: null
26 server: null
27 options:
28 schemas: null
29 reporters: false
30 reporter: null
31 header: null
32 names: false
33 hookfiles: null
34 grep: ''
35 invert: false
36 'hooks-only': false
37
38 # normalize options and config
392 for own key, value of config
400 configuration[key] = value
41
42 # coerce some options into an dict
432 configuration.options.header = coerceToDict(configuration.options.header)
44
45 # TODO(quanlong): OAuth2 Bearer Token
462 if configuration.options.oauth2Token?
470 configuration.options.headers['Authorization'] = "Bearer #{configuration.options.oauth2Token}"
48
492 return configuration
50
51
521module.exports = applyConfiguration
53
54

hooks.coffee

100%
37
37
0
LineHitsSource
11async = require 'async'
21_ = require 'underscore'
3
41class Hooks
51 constructor: () ->
61 @beforeHooks = {}
71 @afterHooks = {}
81 @beforeAllHooks = []
91 @afterAllHooks = []
101 @beforeEachHooks = []
111 @afterEachHooks = []
121 @contentTests = {}
13
14 before: (name, hook) =>
155 @addHook(@beforeHooks, name, hook)
16
17 after: (name, hook) =>
185 @addHook(@afterHooks, name, hook)
19
20 beforeAll: (hook) =>
212 @beforeAllHooks.push hook
22
23 afterAll: (hook) =>
242 @afterAllHooks.push hook
25
26 beforeEach: (hook) =>
273 @beforeEachHooks.push(hook)
28
29 afterEach: (hook) =>
303 @afterEachHooks.push(hook)
31
32 addHook: (hooks, name, hook) =>
3310 if hooks[name]
344 hooks[name].push hook
35 else
366 hooks[name] = [hook]
37
38 test: (name, hook) =>
393 if @contentTests[name]?
401 throw new Error("Cannot have more than one test with the name: #{name}")
412 @contentTests[name] = hook
42
43 runBeforeAll: (callback) =>
445 async.series @beforeAllHooks, (err, results) ->
454 callback(err)
46
47 runAfterAll: (callback) =>
485 async.series @afterAllHooks, (err, results) ->
495 callback(err)
50
51 runBefore: (test, callback) =>
527 return callback() unless (@beforeHooks[test.name] or @beforeEachHooks)
53
547 hooks = @beforeEachHooks.concat(@beforeHooks[test.name] ? [])
557 async.eachSeries hooks, (hook, callback) ->
567 hook test, callback
57 , callback
58
59 runAfter: (test, callback) =>
607 return callback() unless (@afterHooks[test.name] or @afterEachHooks)
61
627 hooks = (@afterHooks[test.name] ? []).concat(@afterEachHooks)
637 async.eachSeries hooks, (hook, callback) ->
645 hook test, callback
65 , callback
66
67 hasName: (name) =>
6811 _.has(@beforeHooks, name) || _.has(@afterHooks, name)
69
70
711module.exports = new Hooks()
72
73

index.coffee

100%
2
2
0
LineHitsSource
11abao = require './abao'
2
31module.exports = abao
4
5

options.coffee

100%
2
2
0
LineHitsSource
11options =
2 hookfiles:
3 alias: 'f'
4 description: 'Specifies a pattern to match files with before/after hooks for running tests'
5 default: null
6
7 schemas:
8 alias: 's'
9 description: 'Specifies a pattern to match schema files to be loaded for use as JSON refs'
10 default: null
11
12 names:
13 alias: 'n'
14 description: 'Only list names of requests (for use in a hookfile). No requests are made.'
15 default: false
16
17 reporter:
18 alias: "r"
19 description: "Specify the reporter to use"
20 default: "spec"
21
22 header:
23 alias: "h"
24 description: "Extra header to include in every request. The header must be in KEY:VALUE format, e.g. '-h Accept:application/json'.\nThis option can be used multiple times to add multiple headers"
25
26 'hooks-only':
27 alias: "H"
28 description: "Run test only if defined either before or after hooks"
29
30 grep:
31 alias: "g"
32 description: "only run tests matching <pattern>"
33
34 invert:
35 alias: "i"
36 description: "inverts --grep matches"
37
38 timeout:
39 alias: "t"
40 description: "set test-case timeout in milliseconds"
41 default: 2000
42
43 reporters:
44 description: "Display available reporters"
45
46 help:
47 description: "Show usage information"
48
49 version:
50 description: "Show version number"
51
52
531module.exports = options
54
55

test-runner.coffee

95%
46
44
2
LineHitsSource
12Mocha = require 'mocha'
22async = require 'async'
32_ = require 'underscore'
4
5
62class TestRunner
72 constructor: (server, options = {}) ->
89 @server = server
99 @options = options
109 @mocha = new Mocha options
11
12 addTestToMocha: (test, hooks) =>
138 mocha = @mocha
148 options = @options
15
16 # Generate Test Suite
178 suite = Mocha.Suite.create mocha.suite, test.name
18
19 # No Response defined
208 if !test.response.status
211 suite.addTest new Mocha.Test 'Skip as no response code defined'
221 return
23
24 # No Hooks for this test
257 if not hooks.hasName(test.name) and options['hooks-only']
260 suite.addTest new Mocha.Test 'Skip as no hooks defined'
270 return
28
29 # Setup hooks
307 if hooks
317 suite.beforeAll _.bind (done) ->
322 @hooks.runBefore @test, done
33 , {hooks, test}
34
357 suite.afterAll _.bind (done) ->
362 @hooks.runAfter @test, done
37 , {hooks, test}
38
39 # Setup test
40 # Vote test name
417 title = if test.response.schema then 'Validate response code and body' else 'Validate response code only'
427 suite.addTest new Mocha.Test title, _.bind (done) ->
432 @test.run done
44 , {test}
45
46 run: (tests, hooks, callback) ->
479 server = @server
489 options = @options
499 addTestToMocha = @addTestToMocha
509 mocha = @mocha
51
529 async.waterfall [
53 (callback) ->
549 async.each tests, (test, done) ->
55 # list tests
569 if options.names
571 console.log test.name
581 return done()
59
60 # Update test.request
618 test.request.server = server
628 _.extend(test.request.headers, options.header)
63
648 addTestToMocha test, hooks
658 done()
66 , callback
67 , # Run mocha
68 (callback) ->
699 return callback(null, 0) if options.names
70
718 mocha.suite.beforeAll _.bind (done) ->
723 @hooks.runBeforeAll done
73 , {hooks}
748 mocha.suite.afterAll _.bind (done) ->
753 @hooks.runAfterAll done
76 , {hooks}
77
788 mocha.run (failures) ->
798 callback(null, failures)
80 ], callback
81
82
832module.exports = TestRunner
84
85

test.coffee

98%
63
62
1
LineHitsSource
13chai = require 'chai'
23request = require 'request'
33_ = require 'underscore'
43async = require 'async'
53tv4 = require 'tv4'
63fs = require 'fs'
73glob = require 'glob'
8
93assert = chai.assert
10
113String::contains = (it) ->
129 @indexOf(it) != -1
13
143class TestFactory
153 constructor: (schemaLocation) ->
1628 if schemaLocation
17
182 files = glob.sync schemaLocation
192 console.error 'Found JSON ref schemas: ' + files
202 console.error ''
21
222 tv4.banUnknown = true
23
242 for file in files
252 tv4.addSchema(JSON.parse(fs.readFileSync(file, 'utf8')))
26
27 create: (name, contentTest) ->
2827 return new Test(name, contentTest)
29
303class Test
313 constructor: (@name, @contentTest) ->
3227 @name ?= ''
3327 @skip = false
34
3527 @request =
36 server: ''
37 path: ''
38 method: 'GET'
39 params: {}
40 query: {}
41 headers: {}
42 body: ''
43
4427 @response =
45 status: ''
46 schema: null
47 headers: null
48 body: null
49
5027 @contentTest ?= (response, body, done) ->
511 done()
52
53 url: () ->
544 path = @request.server + @request.path
55
564 for key, value of @request.params
574 path = path.replace "{#{key}}", value
584 return path
59
60 run: (callback) ->
612 assertResponse = @assertResponse
622 contentTest = @contentTest
63
642 options = _.pick @request, 'headers', 'method'
652 options['url'] = @url()
662 if typeof @request.body is 'string'
670 options['body'] = @request.body
68 else
692 options['body'] = JSON.stringify @request.body
702 options['qs'] = @request.query
71
722 async.waterfall [
73 (callback) ->
742 request options, (error, response, body) ->
752 callback null, error, response, body
76 ,
77 (error, response, body, callback) ->
782 assertResponse(error, response, body)
792 contentTest(response, body, callback)
80 ], callback
81
82 assertResponse: (error, response, body) =>
835 assert.isNull error
845 assert.isNotNull response, 'Response'
85
86 # Headers
875 @response.headers = response.headers
88
89 # Status code
905 assert.equal response.statusCode, @response.status, """
91 Got unexpected response code:
925 #{body}
93 Error
94 """
955 response.status = response.statusCode
96
97 # Body
985 if @response.schema
995 schema = @response.schema
1005 validateJson = _.partial JSON.parse, body
1015 body = '[empty]' if body is ''
1025 assert.doesNotThrow validateJson, JSON.SyntaxError, """
103 Invalid JSON:
1045 #{body}
105 Error
106 """
107
1084 json = validateJson()
1094 result = tv4.validateResult json, schema
1104 assert.ok result.valid, """
111 Got unexpected response body:
1124 #{JSON.stringify(json, null, 4)}
113 Error
114 """
115
116 # Update @response
1173 @response.body = json
118
119
1203module.exports = TestFactory
121
122