Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(http_mock): improvements & tapping #11182

Merged
merged 4 commits into from
Jul 11, 2023
Merged
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
58 changes: 58 additions & 0 deletions spec/02-integration/01-helpers/03-http_mock_spec.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
local http_mock = require "spec.helpers.http_mock"
local tapping = require "spec.helpers.http_mock.tapping"
local pl_file = require "pl.file"

for _, tls in ipairs {true, false} do
Expand Down Expand Up @@ -220,3 +221,60 @@ describe("http_mock config", function()
assert(pl_file.access_time(pid_filename) ~= nil, "mocking not in the correct place")
end)
end)

local function remove_volatile_headers(req_t)
req_t.headers["Connection"] = nil
req_t.headers["Host"] = nil
req_t.headers["User-Agent"] = nil
req_t.headers["Content-Length"] = nil
end

describe("http_mock.tapping", function()
local tapped, tapped_port
lazy_setup(function()
tapped, tapped_port = http_mock.new(nil, nil, {
log_opts = {
req = true,
req_body = true,
req_body_large = true,
}
})
tapped:start()
end)
lazy_teardown(function()
tapped:stop(true)
end)

it("works", function()
local tapping_mock = tapping.new(tapped_port)
tapping_mock:start()
finally(function()
tapping_mock:stop(true)
end)
local client = tapping_mock:get_client()
local request = {
headers = {
["test"] = "mock_debug"
},
method = "POST",
path = "/test!",
body = "hello world",
}
local res = assert(client:send(request))
assert.response(res).has.status(200)
assert.same(res:read_body(), "ok")

request.uri = request.path
request.path = nil

local record = tapping_mock:retrieve_mocking_logs()
local req_t = assert(record[1].req)
remove_volatile_headers(req_t)
assert.same(request, req_t)

local upstream_record = tapped:retrieve_mocking_logs()
local upstream_req_t = assert(upstream_record[1].req)
remove_volatile_headers(upstream_req_t)
assert.same(request, upstream_req_t)
end)
end)
118 changes: 88 additions & 30 deletions spec/helpers/http_mock.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
--- Module implementing http_mock, a HTTP mocking server for testing.
-- @module spec.helpers.http_mock

local helpers = require "spec.helpers"

local pairs = pairs
Expand Down Expand Up @@ -42,11 +45,24 @@ local function default_field(tbl, key, default)
end
end

-- create a mock instance which represents a HTTP mocking server
-- @param listens: the listen directive of the mock server, defaults to "0.0.0.0:8000"
-- @param code: the code of the mock server, defaults to a simple response.
-- @param opts: options for the mock server, left it empty to use the defaults
-- @return: a mock instance
--- 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
-- @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")
Expand Down Expand Up @@ -75,15 +91,8 @@ end
-- 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:
-- @usage
-- -- routes can be a table like this:
-- routes = {
-- ["/"] = {
-- access = [[
Expand All @@ -98,19 +107,15 @@ end
-- },
-- },
-- }
-- or a string, which will be used as the access phase handler.
--
-- opts:
-- prefix: the prefix of the mock server, defaults to "mockserver"
-- hostname: the hostname of the mock server, defaults to "_"
-- directives: the extra directives of the mock server, defaults to {}
-- log_opts: the options for logging with fields listed below:
-- collect_req: whether to log requests(), defaults to true
-- collect_req_body_large: whether to log large request bodies, defaults to true
-- collect_resp: whether to log responses, defaults to false
-- collect_resp_body: whether to log response bodies, defaults to false
-- collect_err: whether to log errors, defaults to true
-- tls: whether to use tls, defaults to false
-- -- 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 {}

Expand All @@ -135,7 +140,7 @@ function http_mock.new(listens, routes, opts)
}
}
end

opts.log_opts = opts.log_opts or {}
local log_opts = opts.log_opts
default_field(log_opts, "req", true)
Expand All @@ -145,7 +150,7 @@ function http_mock.new(listens, routes, opts)
default_field(log_opts, "resp_body", false)
default_field(log_opts, "err", true)

local prefix = opts.prefix or "mockserver"
local prefix = opts.prefix or "servroot_mock"
local hostname = opts.hostname or "_"
local directives = opts.directives or {}

Expand All @@ -172,11 +177,64 @@ function http_mock.new(listens, routes, opts)

_self:_set_eventually_table()
_self:_setup_debug()
return _self
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

return http_mock
--- 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
-- @class 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
4 changes: 4 additions & 0 deletions spec/helpers/http_mock/asserts.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ local pairs = pairs
local pcall = pcall
local error = error

---@class http_mock
local http_mock = {}

local build_in_checks = {}

---@class http_mock_asserts
local eventually_MT = {}
eventually_MT.__index = eventually_MT

Expand Down Expand Up @@ -114,6 +116,8 @@ local function register_assert(name, impl)
eventually_MT["not_all_" .. name] = function(self, ...)
return eventually_has(reverse_impl, self.__mock, ...)
end

eventually_MT["has_one_without_" .. name] = eventually_MT["not_all_" .. name]
end

for name, impl in pairs(build_in_checks) do
Expand Down
13 changes: 11 additions & 2 deletions spec/helpers/http_mock/clients.lua
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
--- part of http_mock
-- @submodule spec.helpers.http_mock

local helpers = require "spec.helpers"
local http_client = helpers.http_client

local http_mock = {}

-- we need to get rid of dependence to the "helpers"
--- get a `helpers.http_client` to access the mock server
-- @function http_mock:get_client
-- @treturn http_client a `helpers.http_client` instance
-- @within http_mock
-- @usage
-- httpc = http_mock:get_client()
-- result = httpc:get("/services/foo", opts)
function http_mock:get_client()
local client = self.client
if not client then
client = http_client({
scheme = self.client_opts.tls and "https" or "http",
host = "localhost",
host = "localhost",
port = self.client_opts.port,
})

Expand Down
3 changes: 2 additions & 1 deletion spec/helpers/http_mock/debug_port.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ local ipairs = ipairs
local insert = table.insert
local assert = assert

---@class http_mock
local http_mock = {}

-- POST as it's not idempotent
Expand Down Expand Up @@ -36,7 +37,7 @@ local get_status_param = {
-- internal API
function http_mock:_setup_debug(debug_param)
local debug_port = helpers.get_available_port()
local debug_client = http.new()
local debug_client = assert(http.new())
local debug_connect = {
scheme = "http",
host = "localhost",
Expand Down
19 changes: 15 additions & 4 deletions spec/helpers/http_mock/nginx_instance.lua
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
--- part of http_mock
-- @submodule spec.helpers.http_mock

local template_str = require "spec.helpers.http_mock.template"
local pl_template = require "pl.template"
local pl_path = require "pl.path"
Expand All @@ -15,16 +18,18 @@ local shallow_copy = require "kong.tools.utils".shallow_copy

local template = assert(pl_template.compile(template_str))
local render_env = {ipairs = ipairs, pairs = pairs, error = error, }

local http_mock = {}

-- start a dedicate nginx instance for this mock
--- start a dedicate nginx instance for this mock
-- @tparam[opt=false] bool error_on_exist whether to throw error if the directory already exists
-- @within http_mock
-- @usage http_mock:start(true)
function http_mock:start(error_on_exist)
local ok = (pl_path.mkdir(self.prefix))
and (pl_path.mkdir(self.prefix .. "/logs"))
and (pl_path.mkdir(self.prefix .. "/conf"))
if error_on_exist then assert(ok, "failed to create directory " .. self.prefix) end

local render = assert(template:render(shallow_copy(self), render_env))
local conf_path = self.prefix .. "/conf/nginx.conf"
local conf_file = assert(io.open(conf_path, "w"))
Expand All @@ -39,7 +44,13 @@ end

local sleep_step = 0.01

-- stop a dedicate nginx instance for this mock
--- stop a dedicate nginx instance for this mock
-- @function http_mock:stop
-- @tparam[opt=false] bool no_clean whether to preserve the logs
-- @tparam[opt="TERM"] string signal the signal name to send to the nginx process
-- @tparam[opt=10] number timeout the timeout to wait for the nginx process to exit
-- @within http_mock
-- @usage http_mock:stop(false, "TERM", 10)
function http_mock:stop(no_clean, signal, timeout)
signal = signal or "TERM"
timeout = timeout or 10
Expand Down
47 changes: 47 additions & 0 deletions spec/helpers/http_mock/tapping.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
--- A http_mock subclass for tapping.
-- @module spec.helpers.http_mock.tapping

local http_mock = require "spec.helpers.http_mock"

local tapping = {}

-- create a new tapping route
-- @tparam string|number target the target host/port of the tapping route
-- @return the tapping route instance
function tapping.new_tapping_route(target)
if tonumber(target) then
-- TODO: handle the resovler!
target = "http://127.0.0.1:" .. target
end

if not target:find("://") then
target = "http://" .. target
end

return {
["/"] = {
directives = [[proxy_pass ]] .. target .. [[;]],
}
}
end

--- create a new `http_mock.tapping` instance with a tapping route
-- @tparam string|number target the target host/port of the tapping route
-- @tparam[opt] table|string|number listens see `http_mock.new`
-- @tparam[opt="servroot_tapping"] string prefix the prefix of the mock server
-- @tparam[opt={}] table log_opts see `http_mock.new`, uses the defaults, with `req_large_body` enabled
-- @treturn http_mock.tapping a tapping instance
-- @treturn string the port the mock server listens to
function tapping.new(target, listens, prefix, log_opts)
---@diagnostic disable-next-line: return-type-mismatch
return http_mock.new(listens, tapping.new_tapping_route(target), {
prefix = prefix or "servroot_tapping",
log_opts = log_opts or {
req = true,
req_body = true,
req_large_body = true,
},
})
end

return tapping
Loading