romele / git-trace public
62 commits · 1 branch · 1 tag · 1 contributor (top: Romele) · first commit 3d ago · last push 12m ago
tests/e2e.sh
#!/usr/bin/env bash # git-trace end-to-end test harness. # Runs from the host against a throwaway container whose data dir is # fresh on every run. Usage: # make && bash tests/e2e.sh (from repo root) # env: IMG (image), CT (container name), PORT (host port) set -u IMG=${IMG:-git-trace-dev} CT=${CT:-git-trace-test} PORT=${PORT:-8191} BASE="http://localhost:$PORT" SRC=$(cd "$(dirname "$0")/.." && (pwd -W 2>/dev/null || pwd)) # NB: docker.exe needs Windows paths; curl/git are fine either way. # Do NOT export MSYS_NO_PATHCONV globally — it would break the native # curl/git binaries' argument path translation. pass=0; fail=0 ok() { pass=$((pass+1)); echo " ok $1"; } bad() { fail=$((fail+1)); echo " FAIL $1 ($2)"; } ck() { [ "$2" = "$3" ] && ok "$1" || bad "$1" "want '$2' got '$3'"; } has() { grep -q "$2" "$3" 2>/dev/null && ok "$1" || { bad "$1" "'$2' not found"; tail -c 400 "$3" 2>/dev/null | tr '\n' ' '; echo; }; } nth() { grep -q "$2" "$3" 2>/dev/null && bad "$1" "'$2' found" || ok "$1"; } echo "== build ==" MSYS_NO_PATHCONV=1 docker run --rm -v "$SRC:/src" -w /src "$IMG" make >/dev/null 2>&1 \ && ok build || { bad build "make failed"; exit 1; } echo "== container ==" MSYS_NO_PATHCONV=1 docker rm -f "$CT" >/dev/null 2>&1 MSYS_NO_PATHCONV=1 docker run -d --name "$CT" -p "$PORT:8081" \ -e GIT_TRACE_RL_MAXFAIL=4 -e GIT_TRACE_RL_WINDOW=120 \ -e GIT_TRACE_OPEN_REG=1 \ -e GIT_TRACE_TIER_GATE=1 \ -e GIT_TRACE_SSH_URL=ssh://git@localhost:2222 \ -e GIT_TRACE_BASE_URL=http://alt.example:9999 \ -v "$SRC:/src" -w /srv "$IMG" sh -c ' mkdir -p /srv/repos /srv/var export GIT_TRACE_REPOS=/srv/repos GIT_TRACE_DB=/srv/var/gt.db echo secretpw1 | /src/git-trace useradd --email=alice@e2e.test alice echo secretpw2 | /src/git-trace useradd bob echo secretpw0 | /src/git-trace useradd --admin root /src/git-trace tier alice pro never /src/git-trace mkrepo alice demo "public repo" /src/git-trace mkrepo --private alice priv /src/git-trace mkrepo bob work exec /src/git-trace serve -p 8081' >/dev/null || { bad container "docker run"; exit 1; } sleep 1 T=$(mktemp -d) if [ "${KEEP:-0}" = 1 ]; then trap 'rm -rf "$T"; echo "container kept: docker rm -f $CT"' EXIT else trap 'rm -rf "$T"; MSYS_NO_PATHCONV=1 docker rm -f "$CT" >/dev/null 2>&1' EXIT fi echo "== anonymous access ==" OUT=$T/idx; curl -sS -o $OUT -w '%{http_code}' $BASE/ > $T/code; ck "index 200" 200 "$(cat $T/code)" has "public repo listed" "alice/demo" $OUT nth "private repo hidden" "alice/priv" $OUT # landing: brand hero, version badge, anon call-to-action has "landing hero" "trace every change" $OUT has "version badge" "v1.0" $OUT has "landing login cta" "/login" $OUT has "landing reg cta" "/register" $OUT has "landing logo svg" "<svg" $OUT curl -s -o /dev/null -w '%{http_code}' "$BASE/alice/demo.git/info/refs?service=git-upload-pack" > $T/code ck "anon fetch advert" 200 "$(cat $T/code)" curl -s -o /dev/null -w '%{http_code}' "$BASE/alice/priv.git/info/refs?service=git-upload-pack" > $T/code ck "anon priv fetch -> 401" 401 "$(cat $T/code)" curl -s -o /dev/null -w '%{http_code}' -X POST "$BASE/alice/demo.git/git-receive-pack" > $T/code ck "anon push -> 401" 401 "$(cat $T/code)" echo "== real clone/push ==" git clone -q "$BASE/alice/demo.git" $T/demo 2>/dev/null cd $T/demo && git -c user.name=t -c user.email=t@t commit -q --allow-empty -m seed && cd - >/dev/null git -C $T/demo push -q "http://alice:secretpw1@localhost:$PORT/alice/demo.git" master 2>/dev/null git ls-remote "$BASE/alice/demo.git" refs/heads/master > $T/lr has "alice push lands" "refs/heads/master" $T/lr curl -s -o /dev/null -w '%{http_code}' -u bob:secretpw2 "$BASE/alice/priv.git/info/refs?service=git-upload-pack" > $T/code ck "bob no-ACL priv -> 403" 403 "$(cat $T/code)" git clone -q "http://alice:secretpw1@localhost:$PORT/alice/priv.git" $T/priv 2>/dev/null [ -d $T/priv ] && ok "alice clones priv" || bad "alice clones priv" "clone failed" echo "== shallow negotiation ==" # Regression: git deflates RPC request bodies >~1KB and marks them # Content-Encoding: deflate. Without HTTP_CONTENT_ENCODING forwarded to # git http-backend the compressed bytes were parsed as pkt-lines and the # fetch died with "expected 'acknowledgments'". A shallow pull on a repo # with enough commits sends 30+ haves, crossing the threshold. git clone -q "http://alice:secretpw1@localhost:$PORT/alice/demo.git" $T/deep 2>/dev/null for i in $(seq 1 45); do git -C $T/deep -c user.name=t -c user.email=t@t commit -qm "d$i" --allow-empty done git -C $T/deep push -q origin master 2>/dev/null git clone -q --depth 30 "$BASE/alice/demo.git" $T/shallow 2>/dev/null [ -d $T/shallow ] && ok "shallow clone" || bad "shallow clone" "clone failed" git -C $T/deep -c user.name=t -c user.email=t@t commit -qm newest --allow-empty git -C $T/deep push -q origin master 2>/dev/null git -C $T/shallow pull -q 2>$T/pullerr && ok "shallow pull negotiates" \ || bad "shallow pull negotiates" "$(tail -1 $T/pullerr)" git -C $T/shallow log --oneline 2>/dev/null | grep -q newest \ && ok "shallow pull applied" || bad "shallow pull applied" "newest missing" git -C $T/shallow fetch -q --depth 10 2>$T/fetcherr && ok "shallow fetch --depth" \ || bad "shallow fetch --depth" "$(tail -1 $T/fetcherr)" echo "== web session + CSRF ==" curl -s -X POST -d "name=alice&pass=wrongpw" $BASE/login > $T/login.html has "wrong pw -> error" "invalid credentials" $T/login.html CK=$(curl -s -D- -o /dev/null -X POST -d "name=alice&pass=secretpw1" $BASE/login \ | grep -oi 'gt_session=[0-9a-f]*' | head -1 | cut -d= -f2) [ -n "$CK" ] && ok "session cookie" || bad "session cookie" "none" curl -s -b "gt_session=$CK" $BASE/ > $T/idx2 has "logged-in index shows priv" "alice/priv" $T/idx2 has "logged-in dashboard" "dashboard" $T/idx2 curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$CK" -X POST \ -d "action=comment&body=x" $BASE/alice/demo/issues/1 > $T/code ck "POST w/o csrf -> 403" 403 "$(cat $T/code)" curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$CK" \ -H "Origin: http://evil.example" -X POST \ -d "csrf=$CK&action=comment&body=x" $BASE/alice/demo/issues/1 > $T/code ck "foreign Origin -> 403" 403 "$(cat $T/code)" # reverse-proxy deployments: the configured public base URL is a # valid Origin even when Host differs curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$CK" \ -H "Origin: http://alt.example:9999" -X POST \ -d "csrf=$CK&action=comment&body=x" $BASE/alice/demo/issues/1 > $T/code ck "base-url Origin allowed" 302 "$(cat $T/code)" # dev ergonomics: a loopback-facing daemon accepts loopback origins # on ANY port (IDE previews proxy 127.0.0.1:X -> localhost:8081) curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$CK" \ -H "Origin: http://127.0.0.1:60363" -X POST \ -d "csrf=$CK&action=comment&body=x" $BASE/alice/demo/issues/1 > $T/code ck "loopback Origin allowed" 302 "$(cat $T/code)" # ...but a foreign-domain origin is still refused even on loopback curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$CK" \ -H "Origin: http://evil.example" -X POST \ -d "csrf=$CK&action=comment&body=x" $BASE/alice/demo/issues/1 > $T/code ck "foreign domain still 403" 403 "$(cat $T/code)" # sandboxed embedders (IDE previews) send Origin: null — accepted on # loopback hosts; session routes are still guarded by the CSRF token curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$CK" \ -H "Origin: null" -X POST \ -d "csrf=$CK&action=comment&body=x" $BASE/alice/demo/issues/1 > $T/code ck "null origin on loopback" 302 "$(cat $T/code)" # ...but without the CSRF token a forged POST still dies at the form gate curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$CK" \ -H "Origin: null" -X POST \ -d "action=comment&body=x" $BASE/alice/demo/issues/1 > $T/code ck "null origin + no csrf -> 403" 403 "$(cat $T/code)" echo "== issues ==" curl -s -o /dev/null -w '%{redirect_url}' -b "gt_session=$CK" -X POST \ -d "csrf=$CK&title=e2e bug&body=body" $BASE/alice/demo/issues/new > $T/loc has "issue created" "/issues/1" $T/loc curl -s -b "gt_session=$CK" $BASE/alice/demo/issues > $T/il has "issue listed" "e2e bug" $T/il curl -s -o /dev/null -b "gt_session=$CK" -X POST -d "csrf=$CK&action=comment&body=worksforme" $BASE/alice/demo/issues/1 curl -s -o /dev/null -b "gt_session=$CK" -X POST -d "csrf=$CK&action=close" $BASE/alice/demo/issues/1 curl -s -b "gt_session=$CK" $BASE/alice/demo/issues/1 > $T/i1 has "comment visible" "worksforme" $T/i1 has "issue closed" "closed" $T/i1 echo "== XSS / output escaping ==" curl -s -o /dev/null -b "gt_session=$CK" -X POST \ --data-urlencode "csrf=$CK" \ --data-urlencode 'title=<script>alert(1)</script>' \ --data-urlencode 'body=<img src=x onerror=alert(2)>' \ $BASE/alice/demo/issues/new curl -s -b "gt_session=$CK" $BASE/alice/demo/issues/2 > $T/i2 has "title escaped" "script>alert" $T/i2 nth "no raw <script>" "<script>alert" $T/i2 has "body escaped" "<img" $T/i2 curl -s -o /dev/null -b "gt_session=$CK" -X POST \ --data-urlencode "csrf=$CK" --data-urlencode "action=comment" \ --data-urlencode 'body=<b onmouseover=alert(3)>x</b>' \ $BASE/alice/demo/issues/2 curl -s -b "gt_session=$CK" $BASE/alice/demo/issues/2 > $T/i2b has "comment escaped" "<b onmouseover" $T/i2b nth "no raw attr" "<b onmouseover" $T/i2b echo "== merge request ==" git -C $T/demo checkout -qb feat && echo x > $T/demo/feat.txt \ && git -C $T/demo add feat.txt \ && git -C $T/demo -c user.name=t -c user.email=t@t commit -qm feat \ && git -C $T/demo push -q "http://alice:secretpw1@localhost:$PORT/alice/demo.git" feat 2>/dev/null curl -s -o /dev/null -w '%{redirect_url}' -b "gt_session=$CK" -X POST \ -d "csrf=$CK&title=feat mr&src=feat&dst=master" $BASE/alice/demo/mr/new > $T/loc has "mr created" "/mr/1" $T/loc curl -s -o /dev/null -b "gt_session=$CK" -X POST -d "csrf=$CK&action=merge" $BASE/alice/demo/mr/1 git ls-remote "$BASE/alice/demo.git" refs/heads/master | cut -c1-8 > $T/m1 git -C $T/demo fetch -q origin 2>/dev/null || git -C $T/demo remote add origin "http://alice:secretpw1@localhost:$PORT/alice/demo.git" sleep 0.2; git -C $T/demo fetch -q "http://alice:secretpw1@localhost:$PORT/alice/demo.git" master:refs/remotes/t/m 2>/dev/null git -C $T/demo log --format='%s' -1 $(cat $T/m1) 2>/dev/null > $T/ml || true curl -s -b "gt_session=$CK" $BASE/alice/demo/mr/1 > $T/mr has "mr merged state" "merged" $T/mr echo "== patch sets (GNU flow) ==" git -C $T/demo checkout -qb ps1 HEAD~1 && echo y > $T/demo/ps.txt \ && git -C $T/demo add ps.txt \ && git -C $T/demo -c user.name="P Atch" -c user.email=p@t commit -qm "add ps file" \ && git -C $T/demo format-patch --stdout HEAD~1 > $T/series.mbox curl -s -u alice:secretpw1 -H "Content-Type: text/plain" \ --data-binary @$T/series.mbox -X POST "$BASE/alice/demo/patches/new" > $T/ps has "patch set submitted via API" "patch set #1" $T/ps curl -s -o /dev/null -w '%{http_code}' -H "Content-Type: text/plain" \ --data-binary @$T/series.mbox -X POST "$BASE/alice/demo/patches/new" > $T/code ck "anon patch submit -> 401" 401 "$(cat $T/code)" curl -s -b "gt_session=$CK" -X POST -d "csrf=$CK&action=check" $BASE/alice/demo/patches/1 > $T/pc has "series applies cleanly" "applies cleanly" $T/pc curl -s -o /dev/null -b "gt_session=$CK" -X POST -d "csrf=$CK&action=apply" $BASE/alice/demo/patches/1 curl -s -b "gt_session=$CK" $BASE/alice/demo/patches/1 > $T/pv has "patch set applied" "applied" $T/pv echo "== convergence: MR <-> patch set ==" # web MR -> mbox -> `git am` round-trip git -C $T/demo fetch -q "http://alice:secretpw1@localhost:$PORT/alice/demo.git" master git -C $T/demo checkout -qb feat2 FETCH_HEAD echo z > $T/demo/feat2.txt && git -C $T/demo add feat2.txt \ && git -C $T/demo -c user.name=t -c user.email=t@t commit -qm "add feat2" \ && git -C $T/demo push -q "http://alice:secretpw1@localhost:$PORT/alice/demo.git" feat2 curl -s -o /dev/null -b "gt_session=$CK" -X POST \ -d "csrf=$CK&title=second mr&src=feat2&dst=master" $BASE/alice/demo/mr/new curl -s -u alice:secretpw1 "$BASE/alice/demo/mr/2.patch" > $T/mr2.patch grep -q "^From [0-9a-f]\{40\}" $T/mr2.patch && ok "mr -> mbox" || bad "mr -> mbox" "no From line" has "mbox has subject" "add feat2" $T/mr2.patch git clone -q "http://alice:secretpw1@localhost:$PORT/alice/demo.git" $T/rt 2>/dev/null git -C $T/rt -c user.name=t -c user.email=t@t am -q --keep-non-patch $T/mr2.patch [ -f $T/rt/feat2.txt ] && ok "mbox round-trips via git am" || bad "mbox round-trip" "file missing after am" # patch set -> materialize as MR (fresh series: ps.txt already landed) git -C $T/demo checkout -qb ps2 FETCH_HEAD echo w > $T/demo/ps2.txt && git -C $T/demo add ps2.txt \ && git -C $T/demo -c user.name="P Atch" -c user.email=p@t commit -qm "add ps2 file" git -C $T/demo format-patch --stdout HEAD~1 > $T/series2.mbox curl -s -u alice:secretpw1 -H "Content-Type: text/plain" \ --data-binary @$T/series2.mbox -X POST "$BASE/alice/demo/patches/new" > $T/ps2 has "second patch set" "patch set #2" $T/ps2 curl -s -o /dev/null -w '%{redirect_url}' -b "gt_session=$CK" -X POST \ -d "csrf=$CK&action=materialize" $BASE/alice/demo/patches/2 > $T/mloc has "materialized to MR" "/mr/" $T/mloc MRID=$(grep -o '[0-9]*' <<<"$(cat $T/mloc)" | tail -1) curl -s -b "gt_session=$CK" $BASE/alice/demo/mr/$MRID > $T/mrv has "MR back-link to patch set" "patch set" $T/mrv curl -s -b "gt_session=$CK" $BASE/alice/demo/patches/2 > $T/pv2 has "patch-set link to MR" "merge request" $T/pv2 curl -s -o /dev/null -b "gt_session=$CK" -X POST -d "csrf=$CK&action=merge" $BASE/alice/demo/mr/$MRID curl -s -b "gt_session=$CK" $BASE/alice/demo/mr/$MRID > $T/mrm has "materialized MR merges" "merged" $T/mrm echo "== patch-set revisions (v2/v3) ==" # bob (who cannot push master directly) iterates a series git -C $T/demo checkout -qb revb FETCH_HEAD 2>/dev/null \ || git -C $T/demo checkout -qb revb echo one > $T/demo/rev.txt && git -C $T/demo add rev.txt \ && git -C $T/demo -c user.name=bob -c user.email=b@t \ commit -qm "rev series" git -C $T/demo format-patch --stdout HEAD~1 > $T/rev1.mbox curl -s -u bob:secretpw2 -H "Content-Type: text/plain" \ --data-binary @$T/rev1.mbox -X POST \ "$BASE/alice/demo/patches/new" > $T/r1 has "v1 creates new set" "patch set #" $T/r1 SID=$(grep -oE '#[0-9]+' $T/r1 | head -1 | tr -d '#') # v2: same subject + [PATCH v2] tag -> auto-attaches to the open set echo two > $T/demo/rev.txt && git -C $T/demo add rev.txt \ && git -C $T/demo -c user.name=bob -c user.email=b@t \ commit -q --amend -m "rev series" git -C $T/demo format-patch --stdout -v2 HEAD~1 > $T/rev2.mbox curl -s -u bob:secretpw2 -H "Content-Type: text/plain" \ --data-binary @$T/rev2.mbox -X POST \ "$BASE/alice/demo/patches/new" > $T/r2 has "v2 auto-attaches by tag" "patch set #$SID v2" $T/r2 # v3: explicit ?set=N attach (no version tag needed) echo three > $T/demo/rev.txt && git -C $T/demo add rev.txt \ && git -C $T/demo -c user.name=bob -c user.email=b@t \ commit -q --amend -m "rev series" git -C $T/demo format-patch --stdout HEAD~1 > $T/rev3.mbox curl -s -u bob:secretpw2 -H "Content-Type: text/plain" \ --data-binary @$T/rev3.mbox -X POST \ "$BASE/alice/demo/patches/new?set=$SID" > $T/r3 has "v3 explicit set attach" "patch set #$SID v3" $T/r3 # timeline UI curl -s $BASE/alice/demo/patches/$SID > $T/psv has "view shows v3" ">v3<" $T/psv has "revision nav" "revisions:" $T/psv has "latest diff is v3" "+three" $T/psv curl -s "$BASE/alice/demo/patches/$SID?v=1" > $T/psv1 has "v1 shows its own diff" "+one" $T/psv1 nth "v1 hides latest diff" "+three" $T/psv1 has "old rev banner" "latest is" $T/psv1 curl -s -b "gt_session=$CK" $BASE/alice/demo/patches > $T/psl has "list shows v3" ">v3<" $T/psl CKBV=$(curl -s -D- -o /dev/null -X POST -d "name=bob&pass=secretpw2" \ $BASE/login | grep -oi 'gt_session=[0-9a-f]*' | head -1 | cut -d= -f2) curl -s -b "gt_session=$CKBV" $BASE/alice/demo/patches/$SID > $T/psvb has "author sees new-version button" "submit new version" $T/psvb # attach policy: non-author, closed set, missing set curl -s -o /dev/null -w '%{http_code}' -u bob:secretpw2 \ -H "Content-Type: text/plain" --data-binary @$T/rev1.mbox \ -X POST "$BASE/alice/demo/patches/new?set=2" > $T/code ck "non-author attach -> 403" 403 "$(cat $T/code)" curl -s -o /dev/null -w '%{http_code}' -u alice:secretpw1 \ -H "Content-Type: text/plain" --data-binary @$T/rev1.mbox \ -X POST "$BASE/alice/demo/patches/new?set=1" > $T/code ck "closed set attach -> 409" 409 "$(cat $T/code)" curl -s -o /dev/null -w '%{http_code}' -u bob:secretpw2 \ -H "Content-Type: text/plain" --data-binary @$T/rev1.mbox \ -X POST "$BASE/alice/demo/patches/new?set=999" > $T/code ck "missing set attach -> 404" 404 "$(cat $T/code)" # the latest revision is the one checked/applied curl -s -b "gt_session=$CK" -X POST -d "csrf=$CK&action=check" \ $BASE/alice/demo/patches/$SID > $T/rc has "v3 applies cleanly" "applies cleanly" $T/rc echo "== mail-in endpoint (mailrecv) ==" # the MTA pipes each RFC822 message to `git-trace mailrecv` on stdin MTOK=$(MSYS_NO_PATHCONV=1 docker exec -e GIT_TRACE_REPOS=/srv/repos \ -e GIT_TRACE_DB=/srv/var/gt.db $CT /src/git-trace mktoken alice mail \ | grep -oE '[0-9a-f]{64}' | head -1) gtmail() { MSYS_NO_PATHCONV=1 docker exec -i -e GIT_TRACE_REPOS=/srv/repos \ -e GIT_TRACE_DB=/srv/var/gt.db $CT /src/git-trace mailrecv "$1" "$2"; } # a send-email wire mail = an mbox minus the "From <sha>" separator; # outer headers (Subject/Message-ID/token) take precedence { echo "Message-ID: <m1@e2e>" echo "X-Git-Trace-Token: $MTOK" echo "From: alice <alice@e2e.test>" echo "Date: Thu, 01 Jan 2026 00:00:00 +0000" sed '1{/^From /d;}' $T/rev1.mbox } > $T/m1.eml gtmail alice demo < $T/m1.eml > $T/mo1 has "single mail creates set" "patch set #" $T/mo1 MSID=$(grep -oE '#[0-9]+' $T/mo1 | head -1 | tr -d '#') # auth failures { echo "From: Mallory <mal@evil.test>" echo "Subject: [PATCH] backdoor" echo "Message-ID: <evil@e2e>"; echo echo "diff --git a/x b/x"; echo "@@ -1 +1 @@"; } > $T/evil.eml gtmail alice demo < $T/evil.eml > /dev/null 2>$T/me1 has "unauthenticated mail rejected" "unauthenticated" $T/me1 gtmail alice demo < $T/evil.eml > /dev/null 2>&1 && echo ok > $T/me2 nth "reject exit nonzero" "ok" $T/me2 printf 'garbage, not an email\n\n' | gtmail alice demo > /dev/null 2>&1 \ || echo rejected > $T/mj has "malformed mail rejected" "rejected" $T/mj # From-address auth: registered users.email, no token needed { echo "From: alice <alice@e2e.test>" echo "Subject: [PATCH] mail from auth" echo "Message-ID: <mfa@e2e>" echo "Date: Thu, 01 Jan 2026 00:00:00 +0000"; echo echo "diff --git a/mfa.txt b/mfa.txt" echo "new file mode 100644" echo "--- /dev/null" echo "+++ b/mfa.txt" echo "@@ -0,0 +1 @@" echo "+x"; } > $T/mfa.eml gtmail alice demo < $T/mfa.eml > $T/mo2 has "From-address auth creates set" "patch set #" $T/mo2 # non-patch mail from an authenticated sender -> 422 path { echo "From: alice <alice@e2e.test>"; echo "Subject: hello"; echo echo "no diff here"; } | gtmail alice demo > /dev/null 2>$T/mn \ || echo rejected > $T/mn has "non-patch mail rejected" "rejected" $T/mn # send-email series: out-of-order parts collect in mail_items { echo "Subject: [PATCH 1/2] mail series part one" echo "Message-ID: <mp1@e2e>"; echo "In-Reply-To: <mcover@e2e>" echo "X-Git-Trace-Token: $MTOK" echo "From: alice <alice@e2e.test>" echo "Date: Thu, 01 Jan 2026 00:00:00 +0000" sed '1{/^From /d;}' $T/rev1.mbox ; } > $T/mp1.eml { echo "Subject: [PATCH 2/2] mail series part two" echo "Message-ID: <mp2@e2e>"; echo "In-Reply-To: <mcover@e2e>" echo "X-Git-Trace-Token: $MTOK" echo "From: alice <alice@e2e.test>" echo "Date: Thu, 01 Jan 2026 00:00:00 +0000" sed '1{/^From /d;}' $T/rev2.mbox ; } > $T/mp2.eml { echo "Subject: [PATCH 0/2] mail series cover" echo "Message-ID: <mcover@e2e>" echo "X-Git-Trace-Token: $MTOK" echo "From: alice <alice@e2e.test>" echo "Date: Thu, 01 Jan 2026 00:00:00 +0000"; echo echo "a two-part series by mail"; } > $T/mcov.eml # send-email order: cover first, then numbered parts (arriving # out of order is fine — completeness is parts 1..n present) gtmail alice demo < $T/mcov.eml > $T/mq1 has "cover queued" "queued" $T/mq1 gtmail alice demo < $T/mp2.eml > $T/mq2 has "part 2 queued" "queued" $T/mq2 gtmail alice demo < $T/mp1.eml > $T/mq3 has "last part completes set" "patch set #" $T/mq3 SSID=$(grep -oE '#[0-9]+' $T/mq3 | head -1 | tr -d '#') curl -s $BASE/alice/demo/patches/$SSID > $T/mps has "assembled part 1" "+one" $T/mps has "assembled part 2" "+two" $T/mps has "mail cover letter" "two-part series by mail" $T/mps nth "mail token not stored" "$MTOK" $T/mps curl -s $BASE/alice/demo/patches > $T/ml has "cover title in list" "mail series cover" $T/ml # an MTA redelivery must not open a duplicate set gtmail alice demo < $T/mp1.eml > $T/mrd nth "redelivery no dup" "patch set" $T/mrd # iteration by mail: [PATCH v2] on alice's own mail-created set { echo "Message-ID: <mv2@e2e>" echo "X-Git-Trace-Token: $MTOK" echo "From: alice <alice@e2e.test>" echo "Date: Thu, 01 Jan 2026 00:00:00 +0000" sed '1{/^From /d;}' $T/rev2.mbox ; } > $T/mv2.eml gtmail alice demo < $T/mv2.eml > $T/mo3 has "mail v2 auto-attaches" "patch set #$MSID v2" $T/mo3 echo "== self-registration ==" curl -s $BASE/register > $T/reg has "register form" "Register" $T/reg CK2=$(curl -s -D- -o /dev/null -X POST \ --data-urlencode "name=carol" \ --data-urlencode "pass=carolpass9" \ --data-urlencode "pass2=carolpass9" $BASE/register \ | grep -oi 'gt_session=[0-9a-f]*' | head -1 | cut -d= -f2) [ -n "$CK2" ] && ok "register -> session" || bad "register -> session" "no cookie" curl -s -X POST -d "name=carol&pass=carolpass9&pass2=carolpass9" \ $BASE/register > $T/reg2 has "duplicate name rejected" "already taken" $T/reg2 curl -s -X POST -d "name=dave&pass=short&pass2=short" $BASE/register > $T/reg3 has "weak pw rejected" "too short" $T/reg3 curl -s -o /dev/null -w '%{http_code}' -u carol:carolpass9 \ "$BASE/alice/demo.git/info/refs?service=git-upload-pack" > $T/code ck "carol basic auth works" 200 "$(cat $T/code)" echo "== storage quota ==" MSYS_NO_PATHCONV=1 docker exec -e GIT_TRACE_REPOS=/srv/repos \ -e GIT_TRACE_DB=/srv/var/gt.db $CT /src/git-trace quota alice 1 > $T/q1 has "quota set to 1 MiB" "1 MiB" $T/q1 git -C $T/demo checkout -qb qb FETCH_HEAD 2>/dev/null \ || git -C $T/demo checkout -qb qb head -c 2000000 /dev/urandom > $T/demo/big.bin git -C $T/demo add big.bin \ && git -C $T/demo -c user.name=t -c user.email=t@t commit -qm big # 2 MiB push > git's 1 MiB postBuffer -> real chunked request body, # exercises the dechunker end to end through git-receive-pack git -C $T/demo push \ "http://alice:secretpw1@localhost:$PORT/alice/demo.git" \ qb > $T/push.out 2>&1 has "over-quota push rejected" "quota exceeded" $T/push.out MSYS_NO_PATHCONV=1 docker exec -e GIT_TRACE_REPOS=/srv/repos \ -e GIT_TRACE_DB=/srv/var/gt.db $CT /src/git-trace quota alice 0 > /dev/null git -C $T/demo push -q \ "http://alice:secretpw1@localhost:$PORT/alice/demo.git" \ qb > /dev/null 2>&1 git ls-remote "$BASE/alice/demo.git" refs/heads/qb > $T/lqb has "chunked push lands" "refs/heads/qb" $T/lqb echo "== chunked request bodies ==" curl -s -u alice:secretpw1 -H "Transfer-Encoding: chunked" \ -H "Content-Type: text/plain" \ --data-binary @$T/series2.mbox -X POST \ "$BASE/alice/demo/patches/new" > $T/ps3 has "chunked patch submit" "patch set" $T/ps3 curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$CK" \ -H "Transfer-Encoding: chunked" \ --data-binary "csrf=$CK&name=chunkrepo" -X POST $BASE/new > $T/code ck "chunked form POST" 302 "$(cat $T/code)" # a push that exceeds GIT_TRACE_BODY_MAX would 413; the cap is # 256 MiB here, so the 2 MiB pushes above cover the happy path echo "== audit log ==" curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$CK" $BASE/audit > $T/code ck "non-admin /audit -> 403" 403 "$(cat $T/code)" CKA=$(curl -s -D- -o /dev/null -X POST -d "name=root&pass=secretpw0" \ $BASE/login | grep -oi 'gt_session=[0-9a-f]*' | head -1 | cut -d= -f2) [ -n "$CKA" ] && ok "admin session" || bad "admin session" "none" curl -s -b "gt_session=$CKA" $BASE/audit > $T/au has "push audited" "push" $T/au has "repo_create row" "repo_create" $T/au has "register audited" "register" $T/au has "login audited" "login" $T/au curl -s -b "gt_session=$CK" $BASE/alice/demo/activity > $T/act has "repo activity shows push" "push" $T/act echo "== M8: dashboard & repo UI ==" curl -s -b "gt_session=$CK" $BASE/ > $T/dash has "dashboard storage bar" "storage" $T/dash has "dashboard New button" "New repository" $T/dash has "dashboard public badge" "public" $T/dash has "dashboard private badge" "private" $T/dash has "dashboard activity feed" "Recent activity" $T/dash has "feed shows push event" "push" $T/dash curl -s $BASE/ > $T/dasha nth "anon: no storage bar" "account storage" $T/dasha nth "anon: no New button" "New repository" $T/dasha echo "== M8: repo header & clone URLs ==" curl -s $BASE/alice/demo/tree > $T/tree has "repo header public badge" "public" $T/tree has "http clone url" "git clone http://localhost:$PORT/alice/demo.git" $T/tree has "ssh clone url" "ssh://git@localhost:2222/alice/demo.git" $T/tree has "repo quota line" "this repo:" $T/tree nth "anon: no settings tab" "/settings" $T/tree curl -s -b "gt_session=$CK" $BASE/alice/demo/tree > $T/atree has "owner sees settings tab" "/settings" $T/atree has "sidebar layout" "class=repo" $T/atree has "clone panel rows" "class=clonerow" $T/atree has "active tab marked" "class=on" $T/atree CKB=$(curl -s -D- -o /dev/null -X POST -d "name=bob&pass=secretpw2" \ $BASE/login | grep -oi 'gt_session=[0-9a-f]*' | head -1 | cut -d= -f2) curl -s -b "gt_session=$CKB" $BASE/bob/work/tree > $T/btree has "owner storage in sidebar" "class=sidestat" $T/btree nth "anon: no sidestat" "class=sidestat" $T/tree curl -s -b "gt_session=$CK" $BASE/alice/priv/tree > $T/ptree has "private repo badge" "private" $T/ptree has "empty repo panel" "empty repository" $T/ptree echo "== M8: /new web repo creation ==" curl -s -o /dev/null -w '%{http_code}' $BASE/new > $T/code ck "anon GET /new -> redirect" 302 "$(cat $T/code)" curl -s -b "gt_session=$CK" $BASE/new > $T/nf has "new repo form" "Create repository" $T/nf curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$CK" -X POST \ -d "name=webrepo" $BASE/new > $T/code ck "POST /new no csrf -> 403" 403 "$(cat $T/code)" curl -s -o /dev/null -w '%{redirect_url}' -b "gt_session=$CK" -X POST \ -d "csrf=$CK&name=webrepo&desc=via+web&private=on" $BASE/new > $T/nloc has "web repo created" "/alice/webrepo/tree" $T/nloc curl -s -b "gt_session=$CK" $BASE/alice/webrepo/tree > $T/wt has "web repo page loads" "webrepo" $T/wt has "web repo is private" "private" $T/wt curl -s -b "gt_session=$CK" -X POST -d "csrf=$CK&name=webrepo" \ $BASE/new > $T/nd has "duplicate repo rejected" "already exists" $T/nd curl -s -o /dev/null -w '%{http_code}' -u alice:secretpw1 \ "$BASE/alice/webrepo.git/info/refs?service=git-upload-pack" > $T/code ck "web repo serves git" 200 "$(cat $T/code)" curl -s -o /dev/null -w '%{http_code}' \ "$BASE/alice/webrepo.git/info/refs?service=git-upload-pack" > $T/code ck "web priv anon -> 401" 401 "$(cat $T/code)" echo "== M8: unified /changes ==" curl -s -b "gt_session=$CK" $BASE/alice/demo/changes > $T/ch has "changes: mr badge" ">mr<" $T/ch has "changes: patch badge" ">patch<" $T/ch has "changes: mr link" "/mr/" $T/ch has "changes: patch link" "/patches/" $T/ch echo "== M9: branch protection ==" # bob needs repo rank 'w' to reach receive-pack at all curl -s -o /dev/null -b "gt_session=$CK" -X POST \ -d "csrf=$CK&action=acl_add&user=bob&role=w" \ $BASE/alice/demo/settings # master -> admin-only, ff-only, no-delete; release/* -> write+ff+del curl -s -o /dev/null -b "gt_session=$CK" -X POST \ -d "csrf=$CK&action=rule_add&pattern=master&role=a&ff=on&del=on" \ $BASE/alice/demo/settings curl -s -o /dev/null -b "gt_session=$CK" -X POST \ --data-urlencode "csrf=$CK" --data-urlencode "action=rule_add" \ --data-urlencode "pattern=release/*" --data-urlencode "role=w" \ --data-urlencode "ff=on" --data-urlencode "del=on" \ $BASE/alice/demo/settings curl -s -b "gt_session=$CK" $BASE/alice/demo/settings > $T/sett has "settings: protection section" "branch protection" $T/sett has "rule master listed" "master" $T/sett has "rule glob listed" "release/*" $T/sett # sync the local clone, then run the push x branch x rank matrix git -C $T/demo fetch -q "$BASE/alice/demo.git" master git -C $T/demo reset -q --hard FETCH_HEAD git -C $T/demo -c user.name=t -c user.email=t@t commit -qm bobwork \ --allow-empty BOB="http://bob:secretpw2@localhost:$PORT/alice/demo.git" git -C $T/demo push "$BOB" HEAD:master > $T/bp 2>&1 has "writer -> admin-only master denied" "requires admin" $T/bp git -C $T/demo push -q "$BOB" HEAD:refs/heads/bobfeat \ && ok "writer -> unprotected branch ok" \ || bad "writer -> unprotected branch ok" "push failed" git -C $T/demo push -q "$BOB" HEAD:refs/heads/release/1.0 \ && ok "writer -> release glob (rank w) ok" \ || bad "writer -> release glob (rank w) ok" "push failed" # HEAD~1 doesn't contain the bobwork commit -> non-fast-forward git -C $T/demo push "$BOB" HEAD~1:refs/heads/release/1.0 > $T/nff 2>&1 has "non-ff on ff-only branch denied" "non-fast-forward" $T/nff git -C $T/demo push "$BOB" :refs/heads/release/1.0 > $T/del 2>&1 has "delete on no-delete branch denied" "deletion denied" $T/del git -C $T/demo push -q \ "http://alice:secretpw1@localhost:$PORT/alice/demo.git" \ HEAD:master \ && ok "admin -> protected master ok" \ || bad "admin -> protected master ok" "push failed" curl -s -b "gt_session=$CK" $BASE/alice/demo/activity > $T/act2 has "push_denied in repo activity" "push_denied" $T/act2 # rule_rm on a throwaway rule covers the removal path curl -s -o /dev/null -b "gt_session=$CK" -X POST \ --data-urlencode "csrf=$CK" --data-urlencode "action=rule_add" \ --data-urlencode "pattern=exp/*" --data-urlencode "role=w" \ $BASE/alice/demo/settings curl -s -b "gt_session=$CK" $BASE/alice/demo/settings > $T/sett3 has "throwaway rule added" "exp/*" $T/sett3 RID=$(grep -oE 'name=id value=[0-9]+' $T/sett3 | grep -oE '[0-9]+$' \ | head -1) curl -s -o /dev/null -b "gt_session=$CK" -X POST \ -d "csrf=$CK&action=rule_rm&id=$RID" $BASE/alice/demo/settings curl -s -b "gt_session=$CK" $BASE/alice/demo/settings > $T/sett4 nth "throwaway rule removed" "exp/\*" $T/sett4 echo "== web commits (new-file / edit / upload) ==" # writer with rank w, no session cookie -> Basic auth, no csrf needed curl -s -o /dev/null -u bob:secretpw2 -X POST \ --data-urlencode "ref=bobfeat" --data-urlencode "path=web.txt" \ --data-urlencode "content=from the browser" \ --data-urlencode "msg=web new file" \ -w '%{http_code}' $BASE/alice/demo/new-file > $T/wc1 ck "web new-file -> 302" "302" "$(cat $T/wc1)" curl -s "$BASE/alice/demo/blob?ref=bobfeat&path=web.txt&raw=1" > $T/wc2 has "web blob content" "from the browser" $T/wc2 curl -s "$BASE/alice/demo/log" > $T/wc3 # trace graph: the commit DAG renders as SVG lanes, nodes are links has "trace graph rows" "class=tg><svg" $T/wc3 has "trace graph node" "<circle" $T/wc3 has "graph node links" "/commit/" $T/wc3 # repo identity stats on every repo page (repostat bar) has "stats contributors" "contributor" $T/wc3 has "stats top author" "top:" $T/wc3 has "stats first commit" "first commit" $T/wc3 # the default-branch log may not show bobfeat; fetch the blob instead curl -s -u bob:secretpw2 -X POST \ --data-urlencode "ref=bobfeat" --data-urlencode "path=web.txt" \ --data-urlencode "content=edited in browser" \ -o /dev/null -w '%{http_code}' $BASE/alice/demo/edit > $T/wc4 ck "web edit -> 302" "302" "$(cat $T/wc4)" curl -s "$BASE/alice/demo/blob?ref=bobfeat&path=web.txt&raw=1" > $T/wc5 has "edited content" "edited in browser" $T/wc5 # multipart upload: dir-only path picks up the client filename printf 'web upload \x00\x01\x02 tail' > $T/up.bin # relative -F path: MSYS path conversion can't see inside file=@<p> (cd $T && curl -s -o /dev/null -u bob:secretpw2 -X POST \ -F "ref=bobfeat" -F "path=files/" -F "msg=web upload" \ -F "file=@up.bin;filename=data.bin" \ -w '%{http_code}' $BASE/alice/demo/upload > wc6) ck "web upload -> 302" "302" "$(cat $T/wc6)" curl -s "$BASE/alice/demo/blob?ref=bobfeat&path=files/data.bin&raw=1" \ > $T/wc7 has "upload content" "web upload" $T/wc7 # policy parity with pushes: protected master rejects rank w curl -s -u bob:secretpw2 -X POST \ --data-urlencode "ref=master" --data-urlencode "path=pwn.txt" \ --data-urlencode "content=x" \ $BASE/alice/demo/new-file > $T/wc8 has "protected branch web commit denied" "protected branch" $T/wc8 # readers and anonymous users cannot reach the write surface curl -s -o /dev/null -w '%{http_code}' $BASE/alice/demo/new-file > $T/wc9 ck "anon new-file -> 302 login" "302" "$(cat $T/wc9)" curl -s -o /dev/null -w '%{http_code}' -u carol:carolpass9 \ $BASE/alice/demo/new-file > $T/wc10 ck "reader new-file -> 404" "404" "$(cat $T/wc10)" # csrf + path hygiene on session-authed posts curl -s -o /dev/null -b "gt_session=$CK" -X POST \ -d "ref=bobfeat&path=x.txt&content=hi" \ -w '%{http_code}' $BASE/alice/demo/new-file > $T/wc11 ck "web commit no csrf -> 403" "403" "$(cat $T/wc11)" curl -s -o /dev/null -b "gt_session=$CK" -X POST \ --data-urlencode "csrf=$CK" --data-urlencode "ref=bobfeat" \ --data-urlencode "path=../evil" --data-urlencode "content=x" \ -w '%{http_code}' $BASE/alice/demo/new-file > $T/wc12 ck "web commit traversal -> 400" "400" "$(cat $T/wc12)" curl -s -o /dev/null -b "gt_session=$CK" -X POST \ --data-urlencode "csrf=$CK" --data-urlencode "ref=bobfeat" \ --data-urlencode "path=.git/hooks/pwn" --data-urlencode "content=x" \ -w '%{http_code}' $BASE/alice/demo/new-file > $T/wc13 ck "web commit .git path -> 400" "400" "$(cat $T/wc13)" echo "== scrypt KDF + hash migration ==" MSYS_NO_PATHCONV=1 docker exec -e GIT_TRACE_REPOS=/srv/repos \ -e GIT_TRACE_DB=/srv/var/gt.db $CT /src/git-trace kdf-selftest > $T/kdf has "scrypt vector 1" "vector 1: ok" $T/kdf has "scrypt vector 2" "vector 2: ok" $T/kdf has "scrypt vector 3" "vector 3: ok" $T/kdf gtpw() { MSYS_NO_PATHCONV=1 docker exec -e GIT_TRACE_REPOS=/srv/repos \ -e GIT_TRACE_DB=/srv/var/gt.db $CT /src/git-trace pwhash-status "$1"; } ck "new users default to scrypt" "scrypt" "$(gtpw alice)" # a legacy pbkdf2 row still verifies, and upgrades on next login MSYS_NO_PATHCONV=1 docker exec -e GIT_TRACE_REPOS=/srv/repos \ -e GIT_TRACE_DB=/srv/var/gt.db -e GIT_TRACE_KDF=pbkdf2 $CT \ sh -c 'echo davepw99 | /src/git-trace useradd dave' ck "legacy user minted on pbkdf2" "pbkdf2" "$(gtpw dave)" curl -s -D- -o /dev/null -X POST -d "name=dave&pass=davepw99" \ $BASE/login > $T/dl1 has "legacy login still works" "gt_session" $T/dl1 ck "rehash-on-login upgrades" "scrypt" "$(gtpw dave)" curl -s -D- -o /dev/null -X POST -d "name=dave&pass=davepw99" \ $BASE/login > $T/dl2 has "scrypt login works" "gt_session" $T/dl2 curl -s -X POST -d "name=dave&pass=wrongpw" $BASE/login > $T/dl3 has "wrong password still denied" "invalid credentials" $T/dl3 echo "== billing tiers (manual entitlement) ==" GT() { MSYS_NO_PATHCONV=1 docker exec -e GIT_TRACE_REPOS=/srv/repos \ -e GIT_TRACE_DB=/srv/var/gt.db $CT /src/git-trace "$@"; } ck "dave starts free" "dave: tier=free" "$(GT tier dave)" DKC=$(curl -s -D- -o /dev/null -X POST -d "name=dave&pass=davepw99" \ $BASE/login | grep -oi 'gt_session=[0-9a-f]*' | head -1 | cut -d= -f2) curl -s -b "gt_session=$DKC" $BASE/new > $T/nf has "free sees tier hint" "requires the <b>pro</b> tier" $T/nf curl -s -b "gt_session=$DKC" -X POST \ -d "csrf=$DKC&name=dvpriv&private=on" $BASE/new > $T/dn1 has "free private repo denied" "require the pro tier" $T/dn1 curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$DKC" \ $BASE/dave/dvpriv/tree > $T/dc ck "no repo created" 404 "$(cat $T/dc)" GT tier dave pro +30 > $T/ts has "tier set confirms" "tier=pro until" $T/ts GT tier dave > $T/tq has "tier query shows expiry" "tier=pro (expires" $T/tq curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$DKC" -X POST \ -d "csrf=$DKC&name=dvpriv&private=on" $BASE/new > $T/dc ck "pro private repo ok" 302 "$(cat $T/dc)" curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$DKC" \ $BASE/dave/dvpriv/tree > $T/dc ck "private repo reachable" 200 "$(cat $T/dc)" # expiry is lazy: a past-dated grant sweeps back to free on next read GT tier dave pro 2020-01-01 > /dev/null ck "expired tier reads free" "dave: tier=free" "$(GT tier dave)" curl -s -b "gt_session=$DKC" -X POST \ -d "csrf=$DKC&name=dvpriv2&private=on" $BASE/new > $T/dn2 has "expired tier denied" "require the pro tier" $T/dn2 GT tier dave pro never > /dev/null curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$DKC" -X POST \ -d "csrf=$DKC&name=dvpriv2&private=on" $BASE/new > $T/dc ck "pro no-expiry ok" 302 "$(cat $T/dc)" GT tier dave bogus > /dev/null 2>&1 && echo bad > $T/tb nth "bogus tier rejected" "bad" $T/tb GT tier nobody > /dev/null 2>&1 && echo bad > $T/tb nth "tier on missing user" "bad" $T/tb # the audit journal records every entitlement change curl -s -b "gt_session=$CKA" $BASE/audit > $T/tau has "tier_change audited" "tier_change" $T/tau has "tier_denied audited" "tier_denied" $T/tau echo "== account self-service (/account) ==" curl -s -o /dev/null -w '%{http_code}' $BASE/account > $T/code ck "anon /account -> redirect" 302 "$(cat $T/code)" curl -s -b "gt_session=$DKC" $BASE/account > $T/acct has "account page" "api tokens" $T/acct has "ssh keys section" "ssh keys" $T/acct has "tier shown" "tier <b>pro" $T/acct # email (feeds mail-in From auth) curl -s -b "gt_session=$DKC" -X POST \ -d "csrf=$DKC&action=email&email=dave@e2e.test" $BASE/account > $T/ae has "email set" "email updated" $T/ae curl -s -b "gt_session=$DKC" -X POST \ -d "csrf=$DKC&action=email&email=not-an-email" $BASE/account > $T/aeb has "bad email rejected" "invalid email" $T/aeb # token lifecycle: create -> auth -> revoke curl -s -b "gt_session=$DKC" -X POST \ -d "csrf=$DKC&action=tok_add&label=laptop" $BASE/account > $T/ta has "token shown once" "new token" $T/ta WTOK=$(grep -oE 'new token \(shown once\): <code class=mono>[0-9a-f]{64}' \ $T/ta | grep -oE '[0-9a-f]{64}') curl -s -o /dev/null -w '%{http_code}' -u "dave:$WTOK" \ "$BASE/dave/dvpriv.git/info/refs?service=git-upload-pack" > $T/code ck "web token authenticates git" 200 "$(cat $T/code)" TID=$(curl -s -b "gt_session=$DKC" $BASE/account \ | grep -oE 'value=tok_rm><input type=hidden name=id value=[0-9]+' \ | grep -oE '[0-9]+$' | tail -1) curl -s -b "gt_session=$DKC" -X POST \ -d "csrf=$DKC&action=tok_rm&id=$TID" $BASE/account > $T/tr has "token revoked" "token revoked" $T/tr curl -s -o /dev/null -w '%{http_code}' -u "dave:$WTOK" \ "$BASE/dave/dvpriv.git/info/refs?service=git-upload-pack" > $T/code ck "revoked token denied" 401 "$(cat $T/code)" # ssh key lifecycle: add -> visible to ssh machinery -> remove curl -s -b "gt_session=$DKC" -X POST \ --data-urlencode "csrf=$DKC" --data-urlencode "action=key_add" \ --data-urlencode "key=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILvMockKeyDataForE2E dave@e2e" \ --data-urlencode "label=webkey" $BASE/account > $T/ka has "key added" "key added" $T/ka GT sshkey list > $T/skl has "key reaches ssh keyfile" "webkey" $T/skl KID=$(curl -s -b "gt_session=$DKC" $BASE/account \ | grep -oE 'value=key_rm><input type=hidden name=id value=[0-9]+' \ | grep -oE '[0-9]+$' | tail -1) curl -s -b "gt_session=$DKC" -X POST \ -d "csrf=$DKC&action=key_rm&id=$KID" $BASE/account > $T/kr has "key removed" "key removed" $T/kr GT sshkey list > $T/skl2 nth "key gone from list" "webkey" $T/skl2 curl -s -b "gt_session=$DKC" -X POST \ --data-urlencode "csrf=$DKC" --data-urlencode "action=key_add" \ --data-urlencode "key=not-a-key" $BASE/account > $T/kb has "bogus key rejected" "invalid or duplicate" $T/kb # password change: wrong current is refused, right one rotates curl -s -b "gt_session=$DKC" -X POST \ -d "csrf=$DKC&action=passwd&cur=nope&pass=newdavepw1&pass2=newdavepw1" \ $BASE/account > $T/pf has "wrong current denied" "current password incorrect" $T/pf curl -s -b "gt_session=$DKC" -X POST \ -d "csrf=$DKC&action=passwd&cur=davepw99&pass=newdavepw1&pass2=newdavepw1" \ $BASE/account > $T/pp has "password updated" "password updated" $T/pp curl -s -D- -o /dev/null -X POST -d "name=dave&pass=newdavepw1" \ $BASE/login > $T/nl has "new password works" "gt_session" $T/nl # repo delete: requires typing the name curl -s -o /dev/null -b "gt_session=$DKC" -X POST \ -d "csrf=$DKC&name=throwaway" $BASE/new curl -s -b "gt_session=$DKC" -X POST \ -d "csrf=$DKC&action=delete&confirm=wrong" \ $BASE/dave/throwaway/settings > /dev/null curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$DKC" \ $BASE/dave/throwaway/tree > $T/code ck "wrong confirm keeps repo" 200 "$(cat $T/code)" curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$DKC" -X POST \ -d "csrf=$DKC&action=delete&confirm=throwaway" \ $BASE/dave/throwaway/settings > $T/code ck "delete redirects" 302 "$(cat $T/code)" curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$DKC" \ $BASE/dave/throwaway/tree > $T/code ck "repo gone" 404 "$(cat $T/code)" curl -s -b "gt_session=$CKA" $BASE/audit > $T/dau has "repo_delete audited" "repo_delete" $T/dau echo "== multi-tenancy hardening ==" # ownerless (_/) repos: managed by site admins, not by ordinary users; # a global admin must NOT gain read on user-owned private repos GT mkrepo _ site "platform repo" > /dev/null curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$CKA" \ $BASE/_/site/settings > $T/code ck "admin manages _/ repo" 200 "$(cat $T/code)" curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$CK" \ $BASE/_/site/settings > $T/code ck "non-admin denied _/ settings" 403 "$(cat $T/code)" curl -s -o /dev/null -w '%{http_code}' $BASE/_/site/tree > $T/code ck "_ repo stays public read" 200 "$(cat $T/code)" curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$CKA" \ $BASE/alice/priv/tree > $T/code ck "admin cannot read private repo" 404 "$(cat $T/code)" curl -s -o /dev/null -w '%{http_code}' -b "gt_session=$CKA" -X POST \ -d "csrf=$CKA&action=delete&confirm=site" \ $BASE/_/site/settings > $T/code ck "admin deletes _ repo" 302 "$(cat $T/code)" curl -s -o /dev/null -w '%{http_code}' $BASE/_/site/tree > $T/code ck "_ repo gone" 404 "$(cat $T/code)" echo "== UX: highlight, atom feed, code search ==" # land a .c file on master (ff-only rule -> base on fetched master) git -C $T/demo fetch -q \ "http://alice:secretpw1@localhost:$PORT/alice/demo.git" master \ 2>/dev/null git -C $T/demo checkout -qb uxtest FETCH_HEAD printf '/* block */\n#include <stdio.h>\nint main(void) {\n // line comment\n char *s = "hi <there>";\n return 42;\n}\n' > $T/demo/probe.c git -C $T/demo add probe.c \ && git -C $T/demo -c user.name=t -c user.email=t@t commit -qm probe \ && git -C $T/demo push -q \ "http://alice:secretpw1@localhost:$PORT/alice/demo.git" HEAD:master curl -s "$BASE/alice/demo/blob?path=probe.c&ref=master" > $T/hl has "keyword span" "<span class=hl-k>int</span>" $T/hl has "comment span" "<span class=hl-c>" $T/hl has "string span" "<span class=hl-s>" $T/hl has "number span" "<span class=hl-n>42</span>" $T/hl has "preproc span" "<span class=hl-p>#include" $T/hl has "string escaped" "<there>" $T/hl nth "no raw < in src" "hi <there>" $T/hl curl -s "$BASE/alice/demo/blob?path=feat.txt&ref=master" > $T/h2 nth "no spans on .txt" "class=hl-" $T/h2 curl -s "$BASE/alice/demo/feed" > $T/feedh has "feed page" "subscribe" $T/feedh has "feed atom url" "feed.atom" $T/feedh has "feed tab active" "feed" $T/feedh curl -s -D $T/fh "$BASE/alice/demo/feed.atom" -o $T/feed has "atom content-type" "application/atom+xml" $T/fh has "atom stylesheet PI" "xml-stylesheet" $T/feed has "atom relaxed csp" "default-src 'self'" $T/fh curl -s -o $T/xsl -w '%{http_code}' $BASE/feed.xsl > $T/code ck "feed.xsl served" 200 "$(cat $T/code)" has "xsl is atom-aware" "xmlns:a='http://www.w3.org/2005/Atom'" $T/xsl has "atom feed tag" "<feed xmlns" $T/feed has "atom entry" "<entry>" $T/feed has "atom commit link" "/alice/demo/commit/" $T/feed has "atom rfc3339" "T" $T/feed curl -s -o /dev/null -w '%{http_code}' $BASE/alice/priv/feed.atom > $T/code ck "private feed hidden" 404 "$(cat $T/code)" curl -s -o /dev/null -w '%{http_code}' $BASE/alice/priv/feed > $T/code ck "private feed page hidden" 404 "$(cat $T/code)" curl -s "$BASE/alice/demo/search?q=main&ref=master" > $T/se has "search hit" "probe.c" $T/se has "search blob link" "/alice/demo/blob?path=probe.c" $T/se curl -s "$BASE/alice/demo/search?q=zzz_nomatch&ref=master" > $T/sn has "no matches shown" "no matches" $T/sn curl -s -o /dev/null -w '%{http_code}' \ "$BASE/alice/priv/search?q=x" > $T/code ck "private search hidden" 404 "$(cat $T/code)" echo "== static export ==" GT export -o /srv/var/xp -n 5 alice/demo > $T/xo has "explicit export line" "alice/demo -> " $T/xo MSYS_NO_PATHCONV=1 docker exec $CT test -f \ /srv/var/xp/alice/demo/index.html && ok "repo index written" \ || bad "repo index written" "missing" MSYS_NO_PATHCONV=1 docker exec $CT test -f \ /srv/var/xp/alice/demo/log.html && ok "log page written" \ || bad "log page written" "missing" MSYS_NO_PATHCONV=1 docker exec $CT sh -c \ 'ls /srv/var/xp/alice/demo/commit/*.html | head -1' > $T/xc has "commit page written" ".html" $T/xc MSYS_NO_PATHCONV=1 docker exec $CT cat \ /srv/var/xp/alice/demo/file/probe.c.html > $T/xf has "file page highlighted" "class=hl-" $T/xf GT export -o /srv/var/deep/nest/dir -n 2 bob/work > $T/xn has "nested -o works" "bob/work -> " $T/xn GT export -o /srv/var/xall -n 3 > $T/xa has "auto lists public" "alice/demo -> " $T/xa has "auto lists bob" "bob/work -> " $T/xa nth "auto skips private" "alice/priv -> " $T/xa nth "auto skips dave priv" "dave/dvpriv -> " $T/xa GT export --private -o /srv/var/xprv -n 1 > $T/xp2 has "--private includes" "alice/priv -> " $T/xp2 MSYS_NO_PATHCONV=1 docker exec $CT cat /srv/var/xall/index.html > $T/xi has "root index links repo" "alice/demo/index.html" $T/xi echo "== webhooks ==" # the forge doubles as receiver: POST /login always returns 200, # which exercises the in-tree HTTP client end-to-end curl -s -o /dev/null -b "gt_session=$CK" -X POST \ -d "csrf=$CK&action=wh_add&url=http://localhost:8081/login&secret=s3cret&ev_push=on" \ $BASE/alice/demo/settings curl -s -b "gt_session=$CK" $BASE/alice/demo/settings > $T/ws has "webhook listed" "localhost:8081/login" $T/ws # push -> post-receive -> hook-post -> enqueue + detached dispatch git -C $T/demo push -q \ "http://alice:secretpw1@localhost:$PORT/alice/demo.git" \ master:refs/heads/whprobe 2>/dev/null sleep 1 GT webhook-run > /dev/null 2>&1 curl -s -b "gt_session=$CK" $BASE/alice/demo/settings > $T/ws2 has "delivery ok" ">ok<" $T/ws2 has "delivery http 200" ">200<" $T/ws2 WID=$(grep -o 'name=id value=[0-9]*' $T/ws2 | head -1 | grep -o '[0-9]*') [ -n "$WID" ] && curl -s -o /dev/null -b "gt_session=$CK" -X POST \ -d "csrf=$CK&action=wh_del&id=$WID" $BASE/alice/demo/settings curl -s -b "gt_session=$CK" $BASE/alice/demo/settings > $T/ws3 nth "webhook removed" "localhost:8081/login" $T/ws3 # https endpoint without curl in the image -> graceful fail row curl -s -o /dev/null -b "gt_session=$CK" -X POST \ -d "csrf=$CK&action=wh_add&url=https://example.invalid/hook&ev_push=on" \ $BASE/alice/demo/settings git -C $T/demo push -q \ "http://alice:secretpw1@localhost:$PORT/alice/demo.git" \ master:refs/heads/whprobe2 2>/dev/null sleep 1 GT webhook-run > /dev/null 2>&1 curl -s -b "gt_session=$CK" $BASE/alice/demo/settings > $T/ws4 has "https w/o curl fails" ">fail<" $T/ws4 echo "== ci runner ==" # opt-in via settings (meta keeps public=on) curl -s -o /dev/null -b "gt_session=$CK" -X POST \ -d "csrf=$CK&action=meta&desc=public+repo&public=on&ci=on" \ $BASE/alice/demo/settings # local master is stale after earlier pushes — base on FETCH_HEAD git -C $T/demo fetch -q \ "http://alice:secretpw1@localhost:$PORT/alice/demo.git" master \ && git -C $T/demo checkout -qb citest FETCH_HEAD printf 'echo GTCI-PASS-MARKER\nexit 0\n' > $T/demo/.git-trace-ci git -C $T/demo add .git-trace-ci \ && git -C $T/demo -c user.email=t@t -c user.name=t \ commit -qm "add ci script" \ && git -C $T/demo push \ "http://alice:secretpw1@localhost:$PORT/alice/demo.git" \ HEAD:master > $T/pc 2>&1 has "ci script push lands" "HEAD -> master" $T/pc sleep 1 GT ci-run > /dev/null 2>&1 curl -s -b "gt_session=$CK" $BASE/alice/demo/ci > $T/ci1 has "ci run pass" ">pass<" $T/ci1 CID=$(grep -o "/alice/demo/ci/[0-9]*" $T/ci1 | head -1 | grep -o '[0-9]*$') curl -s -b "gt_session=$CK" $BASE/alice/demo/ci/$CID > $T/ci2 has "ci log captured" "GTCI-PASS-MARKER" $T/ci2 # a hostile/buggy script on a feature branch CANNOT hijack the # pipeline: the runner only reads .git-trace-ci from master git -C $T/demo checkout -qb cihack printf 'echo SHOULD-NEVER-RUN\nexit 1\n' > $T/demo/.git-trace-ci git -C $T/demo add .git-trace-ci \ && git -C $T/demo -c user.email=t@t -c user.name=t \ commit -qm "evil ci" \ && git -C $T/demo push -q \ "http://alice:secretpw1@localhost:$PORT/alice/demo.git" \ cihack 2>/dev/null sleep 1 GT ci-run > /dev/null 2>&1 curl -s -b "gt_session=$CK" $BASE/alice/demo/ci > $T/ci3 nth "branch cannot hijack" "SHOULD-NEVER-RUN" $T/ci3 CID2=$(grep -o "/alice/demo/ci/[0-9]*" $T/ci3 | head -1 | grep -o '[0-9]*$') curl -s -b "gt_session=$CK" $BASE/alice/demo/ci/$CID2 > $T/ci4 has "branch run still passes" "GTCI-PASS-MARKER" $T/ci4 # failing script on master -> fail status git -C $T/demo fetch -q \ "http://alice:secretpw1@localhost:$PORT/alice/demo.git" master \ && git -C $T/demo checkout -qb cibreak FETCH_HEAD printf 'echo GTCI-FAIL-MARKER\nexit 1\n' > $T/demo/.git-trace-ci git -C $T/demo add .git-trace-ci \ && git -C $T/demo -c user.email=t@t -c user.name=t \ commit -qm "break ci" \ && git -C $T/demo push \ "http://alice:secretpw1@localhost:$PORT/alice/demo.git" \ HEAD:master > $T/pc2 2>&1 has "break ci push lands" "HEAD -> master" $T/pc2 sleep 1 GT ci-run > /dev/null 2>&1 curl -s -b "gt_session=$CK" $BASE/alice/demo/ci > $T/ci5 has "ci run fail" ">fail<" $T/ci5 # non-enabled repo: no tab, informative page curl -s -b "gt_session=$CK" $BASE/bob/work/tree > $T/wt nth "no ci tab when off" "/ci'" $T/wt curl -s -b "gt_session=$CK" $BASE/bob/work/ci > $T/ci6 has "ci disabled notice" "not enabled" $T/ci6 # restore a green pipeline for later runs git -C $T/demo fetch -q \ "http://alice:secretpw1@localhost:$PORT/alice/demo.git" master \ && git -C $T/demo checkout -qb cifix FETCH_HEAD printf 'echo GTCI-PASS-MARKER\nexit 0\n' > $T/demo/.git-trace-ci git -C $T/demo add .git-trace-ci \ && git -C $T/demo -c user.email=t@t -c user.name=t \ commit -qm "fix ci" \ && git -C $T/demo push -q \ "http://alice:secretpw1@localhost:$PORT/alice/demo.git" \ HEAD:master 2>/dev/null sleep 1 GT ci-run > /dev/null 2>&1 echo "== v1.0 UX: dynamic nav, index search, dashboard ==" # anonymous header: log in + register (GIT_TRACE_OPEN_REG=1) curl -s $BASE/ > $T/na0 has "anon nav: log in" "href='/login'" $T/na0 has "anon nav: register" "href='/register'" $T/na0 nth "anon nav: no logout" "log out" $T/na0 # logged-in header curl -s -b "gt_session=$CK" $BASE/ > $T/na1 has "auth nav: account link" "class=who" $T/na1 has "auth nav: + new repo" "href='/new'" $T/na1 has "auth nav: log out" "href='/logout'" $T/na1 # instance metrics line has "index metrics" "hosted" $T/na1 # global search filters curl -s "$BASE/?q=demo" > $T/q1 has "search keeps match" "alice/demo" $T/q1 nth "search filters others" "bob/work" $T/q1 curl -s "$BASE/?q=no-such-thing" > $T/q9 has "empty search msg" "No repositories match" $T/q9 # ordering: most recently touched repo sorts first — # push a throwaway branch to alice/demo, then it must lead the table git -C $T/demo push -q "http://alice:secretpw1@localhost:$PORT/alice/demo.git" \ HEAD:refs/heads/uxprobe 2>/dev/null curl -s $BASE/ > $T/na2 FIRST=$(grep -oE "href='/[A-Za-z0-9._/-]+/tree'" $T/na2 | head -1) ck "recent repo first" "href='/alice/demo/tree'" "$FIRST" # dashboard: own repos + shared repos listed on /account curl -s -b "gt_session=$CK" $BASE/account > $T/acct2 has "acct: your repos" "your repositories" $T/acct2 has "acct: own repo listed" "alice / demo" $T/acct2 has "acct: settings link" "/alice/demo/settings" $T/acct2 echo "== mirrors / federation ==" GT mkrepo alice mirror # file:// must be refused (local path exfiltration) curl -s -o /dev/null -b "gt_session=$CK" -X POST \ -d "csrf=$CK&action=mir_add&url=file:///srv/repos/alice/demo.git" \ $BASE/alice/mirror/settings curl -s -b "gt_session=$CK" $BASE/alice/mirror/settings > $T/ms0 nth "file:// url refused" "file:///srv/repos" $T/ms0 # real mirror: upstream demo over loopback smart-http, trigger secret set curl -s -o /dev/null -b "gt_session=$CK" -X POST \ -d "csrf=$CK&action=mir_add&url=http://localhost:8081/alice/demo.git&secret=mirs3c" \ $BASE/alice/mirror/settings curl -s -b "gt_session=$CK" $BASE/alice/mirror/settings > $T/ms1 has "mirror listed" "localhost:8081/alice/demo" $T/ms1 has "default refspec" "refs/mirror/" $T/ms1 # CLI sync: refs land under refs/mirror/, not refs/heads GT mirror-sync alice/mirror > $T/mso 2>&1 has "sync ran" "alice/mirror <-" $T/mso MREFS=$(MSYS_NO_PATHCONV=1 docker exec $CT \ git -C /srv/repos/alice/mirror.git for-each-ref --format='%(refname)' \ refs/mirror refs/heads | tr '\n' ' ') case "$MREFS" in *refs/mirror/master*) ck "mirror refs present" ok ok ;; *) ck "mirror refs present" "refs/mirror/master" "$MREFS" ;; esac case "$MREFS" in *refs/heads/*) ck "no local branch leak" "mirror-only" "$MREFS" ;; *) ck "no local branch leak" ok ok ;; esac # trigger endpoint: no mirrors on bob/work -> 404 (no existence oracle) code=$(curl -s -o /dev/null -w '%{http_code}' -X POST \ $BASE/bob/work/mirror-sync) ck "no mirrors -> 404" 404 "$code" # bad signature -> 403 BAD=$(printf 'x%.0s' $(seq 64)) code=$(curl -s -o /dev/null -w '%{http_code}' -X POST \ -H "X-Git-Trace-Signature: sha256=$BAD" -d '{}' \ $BASE/alice/mirror/mirror-sync) ck "bad signature -> 403" 403 "$code" # event-driven federation: push webhook on demo -> trigger -> detached sync curl -s -o /dev/null -b "gt_session=$CK" -X POST \ -d "csrf=$CK&action=wh_add&url=http://localhost:8081/alice/mirror/mirror-sync&secret=mirs3c&ev_push=on" \ $BASE/alice/demo/settings git -C $T/demo fetch -q \ "http://alice:secretpw1@localhost:$PORT/alice/demo.git" master \ && git -C $T/demo checkout -qb mirprobe FETCH_HEAD \ && git -C $T/demo -c user.email=t@t -c user.name=t \ commit -q --allow-empty -m "mirror probe" \ && git -C $T/demo push -q \ "http://alice:secretpw1@localhost:$PORT/alice/demo.git" \ HEAD:refs/heads/mirprobe 2>/dev/null # push -> webhook-run (detached) -> POST trigger -> mirror-sync (detached) ok="" for i in $(seq 1 10); do sleep 1 MREFS=$(MSYS_NO_PATHCONV=1 docker exec $CT \ git -C /srv/repos/alice/mirror.git for-each-ref \ --format='%(refname)' refs/mirror | tr '\n' ' ') case "$MREFS" in *mirprobe*) ok=1; break ;; esac done ck "webhook-driven sync" 1 "${ok:-0}" # refs page exposes mirror refs as read-only curl -s $BASE/alice/mirror/refs > $T/mr has "refs page: mirror ref" "refs/mirror/master" $T/mr has "refs page: read-only" "read-only mirror" $T/mr # last sync status surfaced in settings curl -s -b "gt_session=$CK" $BASE/alice/mirror/settings > $T/ms2 has "settings: sync status" "refs" $T/ms2 echo "== M15b: push mirroring ==" # downstream repo over loopback smart-http, creds in the url curl -s -o /dev/null -b "gt_session=$CK" -X POST \ -d "csrf=$CK&name=down" $BASE/new # bad push refspecs rejected: dst not heads, src not heads curl -s -o /dev/null -b "gt_session=$CK" -X POST \ -d "csrf=$CK&action=mir_add&dir=push&url=http://localhost:8081/alice/down.git&refspec=%2Brefs%2Fheads%2F%2A%3Arefs%2Fmirror%2F%2A" \ $BASE/alice/demo/settings curl -s -o /dev/null -b "gt_session=$CK" -X POST \ -d "csrf=$CK&action=mir_add&dir=push&url=http://localhost:8081/alice/down.git&refspec=%2Brefs%2Fmirror%2F%2A%3Arefs%2Fheads%2F%2A" \ $BASE/alice/demo/settings # real push mirror: creds embedded — UI must mask the password curl -s -o /dev/null -b "gt_session=$CK" -X POST \ -d "csrf=$CK&action=mir_add&dir=push&url=http%3A%2F%2Falice%3Asecretpw1%40localhost%3A8081%2Falice%2Fdown.git" \ $BASE/alice/demo/settings curl -s -b "gt_session=$CK" $BASE/alice/demo/settings > $T/pm has "push mirror listed" "alice:\*\*\*@localhost" $T/pm nth "push creds not leaked" "alice:secretpw1" $T/pm # manual run: all heads fan out to the downstream repo GT mirror-push alice/demo > $T/pmo 2>&1 has "push-mirror ran" "alice/demo ->" $T/pmo DREFS=$(MSYS_NO_PATHCONV=1 docker exec $CT \ git -C /srv/repos/alice/down.git for-each-ref \ --format='%(refname)' refs/heads | tr '\n' ' ') case "$DREFS" in *refs/heads/master*) ck "heads pushed" ok ok ;; *) ck "heads pushed" "refs/heads/master" "$DREFS" ;; esac # event-driven: a real push on demo triggers mirror-push via # post-receive — the new branch shows up downstream on its own git -C $T/demo fetch -q \ "http://alice:secretpw1@localhost:$PORT/alice/demo.git" master \ && git -C $T/demo checkout -qb pushmirror FETCH_HEAD \ && git -C $T/demo -c user.email=t@t -c user.name=t \ commit -q --allow-empty -m "push mirror probe" \ && git -C $T/demo push -q \ "http://alice:secretpw1@localhost:$PORT/alice/demo.git" \ HEAD:refs/heads/pushmirror 2>/dev/null ok="" for i in $(seq 1 10); do sleep 1 DREFS=$(MSYS_NO_PATHCONV=1 docker exec $CT \ git -C /srv/repos/alice/down.git for-each-ref \ --format='%(refname)' refs/heads | tr '\n' ' ') case "$DREFS" in *pushmirror*) ok=1; break ;; esac done ck "post-receive fans out" 1 "${ok:-0}" # loop convergence: down -> demo push mirror must not ping-pong curl -s -o /dev/null -b "gt_session=$CK" -X POST \ -d "csrf=$CK&action=mir_add&dir=push&url=http%3A%2F%2Falice%3Asecretpw1%40localhost%3A8081%2Falice%2Fdemo.git" \ $BASE/alice/down/settings git -C $T/demo push -q \ "http://alice:secretpw1@localhost:$PORT/alice/demo.git" \ HEAD:refs/heads/loopprobe 2>/dev/null sleep 6 # let the fan-out + bounce-back settle ck "loop converges: server alive" 200 \ "$(curl -s -o /dev/null -w '%{http_code}' $BASE/)" DREFS=$(MSYS_NO_PATHCONV=1 docker exec $CT \ git -C /srv/repos/alice/down.git for-each-ref \ --format='%(refname)' refs/heads | tr '\n' ' ') case "$DREFS" in *loopprobe*) ck "loop ref arrived" ok ok ;; *) ck "loop ref arrived" "loopprobe" "$DREFS" ;; esac echo "== ops: doctor + backup ==" MSYS_NO_PATHCONV=1 docker exec $CT sh -c \ 'GIT_TRACE_REPOS=/srv/repos GIT_TRACE_DB=/srv/var/gt.db \ /src/git-trace doctor' > $T/doc 2>&1 has "doctor: db ok" "ok \] database" $T/doc has "doctor: repos ok" "ok \] repos" $T/doc has "doctor: no fails" "0 checks failed" $T/doc MSYS_NO_PATHCONV=1 docker exec $CT sh -c \ 'GIT_TRACE_REPOS=/srv/repos GIT_TRACE_DB=/srv/var/gt.db \ /src/git-trace backup /tmp/e2ebk' > $T/bk 2>&1 has "backup ran" "backup complete" $T/bk MSYS_NO_PATHCONV=1 docker exec $CT sh -c \ 'test -f /tmp/e2ebk/git-trace.db && test -f /tmp/e2ebk/repos.tar.gz \ && echo ok' > $T/bkf has "backup artifacts" "ok" $T/bkf echo "== version ==" MSYS_NO_PATHCONV=1 docker exec $CT /src/git-trace --version > $T/ver has "version banner" "git-trace v" $T/ver has "build sha" "(" $T/ver echo "== security headers + rate limit ==" curl -s -D$T/h -o /dev/null $BASE/ has "X-Frame-Options" "X-Frame-Options" $T/h has "nosniff" "X-Content-Type-Options" $T/h has "CSP" "Content-Security-Policy" $T/h code="" for i in 1 2 3 4 5 6; do code=$(curl -s -o /dev/null -w '%{http_code}' -u alice:WRONG \ "$BASE/alice/demo.git/info/refs?service=git-upload-pack") [ "$code" = 429 ] && break done ck "rate limit ban -> 429" 429 "$code" echo echo "== $pass passed, $fail failed ==" [ $fail -eq 0 ]