diff options
context:
space:
mode:
authorBryce Kwon <bryce@brycekwon.com>
committerBryce Kwon <bryce@brycekwon.com>
commit
parent
tree
download
Add a test suite for the shipped extensions
Diffstat (limited to 'tests/extensions/harness.lua')
-rw-r--r--tests/extensions/harness.lua268
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 = { ["&"] = "&amp;", ["<"] = "&lt;", [">"] = "&gt;" }
+local attr_map = {
+ ["&"] = "&amp;", ["<"] = "&lt;", [">"] = "&gt;",
+ ["'"] = "&#x27;", ['"'] = "&quot;",
+}
+
+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