-- SPDX-FileCopyrightText: © 2022—2026 Vladimir Zorin -- SPDX-License-Identifier: LicenseRef-OWL-1.0-or-later -- Licensed under OWL v1.0+. See LICENSE. --[[ End-to-end smoke test: forks a real lilgit server over the fixture and drives it with the git and curl binaries. Gated behind LILGIT_E2E=1 because it needs network loopback, curl, and a free port. ]] local testimony = require("testimony") local std = require("std") local fixture = require("tests.fixture") local lilgit = require("lilgit") local testify = testimony.new("== lilgit e2e ==") if not os.getenv("LILGIT_E2E") then print("LILGIT_E2E not set, skipping e2e_spec") testify:conclude() return end if not fixture.has_git() or not fixture.sh("curl --version") then print("git or curl not available, skipping e2e_spec") testify:conclude() return end local fx = fixture.make() assert(fx, "failed to build git fixture") local port = 18000 + os.time() % 2000 local base = "http://127.0.0.1:" .. port local child = std.ps.fork() assert(child >= 0, "fork failed") if child == 0 then local app, err = lilgit.new({ ip = "127.0.0.1", port = port, repos_dir = fx.root, log_level = 100, auth = { users = { alice = "hunter2" }, private = { "cat2/repo1" } }, }) if not app then print("e2e server failed to init: " .. tostring(err)) os.exit(1) end app:run() os.exit(0) end local ready = false for _ = 1, 100 do if fixture.sh("curl -sf " .. base .. "/ -o /dev/null") then ready = true break end std.ps.sleep_ms(100) end assert(ready, "server did not become ready") local finish = function() std.ps.kill(child, 15) std.ps.wait(child) fixture.destroy(fx) end local b = fx.branch testify:that("git clone works with and without the .git suffix", function() testimony.assert_true(fixture.sh("git clone -q " .. base .. "/bare " .. fx.root .. "/clone1")) testimony.assert_true(fixture.sh("git clone -q " .. base .. "/bare.git " .. fx.root .. "/clone2")) local log = fixture.sh_out("git -C " .. fx.root .. "/clone1 log --oneline") testimony.assert_match("first", log) end) testify:that("index lists the repositories", function() local content = fixture.sh_out("curl -s " .. base .. "/") testimony.assert_match("/bare/", content) testimony.assert_match("/fix/", content) end) testify:that("repo root 302s to the default branch tree", function() local headers = fixture.sh_out("curl -s -i --max-time 2 " .. base .. "/bare") testimony.assert_match("HTTP/1%.1 302", headers) testimony.assert_match("location: /bare/tree/" .. b .. "/", headers) -- An empty-body redirect must still be framed: without a Content-Length -- a keep-alive client waits for the connection to close before it -- follows the redirect (hence the --max-time guard above). testimony.assert_match("content%-length: 0", headers) end) testify:that("tree route redirects a blob path without stalling", function() local url = base .. "/bare/tree/" .. b .. "/assets/logo.png" local headers = fixture.sh_out("curl -s -i --max-time 2 " .. url) testimony.assert_match("HTTP/1%.1 302", headers) testimony.assert_match("location: /bare/blob/" .. b .. "/assets/logo%.png", headers) testimony.assert_match("content%-length: 0", headers) end) testify:that("tree page renders entries and the README title", function() local content = fixture.sh_out("curl -s " .. base .. "/bare/tree/" .. b .. "/") testimony.assert_match("Fixture Repo", content) testimony.assert_match("src", content) local slashed = fixture.sh_out("curl -s -o /dev/null -w '%{http_code}' " .. base .. "/bare/tree/feature/foo/") testimony.assert_equal("200", slashed) end) testify:that("raw blob is byte-identical to git show", function() fixture.sh("curl -s " .. base .. "/bare/raw/" .. b .. "/assets/logo.png -o " .. fx.root .. "/served.png") -- fixture.sh appends its own stdout redirect, which would override the -- `>` here, so run this one raw os.execute("git -C " .. fx.dir .. " show " .. b .. ":assets/logo.png > " .. fx.root .. "/expected.png 2>/dev/null") testimony.assert_true(fixture.sh("cmp -s " .. fx.root .. "/served.png " .. fx.root .. "/expected.png")) end) testify:that("markdown blob is served as HTML", function() local content = fixture.sh_out("curl -s " .. base .. "/bare/blob/" .. b .. "/README.md") testimony.assert_match("

Fixture Repo

", content) end) testify:that("conditional requests revalidate to 304, HEAD returns headers only", function() local url = base .. "/bare/blob/" .. b .. "/src/main.lua" local etag = fixture.sh_out("curl -sI " .. url .. " | grep -i '^etag:' | cut -d' ' -f2") testimony.assert_not_nil(etag) testimony.assert_match('^"', etag) local code = fixture.sh_out("curl -s -o /dev/null -w '%{http_code}' -H 'If-None-Match: " .. etag .. "' " .. url) testimony.assert_equal("304", code) local head = fixture.sh_out("curl -sI " .. url) testimony.assert_match("HTTP/1%.1 200", head) testimony.assert_match("etag:", head) end) testify:that("log and commit pages are served", function() local log = fixture.sh_out("curl -s " .. base .. "/bare/log/" .. b) testimony.assert_match("second", log) testimony.assert_match("first", log) local commit = fixture.sh_out("curl -s " .. base .. "/bare/commit/" .. fx.head) testimony.assert_match("plain%.xyz", commit) end) testify:that("refs page and switcher are served", function() local content = fixture.sh_out("curl -s " .. base .. "/bare/refs") testimony.assert_match("feature/foo", content) testimony.assert_match("v1%.0", content) local tree = fixture.sh_out("curl -s " .. base .. "/bare/tree/" .. b .. "/") testimony.assert_match("switcher", tree) end) testify:that("nested repo clones and browses", function() testimony.assert_true(fixture.sh("git clone -q " .. base .. "/cat1/repo1 " .. fx.root .. "/clone3")) testimony.assert_true(fixture.sh("git clone -q " .. base .. "/cat1/repo1.git " .. fx.root .. "/clone4")) local log = fixture.sh_out("git -C " .. fx.root .. "/clone3 log --oneline") testimony.assert_match("first", log) local tree = fixture.sh_out("curl -s " .. base .. "/cat1/repo1/tree/" .. b .. "/") testimony.assert_match("Fixture Repo", tree) testimony.assert_match('href="/cat1/repo1/blob/', tree) local index = fixture.sh_out("curl -s " .. base .. "/") testimony.assert_match("/cat1/repo1", index) -- cat2/repo1 is private in this server: it reaches the index only for a -- visitor holding credentials. local authed_index = fixture.sh_out("curl -s -u alice:hunter2 " .. base .. "/") testimony.assert_match("/cat2/repo1", authed_index) testimony.assert_nil(index:find("/cat2/repo1", 1, true)) end) testify:that("a private repo challenges for credentials and then serves", function() local anon = fixture.sh_out("curl -s -i --max-time 2 " .. base .. "/cat2/repo1/") testimony.assert_match("HTTP/1%.1 401", anon) testimony.assert_match('www%-authenticate: Basic realm="lilgit"', anon) local tree = fixture.sh_out("curl -s -u alice:hunter2 " .. base .. "/cat2/repo1/tree/" .. b .. "/") testimony.assert_match("Fixture Repo", tree) local wrong = fixture.sh_out("curl -s -o /dev/null -w '%{http_code}' -u alice:nope " .. base .. "/cat2/repo1/") testimony.assert_equal("401", wrong) local auth_base = "http://alice:hunter2@127.0.0.1:" .. port testimony.assert_true(fixture.sh("git clone -q " .. auth_base .. "/cat2/repo1 " .. fx.root .. "/clone5")) testimony.assert_false( fixture.sh("GIT_TERMINAL_PROMPT=0 git clone -q " .. base .. "/cat2/repo1 " .. fx.root .. "/clone6") ) end) testify:that("push over HTTP is refused", function() fixture.sh("git -C " .. fx.root .. "/clone1 -c user.name=t -c user.email=t@t commit -q --allow-empty -m push-me") testimony.assert_false(fixture.sh("git -C " .. fx.root .. "/clone1 push -q origin " .. b)) end) finish() testify:conclude()