diff options
Diffstat (limited to 'tests/extensions/harness.lua')
| -rw-r--r-- | tests/extensions/harness.lua | 268 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
1 file changed, 268 insertions, 0 deletions
diff --git a/tests/extensions/harness.lua b/tests/extensions/harness.lua new file mode 100644 index 0000000..9085010 --- /dev/null +++ b/tests/extensions/harness.lua @@ -0,0 +1,268 @@ +-- Stand-in for cgit's Lua filter host, dofiled by the test-*.lua files beside +-- it and returning a table of helpers. It provides the html output sinks with +-- the same escaping source/html.c performs, collects everything a filter +-- emits so a check can look at the finished piece of page, and carries the +-- check bookkeeping whose result the t05xx scripts read as the exit code. + +local harness = {} + +local pieces = {} + +-- cgit hands a sink's argument to lua_tostring, which turns a number into +-- digits and anything else into no output at all, and the C side then +-- measures the string with strlen, so everything from the first NUL byte on +-- is dropped. Both behaviours are kept here because the extensions document +-- the truncation and a test has to prove it rather than pass the bytes +-- through. +local function sink(escape) + return function(value) + if type(value) == "number" then + value = tostring(value) + elseif type(value) ~= "string" then + return + end + local nul = value:find("\0", 1, true) + if nul then + value = value:sub(1, nul - 1) + end + if escape then + value = escape(value) + end + pieces[#pieces + 1] = value + end +end + +local txt_map = { ["&"] = "&", ["<"] = "<", [">"] = ">" } +local attr_map = { + ["&"] = "&", ["<"] = "<", [">"] = ">", + ["'"] = "'", ['"'] = """, +} + +html = sink(nil) +html_txt = sink(function(s) + return (s:gsub("[&<>]", txt_map)) +end) +html_attr = sink(function(s) + return (s:gsub("[&<>'\"]", attr_map)) +end) + +-- No shipped extension calls the remaining sinks, so rather than risk an +-- unfaithful copy quietly passing a test, using one fails loudly until its +-- escaping is mirrored from source/html.c the way the three above are. +local function unmirrored(name) + return function() + error(name .. " is not mirrored by the test harness yet") + end +end + +html_url_path = unmirrored("html_url_path") +html_url_arg = unmirrored("html_url_arg") +html_include = unmirrored("html_include") + +function harness.reset() + pieces = {} +end + +function harness.output() + return table.concat(pieces) +end + +function harness.load(script) + dofile(script) +end + +local unpack_args = unpack or table.unpack + +-- One whole filter round trip, an open with the given arguments, a write per +-- string and the close, returning the collected output and the close's +-- answer. +function harness.run(args, writes) + harness.reset() + filter_open(unpack_args(args or {})) + for _, text in ipairs(writes or {}) do + filter_write(text) + end + local ret = filter_close() + return harness.output(), ret +end + +-- Loaders registered here win over any real module on the package path, so a +-- test can hand a script a deterministic stand-in, or with a failing loader +-- prove the script's fallback for a module that is not installed. +function harness.preload(name, module) + package.preload[name] = function() + return module + end +end + +function harness.preload_failure(name) + package.preload[name] = function() + error(name .. " deliberately unavailable in this test") + end +end + +-- Redirect chosen absolute paths to fixtures in the test directory, for the +-- scripts that read configuration from fixed locations like /etc. +local path_map = nil + +function harness.redirect_file(from, to) + if path_map == nil then + path_map = {} + local real_open = io.open + io.open = function(path, mode) + return real_open(path_map[path] or path, mode) + end + end + path_map[from] = to +end + +-- Deterministic bytes standing in for a digest, built from djb2 style lanes +-- in plain arithmetic so they compute the same on every Lua version. Not +-- remotely cryptographic, and enough for what the checks assert, that equal +-- input hashes equal, different input hashes different and a tampered +-- payload no longer verifies. +local function fake_digest_bytes(text) + local lanes = { 5381, 52711, 1313, 7919 } + for i = 1, #text do + local byte = text:byte(i) + for j = 1, 4 do + lanes[j] = (lanes[j] * 33 + byte + j) % 4294967296 + end + end + local bytes = {} + for j = 1, 4 do + local value = lanes[j] + for _ = 1, 4 do + bytes[#bytes + 1] = string.char(value % 256) + value = math.floor(value / 256) + end + end + return table.concat(bytes) +end + +harness.fake_digest_bytes = fake_digest_bytes + +-- The slice of the luaossl digest interface the avatar filters use. +function harness.stub_digest() + harness.preload("openssl.digest", { + new = function(algorithm) + return { + final = function(self, text) + return fake_digest_bytes(algorithm .. "\0" .. text) + end, + } + end, + }) +end + +-- crypt(3) reuses the salt fields of the setting it is handed and appends a +-- hash of the password, so this stand-in keeps the fields and appends the +-- password itself. A stored value of the salt fields plus the password then +-- verifies exactly when the password matches, which is all the login checks +-- need. +local function fake_crypt(password, setting) + local prefix = setting:match("^(%$[^$]+%$[^$]+%$[^$]*%$)") + if prefix == nil then + prefix = setting .. "$" + end + return prefix .. password +end + +harness.fake_crypt = fake_crypt + +-- The slices of luaossl and luaposix the auth filters use. The link and +-- unlink stubs serve the secret creation path, which the auth checks bypass +-- by replacing get_secret, so they only have to exist. +function harness.stub_auth_modules() + local rand_counter = 0 + harness.preload("posix.sys.stat", { + umask = function(mask) + return 18 + end, + }) + harness.preload("posix.unistd", { + crypt = fake_crypt, + link = function(from, to) + return nil + end, + unlink = function(path) + os.remove(path) + return 0 + end, + }) + harness.preload("openssl.rand", { + bytes = function(count) + local out = {} + for i = 1, count do + rand_counter = rand_counter + 1 + out[i] = string.char((rand_counter * 37 + 11) % 256) + end + return table.concat(out) + end, + }) + harness.preload("openssl.hmac", { + new = function(key, algorithm) + return { + final = function(self, payload) + return fake_digest_bytes(key .. "\1" .. + algorithm .. "\1" .. payload) + end, + } + end, + }) +end + +local checks = 0 +local failures = 0 + +local function fail(name, detail) + failures = failures + 1 + io.write("failed check ", name, "\n") + if detail then + io.write(detail, "\n") + end +end + +function harness.check(name, ok, detail) + checks = checks + 1 + if not ok then + fail(name, detail) + end +end + +function harness.equals(name, got, want) + checks = checks + 1 + if got ~= want then + fail(name, "wanted " .. tostring(want) .. + "\n got " .. tostring(got)) + end +end + +function harness.contains(name, haystack, needle) + checks = checks + 1 + if type(haystack) ~= "string" + or not haystack:find(needle, 1, true) then + fail(name, "wanted " .. needle .. + "\nwithin " .. tostring(haystack)) + end +end + +function harness.excludes(name, haystack, needle) + checks = checks + 1 + if type(haystack) ~= "string" + or haystack:find(needle, 1, true) then + fail(name, "did not want " .. needle .. + "\nwithin " .. tostring(haystack)) + end +end + +function harness.finish() + if failures > 0 then + io.write(failures, " of ", checks, " checks failed\n") + os.exit(1) + end + io.write("passed ", checks, " checks\n") + os.exit(0) +end + +return harness |
