internal/httpd/buildpages_test.go
365 lines · 12559 bytes
14 symbols in this file
testRepoPageTestBuildsPageRendersCommandOutputTestBuildsEmptyStateOmitsInstructionForReadersTestBuildsEmptyStateShowsInstructionForWritersTestBuildsPageBadgeSnippetsMarkdownAndOrgTestBuildPageRendersCommandOutputTestBuildsPageCountsBuildsAndRunsTestBuildsPageLeadsWithTheSubjectTestBuildsPagePagesAndSaysSoTestOlderBuildsCarriesFiltersrenderBuildsTestBuildPageFoldsStepsAndOpensFailureTestBuildPageStepSummaryIsFirstLineTestBuildPageLiveNoteSaysItUpdatesItself
1package httpd
2
3import (
4 "gitbay.org/gitbay/internal/control"
5 "strings"
6 "testing"
7
8 "gitbay.org/gitbay/internal/store"
9 "gitbay.org/gitbay/internal/web"
10)
11
12func testRepoPage() repoPage {
13 var p repoPage
14 p.Repo = store.Repo{OwnerName: "krz", Name: "gitbay", DefaultBranch: "main"}
15 p.Host = "gitbay.org"
16 return p
17}
18
19// The build pages read their data from the build commands' JSON now, not
20// from store.Build. A renamed field would be a blank cell rather than a
21// compile error, so render both pages and look for the values.
22func TestBuildsPageRendersCommandOutput(t *testing.T) {
23 var sb strings.Builder
24 builds := []control.BuildOut{{
25 Number: 60, Job: "build", Status: "success",
26 SHA: "ff6271a9d4570cd46f169091637a9d2e40ad5c2b",
27 Ref: "cli-coverage", CreatedAt: "2026-08-28T04:42:54Z",
28 }}
29 jobs := []control.JobOut{{Name: "build"}, {Name: "nightly", Schedule: "0 3 * * *"}}
30 filter := buildFilter{}
31 err := web.Render(&sb, "builds.html", struct {
32 repoPage
33 Builds []control.BuildOut
34 Jobs []control.JobOut
35 Runs []buildRun
36 Filter buildFilter
37 Facets []facetGroup
38 Refs []string
39 Older string
40 CanWrite bool
41 Notice string
42 }{
43 testRepoPage(), builds, jobs, groupRuns(builds), filter, nil,
44 distinctRefs(builds, filter.Ref), "", true, "",
45 })
46 if err != nil {
47 t.Fatalf("render: %v", err)
48 }
49 out := sb.String()
50 for _, want := range []string{
51 "build", "success", "cli-coverage", "ff6271a9d4",
52 `value="build"`, `value="nightly"`, "schedule 0 3 * * *",
53 } {
54 if !strings.Contains(out, want) {
55 t.Errorf("builds.html missing %q", want)
56 }
57 }
58}
59
60// A writer sees the instruction to add CI; a reader without push access
61// sees only the fact, since the instruction is not theirs to act on
62// (#270).
63func TestBuildsEmptyStateOmitsInstructionForReaders(t *testing.T) {
64 var sb strings.Builder
65 filter := buildFilter{}
66 err := web.Render(&sb, "builds.html", struct {
67 repoPage
68 Builds []control.BuildOut
69 Jobs []control.JobOut
70 Runs []buildRun
71 Filter buildFilter
72 Facets []facetGroup
73 Refs []string
74 Older string
75 CanWrite bool
76 Notice string
77 }{
78 testRepoPage(), nil, nil, nil, filter, nil, nil, "", false, "",
79 })
80 if err != nil {
81 t.Fatalf("render: %v", err)
82 }
83 out := sb.String()
84 if !strings.Contains(out, "no builds") {
85 t.Error(`missing "no builds"`)
86 }
87 if strings.Contains(out, "ci.yml") {
88 t.Error("a reader without push access should not see the push instruction")
89 }
90}
91
92// A writer with zero builds gets the one instruction the page can give
93// them (#270).
94func TestBuildsEmptyStateShowsInstructionForWriters(t *testing.T) {
95 var sb strings.Builder
96 filter := buildFilter{}
97 err := web.Render(&sb, "builds.html", struct {
98 repoPage
99 Builds []control.BuildOut
100 Jobs []control.JobOut
101 Runs []buildRun
102 Filter buildFilter
103 Facets []facetGroup
104 Refs []string
105 Older string
106 CanWrite bool
107 Notice string
108 }{
109 testRepoPage(), nil, nil, nil, filter, nil, nil, "", true, "",
110 })
111 if err != nil {
112 t.Fatalf("render: %v", err)
113 }
114 out := sb.String()
115 if !strings.Contains(out, "no builds") {
116 t.Error(`missing "no builds"`)
117 }
118 if !strings.Contains(out, "push a commit with a .gitbay/ci.yml") {
119 t.Error("a writer with no builds should see the push instruction")
120 }
121}
122
123// The badge section offers the snippet for a README.md and a README.org.
124func TestBuildsPageBadgeSnippetsMarkdownAndOrg(t *testing.T) {
125 var sb strings.Builder
126 rp := testRepoPage()
127 err := web.Render(&sb, "builds.html", struct {
128 repoPage
129 Builds []control.BuildOut
130 Jobs []control.JobOut
131 Runs []buildRun
132 Filter buildFilter
133 Facets []facetGroup
134 Refs []string
135 Older string
136 CanWrite bool
137 Notice string
138 }{
139 rp, nil, nil, nil, buildFilter{}, nil, nil, "", false, "",
140 })
141 if err != nil {
142 t.Fatalf("render: %v", err)
143 }
144 base := "https://" + rp.Host + "/" + rp.Repo.OwnerName + "/" + rp.Repo.Name
145 for _, want := range []string{
146 "[](" + base + "/builds)",
147 "[[" + base + "/builds][" + base + "/badge/build.svg]]",
148 } {
149 if !strings.Contains(sb.String(), want) {
150 t.Errorf("badge section lacks %s", want)
151 }
152 }
153}
154
155func TestBuildPageRendersCommandOutput(t *testing.T) {
156 var sb strings.Builder
157 err := web.Render(&sb, "build.html", buildView{
158 repoPage: testRepoPage(),
159 Build: control.BuildOut{
160 Number: 60, Job: "build", Status: "success",
161 SHA: "ff6271a9d4570cd46f169091637a9d2e40ad5c2b", Ref: "cli-coverage",
162 CreatedAt: "2026-08-28T04:42:54Z", FinishedAt: "2026-08-28T04:43:06Z",
163 },
164 Log: "step 1 ok",
165 CanWrite: true,
166 })
167 if err != nil {
168 t.Fatalf("render: %v", err)
169 }
170 out := sb.String()
171 for _, want := range []string{
172 "Build 60", "success", "build on cli-coverage", "ff6271a9d4",
173 "2026-08-28 04:42", "2026-08-28 04:43", "step 1 ok",
174 } {
175 if !strings.Contains(out, want) {
176 t.Errorf("build.html missing %q", want)
177 }
178 }
179 // A finished build offers no cancel control, even to a writer.
180 if strings.Contains(out, "/cancel") {
181 t.Error("build.html offers cancel on a finished build")
182 }
183}
184
185// The count under the heading said "15 runs" while listing 23 builds
186// (#240). It names both numbers now, so neither is mistaken for the other.
187func TestBuildsPageCountsBuildsAndRuns(t *testing.T) {
188 builds := []control.BuildOut{
189 {Number: 4, Job: "instances", Status: "success", SHA: "aaa", Ref: "main", CreatedAt: "2026-09-20T06:00:00Z"},
190 {Number: 3, Job: "instances", Status: "success", SHA: "aaa", Ref: "main", CreatedAt: "2026-09-19T06:00:00Z"},
191 {Number: 2, Job: "lint", Status: "success", SHA: "aaa", Ref: "main", CreatedAt: "2026-09-12T11:20:03Z"},
192 {Number: 1, Job: "unit", Status: "success", SHA: "aaa", Ref: "main", CreatedAt: "2026-09-12T11:20:03Z"},
193 }
194 var sb strings.Builder
195 filter := buildFilter{}
196 err := web.Render(&sb, "builds.html", struct {
197 repoPage
198 Builds []control.BuildOut
199 Jobs []control.JobOut
200 Runs []buildRun
201 Filter buildFilter
202 Facets []facetGroup
203 Refs []string
204 Older string
205 CanWrite bool
206 Notice string
207 }{
208 testRepoPage(), builds, nil, groupRuns(builds), filter, nil,
209 distinctRefs(builds, filter.Ref), "", true, "",
210 })
211 if err != nil {
212 t.Fatalf("render: %v", err)
213 }
214 if !strings.Contains(sb.String(), "4 builds in 3 runs") {
215 t.Errorf("builds.html count line: %q", sb.String())
216 }
217}
218
219// A run row led with a ten-character sha and nothing said what the commit
220// was (#241). The subject leads now, the sha follows as metadata, and a
221// build whose commit is gone falls back to the sha alone.
222func TestBuildsPageLeadsWithTheSubject(t *testing.T) {
223 builds := []control.BuildOut{
224 {Number: 2, Job: "unit", Status: "success", SHA: "ff6271a9d4570cd46f169091637a9d2e40ad5c2b",
225 Ref: "main", CreatedAt: "2026-09-20T06:00:00Z", Subject: "runner: cap a build's container"},
226 {Number: 1, Job: "unit", Status: "failure", SHA: "aa11bb22cc33dd44ee55ff6677889900aabbccdd",
227 Ref: "main", CreatedAt: "2026-09-19T06:00:00Z"},
228 }
229 out := renderBuilds(t, builds, buildFilter{}, "?cursor=abc")
230 if !strings.Contains(out, "runner: cap a build's container") {
231 t.Errorf("builds.html does not lead with the subject:\n%s", out)
232 }
233 if !strings.Contains(out, "<code>ff6271a9d4</code>") {
234 t.Errorf("builds.html drops the sha from the metadata line:\n%s", out)
235 }
236 // The commit is gone, so the sha is all there is to name the row by.
237 if !strings.Contains(out, ">aa11bb22cc</a>") {
238 t.Errorf("a subjectless run does not fall back to its sha:\n%s", out)
239 }
240}
241
242// Filtering appeared to raise the run count because the list was capped
243// at 50 with the window unstated (#244). The page is paged now: the count
244// says it counts this page, and the link to the next one carries every
245// filter.
246func TestBuildsPagePagesAndSaysSo(t *testing.T) {
247 builds := []control.BuildOut{{Number: 1, Job: "unit", Status: "failure", SHA: "aaa", Ref: "main", CreatedAt: "2026-09-19T06:00:00Z"}}
248 out := renderBuilds(t, builds, buildFilter{Status: "failure"}, "?cursor=abc&status=failure")
249 if !strings.Contains(out, "1 build in 1 run on this page") {
250 t.Errorf("count line does not name the page:\n%s", out)
251 }
252 if !strings.Contains(out, `href="?cursor=abc&status=failure"`) {
253 t.Errorf("pager link missing or drops the filter:\n%s", out)
254 }
255 // With everything on one page the count is the whole count.
256 if out := renderBuilds(t, builds, buildFilter{}, ""); strings.Contains(out, "on this page") {
257 t.Errorf("an unpaged listing still hedges the count:\n%s", out)
258 }
259}
260
261func TestOlderBuildsCarriesFilters(t *testing.T) {
262 if got := olderBuilds(buildFilter{Ref: "main"}, ""); got != "" {
263 t.Errorf("no next cursor should mean no link, got %q", got)
264 }
265 got := olderBuilds(buildFilter{Ref: "feature/x", Status: "failure", Job: "unit"}, "c1")
266 for _, want := range []string{"cursor=c1", "ref=feature%2Fx", "status=failure", "job=unit"} {
267 if !strings.Contains(got, want) {
268 t.Errorf("olderBuilds = %q, missing %q", got, want)
269 }
270 }
271}
272
273func renderBuilds(t *testing.T, builds []control.BuildOut, filter buildFilter, older string) string {
274 t.Helper()
275 var sb strings.Builder
276 err := web.Render(&sb, "builds.html", struct {
277 repoPage
278 Builds []control.BuildOut
279 Jobs []control.JobOut
280 Runs []buildRun
281 Filter buildFilter
282 Facets []facetGroup
283 Refs []string
284 Older string
285 CanWrite bool
286 Notice string
287 }{
288 testRepoPage(), builds, nil, groupRuns(builds), filter, nil,
289 distinctRefs(builds, filter.Ref), older, true, "",
290 })
291 if err != nil {
292 t.Fatalf("render: %v", err)
293 }
294 return sb.String()
295}
296
297// A failed build's page folds its log by step, opens the step that
298// failed and links to it; no JavaScript (#266).
299func TestBuildPageFoldsStepsAndOpensFailure(t *testing.T) {
300 b := control.BuildOut{Number: 61, Job: "test", Status: "failure",
301 SHA: "ff6271a9d4570cd46f169091637a9d2e40ad5c2b", Ref: "main",
302 CreatedAt: "2026-08-28T04:42:54Z", FinishedAt: "2026-08-28T04:53:50Z", DurationS: 656,
303 Steps: []string{"go build ./...", "go test ./..."}, FailedStep: 2, FailedReason: "exit 1"}
304 log := "$ git clone x (ff6271a9d4)\n$ go build ./...\n$ go test ./...\n--- FAIL: TestCLI\nstep 2/2 failed: exit 1\n"
305 v := buildView{repoPage: testRepoPage(), Build: b, Log: log, Duration: "10m56s"}
306 v.Steps, v.Failed = logSteps(log, b)
307 var sb strings.Builder
308 if err := web.Render(&sb, "build.html", v); err != nil {
309 t.Fatalf("render: %v", err)
310 }
311 out := sb.String()
312 for _, want := range []string{
313 `<details class="difffold buildstep" id="failed" open>`,
314 "step 2/2", "<code>go test ./...</code>", `href="#failed"`, "Jump to failure",
315 "ran 10m56s", "--- FAIL: TestCLI",
316 } {
317 if !strings.Contains(out, want) {
318 t.Errorf("build.html missing %q", want)
319 }
320 }
321 if n := strings.Count(out, `class="difffold buildstep"`); n != 3 {
322 t.Errorf("%d step folds, want 3 (setup and two steps)", n)
323 }
324 if n := strings.Count(out, `id="failed"`); n != 1 {
325 t.Errorf("%d failed anchors, want 1", n)
326 }
327}
328
329// A multi-line step's fold summary shows only its first line, as build
330// show does; the whole step is in the log (#266).
331func TestBuildPageStepSummaryIsFirstLine(t *testing.T) {
332 step := "set -e\ngo test ./..."
333 b := control.BuildOut{Number: 62, Job: "test", Status: "failure",
334 SHA: "ff6271a9d4570cd46f169091637a9d2e40ad5c2b", Ref: "main",
335 CreatedAt: "2026-08-28T04:42:54Z", FinishedAt: "2026-08-28T04:43:54Z", DurationS: 60,
336 Steps: []string{step}, FailedStep: 1, FailedReason: "exit 1"}
337 log := "$ git clone x (ff6271a9d4)\n$ " + step + "\n--- FAIL: TestCLI\nstep 1/1 failed: exit 1\n"
338 v := buildView{repoPage: testRepoPage(), Build: b, Log: log, Duration: "1m0s"}
339 v.Steps, v.Failed = logSteps(log, b)
340 var sb strings.Builder
341 if err := web.Render(&sb, "build.html", v); err != nil {
342 t.Fatalf("render: %v", err)
343 }
344 out := sb.String()
345 if !strings.Contains(out, "<code>set -e</code>") {
346 t.Errorf("summary does not show the step's first line:\n%s", out)
347 }
348 if strings.Contains(out, "<code>set -e\ngo test") {
349 t.Errorf("summary shows the whole multi-line step")
350 }
351}
352
353// The Live note says the page updates itself, in plain words, rather
354// than the more technical "streams here" (#271).
355func TestBuildPageLiveNoteSaysItUpdatesItself(t *testing.T) {
356 var sb strings.Builder
357 if err := web.Render(&sb, "build.html", buildView{
358 repoPage: testRepoPage(), Live: true,
359 }); err != nil {
360 t.Fatalf("render: %v", err)
361 }
362 if !strings.Contains(sb.String(), "This page updates itself") {
363 t.Error(`Live note does not say the page updates itself`)
364 }
365}