Coverage

88%
350
311
39

abao.coffee

78%
32
25
7
LineHitsSource
11sms = require("source-map-support").install({handleUncaughtExceptions: false})
21ramlParser = require 'raml-parser'
31async = require 'async'
4
51options = require './options'
61addTests = require './add-tests'
71TestFactory = require './test'
81addHooks = require './add-hooks'
91Runner = require './test-runner'
101applyConfiguration = require './apply-configuration'
111hooks = require './hooks'
12
13
141class Abao
151 constructor: (config) ->
162 @configuration = applyConfiguration(config)
172 @tests = []
182 @hooks = hooks
19
20 run: (done) ->
211 config = @configuration
221 tests = @tests
231 hooks = @hooks
24
25 # Inject the JSON refs schemas
261 factory = new TestFactory(config.options.schemas)
27
281 async.waterfall [
29 # Parse hooks
30 (callback) ->
311 addHooks hooks, config.options.hookfiles
321 callback()
33 ,
34 # Load RAML
35 (callback) ->
361 ramlParser.loadFile(config.ramlPath).then (raml) ->
370 callback(null, raml)
38 , callback
39 ,
40 # Parse tests from RAML
41 (raml, callback) ->
420 if !config.options.server
430 if raml.baseUri
440 config.options.server = raml.baseUri
450 addTests raml, tests, hooks, callback, factory, config.options.sorted
46 ,
47 # Run tests
48 (callback) ->
490 runner = new Runner config.options, config.ramlPath
500 runner.run tests, hooks, callback
51 ], done
52
53
541module.exports = Abao
551module.exports.options = options
56
57

add-hooks.coffee

100%
18
18
0
LineHitsSource
12require 'coffee-script/register'
22proxyquire = require('proxyquire').noCallThru()
32glob = require 'glob'
42path = require 'path'
5
6
72addHooks = (hooks, pattern) ->
8
97 return unless pattern
10
115 files = glob.sync pattern
12
135 console.error 'Found Hookfiles: ' + files
14
155 try
165 for file in files
178 proxyquire path.resolve(process.cwd(), file), {
18 'hooks': hooks
19 }
20 catch error
212 console.error 'Skipping hook loading...'
222 console.error 'Error reading hook files (' + files + ')'
232 console.error 'This probably means one or more of your hookfiles is invalid.'
242 console.error 'Message: ' + error.message if error.message?
252 console.error 'Stack: ' + error.stack if error.stack?
262 return
27
28
292module.exports = addHooks
30
31

add-tests.coffee

98%
80
79
1
LineHitsSource
12async = require 'async'
22_ = require 'lodash'
32csonschema = require 'csonschema'
4
5
62parseSchema = (source) ->
713 if source.contains('$schema')
8 #jsonschema
9 # @response.schema = JSON.parse @response.schema
103 JSON.parse source
11 else
1210 csonschema.parse source
13 # @response.schema = csonschema.parse @response.schema
14
152parseHeaders = (raml) ->
1615 return {} unless raml
17
181 headers = {}
191 for key, v of raml
201 headers[key] = v.example
21
221 headers
23
242addTests = (raml, tests, hooks, parent, callback, testFactory, sortFirst) ->
25
26 # Handle 4th optional param
2725 if _.isFunction(parent)
2810 sortFirst = testFactory
2910 testFactory = callback
3010 callback = parent
3110 parent = null
32
3325 return callback() unless raml.resources
34
35 # Iterate endpoint
3614 async.each raml.resources, (resource, callback) ->
3714 path = resource.relativeUri
3814 params = {}
3914 query = {}
40
41 # Apply parent properties
4214 if parent
433 path = parent.path + path
443 params = _.clone parent.params # shallow copy
45
46 # Setup param
4714 if resource.uriParameters
482 for key, param of resource.uriParameters
492 params[key] = param.example
50
51
52 # In case of issue #8, resource does not define methods
5314 resource.methods ?= []
54
5514 if sortFirst && resource.methods.length > 1
561 methodTests = [
57 method: 'CONNECT', tests: []
58 ,
59 method: 'OPTIONS', tests: []
60 ,
61 method: 'POST', tests: []
62 ,
63 method: 'GET', tests: []
64 ,
65 method: 'HEAD', tests: []
66 ,
67 method: 'PUT', tests: []
68 ,
69 method: 'PATCH', tests: []
70 ,
71 method: 'DELETE', tests: []
72 ,
73 method: 'TRACE', tests: []
74 ]
75
76 # Group endpoint tests by method name
771 _.each methodTests, (methodTest) ->
789 isSameMethod = (test) ->
797 return methodTest.method == test.method.toUpperCase()
80
819 ans = _.partition resource.methods, isSameMethod
829 if ans[0].length != 0
832 _.each ans[0], (test) -> methodTest.tests.push test
842 resource.methods = ans[1]
85
86 # Shouldn't happen unless new HTTP method introduced...
871 leftovers = resource.methods
881 if leftovers.length > 1
890 console.error 'unknown method calls present!', leftovers
90
91 # Now put them back, but in order of methods listed above
921 sortedTests = _.map methodTests, (methodTest) -> return methodTest.tests
931 leftoverTests = _.map leftovers, (leftover) -> return leftover
941 reassembled = _.flattenDeep [_.reject sortedTests, _.isEmpty,
95 _.reject leftoverTests, _.isEmpty]
961 resource.methods = reassembled
97
98 # Iterate response method
9914 async.each resource.methods, (api, callback) ->
10015 method = api.method.toUpperCase()
101
102 # Setup query
10315 if api.queryParameters
1042 for qkey, qvalue of api.queryParameters
1052 if (!!qvalue.required)
1061 query[qkey] = qvalue.example
107
108
109 # Iterate response status
11015 for status, res of api.responses
111
11215 testName = "#{method} #{path} -> #{status}"
113
114 # Append new test to tests
11515 test = testFactory.create(testName, hooks.contentTests[testName])
11615 tests.push test
117
118 # Update test.request
11915 test.request.path = path
12015 test.request.method = method
12115 test.request.headers = parseHeaders(api.headers)
122
123 # select compatible content-type in request body (to support vendor tree types, i.e. application/vnd.api+json)
12415 contentType = (type for type of api.body when type.match(/^application\/(.*\+)?json/i))?[0]
12515 if contentType
1264 test.request.headers['Content-Type'] = contentType
1274 try
1284 test.request.body = JSON.parse api.body[contentType]?.example
129 catch
1301 console.warn "cannot parse JSON example request body for #{test.name}"
13115 test.request.params = params
13215 test.request.query = query
133
134 # Update test.response
13515 test.response.status = status
13615 test.response.schema = null
137
13815 if res?.body
139 # expect content-type of response body to be identical to request body
14013 if contentType && res.body[contentType]?.schema
1413 test.response.schema = parseSchema res.body[contentType].schema
142 # otherwise filter in responses section for compatible content-types
143 # (vendor tree, i.e. application/vnd.api+json)
144 else
14510 contentType = (type for type of res.body when type.match(/^application\/(.*\+)?json/i))?[0]
14610 if res.body[contentType]?.schema
14710 test.response.schema = parseSchema res.body[contentType].schema
148
14915 callback()
150 , (err) ->
15114 return callback(err) if err
152
153 # Recursive
15414 addTests resource, tests, hooks, {path, params}, callback, testFactory, sortFirst
155 , callback
156
157
1582module.exports = addTests
159
160

apply-configuration.coffee

64%
25
16
9
LineHitsSource
11applyConfiguration = (config) ->
2
32 coerceToArray = (value) ->
42 if typeof value is 'string'
50 value = [value]
62 else if !value?
72 value = []
80 else if value instanceof Array
90 value
100 else value
11
122 coerceToDict = (value) ->
132 array = coerceToArray value
142 @dict = {}
15
162 if array.length > 0
170 for item in array
180 splitItem = item.split(':')
190 @dict[splitItem[0]] = splitItem[1]
20
212 return @dict
22
232 configuration =
24 ramlPath: null
25 options:
26 server: null
27 schemas: null
28 reporters: false
29 reporter: null
30 header: null
31 names: false
32 hookfiles: null
33 grep: ''
34 invert: false
35 'hooks-only': false
36 sorted: 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

generate-hooks.coffee

21%
19
4
15
LineHitsSource
11fs = require 'fs'
21Mustache = require 'mustache'
3
41generateHooks = (names, ramlFile, templateFile, callback) ->
50 if !names
60 callback new Error 'no names found for which to generate hooks'
7
80 if !templateFile
90 callback new Error 'missing template file'
10
110 try
120 template = fs.readFileSync templateFile, 'utf8'
130 datetime = new Date().toISOString().replace('T', ' ').substr(0, 19)
140 view =
15 ramlFile: ramlFile
16 timestamp: datetime
17 hooks:
180 { 'name': name } for name in names
190 view.hooks[0].comment = true
20
210 content = Mustache.render template, view
220 console.log content
23 catch error
240 console.error 'failed to generate skeleton hooks'
250 callback error
26
270 callback
28
291module.exports = generateHooks
30
31

hooks.coffee

100%
40
40
0
LineHitsSource
11async = require 'async'
21_ = require 'underscore'
3
4
51class Hooks
61 constructor: () ->
71 @beforeHooks = {}
81 @afterHooks = {}
91 @beforeAllHooks = []
101 @afterAllHooks = []
111 @beforeEachHooks = []
121 @afterEachHooks = []
131 @contentTests = {}
141 @skippedTests = []
15
16 before: (name, hook) =>
175 @addHook(@beforeHooks, name, hook)
18
19 after: (name, hook) =>
205 @addHook(@afterHooks, name, hook)
21
22 beforeAll: (hook) =>
232 @beforeAllHooks.push hook
24
25 afterAll: (hook) =>
262 @afterAllHooks.push hook
27
28 beforeEach: (hook) =>
293 @beforeEachHooks.push(hook)
30
31 afterEach: (hook) =>
323 @afterEachHooks.push(hook)
33
34 addHook: (hooks, name, hook) ->
3510 if hooks[name]
364 hooks[name].push hook
37 else
386 hooks[name] = [hook]
39
40 test: (name, hook) =>
413 if @contentTests[name]?
421 throw new Error("Cannot have more than one test with the name: #{name}")
432 @contentTests[name] = hook
44
45 runBeforeAll: (callback) =>
465 async.series @beforeAllHooks, (err, results) ->
474 callback(err)
48
49 runAfterAll: (callback) =>
505 async.series @afterAllHooks, (err, results) ->
515 callback(err)
52
53 runBefore: (test, callback) =>
547 return callback() unless (@beforeHooks[test.name] or @beforeEachHooks)
55
567 hooks = @beforeEachHooks.concat(@beforeHooks[test.name] ? [])
577 async.eachSeries hooks, (hook, callback) ->
587 hook test, callback
59 , callback
60
61 runAfter: (test, callback) =>
627 return callback() unless (@afterHooks[test.name] or @afterEachHooks)
63
647 hooks = (@afterHooks[test.name] ? []).concat(@afterEachHooks)
657 async.eachSeries hooks, (hook, callback) ->
665 hook test, callback
67 , callback
68
69 skip: (name) =>
701 @skippedTests.push name
71
72 hasName: (name) =>
7312 _.has(@beforeHooks, name) || _.has(@afterHooks, name)
74
75 skipped: (name) =>
7610 @skippedTests.indexOf(name) != -1
77
78
791module.exports = new Hooks()
80
81

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 server:
3 description: 'Specify API endpoint to use. The RAML-specified baseUri value will be used if not provided'
4 type: 'string'
5
6 hookfiles:
7 alias: 'f'
8 description: 'Specify pattern to match files with before/after hooks for running tests'
9 type: 'string'
10
11 schemas:
12 alias: 's'
13 description: 'Specify pattern to match schema files to be loaded for use as JSON refs'
14 type: 'string'
15
16 reporter:
17 alias: 'r'
18 description: 'Specify reporter to use'
19 type: 'string'
20 default: 'spec'
21
22 header:
23 alias: 'h'
24 description: 'Add header to include in each request. Header must be in KEY:VALUE format ' +
25 '(e.g., "-h Accept:application/json").\nReuse option to add multiple headers'
26 type: 'string'
27
28 'hooks-only':
29 alias: 'H'
30 description: 'Run test only if defined either before or after hooks'
31 type: 'boolean'
32
33 grep:
34 alias: 'g'
35 description: 'Only run tests matching <pattern>'
36 type: 'string'
37
38 invert:
39 alias: 'i'
40 description: 'Invert --grep matches'
41 type: 'boolean'
42
43 sorted:
44 description: 'Sorts requests in a sensible way so that objects are not ' +
45 'modified before they are created.\nOrder: ' +
46 'CONNECT, OPTIONS, POST, GET, HEAD, PUT, PATCH, DELETE, TRACE.'
47 type: 'boolean'
48
49 timeout:
50 alias: 't'
51 description: 'Set test-case timeout in milliseconds'
52 type: 'number'
53 default: 2000
54
55 template:
56 description: 'Specify template file to use for generating hooks'
57 type: 'string'
58 normalize: true
59
60 names:
61 alias: 'n'
62 description: 'List names of requests and exit'
63 type: 'boolean'
64
65 'generate-hooks':
66 description: 'Output hooks generated from template file and exit'
67 type: 'boolean'
68
69 reporters:
70 description: 'Display available reporters and exit'
71 type: 'boolean'
72
731module.exports = options
74
75

test-runner.coffee

90%
66
60
6
LineHitsSource
12Mocha = require 'mocha'
22async = require 'async'
32path = require 'path'
42_ = require 'underscore'
52generateHooks = require './generate-hooks'
6
7
82class TestRunner
92 constructor: (options, ramlFile) ->
1010 @server = options.server
1110 delete options.server
1210 @options = options
1310 @mocha = new Mocha options
1410 @ramlFile = ramlFile
15
16 addTestToMocha: (test, hooks) =>
179 mocha = @mocha
189 options = @options
19
20 # Generate Test Suite
219 suite = Mocha.Suite.create mocha.suite, test.name
22
23 # No Response defined
249 if !test.response.status
251 suite.addTest new Mocha.Test 'Skip as no response code defined'
261 return
27
28 # No Hooks for this test
298 if not hooks.hasName(test.name) and options['hooks-only']
300 suite.addTest new Mocha.Test 'Skip as no hooks defined'
310 return
32
33 # Test skipped in hook file
348 if hooks.skipped(test.name)
351 suite.addTest new Mocha.Test 'Skipped in hooks'
361 return
37
38 # Setup hooks
397 if hooks
407 suite.beforeAll _.bind (done) ->
412 @hooks.runBefore @test, done
42 , {hooks, test}
43
447 suite.afterAll _.bind (done) ->
452 @hooks.runAfter @test, done
46 , {hooks, test}
47
48 # Setup test
49 # Vote test name
507 title = if test.response.schema
514 'Validate response code and body'
52 else
533 'Validate response code only'
547 suite.addTest new Mocha.Test title, _.bind (done) ->
552 @test.run done
56 , {test}
57
58 run: (tests, hooks, done) ->
5910 server = @server
6010 options = @options
6110 addTestToMocha = @addTestToMocha
6210 mocha = @mocha
6310 ramlFile = path.basename @ramlFile
6410 names = []
65
6610 async.waterfall [
67 (callback) ->
6810 async.each tests, (test, cb) ->
6910 if options.names || options['generate-hooks']
70 # Save test names for use by next step
711 names.push test.name
721 return cb()
73
74 # None shall pass without...
759 return callback(new Error 'no API endpoint specified') if !server
76
77 # Update test.request
789 test.request.server = server
799 _.extend(test.request.headers, options.header)
80
819 addTestToMocha test, hooks
829 cb()
83 , callback
84 , # Handle options that don't run tests
85 (callback) ->
8610 if options['generate-hooks']
87 # Generate hooks skeleton file
880 templateFile = if options.template
890 options.template
90 else
910 path.join 'templates', 'hookfile.js'
920 generateHooks names, ramlFile, templateFile, done
9310 else if options.names
94 # Write names to console
951 console.log name for name in names
961 return done(null, 0)
97 else
989 return callback()
99 , # Run mocha
100 (callback) ->
1019 mocha.suite.beforeAll _.bind (done) ->
1023 @hooks.runBeforeAll done
103 , {hooks}
1049 mocha.suite.afterAll _.bind (done) ->
1053 @hooks.runAfterAll done
106 , {hooks}
107
1089 mocha.run (failures) ->
1099 callback(null, failures)
110 ], done
111
112
1132module.exports = TestRunner
114
115

test.coffee

98%
66
65
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
11
123String::contains = (it) ->
1313 @indexOf(it) != -1
14
15
163class TestFactory
173 constructor: (schemaLocation) ->
1832 if schemaLocation
19
202 files = glob.sync schemaLocation
212 console.log '\tJSON ref schemas: ' + files.join(', ')
22
232 tv4.banUnknown = true
24
252 for file in files
262 tv4.addSchema(JSON.parse(fs.readFileSync(file, 'utf8')))
27
28 create: (name, contentTest) ->
2932 return new Test(name, contentTest)
30
31
323class Test
333 constructor: (@name, @contentTest) ->
3432 @name ?= ''
3532 @skip = false
36
3732 @request =
38 server: ''
39 path: ''
40 method: 'GET'
41 params: {}
42 query: {}
43 headers: {}
44 body: ''
45
4632 @response =
47 status: ''
48 schema: null
49 headers: null
50 body: null
51
5232 @contentTest ?= (response, body, done) ->
531 done()
54
55 url: () ->
564 path = @request.server + @request.path
57
584 for key, value of @request.params
594 path = path.replace "{#{key}}", value
604 return path
61
62 run: (callback) ->
632 assertResponse = @assertResponse
642 contentTest = @contentTest
65
662 options = _.pick @request, 'headers', 'method'
672 options['url'] = @url()
682 if typeof @request.body is 'string'
690 options['body'] = @request.body
70 else
712 options['body'] = JSON.stringify @request.body
722 options['qs'] = @request.query
73
742 async.waterfall [
75 (callback) ->
762 request options, (error, response, body) ->
772 callback null, error, response, body
78 ,
79 (error, response, body, callback) ->
802 assertResponse(error, response, body)
812 contentTest(response, body, callback)
82 ], callback
83
84 assertResponse: (error, response, body) =>
855 assert.isNull error
865 assert.isNotNull response, 'Response'
87
88 # Headers
895 @response.headers = response.headers
90
91 # Status code
925 assert.equal response.statusCode, @response.status, """
93 Got unexpected response code:
945 #{body}
95 Error
96 """
975 response.status = response.statusCode
98
99 # Body
1005 if @response.schema
1015 schema = @response.schema
1025 validateJson = _.partial JSON.parse, body
1035 body = '[empty]' if body is ''
1045 assert.doesNotThrow validateJson, JSON.SyntaxError, """
105 Invalid JSON:
1065 #{body}
107 Error
108 """
109
1104 json = validateJson()
1114 result = tv4.validateResult json, schema
1124 assert.lengthOf result.missing, 0, """
1134 Missing/unresolved JSON schema $refs (#{result.missing?.join(', ')}) in schema:
1144 #{JSON.stringify(schema, null, 4)}
115 Error
116 """
1174 assert.ok result.valid, """
1184 Got unexpected response body: #{result.error?.message}
1194 #{JSON.stringify(json, null, 4)}
120 Error
121 """
122
123 # Update @response
1243 @response.body = json
125
126
1273module.exports = TestFactory
128
129