Line | Hits | Source |
---|---|---|
1 | 1 | sms = require("source-map-support").install({handleUncaughtExceptions: false}) |
2 | 1 | raml = require 'raml-parser' |
3 | 1 | async = require 'async' |
4 | 1 | chai = require 'chai' |
5 | ||
6 | 1 | options = require './options' |
7 | 1 | addTests = require './add-tests' |
8 | 1 | TestFactory = require './test' |
9 | 1 | addHooks = require './add-hooks' |
10 | 1 | Runner = require './test-runner' |
11 | 1 | applyConfiguration = require './apply-configuration' |
12 | 1 | hooks = require './hooks' |
13 | ||
14 | ||
15 | 1 | class Abao |
16 | 1 | constructor: (config) -> |
17 | 2 | @configuration = applyConfiguration(config) |
18 | 2 | @tests = [] |
19 | 2 | @hooks = hooks |
20 | ||
21 | run: (done) -> | |
22 | 1 | config = @configuration |
23 | 1 | tests = @tests |
24 | 1 | hooks = @hooks |
25 | ||
26 | # init the test factory to inject the json refs schemas | |
27 | 1 | factory = new TestFactory(config.options.schemas) |
28 | ||
29 | 1 | async.waterfall [ |
30 | # Parse hooks | |
31 | (callback) -> | |
32 | 1 | addHooks hooks, config.options.hookfiles |
33 | 1 | callback() |
34 | , | |
35 | # Load RAML | |
36 | (callback) -> | |
37 | 1 | raml.loadFile(config.ramlPath).then (raml) -> |
38 | 0 | callback(null, raml) |
39 | , callback | |
40 | , | |
41 | # Parse tests from RAML | |
42 | (raml, callback) -> | |
43 | 0 | addTests raml, tests, hooks, callback, factory |
44 | , | |
45 | # Run tests | |
46 | (callback) -> | |
47 | 0 | runner = new Runner config.server, config.options |
48 | 0 | runner.run tests, hooks, callback |
49 | ], done | |
50 | ||
51 | ||
52 | 1 | module.exports = Abao |
53 | 1 | module.exports.options = options |
54 | ||
55 |
Line | Hits | Source |
---|---|---|
1 | 2 | path = require 'path' |
2 | ||
3 | 2 | require 'coffee-script/register' |
4 | 2 | proxyquire = require('proxyquire').noCallThru() |
5 | 2 | glob = require 'glob' |
6 | ||
7 | ||
8 | 2 | addHooks = (hooks, pattern) -> |
9 | ||
10 | 7 | return unless pattern |
11 | ||
12 | 5 | files = glob.sync pattern |
13 | ||
14 | 5 | console.error 'Found Hookfiles: ' + files |
15 | ||
16 | 5 | try |
17 | 5 | for file in files |
18 | 8 | proxyquire path.resolve(process.cwd(), file), { |
19 | 'hooks': hooks | |
20 | } | |
21 | catch error | |
22 | 2 | console.error 'Skipping hook loading...' |
23 | 2 | console.error 'Error reading hook files (' + files + ')' |
24 | 2 | console.error 'This probably means one or more of your hookfiles is invalid.' |
25 | 2 | console.error 'Message: ' + error.message if error.message? |
26 | 2 | console.error 'Stack: ' + error.stack if error.stack? |
27 | 2 | return |
28 | ||
29 | ||
30 | 2 | module.exports = addHooks |
31 | ||
32 |
Line | Hits | Source |
---|---|---|
1 | 2 | async = require 'async' |
2 | 2 | _ = require 'underscore' |
3 | 2 | csonschema = require 'csonschema' |
4 | ||
5 | 2 | parseSchema = (source) -> |
6 | 9 | if source.contains('$schema') |
7 | #jsonschema | |
8 | # @response.schema = JSON.parse @response.schema | |
9 | 3 | JSON.parse source |
10 | else | |
11 | 6 | csonschema.parse source |
12 | # @response.schema = csonschema.parse @response.schema | |
13 | ||
14 | 2 | parseHeaders = (raml) -> |
15 | 11 | return {} unless raml |
16 | ||
17 | 1 | headers = {} |
18 | 1 | for key, v of raml |
19 | 1 | headers[key] = v.example |
20 | ||
21 | 1 | headers |
22 | ||
23 | # addTests(raml, tests, [parent], callback, config) | |
24 | 2 | addTests = (raml, tests, hooks, parent, callback, testFactory) -> |
25 | ||
26 | # Handle 4th optional param | |
27 | 19 | if _.isFunction(parent) |
28 | 8 | testFactory = callback |
29 | 8 | callback = parent |
30 | 8 | parent = null |
31 | ||
32 | 19 | return callback() unless raml.resources |
33 | ||
34 | # Iterate endpoint | |
35 | 11 | async.each raml.resources, (resource, callback) -> |
36 | 11 | path = resource.relativeUri |
37 | 11 | params = {} |
38 | ||
39 | # Apply parent properties | |
40 | 11 | if parent |
41 | 3 | path = parent.path + path |
42 | 3 | params = _.clone parent.params |
43 | ||
44 | # Setup param | |
45 | 11 | if resource.uriParameters |
46 | 2 | for key, param of resource.uriParameters |
47 | 2 | params[key] = param.example |
48 | ||
49 | # In case of issue #8, resource does not define methods | |
50 | 11 | resource.methods ?= [] |
51 | ||
52 | # Iterate response method | |
53 | 11 | async.each resource.methods, (api, callback) -> |
54 | 11 | method = api.method.toUpperCase() |
55 | ||
56 | # Iterate response status | |
57 | 11 | for status, res of api.responses |
58 | ||
59 | 11 | testName = "#{method} #{path} -> #{status}" |
60 | ||
61 | # Append new test to tests | |
62 | 11 | test = testFactory.create(testName, hooks.contentTests[testName]) |
63 | 11 | tests.push test |
64 | ||
65 | # Update test.request | |
66 | 11 | test.request.path = path |
67 | 11 | test.request.method = method |
68 | 11 | 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) | |
71 | 11 | contentType = (type for type of api.body when type.match(/^application\/(.*\+)?json/i))?[0] |
72 | 11 | if contentType |
73 | 3 | test.request.headers['Content-Type'] = contentType |
74 | 3 | try |
75 | 3 | test.request.body = JSON.parse api.body[contentType]?.example |
76 | catch | |
77 | 1 | console.warn "cannot parse JSON example request body for #{test.name}" |
78 | 11 | test.request.params = params |
79 | ||
80 | # Update test.response | |
81 | 11 | test.response.status = status |
82 | 11 | test.response.schema = null |
83 | ||
84 | 11 | if res?.body |
85 | # expect content-type of response body to be identical to request body | |
86 | 9 | if contentType && res.body[contentType]?.schema |
87 | 2 | 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 | |
90 | 7 | contentType = (type for type of res.body when type.match(/^application\/(.*\+)?json/i))?[0] |
91 | 7 | if res.body[contentType]?.schema |
92 | 7 | test.response.schema = parseSchema res.body[contentType].schema |
93 | ||
94 | 11 | callback() |
95 | , (err) -> | |
96 | 11 | return callback(err) if err |
97 | ||
98 | # Recursive | |
99 | 11 | addTests resource, tests, hooks, {path, params}, callback, testFactory |
100 | , callback | |
101 | ||
102 | ||
103 | 2 | module.exports = addTests |
104 | ||
105 |
Line | Hits | Source |
---|---|---|
1 | ||
2 | 1 | applyConfiguration = (config) -> |
3 | ||
4 | 2 | coerceToArray = (value) -> |
5 | 2 | if typeof value is 'string' |
6 | 0 | value = [value] |
7 | 2 | else if !value? |
8 | 2 | value = [] |
9 | 0 | else if value instanceof Array |
10 | 0 | value |
11 | 0 | else value |
12 | ||
13 | 2 | coerceToDict = (value) -> |
14 | 2 | array = coerceToArray value |
15 | 2 | @dict = {} |
16 | ||
17 | 2 | if array.length > 0 |
18 | 0 | for item in array |
19 | 0 | splitItem = item.split(':') |
20 | 0 | @dict[splitItem[0]] = splitItem[1] |
21 | ||
22 | 2 | return @dict |
23 | ||
24 | 2 | 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 | |
39 | 2 | for own key, value of config |
40 | 0 | configuration[key] = value |
41 | ||
42 | # coerce some options into an dict | |
43 | 2 | configuration.options.header = coerceToDict(configuration.options.header) |
44 | ||
45 | # TODO(quanlong): OAuth2 Bearer Token | |
46 | 2 | if configuration.options.oauth2Token? |
47 | 0 | configuration.options.headers['Authorization'] = "Bearer #{configuration.options.oauth2Token}" |
48 | ||
49 | 2 | return configuration |
50 | ||
51 | ||
52 | 1 | module.exports = applyConfiguration |
53 | ||
54 |
Line | Hits | Source |
---|---|---|
1 | 1 | async = require 'async' |
2 | 1 | _ = require 'underscore' |
3 | ||
4 | 1 | class Hooks |
5 | 1 | constructor: () -> |
6 | 1 | @beforeHooks = {} |
7 | 1 | @afterHooks = {} |
8 | 1 | @beforeAllHooks = [] |
9 | 1 | @afterAllHooks = [] |
10 | 1 | @beforeEachHooks = [] |
11 | 1 | @afterEachHooks = [] |
12 | 1 | @contentTests = {} |
13 | ||
14 | before: (name, hook) => | |
15 | 5 | @addHook(@beforeHooks, name, hook) |
16 | ||
17 | after: (name, hook) => | |
18 | 5 | @addHook(@afterHooks, name, hook) |
19 | ||
20 | beforeAll: (hook) => | |
21 | 2 | @beforeAllHooks.push hook |
22 | ||
23 | afterAll: (hook) => | |
24 | 2 | @afterAllHooks.push hook |
25 | ||
26 | beforeEach: (hook) => | |
27 | 3 | @beforeEachHooks.push(hook) |
28 | ||
29 | afterEach: (hook) => | |
30 | 3 | @afterEachHooks.push(hook) |
31 | ||
32 | addHook: (hooks, name, hook) => | |
33 | 10 | if hooks[name] |
34 | 4 | hooks[name].push hook |
35 | else | |
36 | 6 | hooks[name] = [hook] |
37 | ||
38 | test: (name, hook) => | |
39 | 3 | if @contentTests[name]? |
40 | 1 | throw new Error("Cannot have more than one test with the name: #{name}") |
41 | 2 | @contentTests[name] = hook |
42 | ||
43 | runBeforeAll: (callback) => | |
44 | 5 | async.series @beforeAllHooks, (err, results) -> |
45 | 4 | callback(err) |
46 | ||
47 | runAfterAll: (callback) => | |
48 | 5 | async.series @afterAllHooks, (err, results) -> |
49 | 5 | callback(err) |
50 | ||
51 | runBefore: (test, callback) => | |
52 | 7 | return callback() unless (@beforeHooks[test.name] or @beforeEachHooks) |
53 | ||
54 | 7 | hooks = @beforeEachHooks.concat(@beforeHooks[test.name] ? []) |
55 | 7 | async.eachSeries hooks, (hook, callback) -> |
56 | 7 | hook test, callback |
57 | , callback | |
58 | ||
59 | runAfter: (test, callback) => | |
60 | 7 | return callback() unless (@afterHooks[test.name] or @afterEachHooks) |
61 | ||
62 | 7 | hooks = (@afterHooks[test.name] ? []).concat(@afterEachHooks) |
63 | 7 | async.eachSeries hooks, (hook, callback) -> |
64 | 5 | hook test, callback |
65 | , callback | |
66 | ||
67 | hasName: (name) => | |
68 | 11 | _.has(@beforeHooks, name) || _.has(@afterHooks, name) |
69 | ||
70 | ||
71 | 1 | module.exports = new Hooks() |
72 | ||
73 |
Line | Hits | Source |
---|---|---|
1 | 1 | abao = require './abao' |
2 | ||
3 | 1 | module.exports = abao |
4 | ||
5 |
Line | Hits | Source |
---|---|---|
1 | 1 | options = |
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 | ||
53 | 1 | module.exports = options |
54 | ||
55 |
Line | Hits | Source |
---|---|---|
1 | 2 | Mocha = require 'mocha' |
2 | 2 | async = require 'async' |
3 | 2 | _ = require 'underscore' |
4 | ||
5 | ||
6 | 2 | class TestRunner |
7 | 2 | constructor: (server, options = {}) -> |
8 | 9 | @server = server |
9 | 9 | @options = options |
10 | 9 | @mocha = new Mocha options |
11 | ||
12 | addTestToMocha: (test, hooks) => | |
13 | 8 | mocha = @mocha |
14 | 8 | options = @options |
15 | ||
16 | # Generate Test Suite | |
17 | 8 | suite = Mocha.Suite.create mocha.suite, test.name |
18 | ||
19 | # No Response defined | |
20 | 8 | if !test.response.status |
21 | 1 | suite.addTest new Mocha.Test 'Skip as no response code defined' |
22 | 1 | return |
23 | ||
24 | # No Hooks for this test | |
25 | 7 | if not hooks.hasName(test.name) and options['hooks-only'] |
26 | 0 | suite.addTest new Mocha.Test 'Skip as no hooks defined' |
27 | 0 | return |
28 | ||
29 | # Setup hooks | |
30 | 7 | if hooks |
31 | 7 | suite.beforeAll _.bind (done) -> |
32 | 2 | @hooks.runBefore @test, done |
33 | , {hooks, test} | |
34 | ||
35 | 7 | suite.afterAll _.bind (done) -> |
36 | 2 | @hooks.runAfter @test, done |
37 | , {hooks, test} | |
38 | ||
39 | # Setup test | |
40 | # Vote test name | |
41 | 7 | title = if test.response.schema then 'Validate response code and body' else 'Validate response code only' |
42 | 7 | suite.addTest new Mocha.Test title, _.bind (done) -> |
43 | 2 | @test.run done |
44 | , {test} | |
45 | ||
46 | run: (tests, hooks, callback) -> | |
47 | 9 | server = @server |
48 | 9 | options = @options |
49 | 9 | addTestToMocha = @addTestToMocha |
50 | 9 | mocha = @mocha |
51 | ||
52 | 9 | async.waterfall [ |
53 | (callback) -> | |
54 | 9 | async.each tests, (test, done) -> |
55 | # list tests | |
56 | 9 | if options.names |
57 | 1 | console.log test.name |
58 | 1 | return done() |
59 | ||
60 | # Update test.request | |
61 | 8 | test.request.server = server |
62 | 8 | _.extend(test.request.headers, options.header) |
63 | ||
64 | 8 | addTestToMocha test, hooks |
65 | 8 | done() |
66 | , callback | |
67 | , # Run mocha | |
68 | (callback) -> | |
69 | 9 | return callback(null, 0) if options.names |
70 | ||
71 | 8 | mocha.suite.beforeAll _.bind (done) -> |
72 | 3 | @hooks.runBeforeAll done |
73 | , {hooks} | |
74 | 8 | mocha.suite.afterAll _.bind (done) -> |
75 | 3 | @hooks.runAfterAll done |
76 | , {hooks} | |
77 | ||
78 | 8 | mocha.run (failures) -> |
79 | 8 | callback(null, failures) |
80 | ], callback | |
81 | ||
82 | ||
83 | 2 | module.exports = TestRunner |
84 | ||
85 |
Line | Hits | Source |
---|---|---|
1 | 3 | chai = require 'chai' |
2 | 3 | request = require 'request' |
3 | 3 | _ = require 'underscore' |
4 | 3 | async = require 'async' |
5 | 3 | tv4 = require 'tv4' |
6 | 3 | fs = require 'fs' |
7 | 3 | glob = require 'glob' |
8 | ||
9 | 3 | assert = chai.assert |
10 | ||
11 | 3 | String::contains = (it) -> |
12 | 9 | @indexOf(it) != -1 |
13 | ||
14 | 3 | class TestFactory |
15 | 3 | constructor: (schemaLocation) -> |
16 | 28 | if schemaLocation |
17 | ||
18 | 2 | files = glob.sync schemaLocation |
19 | 2 | console.error 'Found JSON ref schemas: ' + files |
20 | 2 | console.error '' |
21 | ||
22 | 2 | tv4.banUnknown = true |
23 | ||
24 | 2 | for file in files |
25 | 2 | tv4.addSchema(JSON.parse(fs.readFileSync(file, 'utf8'))) |
26 | ||
27 | create: (name, contentTest) -> | |
28 | 27 | return new Test(name, contentTest) |
29 | ||
30 | 3 | class Test |
31 | 3 | constructor: (@name, @contentTest) -> |
32 | 27 | @name ?= '' |
33 | 27 | @skip = false |
34 | ||
35 | 27 | @request = |
36 | server: '' | |
37 | path: '' | |
38 | method: 'GET' | |
39 | params: {} | |
40 | query: {} | |
41 | headers: {} | |
42 | body: '' | |
43 | ||
44 | 27 | @response = |
45 | status: '' | |
46 | schema: null | |
47 | headers: null | |
48 | body: null | |
49 | ||
50 | 27 | @contentTest ?= (response, body, done) -> |
51 | 1 | done() |
52 | ||
53 | url: () -> | |
54 | 4 | path = @request.server + @request.path |
55 | ||
56 | 4 | for key, value of @request.params |
57 | 4 | path = path.replace "{#{key}}", value |
58 | 4 | return path |
59 | ||
60 | run: (callback) -> | |
61 | 2 | assertResponse = @assertResponse |
62 | 2 | contentTest = @contentTest |
63 | ||
64 | 2 | options = _.pick @request, 'headers', 'method' |
65 | 2 | options['url'] = @url() |
66 | 2 | if typeof @request.body is 'string' |
67 | 0 | options['body'] = @request.body |
68 | else | |
69 | 2 | options['body'] = JSON.stringify @request.body |
70 | 2 | options['qs'] = @request.query |
71 | ||
72 | 2 | async.waterfall [ |
73 | (callback) -> | |
74 | 2 | request options, (error, response, body) -> |
75 | 2 | callback null, error, response, body |
76 | , | |
77 | (error, response, body, callback) -> | |
78 | 2 | assertResponse(error, response, body) |
79 | 2 | contentTest(response, body, callback) |
80 | ], callback | |
81 | ||
82 | assertResponse: (error, response, body) => | |
83 | 5 | assert.isNull error |
84 | 5 | assert.isNotNull response, 'Response' |
85 | ||
86 | # Headers | |
87 | 5 | @response.headers = response.headers |
88 | ||
89 | # Status code | |
90 | 5 | assert.equal response.statusCode, @response.status, """ |
91 | Got unexpected response code: | |
92 | 5 | #{body} |
93 | Error | |
94 | """ | |
95 | 5 | response.status = response.statusCode |
96 | ||
97 | # Body | |
98 | 5 | if @response.schema |
99 | 5 | schema = @response.schema |
100 | 5 | validateJson = _.partial JSON.parse, body |
101 | 5 | body = '[empty]' if body is '' |
102 | 5 | assert.doesNotThrow validateJson, JSON.SyntaxError, """ |
103 | Invalid JSON: | |
104 | 5 | #{body} |
105 | Error | |
106 | """ | |
107 | ||
108 | 4 | json = validateJson() |
109 | 4 | result = tv4.validateResult json, schema |
110 | 4 | assert.ok result.valid, """ |
111 | Got unexpected response body: | |
112 | 4 | #{JSON.stringify(json, null, 4)} |
113 | Error | |
114 | """ | |
115 | ||
116 | # Update @response | |
117 | 3 | @response.body = json |
118 | ||
119 | ||
120 | 3 | module.exports = TestFactory |
121 | ||
122 |