-- 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