Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "api-mock",
"version": "0.1.1",
"version": "0.1.2",
"description": "A mock server generated from your API Blueprint.",
"author": "Evan Cordell <cordell.evan@gmail.com>",
"main": "lib/api-mock.js",
Expand All @@ -13,9 +13,10 @@
"prepublish": "scripts/prepublish"
},
"dependencies": {
"protagonist": "~0.17.1",
"optimist": "~0.6.0",
"body-parser": "^1.12.0",
"express": "~3.4.7",
"optimist": "~0.6.0",
"protagonist": "~0.17.1",
"uri-template": "~0.4.1",
"winston": "~0.7.2"
},
Expand Down
2 changes: 2 additions & 0 deletions src/api-mock.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class ApiMock
@configuration = config
@app = express()

@app.use(require("body-parser").text())

if @configuration.options['ssl-enable']
sslSupport = new SslSupport(
@app,
Expand Down
85 changes: 85 additions & 0 deletions src/build-express-handler.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
winston = require 'winston'

comparePayloads = (expected, actual) ->
if !expected
return true

if typeof expected == 'object'
if typeof actual != 'object' or actual == null
return false
for k, v of expected
if !actual[k] or !actual.hasOwnProperty(k)
return false

if !module.exports.comparePayloads(v, actual[k])
return false
else
if expected.toString() != actual.toString()
return false
return true

respond = (requestObject, responseObject, payload) ->
respondWithPayload = (response) ->
if response.headers
for header, value of response.headers
responseObject.setHeader value['name'], value['value']
responseObject.setHeader 'Content-Length', Buffer.byteLength(response.body)
return responseObject.send response.status, (response.body || "")

keys = Object.keys(payload.responses)

if "prefer" of requestObject.headers
for status, response of payload.responses
if requestObject.headers["prefer"].toString() != status.toString()
continue
return respondWithPayload(response)
winston.warn "[#{payload.path}] Preferred response #{requestObject.headers['prefer']} not found. Falling back to #{keys[0]}"
return respondWithPayload(payload.responses[keys[0]])

return respondWithPayload(payload.responses[keys[0]])

matchBody = (requestObject, body, headers) ->
if body
try
body = JSON.parse body
catch e
winston.warn "Could not parse body JSON"

try
requestPayload = requestObject.body.toString()
if requestPayload.length == 0 or requestObject.headers['content-type'] != 'application/json'
requestPayload = requestObject.query
else
requestPayload = JSON.parse requestPayload
if requestObject and requestObject.params
for k, v of requestObject.params
requestPayload[k] = v
# Done parsing the request payload. Let's have some fun.
return module.exports.comparePayloads(body, requestPayload)
catch e
# Literal match on body
winston.warn "Could not parse input body"
return (!body and !requestObject.body) or (body.toString() == requestObject.body.toString())

matchHeaders = (requestObject, headers) ->
return comparePayloads(headers, requestObject.headers)

buildExpressHandler = (payloads) ->
return (request, response) ->
l = payloads.length
for payload in payloads
if payload.request and payload.request.body and !matchBody(request, payload.request.body, payload.request.headers)
winston.warn "Did not match body"
continue
if payload.request and payload.request.headers and !matchHeaders(request, payload.request.headers)
winston.warn "Did not match header"
continue
return buildExpressHandler.respond(request, response, payload)
return buildExpressHandler.respond(request, response, payloads[l - 1])

buildExpressHandler.comparePayloads = comparePayloads
buildExpressHandler.matchBody = matchBody
buildExpressHandler.matchHeaders = matchHeaders
buildExpressHandler.respond = respond
module.exports = buildExpressHandler

65 changes: 23 additions & 42 deletions src/walker.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,14 @@ inheritHeaders = require './inherit-headers'
inheritParameters = require './inherit-parameters'
expandUriTemplateWithParameters = require './expand-uri-template-with-parameters'
exampleToHttpPayloadPair = require './example-to-http-payload-pair'
buildExpressHandler = require './build-express-handler'

ut = require 'uri-template'
winston = require 'winston'

walker = (app, resourceGroups) ->

sendResponse = (responses) ->
(req, res) ->
# default response
response = responses[Object.keys(responses)[0]]

# try to find matching response based on PREFER header
if 'prefer' of req.headers
if req.headers['prefer'] of responses
response = responses[req.headers['prefer']]
else
winston.warn("[#{req.url}] Preferrered response #{req.headers['prefer']} not found. Falling back to #{response.status}")

for header, value of response.headers
headerName = value['name']
headerValue = value['value']
res.setHeader headerName, headerValue
res.setHeader 'Content-Length', Buffer.byteLength(response.body)
res.send response.status, response.body

responses = []
responses = {}

for group in resourceGroups
for resource in group['resources']
Expand All @@ -53,33 +35,32 @@ walker = (app, resourceGroups) ->
for error in payload['errors']
winston.error("[#{path}] #{error}")

responses.push {
method: action.method
path: path
responses: payload['pair']['responses']
if !responses[path]?
responses[path] = {}

actionMethod = action.method.toLowerCase()

if !responses[path][actionMethod]?
responses[path][actionMethod] = []

responses[path][actionMethod].push {
request: payload.pair.request
responses: payload.pair.responses
}

paths = Object.keys responses

#sort routes
responses.sort (a,b) ->
if (a.path > b.path)
paths.sort (a,b) ->
if (a > b)
return -1
if (a.path < b.path)
if (a <= b)
return 1
return 0

for response in responses
switch response.method
when 'GET'
app.get response.path, sendResponse(response.responses)
when 'POST'
app.post response.path, sendResponse(response.responses)
when 'PUT'
app.put response.path, sendResponse(response.responses)
when 'DELETE'
app.delete response.path, sendResponse(response.responses)
when 'PATCH'
app.patch response.path, sendResponse(response.responses)


for path in paths
pathMethods = responses[path]
for method, payloads of pathMethods
if app[method]?
app[method](path, buildExpressHandler(payloads))

module.exports = walker
132 changes: 132 additions & 0 deletions test/unit/build-express-handler-test.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
{assert} = require 'chai'
sinon = require 'sinon'
proxyquire = require 'proxyquire'

winstonStub = require 'winston'

expressHandler = proxyquire '../../src/build-express-handler', {
'winston': winstonStub
}

describe 'build-express-handler', () ->
describe 'body comparison', () ->
it 'should correctly match two empty strings', () ->
assert.isTrue expressHandler.comparePayloads(null, null)
it 'should correctly match two differing strings', () ->
assert.isFalse expressHandler.comparePayloads('foo', 'bar')
it 'should correctly match a null expectation with a body containing data', () ->
assert.isTrue expressHandler.comparePayloads(null, {"foo": "bar"})
it 'should identify a non-matching body', () ->
assert.isFalse expressHandler.comparePayloads({"foo":"4"}, {"foo":"3"})
it 'should be lenient on types for non-objects', () ->
assert.isTrue expressHandler.comparePayloads({"foo":4}, {"foo":"4"}), "Comparison works: integer type is typecasted to string"
it 'should correctly recurse for deep-match', () ->
sinon.spy expressHandler, "comparePayloads"
assert.isFalse expressHandler.comparePayloads({"foo":{"bar":"baz"}}, {"foo":{"barry":"baz"}}), "Comparison failed: inside key not matched"
assert.isTrue expressHandler.comparePayloads.calledWith({"foo":{"bar":"baz"}}, {"foo":{"barry":"baz"}}), "Initial call happened"
assert.isTrue expressHandler.comparePayloads.calledWith({"bar":"baz"}, {"barry":"baz"}), "Second recursion happened"
it 'should not match null to a valid object', () ->
assert.isFalse expressHandler.comparePayloads({"foo":{"bar":"baz"}}, null)

describe 'matchBody', () ->

it 'should check for payload headers before parsing json', () ->
body = JSON.stringify {'foo':'bar'}
headers = {'content-type': 'application/json'}
expressHandler.matchBody {'body': body, 'headers': headers}, body, headers
assert.isTrue expressHandler.comparePayloads.calledWith('bar', 'bar')

it 'should check the query object', () ->
body = {'foo': 'bar'}
headers = {}
expressHandler.matchBody {'query': body, 'body': ''}, body, {}
console.log expressHandler.comparePayloads
assert.isTrue expressHandler.comparePayloads.calledWith(body, body)

it 'should also add parameter values', () ->
body = {'foo': 'bar'}
expected = {'foo2':'bar2'}
expressHandler.matchBody {'query': body, 'body': '', 'params': {'foo2': 'bar2'}}, {'foo': 'bar', 'foo2': 'bar2'}, {}
console.log expressHandler.comparePayloads
assert.isTrue expressHandler.comparePayloads.calledWith('bar2', 'bar2')

it 'should fallback to a literal match on malformatted bodies', () ->
sinon.stub winstonStub, 'warn', () ->
body = '{"foo":bar}'
headers = {'content-type': 'application/json'}
assert.isTrue expressHandler.matchBody({'body':body, 'headers': headers}, body, headers)
assert.isTrue winstonStub.warn.calledTwice
winstonStub.warn.restore()

describe 'matcher', () ->
it 'should fallback to the last response if nothing matches', (done) ->
request = {
headers: {}
body: ""
}
payloads = [
{
request: {
headers: {
'content-type': 'application/json'
}
},
response: {
status: 200,
body: 'foo'
}
}
]
o = sinon.stub(expressHandler, "respond", (o1, o2, payload) ->
assert.equal payload.response.status, 200
assert.equal payload.response.body, 'foo'
done()
);
s = expressHandler payloads
foo = {}
s request, foo
assert.isTrue o.calledOnce
o.restore()

it 'should properly attempt to match a body that contains json', () ->
headers = {
"content-type": "application/json"
}
request = {
headers: headers
body: "{\"foo\":2}"
}
payloads = [
{
"request": {
"body": {
"foo": 3
}
}
},
{
"request": {
"body": {
"foo": 2
},
"headers": {
"content-type": "application/json+uml"
}
}
}
{
"response": {
"body": "foo",
"headers": {
"content-type": "text-plain"
}
}
}
]
o = sinon.stub(expressHandler, "respond", () ->);
s = expressHandler payloads
foo = {}
s request, foo
assert.isTrue o.calledOnce
o.restore()