e2e/buildcancelweb_test.go
122 lines · 5246 bytes
1package e2e
2
3import (
4 "net/url"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9)
10
11// TestBuildCancelWeb covers cancelling a build from its page (#162). The
12// control is offered for anything the command accepts — queued or running —
13// and hidden once a build reaches a terminal state; the command decides for
14// real, so a stale or repeated post against a build that is no longer
15// cancellable shows the refusal rather than a broken page.
16func TestBuildCancelWeb(t *testing.T) {
17 inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n")
18 aliceKey := inst.newKey(t, "alice")
19 inst.admin(t, "admin", "user", "create", "alice",
20 "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
21
22 runnerKey := inst.newKey(t, "ci-runner")
23 pub, _ := os.ReadFile(runnerKey + ".pub")
24 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
25 t.Fatalf("repo create: %s", errOut)
26 }
27 // The runner key is attached by alice through repo runner add, which
28 // registers it on her account with scope runner, confining it to the
29 // runner protocol and read-only git rather than reaching for admin.
30 if _, errOut, code := inst.ssh(t, aliceKey, string(pub), "repo", "runner", "add", "alice/app"); code != 0 {
31 t.Fatalf("repo runner add: %s", errOut)
32 }
33 work := t.TempDir()
34 env := inst.gitEnv(aliceKey)
35 mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
36 dir := filepath.Join(work, "w")
37 os.MkdirAll(filepath.Join(dir, ".gitbay"), 0o755)
38 os.WriteFile(filepath.Join(dir, ".gitbay", "ci.yml"), []byte("jobs:\n unit:\n steps:\n - echo fine\n"), 0o644)
39 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
40 mustGit(t, dir, env, "add", ".")
41 mustGit(t, dir, env, "commit", "-q", "-m", "ci")
42 mustGit(t, dir, env, "push", "-q", "origin", "main")
43
44 alice := inst.login(t, aliceKey)
45 build1 := inst.base() + "/alice/app/builds/1"
46
47 // Build 1 is queued: the control is on the page, for someone with
48 // write access.
49 _, body := browserGet(t, alice, build1)
50 if !strings.Contains(body, `action="/alice/app/builds/1/cancel"`) {
51 t.Fatalf("no cancel control on a queued build:\n%s", body)
52 }
53 // A reader gets no control.
54 _, anon := browserGet(t, newBrowser(t), build1)
55 if strings.Contains(anon, "/cancel") {
56 t.Fatal("anonymous visitor sees the cancel control")
57 }
58
59 // Cancelling from the page lands where the CLI sees it.
60 if status, _ := browserPost(t, alice, build1+"/cancel", url.Values{}); status != 200 {
61 t.Fatalf("cancel post: %d", status)
62 }
63 if out, _, _ := inst.ssh(t, aliceKey, "", "build", "list", "alice/app"); !strings.Contains(out, "unit\tcancelled") {
64 t.Fatalf("build not cancelled: %s", out)
65 }
66 // Cancelled, so the control is gone.
67 if _, body = browserGet(t, alice, build1); strings.Contains(body, "/cancel") {
68 t.Fatalf("cancel control still on a cancelled build:\n%s", body)
69 }
70
71 // Build 2: queued, then claimed by a runner without one actually
72 // running any steps — enough to move it to "running".
73 if _, errOut, code := inst.ssh(t, aliceKey, "", "build", "trigger", "alice/app", "unit"); code != 0 {
74 t.Fatalf("trigger: %s", errOut)
75 }
76 if out, _, code := inst.ssh(t, runnerKey, "", "runner", "next", "alice/app"); code != 0 || strings.Contains(out, "no pending") {
77 t.Fatalf("runner claim: %s", out)
78 }
79 build2 := inst.base() + "/alice/app/builds/2"
80 _, body = browserGet(t, alice, build2)
81 if !strings.Contains(body, "running") {
82 t.Fatalf("build 2 not running:\n%s", body)
83 }
84 // The command accepts cancelling a running build too — that is the
85 // case #162 was filed for, a run going nowhere — so the control stays
86 // up, worded so cancelling does not read as instant.
87 if !strings.Contains(body, `action="/alice/app/builds/2/cancel"`) {
88 t.Fatalf("no cancel control on a running build:\n%s", body)
89 }
90 if !strings.Contains(body, "next check") {
91 t.Fatalf("cancel control does not warn it is not instant:\n%s", body)
92 }
93
94 // Cancelling the running build from the page lands where the CLI sees
95 // it, including the runner-facing wording that it stops at its next
96 // check rather than right away.
97 if status, _ := browserPost(t, alice, build2+"/cancel", url.Values{}); status != 200 {
98 t.Fatalf("cancel running build: %d", status)
99 }
100 out, _, _ := inst.ssh(t, aliceKey, "", "build", "list", "alice/app")
101 if !strings.Contains(out, "unit\tcancelled") {
102 t.Fatalf("running build not cancelled: %s", out)
103 }
104 log, _, _ := inst.ssh(t, aliceKey, "", "build", "log", "alice/app", "2")
105 if !strings.Contains(log, "cancelled by alice while running") {
106 t.Fatalf("log missing the while-running cancellation: %s", log)
107 }
108 // Cancelled, so the control is gone here too.
109 if _, body = browserGet(t, alice, build2); strings.Contains(body, "/cancel") {
110 t.Fatalf("cancel control still on a cancelled build:\n%s", body)
111 }
112
113 // A stale or repeated post against a build that can no longer be
114 // cancelled is refused, and the page says so rather than breaking.
115 status, body := browserPost(t, alice, build1+"/cancel", url.Values{})
116 if status != 200 {
117 t.Fatalf("re-cancel post: %d", status)
118 }
119 if !strings.Contains(body, `class="error"`) || !strings.Contains(body, "cancelled") {
120 t.Fatalf("refusal not surfaced on the page:\n%s", body)
121 }
122}