------------------------------------------------------------------ -- 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 -- miscellaneous local pl_path = require("pl.path") local pl_dir = require("pl.dir") local pkey = require("resty.openssl.pkey") local nginx_signals = require("kong.cmd.utils.nginx_signals") local shell = require("spec.internal.shell") local CONSTANTS = require("spec.internal.constants") local sys = require("spec.internal.sys") local private_node = require "kong.pdk.private.node" local pack = function(...) return { n = select("#", ...), ... } end local unpack = function(t) return unpack(t, 1, t.n) end --- Prints all returned parameters. -- Simple debugging aid, it will pass all received parameters, hence will not -- influence the flow of the code. See also `fail`. -- @function intercept -- @see fail -- @usage -- modify -- local a,b = some_func(c,d) -- -- into -- local a,b = intercept(some_func(c,d)) local function intercept(...) local args = pack(...) print(require("pl.pretty").write(args)) return unpack(args) end --- Returns the OpenResty version. -- Extract the current OpenResty version in use and returns -- a numerical representation of it. -- Ex: `1.11.2.2` -> `11122` -- @function openresty_ver_num local function openresty_ver_num() local nginx_bin = assert(nginx_signals.find_nginx_bin()) local _, _, stderr = shell.run(string.format("%s -V", nginx_bin), nil, 0) local a, b, c, d = string.match(stderr or "", "openresty/(%d+)%.(%d+)%.(%d+)%.(%d+)") if not a then error("could not execute 'nginx -V': " .. stderr) end return tonumber(a .. b .. c .. d) end --- Unindent a multi-line string for proper indenting in -- square brackets. -- @function unindent -- @usage -- local u = helpers.unindent -- -- u[[ -- hello world -- foo bar -- ]] -- -- -- will return: "hello world\nfoo bar" local function unindent(str, concat_newlines, spaced_newlines) str = string.match(str, "(.-%S*)%s*$") if not str then return "" end local level = math.huge local prefix = "" local len str = str:match("^%s") and "\n" .. str or str for pref in str:gmatch("\n(%s+)") do len = #prefix if len < level then level = len prefix = pref end end local repl = concat_newlines and "" or "\n" repl = spaced_newlines and " " or repl return (str:gsub("^\n%s*", ""):gsub("\n" .. prefix, repl):gsub("\n$", ""):gsub("\\r", "\r")) end --- Write a yaml file. -- @function make_yaml_file -- @param content (string) the yaml string to write to the file, if omitted the -- current database contents will be written using `kong config db_export`. -- @param filename (optional) if not provided, a temp name will be created -- @return filename of the file written local function make_yaml_file(content, filename) local filename = filename or pl_path.tmpname() .. ".yml" if content then local fd = assert(io.open(filename, "w")) assert(fd:write(unindent(content))) assert(fd:write("\n")) -- ensure last line ends in newline assert(fd:close()) else assert(shell.kong_exec("config db_export --conf "..CONSTANTS.TEST_CONF_PATH.." "..filename)) end return filename end local deep_sort do local function deep_compare(a, b) if a == nil then a = "" end if b == nil then b = "" end deep_sort(a) deep_sort(b) if type(a) ~= type(b) then return type(a) < type(b) end if type(a) == "table" then return deep_compare(a[1], b[1]) end -- compare cjson.null or ngx.null if type(a) == "userdata" and type(b) == "userdata" then return false end return a < b end deep_sort = function(t) if type(t) == "table" then for _, v in pairs(t) do deep_sort(v) end table.sort(t, deep_compare) end return t end end --- Generate asymmetric keys -- @function generate_keys -- @param fmt format to receive the public and private pair -- @param typ (optional) the type of key to generate, default: RSA -- @return `pub, priv` key tuple or `nil + err` on failure local function generate_keys(fmt, typ) fmt = string.upper(fmt) or "JWK" typ = typ and string.upper(typ) or "RSA" local key, err -- only support RSA and EC for now if typ == "RSA" then key, err = pkey.new({ type = 'RSA', bits = 2048, exp = 65537 }) elseif typ == "EC" then key, err = pkey.new({ type = 'EC', curve = 'prime256v1', }) else return nil, "unsupported key type" end assert(key) assert(err == nil, err) local pub = key:tostring("public", fmt) local priv = key:tostring("private", fmt) return pub, priv end -- Case insensitive lookup function, returns the value and the original key. Or -- if not found nil and the search key -- @usage -- sample usage -- local test = { SoMeKeY = 10 } -- print(lookup(test, "somekey")) --> 10, "SoMeKeY" -- print(lookup(test, "NotFound")) --> nil, "NotFound" local function lookup(t, k) local ok = k if type(k) ~= "string" then return t[k], k else k = k:lower() end for key, value in pairs(t) do if tostring(key):lower() == k then return value, key end end return nil, ok end local function with_current_ws(ws,fn, db) local old_ws = ngx.ctx.workspace ngx.ctx.workspace = nil ws = ws or {db.workspaces:select_by_name("default")} ngx.ctx.workspace = ws[1] and ws[1].id local res = fn() ngx.ctx.workspace = old_ws return res end local make_temp_dir do local seeded = false function make_temp_dir() if not seeded then ngx.update_time() math.randomseed(ngx.worker.pid() + ngx.now()) seeded = true end local tmp local ok, err local tries = 1000 for _ = 1, tries do local name = "/tmp/.kong-test" .. math.random() ok, err = pl_path.mkdir(name) if ok then tmp = name break end end assert(tmp ~= nil, "failed to create temporary directory " .. "after " .. tostring(tries) .. " tries, " .. "last error: " .. tostring(err)) return tmp, function() pl_dir.rmtree(tmp) end end end -- This function is used for plugin compatibility test. -- It will use the old version plugin by including the path of the old plugin -- at the first of LUA_PATH. -- The return value is a function which when called will recover the original -- LUA_PATH and remove the temporary directory if it exists. -- For an example of how to use it, please see: -- plugins-ee/rate-limiting-advanced/spec/06-old-plugin-compatibility_spec.lua -- spec/03-plugins/03-http-log/05-old-plugin-compatibility_spec.lua local function use_old_plugin(name) assert(type(name) == "string", "must specify the plugin name") local old_plugin_path local temp_dir if pl_path.exists(CONSTANTS.OLD_VERSION_KONG_PATH .. "/kong/plugins/" .. name) then -- only include the path of the specified plugin into LUA_PATH -- and keep the directory structure 'kong/plugins/...' temp_dir = make_temp_dir() old_plugin_path = temp_dir local dest_dir = old_plugin_path .. "/kong/plugins" assert(pl_dir.makepath(dest_dir), "failed to makepath " .. dest_dir) assert(shell.run("cp -r " .. CONSTANTS.OLD_VERSION_KONG_PATH .. "/kong/plugins/" .. name .. " " .. dest_dir), "failed to copy the plugin directory") else error("the specified plugin " .. name .. " doesn't exist") end local origin_lua_path = os.getenv("LUA_PATH") -- put the old plugin path at first assert(sys.setenv("LUA_PATH", old_plugin_path .. "/?.lua;" .. old_plugin_path .. "/?/init.lua;" .. origin_lua_path), "failed to set LUA_PATH env") return function () sys.setenv("LUA_PATH", origin_lua_path) if temp_dir then pl_dir.rmtree(temp_dir) end end end -- Timer repatching -- this makes `kong` introduced by `spec.internal.db` visible to the timer -- however it breaks some other tests, so you need to undo it after the test local repatch_timer, unrepatch_timer do local original_at = ngx.timer.at local original_every = ngx.timer.every local original_timer = kong and kong.timer local repatched = false function repatch_timer() local _timerng _timerng = require("resty.timerng").new({ min_threads = 16, max_threads = 32, }) _timerng:start() _G.timerng = _timerng _G.ngx.timer.at = function (delay, callback, ...) return _timerng:at(delay, callback, ...) end _G.ngx.timer.every = function (interval, callback, ...) return _timerng:every(interval, callback, ...) end if kong then kong.timer = _timerng end repatched = true end function unrepatch_timer() if not repatched then return end _G.ngx.timer.at = original_at _G.ngx.timer.every = original_every if kong then kong.timer = original_timer end _G.timerng:destroy() _G.timerng = nil repatched = false end end local function patch_worker_events() if not kong then return end if kong.worker_events then return end kong.worker_events = require("resty.events.compat") kong.worker_events.configure({ listening = "unix:", testing = true, }) end return { pack = pack, unpack = unpack, intercept = intercept, openresty_ver_num = openresty_ver_num(), unindent = unindent, make_yaml_file = make_yaml_file, setenv = sys.setenv, unsetenv = sys.unsetenv, deep_sort = deep_sort, generate_keys = generate_keys, lookup = lookup, with_current_ws = with_current_ws, make_temp_dir = make_temp_dir, use_old_plugin = use_old_plugin, get_node_id = private_node.load_node_id, repatch_timer = repatch_timer, unrepatch_timer = unrepatch_timer, patch_worker_events = patch_worker_events, }