diff options
context:
space:
mode:
authorBryce Kwon <bryce@brycekwon.com>
committerBryce Kwon <bryce@brycekwon.com>
commit
parent
tree
download
Test the hardening pass
-rwxr-xr-xtests/setup.sh18
-rwxr-xr-xtests/t0002-html-validity.sh3
-rwxr-xr-xtests/t0003-cache.sh9
-rwxr-xr-xtests/t0004-docs.sh3
-rwxr-xr-xtests/t0110-snapshot.sh4
-rwxr-xr-xtests/t0201-filters.sh9
-rwxr-xr-xtests/t0204-limits.sh7
-rwxr-xr-xtests/t0301-security.sh2
-rwxr-xr-xtests/t0302-home-access.sh5
-rwxr-xr-xtests/t0303-robustness.sh471
10 files changed, 501 insertions, 30 deletions
diff --git a/tests/setup.sh b/tests/setup.sh
index f84b1b6..a1f2d15 100755
--- a/tests/setup.sh
+++ b/tests/setup.sh
@@ -49,11 +49,10 @@ TEST_RESULTS_DIR="$TEST_OUTPUT_DIRECTORY/results"
TEST_RESULTS_BASE="$TEST_RESULTS_DIR/$TEST_NAME$TEST_STRESS_JOB_SFX"
TEST_RESULTS_SAN_DIR="$TEST_RESULTS_BASE.$TEST_RESULTS_SAN_DIR_SFX"
-# The library has moved into the trash directory by now, so paths outside
-# it are anchored to TEST_OUTPUT_DIRECTORY.
-
-# The tests run cgit by name, so the binary just built has to come ahead of any
-# copy already installed. Under Valgrind the wrappers take that place instead.
+# The library has moved into the trash directory by now, so paths outside it
+# are anchored to TEST_OUTPUT_DIRECTORY. The tests run cgit by name, so the
+# binary just built has to come ahead of any copy already installed. Under
+# Valgrind the wrappers take that place instead.
if test -n "$cgit_valgrind"
then
GIT_VALGRIND="$TEST_DIRECTORY/valgrind"
@@ -107,8 +106,7 @@ mkrepo() {
)
}
-setup_repos()
-{
+setup_repos() {
rm -rf cache
mkdir -p cache
mkrepo repos/foo 5 >/dev/null
@@ -171,13 +169,11 @@ EOF
fi
}
-cgit_query()
-{
+cgit_query() {
CGIT_CONFIG="$PWD/cgitrc" QUERY_STRING="$1" cgit
}
-cgit_url()
-{
+cgit_url() {
CGIT_CONFIG="$PWD/cgitrc" QUERY_STRING="url=$1" cgit
}
diff --git a/tests/t0002-html-validity.sh b/tests/t0002-html-validity.sh
index 8b04939..f5e918c 100755
--- a/tests/t0002-html-validity.sh
+++ b/tests/t0002-html-validity.sh
@@ -8,8 +8,7 @@
test_description='Validate html with tidy'
. ./setup.sh
-test_url()
-{
+test_url() {
tidy_options="-eq"
test -z "$NO_TIDY_WARNINGS" || tidy_options="$tidy_options --show-warnings no"
# A second argument names a config of its own, for pages the shared
diff --git a/tests/t0003-cache.sh b/tests/t0003-cache.sh
index 4693988..d3e448f 100755
--- a/tests/t0003-cache.sh
+++ b/tests/t0003-cache.sh
@@ -79,8 +79,7 @@ test_expect_success 'set up a repo with a page larger than the output buffer' '
rm -rf cache2 && mkdir cache2
'
-bigpage_query()
-{
+bigpage_query() {
CGIT_CONFIG="$PWD/bigrc" QUERY_STRING="$1" cgit
}
@@ -149,9 +148,9 @@ test_expect_success 'set up a repo missing a parent object' '
rm -rf cache4 && mkdir cache4
'
-test_expect_success 'an error after output began replays none of it' '
+test_expect_success 'an error after output began ends the page with it and caches nothing' '
CGIT_CONFIG="$PWD/brokenrc" QUERY_STRING="url=broken/commit/&id=$tip" cgit >broken.out &&
- grep "Bad commit" broken.out &&
+ grep "Not a commit" broken.out &&
test $(grep -c "^Status:" broken.out) = 1 &&
ls cache4 >broken.slots &&
test_line_count = 0 broken.slots
@@ -165,7 +164,7 @@ test_expect_success 'an error page leaves no slot behind' '
rm -rf cache3 && mkdir cache3 &&
CGIT_CONFIG="$PWD/bigkeyrc" \
QUERY_STRING="url=bigpage/commit/&id=0123456789abcdef0123456789abcdef01234567" cgit >error.out &&
- grep "Bad commit reference" error.out &&
+ grep "Not a commit" error.out &&
ls cache3 >error.slots &&
test_line_count = 0 error.slots
'
diff --git a/tests/t0004-docs.sh b/tests/t0004-docs.sh
index 65c8275..1eca51e 100755
--- a/tests/t0004-docs.sh
+++ b/tests/t0004-docs.sh
@@ -48,7 +48,8 @@ test_expect_success 'the manual lists its repository settings in order' '
# The policy is spelled out once per server syntax, so nothing but this check
# keeps the three copies from drifting apart.
test_expect_success 'the server configs agree on one content security policy' '
- for server in apache lighttpd nginx; do
+ for server in apache lighttpd nginx
+ do
grep "Content-Security-Policy" "$ROOT/custom/servers/$server.conf" |
grep "default-src" |
sed "s/.*\"\(default-src[^\"]*\)\".*/\1/" || return 1
diff --git a/tests/t0110-snapshot.sh b/tests/t0110-snapshot.sh
index 79fecef..ba38b82 100755
--- a/tests/t0110-snapshot.sh
+++ b/tests/t0110-snapshot.sh
@@ -72,11 +72,11 @@ test_expect_success 'untar and verify the content' '
test_line_count = 1 master/file-5
'
-test_tar_snapshot tar.gz application/gzip gzip GZIP
+test_tar_snapshot tar.gz application/gzip gzip GZIP
test_tar_snapshot tar.bz2 application/x-bzip2 bzip2 BZIP2
test_tar_snapshot tar.lz application/x-lzip lzip LZIP
test_tar_snapshot tar.xz application/x-xz xz XZ
-test_tar_snapshot tar.zst application/zstd zstd ZSTD
+test_tar_snapshot tar.zst application/zstd zstd ZSTD
if command -v unzip >/dev/null 2>&1
then
diff --git a/tests/t0201-filters.sh b/tests/t0201-filters.sh
index 8d67c4f..444932a 100755
--- a/tests/t0201-filters.sh
+++ b/tests/t0201-filters.sh
@@ -69,7 +69,8 @@ test "$CGIT_HAS_LUA" -eq 1 && test_set_prereq CGIT_LUA
test "$CGIT_HAS_LUA" -eq 1 || say 'cgit built without lua, error page check skipped'
# A die inside a lua filter has to reach the visitor as an error page rather
-# than being fed back into the filter that just failed.
+# than being fed back into the filter that just failed, and the reason goes to
+# the log.
test_expect_success CGIT_LUA 'a failing lua filter still renders an error page' '
cat >broken.lua <<-\EOF &&
function filter_open(...) error("boom") end
@@ -82,9 +83,9 @@ test_expect_success CGIT_LUA 'a failing lua filter still renders an error page'
echo "repo.path=$PWD/repos/foo/.git" &&
echo "auth-filter=lua:$PWD/broken.lua"
} >brokenrc &&
- CGIT_CONFIG="$PWD/brokenrc" QUERY_STRING="url=foo/commit" cgit >tmp &&
- grep "Status: 400" tmp &&
- grep "Lua error in" tmp
+ CGIT_CONFIG="$PWD/brokenrc" QUERY_STRING="url=foo/commit" cgit >tmp 2>err &&
+ grep "Status: 500" tmp &&
+ grep "Lua error in" err
'
test_done
diff --git a/tests/t0204-limits.sh b/tests/t0204-limits.sh
index 1195c93..19df797 100755
--- a/tests/t0204-limits.sh
+++ b/tests/t0204-limits.sh
@@ -30,8 +30,11 @@ test_expect_success 'set up limit fixtures' '
test_seq 301 360 >big.c &&
printf "int y;\n" >small.c &&
git commit -am change &&
- for i in 1 2 3; do git branch branch-$i || exit 1; done &&
- for i in 1 2 3; do git tag tag-$i || exit 1; done
+ for i in 1 2 3
+ do
+ git branch branch-$i &&
+ git tag tag-$i || exit 1
+ done
) &&
{
echo "virtual-root=/" &&
diff --git a/tests/t0301-security.sh b/tests/t0301-security.sh
index ece60f1..f1af8e9 100755
--- a/tests/t0301-security.sh
+++ b/tests/t0301-security.sh
@@ -5,7 +5,7 @@
# repository and config that reproduce the original problem and then asks
# for the page that used to mishandle it.
-test_description='Check security fixes and fork-specific behavior'
+test_description='Check security fixes and fork-specific behaviour'
. ./setup.sh
# Most of what follows shares one repository and one config, so the fixture
diff --git a/tests/t0302-home-access.sh b/tests/t0302-home-access.sh
index 89aa00f..e0da1f2 100755
--- a/tests/t0302-home-access.sh
+++ b/tests/t0302-home-access.sh
@@ -24,13 +24,14 @@ strace true 2>/dev/null || {
exit
}
-test_no_home_access () {
+test_no_home_access() {
# A home that happened to exist would be one git may legitimately
# read, leaving the check below with nothing to catch, so extend the
# path until nothing is there.
missing_home="/path/to/some/place/that/does/not/possibly/exist"
depth=0
- while test -d "$missing_home"; do
+ while test -d "$missing_home"
+ do
depth=$((depth + 1))
missing_home="$missing_home/$depth"
done &&
diff --git a/tests/t0303-robustness.sh b/tests/t0303-robustness.sh
new file mode 100755
index 0000000..f7ca26b
--- /dev/null
+++ b/tests/t0303-robustness.sh
@@ -0,0 +1,471 @@
+#!/bin/sh
+
+# Regression tests from the September 2026 audit. Each case is a config,
+# repository or request that used to crash cgit, end it inside git, or hand
+# a visitor something other than what was asked for.
+
+test_description='Check the audit regressions'
+. ./setup.sh
+
+test_expect_success 'set up a repository and a config to vary' '
+ mkrepo repos/rob 3 &&
+ sha=$(git -C repos/rob rev-parse HEAD~1) &&
+ {
+ echo "virtual-root=/" &&
+ echo "cache-size=0" &&
+ echo "snapshots=tar.gz" &&
+ echo "repo.url=rob" &&
+ echo "repo.path=$PWD/repos/rob/.git"
+ } >robrc
+'
+
+robq() { CGIT_CONFIG="$PWD/robrc" QUERY_STRING="$1" cgit; }
+
+test_expect_success 'a repository without a path answers 404' '
+ {
+ echo "virtual-root=/" &&
+ echo "cache-size=0" &&
+ echo "repo.url=nopath" &&
+ echo "repo.url=emptypath" &&
+ echo "repo.path="
+ } >nopathrc &&
+ for r in nopath emptypath
+ do
+ CGIT_CONFIG="$PWD/nopathrc" QUERY_STRING="url=$r/log/" cgit >tmp &&
+ grep "^Status: 404 Not Found" tmp &&
+ grep "Failed to open $r: Not a valid git repository" tmp || return 1
+ done
+'
+
+test_expect_success 'a repository with an empty url is skipped with a warning' '
+ {
+ echo "virtual-root=/" &&
+ echo "cache-size=0" &&
+ echo "repo.url=" &&
+ echo "repo.path=$PWD/repos/rob/.git" &&
+ echo "repo.url=rob" &&
+ echo "repo.path=$PWD/repos/rob/.git"
+ } >nourlrc &&
+ CGIT_CONFIG="$PWD/nourlrc" QUERY_STRING="url=rob/" cgit >tmp 2>err &&
+ grep "^Status: 200" tmp &&
+ grep "Ignoring repository with an empty url" err
+'
+
+test_expect_success 'a scan path that is itself a repository is named after it' '
+ {
+ echo "virtual-root=/" &&
+ echo "cache-size=0" &&
+ echo "remove-suffix=1" &&
+ echo "scan-path=$PWD/repos/rob"
+ } >rootrc &&
+ CGIT_CONFIG="$PWD/rootrc" QUERY_STRING="url=" cgit >tmp &&
+ grep "toplevel-repo.><a href=./rob/.>rob</a>" tmp
+'
+
+test_expect_success 'a repo key after scan-path is reported instead of applied' '
+ {
+ echo "virtual-root=/" &&
+ echo "cache-size=0" &&
+ echo "repo.url=first" &&
+ echo "repo.path=$PWD/repos/rob/.git" &&
+ echo "scan-path=$PWD/repos" &&
+ echo "repo.desc=misplaced"
+ } >stalerc &&
+ CGIT_CONFIG="$PWD/stalerc" QUERY_STRING="url=" cgit >tmp 2>err &&
+ ! grep "misplaced" tmp &&
+ grep "Ignoring repo.desc before any repo.url" err
+'
+
+test_expect_success 'a valueless or broken git config in a scanned repository is skipped' '
+ git clone -q --bare repos/rob/.git scan/a.git &&
+ printf "[cgit]\n\thide\n[cgit\n" >>scan/a.git/config &&
+ {
+ echo "virtual-root=/" &&
+ echo "cache-size=0" &&
+ echo "enable-git-config=1" &&
+ echo "scan-path=$PWD/scan"
+ } >gitcfgrc &&
+ CGIT_CONFIG="$PWD/gitcfgrc" QUERY_STRING="url=" cgit >tmp 2>err &&
+ grep "toplevel-repo.><a href=./a.git/.>" tmp &&
+ grep "Ignoring unreadable config in $PWD/scan/a.git/config" err
+'
+
+test_expect_success 'only descriptive keys are taken from a scanned repository' '
+ git clone -q --bare repos/rob/.git scan/b.git &&
+ {
+ echo "desc=from the repo" &&
+ echo "readme=master:file-1" &&
+ echo "head-content=<script>alert(1)</script>" &&
+ echo "logo-link=javascript:alert(2)"
+ } >scan/b.git/cgitrc &&
+ CGIT_CONFIG="$PWD/gitcfgrc" QUERY_STRING="url=b.git/about/" cgit >tmp 2>err &&
+ grep "from the repo" tmp &&
+ grep "pre class=.plaintext." tmp &&
+ ! grep "alert(" tmp &&
+ grep "Ignoring head-content in $PWD/scan/b.git/: trust-scan-config is not set" err &&
+ grep "Ignoring logo-link in " err
+'
+
+test_expect_success 'a filesystem readme from a scanned repository is refused' '
+ echo "readme=/etc/hosts" >scan/b.git/cgitrc &&
+ CGIT_CONFIG="$PWD/gitcfgrc" QUERY_STRING="url=b.git/about/" cgit >tmp 2>err &&
+ grep "Ignoring readme in " err
+'
+
+test_expect_success SYMLINKS 'a symlink cycle under the scan path is entered once' '
+ ln -s . scan/loop &&
+ CGIT_CONFIG="$PWD/gitcfgrc" QUERY_STRING="url=" cgit >tmp &&
+ test $(grep -c "toplevel-repo" tmp) = 2
+'
+
+test_expect_success 'a bare query parameter does not swallow the next one' '
+ robq "url=rob/commit/&x&id=$sha" >tmp &&
+ grep "<div class=.commit-subject.>commit 2" tmp
+'
+
+test_expect_success 'a filter that cannot run answers one error page' '
+ {
+ echo "commit-filter=exec:$PWD/no/such/filter" &&
+ cat robrc
+ } >badfilterrc &&
+ CGIT_CONFIG="$PWD/badfilterrc" QUERY_STRING="url=rob/commit/" cgit >tmp 2>err &&
+ test $(grep -c "^Status:" tmp) = 1 &&
+ grep "Unable to complete the request" tmp &&
+ grep "Unable to run filter $PWD/no/such/filter" err &&
+ grep "Unable to exec filter" err
+'
+
+test_expect_success 'a filter that exits without reading does not end cgit' '
+ {
+ echo "email-filter=exec:/usr/bin/true" &&
+ cat robrc
+ } >truerc &&
+ CGIT_CONFIG="$PWD/truerc" QUERY_STRING="url=rob/log/" cgit >tmp &&
+ grep "^Status:" tmp
+'
+
+test_expect_success 'a commit encoding header is read without its newline' '
+ (
+ cd repos/rob &&
+ echo more >file-1 &&
+ git add file-1 &&
+ git -c i18n.commitEncoding=ISO-8859-1 commit -m "$(printf "caf\351")"
+ ) &&
+ robq "url=rob/commit/" >tmp &&
+ grep "<div class=.commit-subject.>caf$(printf "\303\251")<" tmp
+'
+
+test_expect_success 'a submodule below the about path does not end the request' '
+ (
+ cd repos/rob &&
+ git update-index --add --cacheinfo 160000,$sha,sub &&
+ git commit -m gitlink
+ ) &&
+ {
+ echo "mimetype.png=image/png" &&
+ cat robrc &&
+ echo "repo.readme=master:file-1"
+ } >aboutrc &&
+ CGIT_CONFIG="$PWD/aboutrc" QUERY_STRING="url=rob/about/sub" cgit >tmp 2>err &&
+ grep "^Status:" tmp &&
+ ! grep "fatal" err
+'
+
+test_expect_success 'a missing image on the about page errors as html' '
+ CGIT_CONFIG="$PWD/aboutrc" QUERY_STRING="url=rob/about/missing.png" cgit >tmp &&
+ grep "^Status: 404" tmp &&
+ grep "^Content-Type: text/html" tmp
+'
+
+test_expect_success 'a snapshot name holding a slash is refused' '
+ robq "url=rob/snapshot/:/../../...tar.gz" >tmp &&
+ grep "^Status: 404" tmp &&
+ robq "url=rob/snapshot/rob-master.tar.gz" >tmp &&
+ grep "^Status: 200" tmp
+'
+
+test_expect_success 'a path opening with a colon is refused on the log and patch pages' '
+ robq "url=rob/log/:%25x" >tmp &&
+ grep "^Status: 400" tmp &&
+ robq "url=rob/patch/:%25x" >tmp &&
+ grep "^Status: 400" tmp &&
+ test $(grep -c "^Status:" tmp) = 1
+'
+
+test_expect_success 'a revision spelled like an option is refused' '
+ git -C repos/rob update-ref refs/heads/--stdin HEAD &&
+ robq "url=rob/log/&h=--stdin" >tmp &&
+ grep "^Status: 404" tmp &&
+ robq "url=rob/blame/file-1&id=--stdin" >tmp &&
+ grep "^Status: 400" tmp &&
+ robq "url=rob/atom/&h=--stdin" >tmp &&
+ grep "^Status: 404" tmp
+'
+
+test_expect_success 'the blob page reads a file through an annotated tag' '
+ git -C repos/rob -c tag.gpgsign=false tag -a -m note ann HEAD &&
+ robq "url=rob/blob/&h=ann&path=file-1" >tmp &&
+ strip_headers <tmp >body &&
+ printf "more\n" >want &&
+ test_cmp want body
+'
+
+test_expect_success 'the commit page shows a message whole when text follows its trailers' '
+ (
+ cd repos/rob &&
+ echo again >file-1 &&
+ git add file-1 &&
+ git commit -F - <<-\EOF
+ Subject
+
+ Body text.
+
+ Signed-off-by: A U Thor <author@example.com>
+
+ Conflicts:
+ file-1
+ EOF
+ ) &&
+ {
+ echo "enable-trailers=1" &&
+ cat robrc
+ } >trailrc &&
+ CGIT_CONFIG="$PWD/trailrc" QUERY_STRING="url=rob/commit/" cgit >tmp &&
+ ! grep "commit-trailers" tmp &&
+ grep "Conflicts:" tmp
+'
+
+test_expect_success 'a revision expression that walks the history is refused' '
+ robq "url=rob/log/&h=:/zzzz" >tmp &&
+ grep "^Status: 404" tmp &&
+ robq "url=rob/commit/&id=HEAD^{/zzzz}" >tmp &&
+ grep "^Status: 400" tmp &&
+ robq "url=rob/log/&qt=range&q=:/zzzz" >tmp &&
+ grep "^Status: 400" tmp &&
+ robq "url=rob/log/&qt=range&q=master~1..master" >tmp &&
+ grep "^Status: 400" tmp &&
+ robq "url=rob/log/&qt=range&q=$sha..master" >tmp &&
+ grep "^Status: 200" tmp
+'
+
+test_expect_success 'the log search is literal' '
+ robq "url=rob/log/&qt=grep&q=commit.1" >tmp &&
+ ! grep ">commit 1</a>" tmp &&
+ robq "url=rob/log/&qt=grep&q=commit%201" >tmp &&
+ grep ">commit 1</a>" tmp
+'
+
+test_expect_success 'a commit with a broken tree line does not crash the blob page' '
+ git clone -q repos/rob broken &&
+ (
+ cd broken &&
+ bad=$(git cat-file commit HEAD | sed "s/^tree .*/tree not-a-hash/" |
+ git hash-object -t commit -w --stdin --literally) &&
+ echo $bad >.git/refs/heads/bad
+ ) &&
+ {
+ echo "virtual-root=/" &&
+ echo "cache-size=0" &&
+ echo "repo.url=broken" &&
+ echo "repo.path=$PWD/broken/.git" &&
+ echo "repo.readme=:file-1"
+ } >brokenrc &&
+ CGIT_CONFIG="$PWD/brokenrc" QUERY_STRING="url=broken/blob/&h=bad&path=file-1" cgit >tmp &&
+ grep "^Status: 404" tmp
+'
+
+test_expect_success 'a die inside git answers one 500 page and logs the reason' '
+ printf "[core\n" >>broken/.git/config &&
+ CGIT_CONFIG="$PWD/brokenrc" QUERY_STRING="url=broken/log/" cgit >tmp 2>err &&
+ grep "^Status: 500" tmp &&
+ test $(grep -c "^Status:" tmp) = 1 &&
+ grep "bad config line" err &&
+ ! grep "bad config line" tmp
+'
+
+test_expect_success 'a history with a missing parent renders up to the gap' '
+ git clone -q repos/rob gap &&
+ older=$(git -C gap rev-parse HEAD~3) &&
+ parent=$(git -C gap rev-parse HEAD~2) &&
+ rm gap/.git/objects/$(echo $parent | cut -c1-2)/$(echo $parent | cut -c3-) &&
+ {
+ echo "virtual-root=/" &&
+ echo "cache-size=0" &&
+ echo "repo.url=gap" &&
+ echo "repo.path=$PWD/gap/.git"
+ } >gaprc &&
+ CGIT_CONFIG="$PWD/gaprc" QUERY_STRING="url=gap/log/" cgit >tmp 2>err &&
+ test $(grep -c "^Status:" tmp) = 1 &&
+ grep "Unable to complete the request" tmp &&
+ grep "Failed to traverse parents" err &&
+ CGIT_CONFIG="$PWD/gaprc" QUERY_STRING="url=gap/patch/&id2=$older" cgit >tmp &&
+ test $(grep -c "^Status:" tmp) = 1 &&
+ grep "^Status: 500" tmp
+'
+
+test_expect_success 'a filter pipeline sees the default SIGPIPE disposition' '
+ cat >pipe.sh <<-\EOF &&
+ #!/bin/sh
+ n=0
+ while test $n -lt 20000
+ do
+ echo line
+ n=$((n + 1))
+ done | head -c 5
+ EOF
+ chmod +x pipe.sh &&
+ {
+ echo "source-filter=exec:$PWD/pipe.sh" &&
+ cat robrc
+ } >piperc &&
+ CGIT_CONFIG="$PWD/piperc" QUERY_STRING="url=rob/tree/file-1" cgit >tmp 2>err &&
+ grep "^Status: 200" tmp &&
+ ! grep -i "broken pipe" err
+'
+
+test_expect_success 'the fallback branch skips a name spelled like an option' '
+ git -C repos/rob update-ref refs/heads/-first HEAD &&
+ git -C repos/rob symbolic-ref HEAD refs/heads/gone &&
+ robq "url=rob/" >tmp &&
+ git -C repos/rob symbolic-ref HEAD refs/heads/master &&
+ grep "^Status: 200" tmp
+'
+
+test_expect_success 'a blank line in the mimetype file is skipped' '
+ (
+ cd repos/rob &&
+ echo note >note.cgt &&
+ git add note.cgt &&
+ git commit -m note
+ ) &&
+ printf "\n \nimage/x-cgit cgt\n" >mime.types &&
+ {
+ echo "mimetype-file=$PWD/mime.types" &&
+ cat robrc
+ } >mimerc &&
+ CGIT_CONFIG="$PWD/mimerc" QUERY_STRING="url=rob/plain/note.cgt" cgit >tmp &&
+ grep "^Content-Type: image/x-cgit" tmp
+'
+
+test_expect_success 'a refs page named with an id is not cached for ever' '
+ mkdir -p cache &&
+ {
+ echo "cache-size=10" &&
+ echo "cache-root=$PWD/cache" &&
+ echo "cache-static-ttl=-1" &&
+ echo "cache-dynamic-ttl=0" &&
+ echo "cache-summary-ttl=0" &&
+ grep -v "^cache-size" robrc
+ } >ttlrc &&
+ full=$(git -C repos/rob rev-parse HEAD) &&
+ CGIT_CONFIG="$PWD/ttlrc" QUERY_STRING="url=rob/refs/&id=$full" cgit >tmp &&
+ git -C repos/rob tag later HEAD &&
+ CGIT_CONFIG="$PWD/ttlrc" QUERY_STRING="url=rob/refs/&id=$full" cgit >tmp &&
+ grep ">later<" tmp
+'
+
+test_expect_success 'the diff caps hold when the path names a directory' '
+ (
+ cd repos/rob &&
+ mkdir -p dir &&
+ for n in 1 2 3
+ do
+ echo $n >dir/f$n || return 1
+ done &&
+ git add dir &&
+ git commit -m dir
+ ) &&
+ {
+ echo "max-diff-files=2" &&
+ cat robrc
+ } >caprc &&
+ CGIT_CONFIG="$PWD/caprc" QUERY_STRING="url=rob/diff/dir" cgit >tmp &&
+ grep "too large to be rendered inline" tmp &&
+ CGIT_CONFIG="$PWD/caprc" QUERY_STRING="url=rob/diff/dir/f1" cgit >tmp &&
+ ! grep "too large to be rendered inline" tmp
+'
+
+test_expect_success 'a snapshot of a tag spelled like an option is archived' '
+ git -C repos/rob update-ref refs/tags/-l HEAD &&
+ robq "url=rob/snapshot/-l.tar.gz" >tmp &&
+ grep "^Status: 200" tmp &&
+ strip_headers <tmp | gzip -dc | tar -tf - >list &&
+ grep "file-1" list
+'
+
+test -n "$CGIT_VALGRIND" || test_set_prereq NO_VALGRIND
+
+test_expect_success NO_VALGRIND 'a snapshot format whose compressor is missing answers 500' '
+ {
+ echo "snapshots=tar.xz" &&
+ grep -v "^snapshots" robrc
+ } >xzrc &&
+ cgitbin=$(command -v cgit) &&
+ mkdir -p nobin &&
+ PATH="$PWD/nobin" CGIT_CONFIG="$PWD/xzrc" \
+ QUERY_STRING="url=rob/snapshot/rob-master.tar.xz" $cgitbin >tmp &&
+ grep "^Status: 500" tmp &&
+ test $(grep -c "^Status:" tmp) = 1
+'
+
+test_expect_success 'a disk readme in the repository directory serves no sibling' '
+ echo hello >repos/rob/.git/README &&
+ {
+ cat robrc &&
+ echo "repo.readme=README"
+ } >diskrc &&
+ CGIT_CONFIG="$PWD/diskrc" QUERY_STRING="url=rob/about/" cgit >tmp &&
+ grep "hello" tmp &&
+ CGIT_CONFIG="$PWD/diskrc" QUERY_STRING="url=rob/about/config" cgit >tmp &&
+ ! grep "repositoryformatversion" tmp
+'
+
+test_expect_success 'a tag pointing at a tag links the inner tag by id' '
+ git -C repos/rob -c tag.gpgsign=false tag -a -m outer outer ann &&
+ inner=$(git -C repos/rob rev-parse ann) &&
+ robq "url=rob/tag/&id=$inner" >tmp &&
+ grep "^Status: 200" tmp &&
+ grep "tagged object" tmp
+'
+
+test_expect_success 'the dumb transport serves an empty repository' '
+ git init -q --bare empty.git &&
+ {
+ echo "virtual-root=/" &&
+ echo "cache-size=0" &&
+ echo "repo.url=empty" &&
+ echo "repo.path=$PWD/empty.git"
+ } >emptyrc &&
+ CGIT_CONFIG="$PWD/emptyrc" QUERY_STRING="url=empty/info/refs" cgit >tmp &&
+ grep "^Status: 200" tmp &&
+ grep "^Content-Type: text/plain" tmp &&
+ CGIT_CONFIG="$PWD/emptyrc" QUERY_STRING="url=empty/objects/info" cgit >tmp &&
+ grep "^Status: 404" tmp
+'
+
+test_expect_success 'a symlink whose target is a large blob is listed without it' '
+ big=$(head -c 5000 /dev/zero | tr "\0" a | git -C repos/rob hash-object -w --stdin) &&
+ (
+ cd repos/rob &&
+ git update-index --add --cacheinfo 120000,$big,biglink &&
+ git commit -m biglink
+ ) &&
+ robq "url=rob/tree/" >tmp &&
+ grep "biglink" tmp &&
+ ! grep "aaaaaaaaaa" tmp
+'
+
+test_expect_success 'a chain of single directories is followed a bounded number of levels' '
+ (
+ cd repos/rob &&
+ deep=$(printf "d/%.0s" $(seq 1 40))leaf &&
+ mkdir -p $(dirname $deep) &&
+ echo x >$deep &&
+ git add d &&
+ git commit -m deep
+ ) &&
+ robq "url=rob/tree/" >tmp &&
+ test $(grep -o "class=.ls-dir." tmp | wc -l) -le 17
+'
+
+test_done