// SPDX-License-Identifier: AGPL-3.0-or-later package app import ( "html" "net/http" "net/url" "regexp" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "bnema.dev/ermite/internal/domain" "bnema.dev/ermite/internal/ports" ) // mrRepo is a public repository "proj" with README.md on main and a branch // feat/search that is one commit ahead of it (search.go, three lines). type mrRepo struct { *cliHarness wt, url string } func newMRRepo(t *testing.T, vis domain.Visibility) *mrRepo { t.Helper() c := newCLIHarness(t) _, err := c.forge.Repos.Create(c.ctx, domain.Root(), createParams(c.harness, "proj", vis)) require.NoError(t, err) r := &mrRepo{cliHarness: c, wt: c.workTree("main", "README.md"), url: c.urlAuth(adminToken, "/proj.git")} c.mustGit(r.wt, "push", "-q", r.url, "main") c.mustGit(r.wt, "checkout", "-q", "-b", "feat/search") c.writeFile(r.wt, "search.go", []byte("package search\n\nfunc Find() {}\n")) c.mustGit(r.wt, "push", "-q", r.url, "feat/search") return r } func (r *mrRepo) headOf(branch string) string { return r.bareGit("proj", "rev-parse", branch) } // form posts url-encoded values as the signed-in owner, from this site. func (b browser) form(path, token string, values url.Values) page { b.h.t.Helper() return b.do("POST", path, post(token), values.Encode()) } var reHeadField = regexp.MustCompile(`name="head_id" value="([0-9a-f]{40})"`) // formHead reads the head the merge form of a page carries. func formHead(t *testing.T, body string) string { t.Helper() m := reHeadField.FindStringSubmatch(body) require.Len(t, m, 2, "the page has a merge form") return m[1] } // AC7: the pages as an anonymous visitor sees them on a public repository: // read-only, no forms, public cache headers; the same pages signed in carry // the forms and the private headers. func TestWebMergeRequestPagesRender(t *testing.T) { r := newMRRepo(t, domain.VisibilityPublic) b := browser{r.harness} token := r.signIn(t) // The compare page: anonymous reads, the owner gets the form. anon := b.get("/proj/compare/feat/search", nil) require.Equal(t, 200, anon.status, anon.body) assert.Equal(t, "no-cache", anon.hdr.Get("Cache-Control")) assert.Equal(t, []string{"Accept-Encoding"}, anon.hdr.Values("Vary")) assert.Contains(t, anon.body, "feat/search") assert.Contains(t, anon.body, "package search", "the diff of the single commit") assert.NotContains(t, anon.body, "
`) assert.Contains(t, owner.body, `name="source" value="feat/search"`) assert.Contains(t, owner.body, `name="target" value="main"`) assert.Equal(t, 404, b.get("/proj/compare/nope", nil).status) assert.Equal(t, 404, b.get("/proj/compare/main/extra", nil).status) main := b.get("/proj/compare/main", nil) assert.Equal(t, 200, main.status) assert.Contains(t, main.body, "nothing to compare") // An empty list, with the way to start one. list := b.get("/proj/merge-requests", nil) require.Equal(t, 200, list.status) assert.Contains(t, list.body, "no open merge requests") assert.Contains(t, list.body, `href="/proj/compare/feat/search"`) assert.Contains(t, list.body, `data-key="m"`, "the section of the nav") assert.NotContains(t, strings.ToLower(list.body), "proposal") // Open one from the compare page. res := b.form("/proj/merge-requests", token, url.Values{ "source": {"feat/search"}, "target": {"main"}, "title": {"Add search"}, "description": {"Finds things.\r\nSee https://example.org/spec) and javascript:alert(1)"}, }) require.Equal(t, http.StatusSeeOther, res.status, res.body) assert.Equal(t, "/proj/merge-requests/1", res.hdr.Get("Location")) assert.Equal(t, "no-store", res.hdr.Get("Cache-Control")) head := r.headOf("feat/search") // The same pair twice is refused with a page, and nothing is created. dup := b.form("/proj/merge-requests", token, url.Values{"source": {"feat/search"}, "target": {"main"}, "title": {"dup"}}) assert.Equal(t, http.StatusConflict, dup.status) assert.Equal(t, "no-store", dup.hdr.Get("Cache-Control")) assert.Contains(t, dup.body, "already exists") bad := b.form("/proj/merge-requests", token, url.Values{"source": {"feat/search"}, "target": {"main"}, "title": {"bidi\u202etitle"}}) assert.Equal(t, http.StatusBadRequest, bad.status) assert.NotContains(t, bad.body, "\u202e") // The compare page now points at it. assert.Contains(t, b.get("/proj/compare/feat/search", as(token)).body, `href="/proj/merge-requests/1"`) assert.NotContains(t, b.get("/proj/compare/feat/search", as(token)).body, `Add <b>search</b>`) assert.Contains(t, list.body, "feat/search → main") assert.Equal(t, "no-cache", list.hdr.Get("Cache-Control")) assertNoInline(t, list.body) assert.Equal(t, "private, no-store", b.get("/proj/merge-requests", as(token)).hdr.Get("Cache-Control")) // The page, anonymous: read-only. mr := b.get("/proj/merge-requests/1", nil) require.Equal(t, 200, mr.status, mr.body) assert.Equal(t, "no-cache", mr.hdr.Get("Cache-Control")) assert.Equal(t, []string{"Accept-Encoding"}, mr.hdr.Values("Vary")) assert.Contains(t, mr.body, "

!1 Add <b>search</b>

") assert.NotContains(t, mr.body, "search") assert.Contains(t, mr.body, "feat/search") assert.Contains(t, mr.body, "package search") assert.Contains(t, mr.body, "no checks") assert.Contains(t, mr.body, "fast-forward, squash") assert.Contains(t, mr.body, `https://example.org/spec)`, "the address is linked, not its closing bracket") assert.NotContains(t, mr.body, `href="javascript:`) assert.Contains(t, mr.body, "Finds things.\nSee ", "line breaks are kept") assert.NotContains(t, mr.body, "Merge") assert.Contains(t, mr.body, `href="/proj/commit/`+head+`"`, "the commits are in the rail") assert.NotContains(t, strings.ToLower(mr.body), "proposal") assertNoInline(t, mr.body) assert.Equal(t, 404, b.get("/proj/merge-requests/1/since", nil).status, "the read position is the owner's") assert.Equal(t, 404, b.get("/proj/merge-requests/9", nil).status) assert.Equal(t, 404, b.get("/proj/merge-requests/01", nil).status) assert.Equal(t, 404, b.get("/proj/merge-requests/x", nil).status) // Signed in: forms, the merge box with the head and the CLI lines. own := b.get("/proj/merge-requests/1", as(token)) require.Equal(t, 200, own.status) assert.Equal(t, "private, no-store", own.hdr.Get("Cache-Control")) assert.Contains(t, own.hdr.Values("Vary"), "Cookie") assert.Equal(t, head, formHead(t, own.body)) assert.Contains(t, own.body, `action="/proj/merge-requests/1/merge"`) assert.Contains(t, own.body, `