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