277 lines
9.2 KiB
Lua
277 lines
9.2 KiB
Lua
--- Module implementing http_mock, a HTTP mocking server for testing.
|
|
-- @module spec.helpers.http_mock
|
|
|
|
local helpers = require "spec.helpers"
|
|
|
|
local pairs = pairs
|
|
local ipairs = ipairs
|
|
local type = type
|
|
local setmetatable = setmetatable
|
|
|
|
local modules = {
|
|
require "spec.helpers.http_mock.nginx_instance",
|
|
require "spec.helpers.http_mock.asserts",
|
|
require "spec.helpers.http_mock.debug_port",
|
|
require "spec.helpers.http_mock.clients",
|
|
}
|
|
|
|
local http_mock = {}
|
|
|
|
-- since http_mock contains a lot of functionality, it is implemented in separate submodules
|
|
-- and combined into one large http_mock module here.
|
|
for _, module in ipairs(modules) do
|
|
for k, v in pairs(module) do
|
|
http_mock[k] = v
|
|
end
|
|
end
|
|
|
|
-- get a session from the logs with a timeout
|
|
-- throws error if no request is recieved within the timeout
|
|
-- @treturn table the session
|
|
function http_mock:get_session()
|
|
local ret
|
|
self.eventually:has_session_satisfy(function(s)
|
|
ret = s
|
|
return true
|
|
end)
|
|
return ret
|
|
end
|
|
|
|
-- get a request from the logs with a timeout
|
|
-- throws error if no request is recieved within the timeout
|
|
-- @treturn table the request
|
|
function http_mock:get_request()
|
|
return self:get_session().req
|
|
end
|
|
|
|
-- get a response from the logs with a timeout
|
|
-- throws error if no request is recieved within the timeout
|
|
-- @treturn table the response
|
|
function http_mock:get_response()
|
|
return self:get_session().resp
|
|
end
|
|
|
|
local http_mock_MT = { __index = http_mock, __gc = http_mock.stop }
|
|
|
|
|
|
-- TODO: make default_mocking the same to the `mock_upstream`
|
|
local default_mocking = {
|
|
["/"] = {
|
|
access = [[
|
|
ngx.req.set_header("X-Test", "test")
|
|
ngx.print("ok")
|
|
ngx.exit(200)
|
|
]],
|
|
},
|
|
}
|
|
|
|
local function default_field(tbl, key, default)
|
|
if tbl[key] == nil then
|
|
tbl[key] = default
|
|
end
|
|
end
|
|
|
|
--- create a mock instance which represents a HTTP mocking server
|
|
-- @tparam[opt] table|string|number listens the listen directive of the mock server. This can be
|
|
-- a single directive (string), or a list of directives (table), or a number which will be used as the port.
|
|
-- Defaults to a random available port
|
|
-- @tparam[opt] table|string routes the code of the mock server, defaults to a simple response. See Examples.
|
|
-- @tparam[opt={}] table opts options for the mock server, supporting fields:
|
|
-- @tparam[opt="servroot_tapping"] string opts.prefix the prefix of the mock server
|
|
-- @tparam[opt="_"] string opts.hostname the hostname of the mock server
|
|
-- @tparam[opt=false] bool opts.tls whether to use tls
|
|
-- @tparam[opt={}] table opts.directives the extra directives of the mock server
|
|
-- @tparam[opt={}] table opts.log_opts the options for logging with fields listed below:
|
|
-- @tparam[opt=true] bool opts.log_opts.collect_req whether to log requests()
|
|
-- @tparam[opt=true] bool opts.log_opts.collect_req_body_large whether to log large request bodies
|
|
-- @tparam[opt=false] bool opts.log_opts.collect_resp whether to log responses
|
|
-- @tparam[opt=false] bool opts.log_opts.collect_resp_body whether to log response bodies
|
|
-- @tparam[opt=true] bool opts.log_opts.collect_err: whether to log errors
|
|
-- @tparam[opt] string opts.init: the lua code injected into the init_by_lua_block
|
|
-- @treturn http_mock a mock instance
|
|
-- @treturn string the port the mock server listens to
|
|
-- @usage
|
|
-- local mock = http_mock.new(8000, [[
|
|
-- ngx.req.set_header("X-Test", "test")
|
|
-- ngx.print("hello world")
|
|
-- ]], {
|
|
-- prefix = "mockserver",
|
|
-- log_opts = {
|
|
-- resp = true,
|
|
-- resp_body = true,
|
|
-- },
|
|
-- tls = true,
|
|
-- })
|
|
--
|
|
-- mock:start()
|
|
-- local client = mock:get_client() -- get a client to access the mocking port
|
|
-- local res = assert(client:send({}))
|
|
-- assert.response(res).has.status(200)
|
|
-- assert.response(res).has.header("X-Test", "test")
|
|
-- assert.response(res).has.body("hello world")
|
|
-- mock.eventually:has_response(function(resp)
|
|
-- assert.same(resp.body, "hello world")
|
|
-- end)
|
|
-- mock:wait_until_no_request() -- wait until all the requests are finished
|
|
-- mock:clean() -- clean the logs
|
|
-- client:send({})
|
|
-- client:send({})
|
|
-- local logs = mock:retrieve_mocking_logs() -- get all the logs of HTTP sessions
|
|
-- mock:stop()
|
|
--
|
|
-- listens can be a number, which will be used as the port of the mock server;
|
|
-- or a string, which will be used as the param of listen directive of the mock server;
|
|
-- or a table represents multiple listen ports.
|
|
-- if the port is not specified, a random port will be used.
|
|
-- call mock:get_default_port() to get the first port the mock server listens to.
|
|
-- if the port is a number and opts.tls is set to ture, ssl will be appended.
|
|
--
|
|
-- routes can be a table like this:
|
|
-- routes = {
|
|
-- ["/"] = {
|
|
-- access = [[
|
|
-- ngx.req.set_header("X-Test", "test")
|
|
-- ngx.print("hello world")
|
|
-- ]],
|
|
-- log = [[
|
|
-- ngx.log(ngx.ERR, "log test!")
|
|
-- ]],
|
|
-- directives = {
|
|
-- "rewrite ^/foo /bar break;",
|
|
-- },
|
|
-- },
|
|
-- }
|
|
--
|
|
-- -- or single a string, which will be used as the access phase handler.
|
|
-- routes = [[ ngx.print("hello world") ]]
|
|
-- -- which is equivalent to:
|
|
-- routes = {
|
|
-- ["/"] = {
|
|
-- access = [[ ngx.print("hello world") ]],
|
|
-- },
|
|
-- }
|
|
function http_mock.new(listens, routes, opts)
|
|
opts = opts or {}
|
|
|
|
if listens == nil then
|
|
listens = helpers.get_available_port()
|
|
end
|
|
|
|
if type(listens) == "number" then
|
|
listens = "0.0.0.0:" .. listens .. (opts.tls and " ssl" or "")
|
|
end
|
|
|
|
if type(listens) == "string" then
|
|
listens = { listens, }
|
|
end
|
|
|
|
if routes == nil then
|
|
routes = default_mocking
|
|
elseif type(routes) == "string" then
|
|
routes = {
|
|
["/"] = {
|
|
access = routes,
|
|
}
|
|
}
|
|
end
|
|
|
|
opts.log_opts = opts.log_opts or {}
|
|
local log_opts = opts.log_opts
|
|
default_field(log_opts, "req", true)
|
|
default_field(log_opts, "req_body_large", true)
|
|
-- usually we can check response from client side
|
|
default_field(log_opts, "resp", false)
|
|
default_field(log_opts, "resp_body", false)
|
|
default_field(log_opts, "err", true)
|
|
|
|
local prefix = opts.prefix or "servroot_mock"
|
|
local hostname = opts.hostname or "_"
|
|
local directives = opts.directives or {}
|
|
|
|
local _self = setmetatable({
|
|
prefix = prefix,
|
|
hostname = hostname,
|
|
listens = listens,
|
|
routes = routes,
|
|
directives = directives,
|
|
dicts = opts.dicts,
|
|
init = opts.init,
|
|
log_opts = log_opts,
|
|
logs = {},
|
|
tls = opts.tls,
|
|
eventually_timeout = opts.eventually_timeout or 5,
|
|
}, http_mock_MT)
|
|
|
|
local port = _self:get_default_port()
|
|
|
|
if port then
|
|
_self.client_opts = {
|
|
port = port,
|
|
tls = opts.tls,
|
|
}
|
|
end
|
|
|
|
_self:_set_eventually_table()
|
|
_self:_setup_debug()
|
|
return _self, port
|
|
end
|
|
|
|
--- @type http_mock
|
|
|
|
--- returns the default port of the mock server.
|
|
-- @function http_mock:get_default_port
|
|
-- @treturn string the port of the mock server (from the first listen directive)
|
|
function http_mock:get_default_port()
|
|
return self.listens[1]:match(":(%d+)")
|
|
end
|
|
|
|
--- retrieve the logs of HTTP sessions
|
|
-- @function http_mock:retrieve_mocking_logs
|
|
-- @treturn table the logs of HTTP sessions
|
|
|
|
--- purge the logs of HTTP sessions
|
|
-- @function http_mock:purge_mocking_logs
|
|
|
|
--- clean the logs of HTTP sessions
|
|
-- @function http_mock:clean
|
|
|
|
--- wait until all the requests are finished
|
|
-- @function http_mock:wait_until_no_request
|
|
-- @tparam[opt=true,default=5] number timeout the timeout to wait for the nginx process to exit
|
|
|
|
--- make assertions on HTTP requests.
|
|
-- with a timeout to wait for the requests to arrive
|
|
-- @table http_mock.eventually
|
|
|
|
--- assert if the condition is true for one of the logs.
|
|
--- Replace "session" in the name of the function to assert on fields of the log.
|
|
--- The field can be one of "session", "request", "response", "error".
|
|
-- @function http_mock.eventually:has_session_satisfy
|
|
-- @tparam function check the check function, accept a log and throw error if the condition is not satisfied
|
|
|
|
--- assert if the condition is true for all the logs.
|
|
-- Replace "session" in the name of the function to assert on fields of the log.
|
|
-- The field can be one of "session", "request", "response", "error".
|
|
-- @function http_mock.eventually:all_session_satisfy
|
|
-- @tparam function check the check function, accept a log and throw error if the condition is not satisfied
|
|
|
|
--- assert if none of the logs satisfy the condition.
|
|
-- Replace "session" in the name of the function to assert on fields of the log.
|
|
-- The field can be one of "session", "request", "response", "error".
|
|
-- @function http_mock.eventually:has_no_session_satisfy
|
|
-- @tparam function check the check function, accept a log and throw error if the condition is not satisfied
|
|
|
|
--- assert if not all the logs satisfy the condition.
|
|
-- Replace "session" in the name of the function to assert on fields of the log.
|
|
-- The field can be one of "session", "request", "response", "error".
|
|
-- @function http_mock.eventually:not_all_session_satisfy
|
|
-- @tparam function check the check function, accept a log and throw error if the condition is not satisfied
|
|
|
|
--- alias for eventually:not_all_{session,request,response,error}_satisfy.
|
|
-- Replace "session" in the name of the function to assert on fields of the log.
|
|
-- The field can be one of "session", "request", "response", "error".
|
|
-- @function http_mock.eventually:has_one_without_session_satisfy
|
|
-- @tparam function check the check function, accept a log and throw error if the condition is not satisfied
|
|
|
|
return http_mock
|