-- SPDX-FileCopyrightText: © 2022—2026 Vladimir Zorin -- SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later -- Licensed under OWL v1.0+. See LICENSE. -- Exercises the router directly against a fixture registry, calling the -- handler with the http.server argument convention: no server involved. local testimony = require("testimony") local fixture = require("tests.fixture") local repos = require("lilgit.repos") local render = require("lilgit.render") local handle = require("lilgit.handle") local http_git = require("http.git") local testify = testimony.new("== lilgit.handle ==") if not fixture.has_git() then print("git binary not available, skipping handle_spec") testify:conclude() return end local fx = fixture.make() assert(fx, "failed to build git fixture") local cfg = { site_title = "test forge", repos = {}, templates = {} } local registry = repos.discover({ repos_dir = fx.root, repos = {} }, nil) local git_handle = assert(http_git.serve(registry.prefix_map)) local handler = handle.new({ cfg = cfg, registry = registry, git_handle = git_handle, version = "test-version", templates = render.build_templates(cfg), }) local get = function(path, headers) return handler("GET", path, "", headers or {}, nil, {}) end local b = fx.branch testify:that("index lists repositories", function() local content, status, headers = get("/") testimony.assert_equal(200, status) testimony.assert_equal("text/html", headers["content-type"]) testimony.assert_match("/fix/", content) testimony.assert_match("/bare/", content) testimony.assert_equal("no-cache", headers["cache-control"]) end) testify:that("repo root redirects to the default branch tree", function() for _, path in ipairs({ "/fix", "/fix/" }) do local _, status, headers = get(path) testimony.assert_equal(302, status) testimony.assert_equal("/fix/tree/" .. b .. "/", headers.location) end end) testify:that("tree without trailing slash 301s to the canonical form", function() local _, status, headers = get("/fix/tree/" .. b) testimony.assert_equal(301, status) testimony.assert_equal("/fix/tree/" .. b .. "/", headers.location) end) testify:that("tree page lists entries and renders the README below", function() local content, status, headers = get("/fix/tree/" .. b .. "/") testimony.assert_equal(200, status) testimony.assert_match("src", content) testimony.assert_match("Fixture Repo", content) testimony.assert_match("Some intro text", content) testimony.assert_equal('"' .. fx.head .. '"', headers.etag) testimony.assert_equal("no-cache", headers["cache-control"]) end) testify:that("slashed branch resolves via longest-ref-first", function() local content, status = get("/fix/tree/feature/foo/") testimony.assert_equal(200, status) testimony.assert_match("feature%.txt", content) end) testify:that("tree URL naming a blob 302s to the blob route", function() local _, status, headers = get("/fix/tree/" .. b .. "/README.md") testimony.assert_equal(302, status) testimony.assert_equal("/fix/blob/" .. b .. "/README.md", headers.location) end) testify:that("blob URL naming a tree 302s to the tree route", function() local _, status, headers = get("/fix/blob/" .. b .. "/assets") testimony.assert_equal(302, status) testimony.assert_equal("/fix/tree/" .. b .. "/assets/", headers.location) end) testify:that("markdown blob renders as an HTML page", function() local content, status, headers = get("/fix/blob/" .. b .. "/README.md") testimony.assert_equal(200, status) testimony.assert_equal("text/html", headers["content-type"]) testimony.assert_match("Fixture Repo", content) testimony.assert_match('href="/fix/raw/' .. b .. '/README%.md"', content) testimony.assert_equal('"' .. fx.head .. '"', headers.etag) end) testify:that("source blob is forced to text/plain", function() local content, status, headers = get("/fix/blob/" .. b .. "/src/main.lua") testimony.assert_equal(200, status) testimony.assert_equal("text/plain; charset=utf-8", headers["content-type"]) testimony.assert_match("hello from fixture", content) end) testify:that("image blob passes through raw and byte-identical to /raw/", function() local blob_content, blob_status, blob_headers = get("/fix/blob/" .. b .. "/assets/logo.png") local raw_content, raw_status, raw_headers = get("/fix/raw/" .. b .. "/assets/logo.png") testimony.assert_equal(200, blob_status) testimony.assert_equal(200, raw_status) testimony.assert_equal("image/png", blob_headers["content-type"]) testimony.assert_equal("image/png", raw_headers["content-type"]) testimony.assert_true(blob_content == raw_content) end) testify:that("unknown extension without NUL bytes sniffs to text/plain on blob", function() local _, _, headers = get("/fix/blob/" .. b .. "/plain.xyz") testimony.assert_equal("text/plain; charset=utf-8", headers["content-type"]) local _, _, raw_headers = get("/fix/raw/" .. b .. "/plain.xyz") testimony.assert_equal("application/octet-stream", raw_headers["content-type"]) end) testify:that("symlink blob is served as its target path text, not rendered", function() local content, _, headers = get("/fix/blob/" .. b .. "/link.md") testimony.assert_equal("text/plain; charset=utf-8", headers["content-type"]) testimony.assert_equal("README.md", content) end) testify:that("If-None-Match yields 304 with the same ETag", function() local _, _, headers = get("/fix/blob/" .. b .. "/src/main.lua") local etag = headers.etag for _, inm in ipairs({ etag, "W/" .. etag, '"other", ' .. etag, "*" }) do local content, status, h2 = get("/fix/blob/" .. b .. "/src/main.lua", { ["if-none-match"] = inm }) testimony.assert_equal(304, status) testimony.assert_equal("", content) testimony.assert_equal(etag, h2.etag) end local _, fresh_status = get("/fix/blob/" .. b .. "/src/main.lua", { ["if-none-match"] = '"stale"' }) testimony.assert_equal(200, fresh_status) end) testify:that("HEAD returns headers with an empty body", function() local content, status, headers = handler("HEAD", "/fix/tree/" .. b .. "/", "", {}, nil, {}) testimony.assert_equal(200, status) testimony.assert_equal("", content) testimony.assert_equal('"' .. fx.head .. '"', headers.etag) end) testify:that("sha-pinned URLs are cached as immutable", function() local _, status, headers = get("/fix/tree/" .. fx.head .. "/") testimony.assert_equal(200, status) testimony.assert_equal("public, max-age=31536000, immutable", headers["cache-control"]) end) testify:that("tag ref resolves and stays revalidatable", function() local _, status, headers = get("/fix/tree/v1.0/") testimony.assert_equal(200, status) testimony.assert_equal("no-cache", headers["cache-control"]) testimony.assert_equal('"' .. fx.tag_commit .. '"', headers.etag) end) testify:that(".git-suffixed repo segment browses the same repo", function() local _, status = get("/fix.git/tree/" .. b .. "/") testimony.assert_equal(200, status) end) testify:that("assets are served with long-lived caching", function() local content, status, headers = get("/_/style.css") testimony.assert_equal(200, status) testimony.assert_equal("text/css", headers["content-type"]) testimony.assert_equal("public, max-age=86400", headers["cache-control"]) testimony.assert_equal('"test-version"', headers.etag) testimony.assert_match("font%-family", content) local _, missing = get("/_/nope.js") testimony.assert_equal(404, missing) end) testify:that("smart-HTTP advertisement is delegated", function() local content, status, headers = handler("GET", "/fix/info/refs", "service=git-upload-pack", {}, nil, {}) testimony.assert_equal(200, status) testimony.assert_equal("application/x-git-upload-pack-advertisement", headers["content-type"]) testimony.assert_equal("001e# service=git-upload-pack\n", content:sub(1, 30)) local _, dumb_status = handler("GET", "/fix/info/refs", "", {}, nil, {}) testimony.assert_equal(403, dumb_status) local _, push_status = handler("POST", "/fix/git-receive-pack", "", {}, "", {}) testimony.assert_equal(403, push_status) local _, wrong_method = handler("GET", "/fix/git-upload-pack", "", {}, nil, {}) testimony.assert_equal(405, wrong_method) end) testify:that("a file path ending in info/refs is served by the UI, not swallowed", function() -- No such file in the fixture, but the route must fall through to -- ref resolution (404), not to the git handler's 403/404 behavior. local content, status = get("/fix/blob/" .. b .. "/info/refs") testimony.assert_equal(404, status) testimony.assert_equal("Not Found", content) end) testify:that("bad methods get 405 with an allow header", function() local _, status, headers = handler("POST", "/fix/tree/" .. b .. "/", "", {}, "", {}) testimony.assert_equal(405, status) testimony.assert_equal("GET, HEAD", headers.allow) local _, index_status = handler("DELETE", "/", "", {}, nil, {}) testimony.assert_equal(405, index_status) end) testify:that("traversal and unknown targets 404", function() local paths = { "/nope/tree/" .. b .. "/", "/fix/tree/no-such-ref/", "/fix/blob/" .. b .. "/../README.md", "/fix/blob/" .. b .. "/no-such-file", "/fix/refs", "/fix/log/" .. b .. "/", } for _, path in ipairs(paths) do local _, status = get(path) testimony.assert_equal(404, status, "expected 404 for " .. path) end end) fixture.destroy(fx) testify:conclude()