832 lines
25 KiB
Lua
832 lines
25 KiB
Lua
------------------------------------------------------------------
|
|
-- Collection of utilities to help testing Kong features and plugins.
|
|
--
|
|
-- @copyright Copyright 2016-2022 Kong Inc. All rights reserved.
|
|
-- @license [Apache 2.0](https://opensource.org/licenses/Apache-2.0)
|
|
-- @module spec.helpers
|
|
|
|
|
|
local cjson = require("cjson.safe")
|
|
local say = require("say")
|
|
local pl_dir = require("pl.dir")
|
|
local pl_file = require("pl.file")
|
|
local colors = require("ansicolors")
|
|
local luassert = require("luassert.assert")
|
|
|
|
|
|
local conf = require("spec.internal.conf")
|
|
local misc = require("spec.internal.misc")
|
|
|
|
|
|
local strip = require("kong.tools.string").strip
|
|
local splitlines = require("pl.stringx").splitlines
|
|
|
|
|
|
--------------------
|
|
-- Custom assertions
|
|
--
|
|
-- @section assertions
|
|
|
|
|
|
|
|
--- Generic modifier "response".
|
|
-- Will set a "response" value in the assertion state, so following
|
|
-- assertions will operate on the value set.
|
|
-- @function response
|
|
-- @param response_obj results from `http_client:send` function (or any of the
|
|
-- shortcuts `client:get`, `client:post`, etc).
|
|
-- @usage
|
|
-- local res = client:get("/request", { .. request options here ..})
|
|
-- local response_length = assert.response(res).has.header("Content-Length")
|
|
local function modifier_response(state, arguments, level)
|
|
assert(arguments.n > 0,
|
|
"response modifier requires a response object as argument")
|
|
|
|
local res = arguments[1]
|
|
|
|
assert(type(res) == "table" and type(res.read_body) == "function",
|
|
"response modifier requires a response object as argument, got: " .. tostring(res))
|
|
|
|
rawset(state, "kong_response", res)
|
|
rawset(state, "kong_request", nil)
|
|
|
|
return state
|
|
end
|
|
luassert:register("modifier", "response", modifier_response)
|
|
|
|
|
|
--- Generic modifier "request".
|
|
-- Will set a "request" value in the assertion state, so following
|
|
-- assertions will operate on the value set.
|
|
--
|
|
-- The request must be inside a 'response' from the `mock_upstream`. If a request
|
|
-- is send to the `mock_upstream` endpoint `"/request"`, it will echo the request
|
|
-- received in the body of the response.
|
|
-- @function request
|
|
-- @param response_obj results from `http_client:send` function (or any of the
|
|
-- shortcuts `client:get`, `client:post`, etc).
|
|
-- @usage
|
|
-- local res = client:post("/request", {
|
|
-- headers = { ["Content-Type"] = "application/json" },
|
|
-- body = { hello = "world" },
|
|
-- })
|
|
-- local request_length = assert.request(res).has.header("Content-Length")
|
|
local function modifier_request(state, arguments, level)
|
|
local generic = "The assertion 'request' modifier takes a http response"
|
|
.. " object as input to decode the json-body returned by"
|
|
.. " mock_upstream, to retrieve the proxied request."
|
|
|
|
local res = arguments[1]
|
|
|
|
assert(type(res) == "table" and type(res.read_body) == "function",
|
|
"Expected a http response object, got '" .. tostring(res) .. "'. " .. generic)
|
|
|
|
local body, request, err
|
|
body = assert(res:read_body())
|
|
request, err = cjson.decode(body)
|
|
|
|
assert(request, "Expected the http response object to have a json encoded body,"
|
|
.. " but decoding gave error '" .. tostring(err) .. "'. Obtained body: "
|
|
.. body .. "\n." .. generic)
|
|
|
|
|
|
if misc.lookup((res.headers or {}),"X-Powered-By") ~= "mock_upstream" then
|
|
error("Could not determine the response to be from mock_upstream")
|
|
end
|
|
|
|
rawset(state, "kong_request", request)
|
|
rawset(state, "kong_response", nil)
|
|
|
|
return state
|
|
end
|
|
luassert:register("modifier", "request", modifier_request)
|
|
|
|
|
|
--- Generic fail assertion. A convenience function for debugging tests, always
|
|
-- fails. It will output the values it was called with as a table, with an `n`
|
|
-- field to indicate the number of arguments received. See also `intercept`.
|
|
-- @function fail
|
|
-- @param ... any set of parameters to be displayed with the failure
|
|
-- @see intercept
|
|
-- @usage
|
|
-- assert.fail(some, value)
|
|
local function fail(state, args)
|
|
local out = {}
|
|
for k,v in pairs(args) do out[k] = v end
|
|
args[1] = out
|
|
args.n = 1
|
|
return false
|
|
end
|
|
say:set("assertion.fail.negative", [[
|
|
Fail assertion was called with the following parameters (formatted as a table);
|
|
%s
|
|
]])
|
|
luassert:register("assertion", "fail", fail,
|
|
"assertion.fail.negative",
|
|
"assertion.fail.negative")
|
|
|
|
|
|
--- Assertion to check whether a value lives in an array.
|
|
-- @function contains
|
|
-- @param expected The value to search for
|
|
-- @param array The array to search for the value
|
|
-- @param pattern (optional) If truthy, then `expected` is matched as a Lua string
|
|
-- pattern
|
|
-- @return the array index at which the value was found
|
|
-- @usage
|
|
-- local arr = { "one", "three" }
|
|
-- local i = assert.contains("one", arr) --> passes; i == 1
|
|
-- local i = assert.contains("two", arr) --> fails
|
|
-- local i = assert.contains("ee$", arr, true) --> passes; i == 2
|
|
local function contains(state, args)
|
|
local expected, arr, pattern = misc.unpack(args)
|
|
local found
|
|
for i = 1, #arr do
|
|
if (pattern and string.match(arr[i], expected)) or arr[i] == expected then
|
|
found = i
|
|
break
|
|
end
|
|
end
|
|
return found ~= nil, {found}
|
|
end
|
|
say:set("assertion.contains.negative", [[
|
|
Expected array to contain element.
|
|
Expected to contain:
|
|
%s
|
|
]])
|
|
say:set("assertion.contains.positive", [[
|
|
Expected array to not contain element.
|
|
Expected to not contain:
|
|
%s
|
|
]])
|
|
luassert:register("assertion", "contains", contains,
|
|
"assertion.contains.negative",
|
|
"assertion.contains.positive")
|
|
|
|
|
|
local function copy_errlog(errlog_path)
|
|
local file_path = "Unknown path"
|
|
local line_number = "Unknown line"
|
|
local errlog_cache_dir = os.getenv("SPEC_ERRLOG_CACHE_DIR") or "/tmp/kong_errlog_cache"
|
|
|
|
local ok, err = pl_dir.makepath(errlog_cache_dir)
|
|
assert(ok, "makepath failed: " .. tostring(err))
|
|
|
|
local info = debug.getinfo(4, "Sl")
|
|
if info then
|
|
file_path = info.source:gsub("^@", "")
|
|
line_number = info.currentline
|
|
end
|
|
|
|
if string.find(file_path, '/', nil, true) then
|
|
file_path = string.gsub(file_path, '/', '_')
|
|
end
|
|
file_path = errlog_cache_dir .. "/" .. file_path:gsub("%.lua$", "_") .. "line_" .. line_number .. '.log'
|
|
|
|
ok, err = pl_file.copy(errlog_path, file_path)
|
|
if ok then
|
|
print(colors("%{yellow}Log saved as: " .. file_path .. "%{reset}"))
|
|
else
|
|
print(colors("%{red}Failed to save error log for test " .. file_path .. ": " .. err))
|
|
end
|
|
end
|
|
|
|
|
|
--- Assertion to check the status-code of a http response.
|
|
-- @function status
|
|
-- @param expected the expected status code
|
|
-- @param response (optional) results from `http_client:send` function,
|
|
-- alternatively use `response`.
|
|
-- @return the response body as a string, for a json body see `jsonbody`.
|
|
-- @usage
|
|
-- local res = assert(client:send { .. your request params here .. })
|
|
-- local body = assert.has.status(200, res) -- or alternativly
|
|
-- local body = assert.response(res).has.status(200) -- does the same
|
|
local function res_status(state, args)
|
|
assert(not rawget(state, "kong_request"),
|
|
"Cannot check statuscode against a request object,"
|
|
.. " only against a response object")
|
|
|
|
local expected = args[1]
|
|
local res = args[2] or rawget(state, "kong_response")
|
|
|
|
assert(type(expected) == "number",
|
|
"Expected response code must be a number value. Got: " .. tostring(expected))
|
|
assert(type(res) == "table" and type(res.read_body) == "function",
|
|
"Expected a http_client response. Got: " .. tostring(res))
|
|
|
|
if expected ~= res.status then
|
|
local body, err = res:read_body()
|
|
if not body then body = "Error reading body: " .. err end
|
|
table.insert(args, 1, strip(body))
|
|
table.insert(args, 1, res.status)
|
|
table.insert(args, 1, expected)
|
|
args.n = 3
|
|
|
|
if res.status == 500 then
|
|
copy_errlog(conf.nginx_err_logs)
|
|
|
|
-- on HTTP 500, we can try to read the server's error logs
|
|
-- for debugging purposes (very useful for travis)
|
|
local str = pl_file.read(conf.nginx_err_logs)
|
|
if not str then
|
|
return false -- no err logs to read in this prefix
|
|
end
|
|
|
|
local lines_t = splitlines(str)
|
|
local str_t = {}
|
|
-- filter out debugs as they are not usually useful in this context
|
|
for i = 1, #lines_t do
|
|
if not lines_t[i]:match(" %[debug%] ") then
|
|
table.insert(str_t, lines_t[i])
|
|
end
|
|
end
|
|
|
|
local first_line = #str_t - math.min(60, #str_t) + 1
|
|
local msg_t = {"\nError logs (" .. conf.nginx_err_logs .. "), only last 60 non-debug logs are displayed:"}
|
|
for i = first_line, #str_t do
|
|
msg_t[#msg_t+1] = str_t[i]
|
|
end
|
|
|
|
table.insert(args, 4, table.concat(msg_t, "\n"))
|
|
args.n = 4
|
|
end
|
|
|
|
return false
|
|
else
|
|
local body, err = res:read_body()
|
|
local output = body
|
|
if not output then output = "Error reading body: " .. err end
|
|
output = strip(output)
|
|
table.insert(args, 1, output)
|
|
table.insert(args, 1, res.status)
|
|
table.insert(args, 1, expected)
|
|
args.n = 3
|
|
return true, { strip(body) }
|
|
end
|
|
end
|
|
say:set("assertion.res_status.negative", [[
|
|
Invalid response status code.
|
|
Status expected:
|
|
%s
|
|
Status received:
|
|
%s
|
|
Body:
|
|
%s
|
|
%s]])
|
|
say:set("assertion.res_status.positive", [[
|
|
Invalid response status code.
|
|
Status not expected:
|
|
%s
|
|
Status received:
|
|
%s
|
|
Body:
|
|
%s
|
|
%s]])
|
|
luassert:register("assertion", "status", res_status,
|
|
"assertion.res_status.negative", "assertion.res_status.positive")
|
|
luassert:register("assertion", "res_status", res_status,
|
|
"assertion.res_status.negative", "assertion.res_status.positive")
|
|
|
|
|
|
--- Checks and returns a json body of an http response/request. Only checks
|
|
-- validity of the json, does not check appropriate headers. Setting the target
|
|
-- to check can be done through the `request` and `response` modifiers.
|
|
--
|
|
-- For a non-json body, see the `status` assertion.
|
|
-- @function jsonbody
|
|
-- @return the decoded json as a table
|
|
-- @usage
|
|
-- local res = assert(client:send { .. your request params here .. })
|
|
-- local json_table = assert.response(res).has.jsonbody()
|
|
local function jsonbody(state, args)
|
|
assert(args[1] == nil and rawget(state, "kong_request") or rawget(state, "kong_response"),
|
|
"the `jsonbody` assertion does not take parameters. " ..
|
|
"Use the `response`/`require` modifiers to set the target to operate on")
|
|
|
|
if rawget(state, "kong_response") then
|
|
local body = rawget(state, "kong_response"):read_body()
|
|
local json, err = cjson.decode(body)
|
|
if not json then
|
|
table.insert(args, 1, "Error decoding: " .. tostring(err) .. "\nResponse body:" .. body)
|
|
args.n = 1
|
|
return false
|
|
end
|
|
return true, {json}
|
|
|
|
else
|
|
local r = rawget(state, "kong_request")
|
|
if r.post_data
|
|
and (r.post_data.kind == "json" or r.post_data.kind == "json (error)")
|
|
and r.post_data.params
|
|
then
|
|
local pd = r.post_data
|
|
return true, { { params = pd.params, data = pd.text, error = pd.error, kind = pd.kind } }
|
|
|
|
else
|
|
error("No json data found in the request")
|
|
end
|
|
end
|
|
end
|
|
say:set("assertion.jsonbody.negative", [[
|
|
Expected response body to contain valid json. Got:
|
|
%s
|
|
]])
|
|
say:set("assertion.jsonbody.positive", [[
|
|
Expected response body to not contain valid json. Got:
|
|
%s
|
|
]])
|
|
luassert:register("assertion", "jsonbody", jsonbody,
|
|
"assertion.jsonbody.negative",
|
|
"assertion.jsonbody.positive")
|
|
|
|
|
|
--- Asserts that a named header in a `headers` subtable exists.
|
|
-- Header name comparison is done case-insensitive.
|
|
-- @function header
|
|
-- @param name header name to look for (case insensitive).
|
|
-- @see response
|
|
-- @see request
|
|
-- @return value of the header
|
|
-- @usage
|
|
-- local res = client:get("/request", { .. request options here ..})
|
|
-- local resp_header_value = assert.response(res).has.header("Content-Length")
|
|
-- local req_header_value = assert.request(res).has.header("Content-Length")
|
|
local function res_header(state, args)
|
|
local header = args[1]
|
|
local res = args[2] or rawget(state, "kong_request") or rawget(state, "kong_response")
|
|
assert(type(res) == "table" and type(res.headers) == "table",
|
|
"'header' assertion input does not contain a 'headers' subtable")
|
|
local value = misc.lookup(res.headers, header)
|
|
table.insert(args, 1, res.headers)
|
|
table.insert(args, 1, header)
|
|
args.n = 2
|
|
if not value then
|
|
return false
|
|
end
|
|
return true, {value}
|
|
end
|
|
say:set("assertion.res_header.negative", [[
|
|
Expected header:
|
|
%s
|
|
But it was not found in:
|
|
%s
|
|
]])
|
|
say:set("assertion.res_header.positive", [[
|
|
Did not expected header:
|
|
%s
|
|
But it was found in:
|
|
%s
|
|
]])
|
|
luassert:register("assertion", "header", res_header,
|
|
"assertion.res_header.negative",
|
|
"assertion.res_header.positive")
|
|
|
|
|
|
---
|
|
-- An assertion to look for a query parameter in a query string.
|
|
-- Parameter name comparison is done case-insensitive.
|
|
-- @function queryparam
|
|
-- @param name name of the query parameter to look up (case insensitive)
|
|
-- @return value of the parameter
|
|
-- @usage
|
|
-- local res = client:get("/request", {
|
|
-- query = { hello = "world" },
|
|
-- })
|
|
-- local param_value = assert.request(res).has.queryparam("hello")
|
|
local function req_query_param(state, args)
|
|
local param = args[1]
|
|
local req = rawget(state, "kong_request")
|
|
assert(req, "'queryparam' assertion only works with a request object")
|
|
local params
|
|
if type(req.uri_args) == "table" then
|
|
params = req.uri_args
|
|
|
|
else
|
|
error("No query parameters found in request object")
|
|
end
|
|
local value = misc.lookup(params, param)
|
|
table.insert(args, 1, params)
|
|
table.insert(args, 1, param)
|
|
args.n = 2
|
|
if not value then
|
|
return false
|
|
end
|
|
return true, {value}
|
|
end
|
|
say:set("assertion.req_query_param.negative", [[
|
|
Expected query parameter:
|
|
%s
|
|
But it was not found in:
|
|
%s
|
|
]])
|
|
say:set("assertion.req_query_param.positive", [[
|
|
Did not expected query parameter:
|
|
%s
|
|
But it was found in:
|
|
%s
|
|
]])
|
|
luassert:register("assertion", "queryparam", req_query_param,
|
|
"assertion.req_query_param.negative",
|
|
"assertion.req_query_param.positive")
|
|
|
|
|
|
---
|
|
-- Adds an assertion to look for a urlencoded form parameter in a request.
|
|
-- Parameter name comparison is done case-insensitive. Use the `request` modifier to set
|
|
-- the request to operate on.
|
|
-- @function formparam
|
|
-- @param name name of the form parameter to look up (case insensitive)
|
|
-- @return value of the parameter
|
|
-- @usage
|
|
-- local r = assert(proxy_client:post("/request", {
|
|
-- body = {
|
|
-- hello = "world",
|
|
-- },
|
|
-- headers = {
|
|
-- host = "mock_upstream",
|
|
-- ["Content-Type"] = "application/x-www-form-urlencoded",
|
|
-- },
|
|
-- })
|
|
-- local value = assert.request(r).has.formparam("hello")
|
|
-- assert.are.equal("world", value)
|
|
local function req_form_param(state, args)
|
|
local param = args[1]
|
|
local req = rawget(state, "kong_request")
|
|
assert(req, "'formparam' assertion can only be used with a mock_upstream request object")
|
|
|
|
local value
|
|
if req.post_data
|
|
and (req.post_data.kind == "form" or req.post_data.kind == "multipart-form")
|
|
then
|
|
value = misc.lookup(req.post_data.params or {}, param)
|
|
else
|
|
error("Could not determine the request to be from either mock_upstream")
|
|
end
|
|
|
|
table.insert(args, 1, req)
|
|
table.insert(args, 1, param)
|
|
args.n = 2
|
|
if not value then
|
|
return false
|
|
end
|
|
return true, {value}
|
|
end
|
|
say:set("assertion.req_form_param.negative", [[
|
|
Expected url encoded form parameter:
|
|
%s
|
|
But it was not found in request:
|
|
%s
|
|
]])
|
|
say:set("assertion.req_form_param.positive", [[
|
|
Did not expected url encoded form parameter:
|
|
%s
|
|
But it was found in request:
|
|
%s
|
|
]])
|
|
luassert:register("assertion", "formparam", req_form_param,
|
|
"assertion.req_form_param.negative",
|
|
"assertion.req_form_param.positive")
|
|
|
|
|
|
---
|
|
-- Assertion to ensure a value is greater than a base value.
|
|
-- @function is_gt
|
|
-- @param base the base value to compare against
|
|
-- @param value the value that must be greater than the base value
|
|
local function is_gt(state, arguments)
|
|
local expected = arguments[1]
|
|
local value = arguments[2]
|
|
|
|
arguments[1] = value
|
|
arguments[2] = expected
|
|
|
|
return value > expected
|
|
end
|
|
say:set("assertion.gt.negative", [[
|
|
Given value (%s) should be greater than expected value (%s)
|
|
]])
|
|
say:set("assertion.gt.positive", [[
|
|
Given value (%s) should not be greater than expected value (%s)
|
|
]])
|
|
luassert:register("assertion", "gt", is_gt,
|
|
"assertion.gt.negative",
|
|
"assertion.gt.positive")
|
|
|
|
|
|
---
|
|
-- Matcher to ensure a value is greater than a base value.
|
|
-- @function is_gt_matcher
|
|
-- @param base the base value to compare against
|
|
-- @param value the value that must be greater than the base value
|
|
local function is_gt_matcher(state, arguments)
|
|
local expected = arguments[1]
|
|
return function(value)
|
|
return value > expected
|
|
end
|
|
end
|
|
luassert:register("matcher", "gt", is_gt_matcher)
|
|
|
|
|
|
--- Generic modifier "certificate".
|
|
-- Will set a "certificate" value in the assertion state, so following
|
|
-- assertions will operate on the value set.
|
|
-- @function certificate
|
|
-- @param cert The cert text
|
|
-- @see cn
|
|
-- @usage
|
|
-- assert.certificate(cert).has.cn("ssl-example.com")
|
|
local function modifier_certificate(state, arguments, level)
|
|
local generic = "The assertion 'certficate' modifier takes a cert text"
|
|
.. " as input to validate certificate parameters"
|
|
.. " against."
|
|
local cert = arguments[1]
|
|
assert(type(cert) == "string",
|
|
"Expected a certificate text, got '" .. tostring(cert) .. "'. " .. generic)
|
|
rawset(state, "kong_certificate", cert)
|
|
return state
|
|
end
|
|
luassert:register("modifier", "certificate", modifier_certificate)
|
|
|
|
|
|
--- Assertion to check whether a CN is matched in an SSL cert.
|
|
-- @function cn
|
|
-- @param expected The CN value
|
|
-- @param cert The cert text
|
|
-- @return the CN found in the cert
|
|
-- @see certificate
|
|
-- @usage
|
|
-- assert.cn("ssl-example.com", cert)
|
|
--
|
|
-- -- alternative:
|
|
-- assert.certificate(cert).has.cn("ssl-example.com")
|
|
local function assert_cn(state, args)
|
|
local expected = args[1]
|
|
if args[2] and rawget(state, "kong_certificate") then
|
|
error("assertion 'cn' takes either a 'certificate' modifier, or 2 parameters, not both")
|
|
end
|
|
local cert = args[2] or rawget(state, "kong_certificate")
|
|
local cn = string.match(cert, "CN%s*=%s*([^%s,]+)")
|
|
args[2] = cn or "(CN not found in certificate)"
|
|
args.n = 2
|
|
return cn == expected
|
|
end
|
|
say:set("assertion.cn.negative", [[
|
|
Expected certificate to have the given CN value.
|
|
Expected CN:
|
|
%s
|
|
Got instead:
|
|
%s
|
|
]])
|
|
say:set("assertion.cn.positive", [[
|
|
Expected certificate to not have the given CN value.
|
|
Expected CN to not be:
|
|
%s
|
|
Got instead:
|
|
%s
|
|
]])
|
|
luassert:register("assertion", "cn", assert_cn,
|
|
"assertion.cn.negative",
|
|
"assertion.cn.positive")
|
|
|
|
|
|
do
|
|
--- Generic modifier "logfile"
|
|
-- Will set an "errlog_path" value in the assertion state.
|
|
-- @function logfile
|
|
-- @param path A path to the log file (defaults to the test prefix's
|
|
-- errlog).
|
|
-- @see line
|
|
-- @see clean_logfile
|
|
-- @usage
|
|
-- assert.logfile("./my/logfile.log").has.no.line("[error]", true)
|
|
local function modifier_errlog(state, args)
|
|
local errlog_path = args[1] or conf.nginx_err_logs
|
|
|
|
assert(type(errlog_path) == "string", "logfile modifier expects nil, or " ..
|
|
"a string as argument, got: " ..
|
|
type(errlog_path))
|
|
|
|
rawset(state, "errlog_path", errlog_path)
|
|
|
|
return state
|
|
end
|
|
|
|
luassert:register("modifier", "errlog", modifier_errlog) -- backward compat
|
|
luassert:register("modifier", "logfile", modifier_errlog)
|
|
|
|
local function substr(subject, pattern)
|
|
if subject:find(pattern, nil, true) ~= nil then
|
|
return subject
|
|
end
|
|
end
|
|
|
|
local function re_match(subject, pattern)
|
|
local pos, _, err = ngx.re.find(subject, pattern, "oj")
|
|
if err then
|
|
error(("invalid regex provided to logfile assertion %q: %s")
|
|
:format(pattern, err), 5)
|
|
end
|
|
|
|
if pos then
|
|
return subject
|
|
end
|
|
end
|
|
|
|
local function find_in_file(fpath, pattern, matcher)
|
|
local fh = assert(io.open(fpath, "r"))
|
|
local found
|
|
|
|
for line in fh:lines() do
|
|
if matcher(line, pattern) then
|
|
found = line
|
|
break
|
|
end
|
|
end
|
|
|
|
fh:close()
|
|
|
|
return found
|
|
end
|
|
|
|
|
|
--- Assertion checking if any line from a file matches the given regex or
|
|
-- substring.
|
|
-- @function line
|
|
-- @param regex The regex to evaluate against each line.
|
|
-- @param plain If true, the regex argument will be considered as a plain
|
|
-- string.
|
|
-- @param timeout An optional timeout after which the assertion will fail if
|
|
-- reached.
|
|
-- @param fpath An optional path to the file (defaults to the filelog
|
|
-- modifier)
|
|
-- @see logfile
|
|
-- @see clean_logfile
|
|
-- @usage
|
|
-- helpers.clean_logfile()
|
|
--
|
|
-- -- run some tests here
|
|
--
|
|
-- assert.logfile().has.no.line("[error]", true)
|
|
local function match_line(state, args)
|
|
local regex = args[1]
|
|
local plain = args[2]
|
|
local timeout = args[3] or 2
|
|
local fpath = args[4] or rawget(state, "errlog_path")
|
|
|
|
assert(type(regex) == "string",
|
|
"Expected the regex argument to be a string")
|
|
assert(type(fpath) == "string",
|
|
"Expected the file path argument to be a string")
|
|
assert(type(timeout) == "number" and timeout >= 0,
|
|
"Expected the timeout argument to be a number >= 0")
|
|
|
|
|
|
local matcher = plain and substr or re_match
|
|
|
|
local found = find_in_file(fpath, regex, matcher)
|
|
local deadline = ngx.now() + timeout
|
|
|
|
while not found and ngx.now() <= deadline do
|
|
ngx.sleep(0.05)
|
|
found = find_in_file(fpath, regex, matcher)
|
|
end
|
|
|
|
args[1] = fpath
|
|
args[2] = regex
|
|
args.n = 2
|
|
|
|
if found then
|
|
args[3] = found
|
|
args.n = 3
|
|
end
|
|
|
|
return found
|
|
end
|
|
|
|
say:set("assertion.match_line.negative", misc.unindent [[
|
|
Expected file at:
|
|
%s
|
|
To match:
|
|
%s
|
|
]])
|
|
say:set("assertion.match_line.positive", misc.unindent [[
|
|
Expected file at:
|
|
%s
|
|
To not match:
|
|
%s
|
|
But matched line:
|
|
%s
|
|
]])
|
|
luassert:register("assertion", "line", match_line,
|
|
"assertion.match_line.negative",
|
|
"assertion.match_line.positive")
|
|
end
|
|
|
|
|
|
--- Assertion to check whether a string matches a regular expression
|
|
-- @function match_re
|
|
-- @param string the string
|
|
-- @param regex the regular expression
|
|
-- @return true or false
|
|
-- @usage
|
|
-- assert.match_re("foobar", [[bar$]])
|
|
--
|
|
|
|
local function match_re(_, args)
|
|
local string = args[1]
|
|
local regex = args[2]
|
|
assert(type(string) == "string",
|
|
"Expected the string argument to be a string")
|
|
assert(type(regex) == "string",
|
|
"Expected the regex argument to be a string")
|
|
local from, _, err = ngx.re.find(string, regex)
|
|
if err then
|
|
error(err)
|
|
end
|
|
if from then
|
|
table.insert(args, 1, string)
|
|
table.insert(args, 1, regex)
|
|
args.n = 2
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
say:set("assertion.match_re.negative", misc.unindent [[
|
|
Expected log:
|
|
%s
|
|
To match:
|
|
%s
|
|
]])
|
|
say:set("assertion.match_re.positive", misc.unindent [[
|
|
Expected log:
|
|
%s
|
|
To not match:
|
|
%s
|
|
But matched line:
|
|
%s
|
|
]])
|
|
luassert:register("assertion", "match_re", match_re,
|
|
"assertion.match_re.negative",
|
|
"assertion.match_re.positive")
|
|
|
|
|
|
---
|
|
-- Assertion to partially compare two lua tables.
|
|
-- @function partial_match
|
|
-- @param partial_table the table with subset of fields expect to match
|
|
-- @param full_table the full table that should contain partial_table and potentially other fields
|
|
local function partial_match(state, arguments)
|
|
|
|
local function deep_matches(t1, t2, parent_keys)
|
|
for key, v in pairs(t1) do
|
|
local compound_key = (parent_keys and parent_keys .. "." .. key) or key
|
|
if type(v) == "table" then
|
|
local ok, compound_key, v1, v2 = deep_matches(t1[key], t2[key], compound_key)
|
|
if not ok then
|
|
return ok, compound_key, v1, v2
|
|
end
|
|
else
|
|
if (state.mod == true and t1[key] ~= t2[key]) or (state.mod == false and t1[key] == t2[key]) then
|
|
return false, compound_key, t1[key], t2[key]
|
|
end
|
|
end
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
local partial_table = arguments[1]
|
|
local full_table = arguments[2]
|
|
|
|
local ok, compound_key, v1, v2 = deep_matches(partial_table, full_table)
|
|
|
|
if not ok then
|
|
arguments[1] = compound_key
|
|
arguments[2] = v1
|
|
arguments[3] = v2
|
|
arguments.n = 3
|
|
|
|
return not state.mod
|
|
end
|
|
|
|
return state.mod
|
|
end
|
|
|
|
say:set("assertion.partial_match.negative", [[
|
|
Values at key %s should not be equal
|
|
]])
|
|
say:set("assertion.partial_match.positive", [[
|
|
Values at key %s should be equal but are not.
|
|
Expected: %s, given: %s
|
|
]])
|
|
luassert:register("assertion", "partial_match", partial_match,
|
|
"assertion.partial_match.positive",
|
|
"assertion.partial_match.negative")
|
|
|
|
|
|
-- the same behivor with other modules
|
|
return true
|