replay
Version:
When API testing slows you down: record and replay HTTP responses like a boss
127 lines (104 loc) • 4.1 kB
text/coffeescript
assert = require("assert")
File = require("fs")
Path = require("path")
Matcher = require("./matcher")
mkdir = (pathname, callback)->
Path.exists pathname, (exists)->
if exists
callback null
return
parent = Path.dirname(pathname)
Path.exists parent, (exists)->
if exists
File.mkdir pathname, callback
else
mkdir parent, ->
File.mkdir pathname, callback
# Only these request headers are stored in the catalog.
REQUEST_HEADERS = [/^accept/, /^content-/, /^host/, /^if-/, /^x-/]
class Catalog
constructor: (@settings)->
# We use this to cache host/host:port mapped to array of matchers.
@matchers = {}
find: (host)->
# Return result from cache.
matchers = @matchers[host]
if matchers
return matchers
# Start by looking for directory and loading each of the files.
pathname = "#{@basedir}/#{host}"
unless Path.existsSync(pathname)
return
stat = File.statSync(pathname)
if stat.isDirectory()
files = File.readdirSync(pathname)
for file in files
matchers = @matchers[host] ||= []
mapping = @_read("#{pathname}/#{file}")
matchers.push Matcher.fromMapping(host, mapping)
else
matchers = @matchers[host] ||= []
mapping = @_read(pathname)
matchers.push Matcher.fromMapping(host, mapping)
return matchers
save: (host, request, response, callback)->
matcher = Matcher.fromMapping(host, request: request, response: response)
matchers = @matchers[host] ||= []
matchers.push matcher
uid = +new Date
tmpfile = "/tmp/node-replay.#{uid}"
pathname = "#{@basedir}/#{host}"
console.log "Creating #{pathname}"
mkdir pathname, (error)->
return callback error if error
filename = "#{pathname}/#{uid}"
try
file = File.createWriteStream(tmpfile, encoding: "utf-8")
file.write "#{request.method.toUpperCase()} #{request.url.path || "/"}\n"
for name, value of request.headers
file.write "#{name}: #{value}\n"
file.write "\n"
# Response part
file.write "#{response.status || 200} HTTP/#{response.version || "1.1"}\n"
for name, value of response.headers
file.write "#{name}: #{value}\n"
file.write "\n"
for part in response.body
file.write part
file.end ->
File.rename tmpfile, filename, callback
callback null
catch error
callback error
@prototype.__defineGetter__ "basedir", ->
@_basedir ?= Path.resolve(@settings.fixtures || "fixtures")
return @_basedir
_read: (filename)->
parse_request = (request)->
assert request, "#{filename} missing request section"
[method_and_path, header_lines...] = request.split(/\n/)
[method, path] = method_and_path.split(/\s/)
assert method && path, "#{filename}: first line must be <method> <path>"
headers = parseHeaders(filename, header_lines, REQUEST_HEADERS)
return { url: path, method: method, headers: headers }
parse_response = (response, body)->
assert response, "#{filename} missing response section"
[status_line, header_lines...] = response.split(/\n/)
status = status_line.split()[0]
version = status_line.match(/\d.\d$/)
headers = parseHeaders(filename, header_lines)
return { status: status, version: version, headers: headers, body: body.join("\n\n") }
[request, response, body...] = File.readFileSync(filename, "utf-8").split(/\n\n/)
return { request: parse_request(request), response: parse_response(response, body) }
parseHeaders = (filename, header_lines, restrict = null)->
headers = {}
for line in header_lines
[_, name, value] = line.match(/^(.*?)\:\s+(.*)$/)
assert name && value, "#{filename}: can't make sense of header line #{line}"
if restrict
for regexp in restrict
if regexp.test(name)
continue
headers[name.toLowerCase()] = value.trim().replace(/^"(.*)"$/, "$1")
return headers
module.exports = Catalog