// SPDX-License-Identifier: AGPL-3.0-or-later package app import ( "fmt" "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "bnema.dev/ermite/internal/domain" ) // AC1: the commit pages render for the fixtures with the structure of the // mockup (golden HTML, normalised). func TestWebCommitGolden(t *testing.T) { h := newHarness(t, harnessOpts{}) f := h.diffRepo("diffs") for _, tt := range []struct{ name, path string }{ {"commit-modify", "/diffs/commit/" + f.modify}, {"commit-modify-split", "/diffs/commit/" + f.modify + "?view=split"}, {"commit-root", "/diffs/commit/" + f.root}, {"commit-rename", "/diffs/commit/" + f.rename}, {"commit-binary", "/diffs/commit/" + f.binary}, {"commit-merge", "/diffs/commit/" + f.merge}, {"commit-trojan", "/diffs/commit/" + f.trojan}, {"commit-big", "/diffs/commit/" + f.big}, } { t.Run(tt.name, func(t *testing.T) { status, hdr, body := h.get(tt.path) require.Equal(t, 200, status) assert.Equal(t, "no-cache", hdr.Get("Cache-Control")) assertNoInline(t, body) golden(t, tt.name, normalise(body)) }) } } // The behaviours of the commit pages, asserted on their own. func TestWebCommit(t *testing.T) { h := newHarness(t, harnessOpts{}) f := h.diffRepo("diffs") get := func(path string, status int) string { t.Helper() st, _, body := h.get(path) require.Equal(t, status, st, path) return body } t.Run("commit", func(t *testing.T) { body := get("/diffs/commit/"+f.modify, 200) assert.Contains(t, body, "

modify a file and make a script executable

") assert.Contains(t, body, "The body has a second paragraph.") assert.Contains(t, body, "And <b>html</b> &", "the message is text") assert.NotContains(t, body, "html") assert.Contains(t, body, "mode 100644 → 100755") assert.Contains(t, body, `+`, "added lines keep their marker") assert.Contains(t, body, `-`, "deleted lines keep their marker") assert.Contains(t, body, `data-diff="unified"`) assert.Contains(t, body, `hx-live="this.dataset.diff = sessionStorage`, "the browser remembers the layout when the URL does not choose") assert.Contains(t, body, `]* open`, body, "a truncated file is collapsed") assert.NotContains(t, body, "line 2000", "no lines of a truncated file") }) t.Run("not found", func(t *testing.T) { _, _, missing := h.get("/diffs/commit/0123456789abcdef0123456789abcdef01234567") for _, p := range []string{ "/diffs/commit/0123456789abcdef0123456789abcdef01234567", // no such commit "/diffs/commit/main", "/diffs/commit/abc", "/diffs/commit/ABCDEF0", "/diffs/commit/v1.0.0", // not a commit id "/nope/commit/" + f.root, "/diffs/commit/" + f.root + "/x", } { st, _, body := h.get(p) assert.Equal(t, 404, st, p) assert.NotContains(t, body, "

root", p) } st, _, body := h.get("/diffs/commit/0123456789abcdef0123456789abcdef01234567") assert.Equal(t, 404, st) assert.Equal(t, normalise(missing), normalise(body)) }) } // Over 4,000 lines only one layout is sent, the one the URL asks for. func TestWebCommitLargeDiffView(t *testing.T) { h := newHarness(t, harnessOpts{}) h.seed("large", domain.VisibilityPublic) wt := h.workTree("main", "README") var lines strings.Builder for i := range 1500 { fmt.Fprintf(&lines, "line %d\n", i) } for _, name := range []string{"a.txt", "b.txt", "c.txt"} { require.NoError(t, os.WriteFile(filepath.Join(wt, name), []byte(lines.String()), 0o644)) } h.mustGit(wt, "add", ".") h.mustGit(wt, "commit", "-q", "-m", "three big files") h.mustGit(wt, "push", "-q", h.urlAuth(adminToken, "/large.git"), "main") sha := strings.TrimSpace(h.mustGit(wt, "rev-parse", "HEAD")) _, _, unified := h.get("/large/commit/" + sha) assert.True(t, strings.Contains(unified, `

`)) } // AC2: the commit pages of a private repository are the 404 page of a // repository that does not exist. func TestWebCommitPrivate(t *testing.T) { h := newHarness(t, harnessOpts{}) wt := h.workTree("main", "secret.txt") h.createRepo("hidden-one", domain.VisibilityPrivate) h.mustGit(wt, "push", "-q", h.urlAuth(adminToken, "/hidden-one.git"), "main") sha := strings.TrimSpace(h.mustGit(wt, "rev-parse", "HEAD")) _, _, missing := h.get("/never-existed") for _, p := range []string{ "/hidden-one/commit/" + sha, "/hidden-one/commit/" + sha + "?view=split", } { st, hdr, body := h.get(p) assert.Equal(t, 404, st, p) assert.Equal(t, "no-cache", hdr.Get("Cache-Control"), p) assert.NotContains(t, body, "hidden-one", p) assert.NotContains(t, body, "secret.txt", p) assert.Equal(t, normalise(missing), normalise(body), p) } } // The syntax colours reach the hidden lines: /context lexes the whole file, so // a block comment that opens in the hidden lines and closes below them is // coloured as one, with the classes of the code view, and the text stays // escaped. func TestWebDiffContextHighlight(t *testing.T) { h := newHarness(t, harnessOpts{}) wt := t.TempDir() h.mustGit(wt, "init", "-q", "--initial-branch=main") var src strings.Builder src.WriteString("package a\n\n/* open \n") for i := 1; i <= 30; i++ { fmt.Fprintf(&src, "still a comment %d\n", i) } src.WriteString("*/\nvar x = 1\n") for i := 1; i <= 20; i++ { fmt.Fprintf(&src, "var y%d = %d\n", i, i) } h.put(wt, "f.go", []byte(src.String()), 0o644) h.commitAt(wt, "2026-02-01T10:00:00Z", "a file") h.put(wt, "f.go", []byte(strings.Replace(src.String(), "var y15 = 15", "var y15 = 150", 1)), 0o644) sha := h.commitAt(wt, "2026-02-02T10:00:00Z", "change y15") h.createRepo("hl", domain.VisibilityPublic) h.mustGit(wt, "push", "-q", h.urlAuth(adminToken, "/hl.git"), "main") _, _, page := h.get("/hl/commit/" + sha) assert.Contains(t, page, `var`, "the hunk is coloured") assertNoInline(t, page) st, _, body := h.get("/hl/context/" + sha + "?key=g0&n=20&new=1&old=1&path=f.go") require.Equal(t, 200, st) assert.Contains(t, body, `package a`) assert.Contains(t, body, `/* open <b>`) assert.Contains(t, body, `still a comment 5`, "the comment opened above is still open") assert.NotContains(t, body, "") assertNoInline(t, body) } // The hidden lines of a diff: the page has a gap before a hunk and after the // last one, and /context serves their rows for both layouts, numbered on both // sides; a private repository's are the 404 page. func TestWebDiffContext(t *testing.T) { h := newHarness(t, harnessOpts{}) wt := t.TempDir() h.mustGit(wt, "init", "-q", "--initial-branch=main") var src strings.Builder for i := 1; i <= 40; i++ { fmt.Fprintf(&src, "line %d \n", i) } h.put(wt, "f.txt", []byte(src.String()), 0o644) h.commitAt(wt, "2026-02-01T10:00:00Z", "forty lines") h.put(wt, "f.txt", []byte(strings.Replace(src.String(), "line 20 ", "line twenty", 1)), 0o644) sha := h.commitAt(wt, "2026-02-02T10:00:00Z", "change line 20") h.createRepo("ctx", domain.VisibilityPublic) h.mustGit(wt, "push", "-q", h.urlAuth(adminToken, "/ctx.git"), "main") _, _, page := h.get("/ctx/commit/" + sha) gap := "/ctx/context/" + sha + "?key=g0&n=16&new=1&old=1&path=f.txt" assert.Contains(t, page, gap, "lines 1 to 16 before the hunk (17 to 23)") assert.Contains(t, page, "16 hidden lines") assert.Contains(t, page, "/ctx/context/"+sha+"?key=tail&n=0&new=24&old=24&path=f.txt") st, _, body := h.get("/ctx/context/" + sha + "?key=g0&n=16&new=1&old=1&path=f.txt") require.Equal(t, 200, st) assert.Equal(t, 3, strings.Count(body, "line 16 <b>"), "unified once, split on both sides, escaped") assert.NotContains(t, body, "line 17") assert.NotContains(t, body, `class="gap"`, "nothing left") st, _, body = h.get("/ctx/context/" + sha + "?key=tail&n=0&new=24&old=24&path=f.txt") require.Equal(t, 200, st) assert.Contains(t, body, "line 40 <b>") assert.NotContains(t, body, "line 23 ") for _, bad := range []string{ "/ctx/context/" + sha + "?n=1&new=1&old=1", // no path "/ctx/context/" + sha + "?n=1&new=0&old=1&path=f.txt", // line 0 "/ctx/context/" + sha + "?n=-1&new=1&old=1&path=f.txt", // negative "/ctx/context/" + sha + "?n=1&new=99&old=99&path=f.txt", // past the end "/ctx/context/" + sha + "?n=1&new=1&old=1&path=nope.txt", // no such file "/ctx/context/" + sha[:7] + "?n=1&new=1&old=1&path=f.txt", // not a full id "/ctx/context/main?n=1&new=1&old=1&path=f.txt", // a ref } { st, _, _ := h.get(bad) assert.Equal(t, 404, st, bad) } // Lines as Git numbers them: a lone CR is not a line break; the last line // of a file without a final newline keeps its marker. var cr strings.Builder cr.WriteString("a\rb\n") for i := 2; i <= 30; i++ { fmt.Fprintf(&cr, "row %d\n", i) } h.put(wt, "cr.txt", []byte(cr.String()+"last"), 0o644) h.commitAt(wt, "2026-02-03T10:00:00Z", "cr") h.put(wt, "cr.txt", []byte(strings.Replace(cr.String(), "row 15\n", "row fifteen\n", 1)+"last"), 0o644) crSHA := h.commitAt(wt, "2026-02-04T10:00:00Z", "change row 15") h.mustGit(wt, "push", "-q", h.urlAuth(adminToken, "/ctx.git"), "main") _, _, crPage := h.get("/ctx/commit/" + crSHA) assert.Contains(t, crPage, "11 hidden lines", "lines 1 to 11 before the hunk at 12") st, _, body = h.get("/ctx/context/" + crSHA + "?n=11&new=1&old=1&path=cr.txt") require.Equal(t, 200, st) assert.Contains(t, body, "row 11") assert.NotContains(t, body, "row 12", "a lone CR does not shift the numbers") st, _, body = h.get("/ctx/context/" + crSHA + "?n=0&new=19&old=19&path=cr.txt") require.Equal(t, 200, st) assert.Contains(t, body, "last