internal/httpd/mrpage_test.go
166 lines · 6082 bytes
1package httpd
2
3import (
4 "html/template"
5 "strings"
6 "testing"
7 "time"
8
9 "gitbay.org/gitbay/internal/control"
10 "gitbay.org/gitbay/internal/gitutil"
11 "gitbay.org/gitbay/internal/store"
12 "gitbay.org/gitbay/internal/web"
13)
14
15// mrPageData mirrors the anonymous struct the mr handler renders with.
16type mrPageData struct {
17 repoPage
18 MR store.MR
19 View string
20 BodyHTML template.HTML
21 Checks []store.Check
22 Combined string
23 Comments []renderedComment
24 Reviews []reviewRow
25 DiffFiles []diffFile
26 Stat diffStat
27 Commits []struct{}
28 Branches []gitutil.Ref
29 CanEdit bool
30 CanWrite bool
31 Unresolved int
32 Revisions []store.MRHead
33 Notice string
34 DetachedThreads []diffThread
35 Gates *control.GatesOut
36 SourceGone bool
37}
38
39func renderMR(t *testing.T, m store.MR, reviews []store.MRReview, checks []store.Check) string {
40 rows := make([]reviewRow, 0, len(reviews))
41 for _, r := range reviews {
42 // The page test renders reviews that count; whether a given
43 // reviewer's does is decided by access, which e2e covers.
44 rows = append(rows, reviewRow{MRReview: r, Counts: true})
45 }
46 t.Helper()
47 var sb strings.Builder
48 if err := web.Render(&sb, "mr.html", mrPageData{
49 repoPage: testRepoPage(), MR: m, View: "conversation",
50 Reviews: rows, Checks: checks, Combined: "",
51 }); err != nil {
52 t.Fatalf("render: %v", err)
53 }
54 return sb.String()
55}
56
57func testMR(state string) store.MR {
58 return store.MR{Number: 42, Title: "org native rendering", Author: "cmc", State: state,
59 SourcePath: "krz/hutch", SourceRef: "org-native-rendering", TargetRef: "main",
60 HeadSHA: "ff6271a9d4570cd46f169091637a9d2e40ad5c2b"}
61}
62
63// The header states what happened to the MR. "wants to merge" is only true
64// while it is still open.
65func TestMRHeaderByState(t *testing.T) {
66 open := renderMR(t, testMR("open"), nil, nil)
67 if !strings.Contains(open, "wants to merge") {
68 t.Errorf("open MR does not say wants to merge:\n%s", open)
69 }
70
71 merged := testMR("merged")
72 merged.MergedAt, merged.MergedBy = "2026-08-27T14:03:11.000Z", "cmc"
73 out := renderMR(t, merged, nil, nil)
74 for _, want := range []string{"merged", "krz/hutch:org-native-rendering", "2026-08-27 14:03"} {
75 if !strings.Contains(out, want) {
76 t.Errorf("merged header missing %q:\n%s", want, out)
77 }
78 }
79 if strings.Contains(out, "wants to merge") {
80 t.Errorf("merged MR still wants to merge:\n%s", out)
81 }
82
83 closed := testMR("closed")
84 closed.ClosedAt, closed.ClosedBy = "2026-08-27T14:03:11.000Z", "cmc"
85 out = renderMR(t, closed, nil, nil)
86 if !strings.Contains(out, "without merging") || strings.Contains(out, "wants to merge") {
87 t.Errorf("closed header:\n%s", out)
88 }
89
90 // Imports and pre-0029 merges carry no stamp; the wording drops the
91 // claim rather than inventing a time.
92 out = renderMR(t, testMR("merged"), nil, nil)
93 if strings.Contains(out, "wants to merge") || strings.Contains(out, " on 20") {
94 t.Errorf("unstamped merged header:\n%s", out)
95 }
96}
97
98// Approvals and checks carry their times in the aside, so reading the MR
99// does not mean opening the build.
100func TestMRAsideTimestamps(t *testing.T) {
101 out := renderMR(t, testMR("open"),
102 []store.MRReview{{Reviewer: "cmc", Verdict: "approve", CreatedAt: "2026-08-27T14:03:11.000Z"}},
103 []store.Check{
104 {CommitStatus: store.CommitStatus{Context: "ci/test", State: "success",
105 UpdatedAt: "2026-08-27T14:05:00.000Z"}, Duration: 72 * time.Second, Build: 60},
106 {CommitStatus: store.CommitStatus{Context: "external/lint", State: "success",
107 UpdatedAt: "2026-08-27T14:06:00.000Z"}},
108 })
109 for _, want := range []string{"2026-08-27 14:03", "2026-08-27 14:05", "1m12s", "2026-08-27 14:06"} {
110 if !strings.Contains(out, want) {
111 t.Errorf("aside missing %q:\n%s", want, out)
112 }
113 }
114}
115
116// A job a path filter excluded has no build behind its status: the page
117// must show it as skipped rather than linking to a build that never ran
118// (#172).
119func TestMRChecksRenderSkippedWithoutBuildLink(t *testing.T) {
120 out := renderMR(t, testMR("open"), nil, []store.Check{
121 {CommitStatus: store.CommitStatus{Context: "ci/unit", State: "skipped",
122 Description: "every changed file matched paths-ignore", UpdatedAt: "2026-08-27T14:05:00.000Z"}},
123 })
124 row := ""
125 for _, line := range strings.Split(out, "\n") {
126 if strings.Contains(line, "ci/unit") {
127 row = line
128 }
129 }
130 if row == "" || !strings.Contains(row, ">skipped<") {
131 t.Fatalf("skipped check not rendered:\n%s", out)
132 }
133 if strings.Contains(row, "<a href") {
134 t.Errorf("skipped check with no build linked anyway: %s", row)
135 }
136}
137
138// The gates block says what the merge is waiting on before a merge is
139// refused (#199): every unmet gate, the approval count, the outstanding
140// owners, and whether a fast-forward is possible.
141func TestMRGatesRender(t *testing.T) {
142 render := func(g *control.GatesOut) string {
143 t.Helper()
144 var sb strings.Builder
145 if err := web.Render(&sb, "mr.html", mrPageData{
146 repoPage: testRepoPage(), MR: testMR("open"), View: "conversation", Gates: g,
147 }); err != nil {
148 t.Fatalf("render: %v", err)
149 }
150 return sb.String()
151 }
152 out := render(&control.GatesOut{ApprovalsRequired: 2, Approvals: []string{"bob"},
153 OwnersOutstanding: []control.OwnersOut{{Files: []string{"svc.go"}, Owners: []string{"carol"}}},
154 Unmet: []string{"krz/hutch requires 2 fresh approval(s); !42 has 1", "CODEOWNERS approval missing for: svc.go (owned by carol)"}})
155 for _, want := range []string{"Merge gates", "requires 2 fresh approval(s)", "approvals: 1 of 2 (bob)", `waiting on <a href="/carol">carol</a>`, "not a fast-forward"} {
156 if !strings.Contains(out, want) {
157 t.Errorf("gates block missing %q:\n%s", want, out)
158 }
159 }
160 if out := render(&control.GatesOut{FastForward: true}); !strings.Contains(out, "All gates met") || !strings.Contains(out, "fast-forward possible") {
161 t.Errorf("met gates not rendered:\n%s", out)
162 }
163 if out := render(nil); strings.Contains(out, "Merge gates") {
164 t.Errorf("gates block on a merge request without gates:\n%s", out)
165 }
166}