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