Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

395 commit(s) on main

update readme Keenan Nemetz · 25m ago 32290114
history blame raw 7.1 KiB
1package server_test
2
3import (
4 "net/http"
5 "net/url"
6 "strings"
7 "testing"
8 "time"
9
10 "rodent.software/vole/ci/runs"
11)
12
13func TestRunPageLinksWorkflowAndProvidesRunActions(t *testing.T) {
14 t.Parallel()
15 f := newFixture(t)
16 f.publish(f.repo)
17 work := workRepo(t, map[string]string{
18 ".vole/workflows/ci.yaml": `version: 1
19name: Verify main
20description: Build and test every push.
21on:
22 push:
23 branches: [main]
24jobs:
25 test:
26 name: Unit tests
27 description: Runs the test suite.
28 image: alpine:3.22
29 steps:
30 - name: build
31 run: go build ./...
32`,
33 })
34 if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil {
35 t.Fatalf("push: %s", errs)
36 }
37
38 pushed := f.waitForRun(t, 5*time.Second, func(r *runs.Run) bool {
39 return r.Job == "test" && r.WorkflowName != ""
40 })
41 if pushed.WorkflowName != "Verify main" || pushed.WorkflowDescription != "Build and test every push." ||
42 pushed.JobName != "Unit tests" || pushed.JobDescription != "Runs the test suite." {
43 t.Fatalf("pinned display metadata = %+v", pushed)
44 }
45 code, body := f.get("/" + f.repo + "/runs/" + pushed.ID)
46 if code != 200 {
47 t.Fatalf("pushed run page = %d", code)
48 }
49 for _, want := range []string{"Run · Unit tests", "Verify main", "Build and test every push.", "Runs the test suite."} {
50 if !strings.Contains(body, want) {
51 t.Fatalf("run page does not show %q:\n%s", want, body)
52 }
53 }
54 workflowURL := "/" + f.repo + "/blob/" + pushed.SHA + "/.vole/workflows/ci.yaml"
55 if !strings.Contains(body, `href="`+workflowURL+`"`) {
56 t.Fatalf("run page does not link the workflow definition at %s:\n%s", workflowURL, body)
57 }
58 if code, _ := f.get("/" + f.repo + "/workflows"); code != http.StatusNotFound {
59 t.Fatalf("the removed workflows page = %d, want 404", code)
60 }
61
62 enqueue := func(id, status string) {
63 run := runs.Run{
64 ID: id, Repo: f.repo, Ref: "refs/heads/main", Branch: "main",
65 SHA: strings.Repeat("a", 40), Workflow: ".vole/workflows/ci.yaml",
66 WorkflowSHA: "bbbb", Job: "test", Status: status, Created: time.Now().UTC(),
67 Network: "bridge",
68 Steps: []runs.Step{{Name: "build", Run: "go build ./...", Timeout: "1m0s"}},
69 Services: []runs.Service{{ID: "db", Image: "postgres:17"}},
70 }
71 if status == runs.StatusSuccess {
72 run.Finished = time.Now().UTC()
73 run.Attempts = 1
74 }
75 if _, err := f.srv.Runs.Enqueue(run, []string{"linux"}); err != nil {
76 t.Fatal(err)
77 }
78 }
79 enqueue("01M5R8XR12EZ5WYJDM0F1928D1", runs.StatusQueued)
80
81 sess, cookie := f.browserSession(t, f.ownerID)
82 resp, body := f.browserGet(t, "/"+f.repo+"/runs/01M5R8XR12EZ5WYJDM0F1928D1", cookie)
83 if resp.StatusCode != 200 || !strings.Contains(body, "Cancel queued run") || strings.Contains(body, ">Rerun<") {
84 t.Fatalf("queued run actions = %d:\n%s", resp.StatusCode, body)
85 }
86 if !strings.Contains(body, "db") || !strings.Contains(body, "postgres:17") {
87 t.Fatalf("queued run page does not show its services:\n%s", body)
88 }
89
90 resp, _ = f.browserPost(t, "/"+f.repo+"/runs/01M5R8XR12EZ5WYJDM0F1928D1/cancel", cookie,
91 url.Values{"csrf": {sess.CSRF}})
92 if resp.StatusCode != 303 {
93 t.Fatalf("cancel = %d", resp.StatusCode)
94 }
95 run, err := f.srv.Runs.Get("01M5R8XR12EZ5WYJDM0F1928D1")
96 if err != nil || run.Status != runs.StatusCanceled {
97 t.Fatalf("canceled run = %+v, %v", run, err)
98 }
99 if resp, body := f.browserGet(t, "/"+f.repo+"/runs/01M5R8XR12EZ5WYJDM0F1928D1", cookie); resp.StatusCode != 200 || !strings.Contains(body, "Rerun") {
100 t.Fatalf("canceled run page = %d:\n%s", resp.StatusCode, body)
101 }
102
103 resp, _ = f.browserPost(t, "/"+f.repo+"/runs/01M5R8XR12EZ5WYJDM0F1928D1/rerun", cookie,
104 url.Values{"csrf": {sess.CSRF}})
105 if resp.StatusCode != 303 {
106 t.Fatalf("rerun = %d", resp.StatusCode)
107 }
108 if resp.Header.Get("Location") != "/"+f.repo+"/runs/01M5R8XR12EZ5WYJDM0F1928D1" {
109 t.Fatalf("rerun location = %q", resp.Header.Get("Location"))
110 }
111 next, err := f.srv.Runs.Get("01M5R8XR12EZ5WYJDM0F1928D1")
112 if err != nil || next.Status != runs.StatusQueued || next.Job != "test" || next.Workflow != ".vole/workflows/ci.yaml" {
113 t.Fatalf("rerun = %+v, %v", next, err)
114 }
115 labels, err := f.srv.Runs.Labels(next.ID)
116 if err != nil || strings.Join(labels, ",") != "linux" {
117 t.Fatalf("rerun labels = %v, %v", labels, err)
118 }
119
120 resp, _ = f.browserPost(t, "/"+f.repo+"/runs/"+next.ID+"/rerun", cookie,
121 url.Values{"csrf": {sess.CSRF}})
122 if resp.StatusCode != 400 {
123 t.Fatalf("rerun queued = %d, want 400", resp.StatusCode)
124 }
125 resp, _ = f.browserPost(t, "/"+f.repo+"/runs/"+next.ID+"/cancel", cookie, url.Values{})
126 if resp.StatusCode != 403 {
127 t.Fatalf("cancel without CSRF = %d, want 403", resp.StatusCode)
128 }
129}
130
131// A running run is stopped from the page by asking its runner. The page says who
132// asked while the runner is still working, and says the run was stopped rather than
133// failed once the attempt comes back.
134func TestStoppingARunningRunFromTheBrowser(t *testing.T) {
135 t.Parallel()
136 f := newFixture(t)
137 f.publish(f.repo)
138
139 const id = "01M5R8XR12EZ5WYJDM0F1928D2"
140 now := time.Now().UTC()
141 run := runs.Run{
142 ID: id, Repo: f.repo, Ref: "refs/heads/main", Branch: "main",
143 SHA: strings.Repeat("a", 40), Workflow: ".vole/workflows/ci.yaml",
144 WorkflowSHA: strings.Repeat("b", 40), StepsSHA: strings.Repeat("c", 40),
145 Job: "test", Status: runs.StatusQueued, Created: now,
146 Steps: []runs.Step{{Name: "build", Run: "go build ./...", Timeout: "1m0s"}},
147 }
148 if _, err := f.srv.Runs.Enqueue(run, nil); err != nil {
149 t.Fatal(err)
150 }
151 leased, err := f.srv.Runs.Lease(runs.LeaseRequest{
152 Owner: f.ownerID, Repos: []string{f.repo}, TTL: time.Hour,
153 }, now)
154 if err != nil || leased == nil {
155 t.Fatalf("lease = %+v, %v", leased, err)
156 }
157
158 sess, cookie := f.browserSession(t, f.ownerID)
159 resp, body := f.browserGet(t, "/"+f.repo+"/runs/"+id, cookie)
160 if resp.StatusCode != 200 || !strings.Contains(body, "Stop running run") {
161 t.Fatalf("running run page = %d:\n%s", resp.StatusCode, body)
162 }
163
164 resp, _ = f.browserPost(t, "/"+f.repo+"/runs/"+id+"/cancel", cookie,
165 url.Values{"csrf": {sess.CSRF}})
166 if resp.StatusCode != 303 {
167 t.Fatalf("stop running run = %d", resp.StatusCode)
168 }
169 got, err := f.srv.Runs.Get(id)
170 if err != nil {
171 t.Fatal(err)
172 }
173 if got.Status != runs.StatusRunning || !got.CancelRequested || got.CancelActor != f.ownerID {
174 t.Fatalf("run after the request = %+v", got)
175 }
176
177 // The page says the stop is on its way, and does not offer the button again.
178 resp, body = f.browserGet(t, "/"+f.repo+"/runs/"+id, cookie)
179 if resp.StatusCode != 200 || !strings.Contains(body, "Stopping") ||
180 !strings.Contains(body, f.ownerID) || strings.Contains(body, "Stop running run") {
181 t.Fatalf("page while stopping = %d:\n%s", resp.StatusCode, body)
182 }
183
184 // The runner stops and reports. The exit code is nonzero because the process was
185 // killed, and the page still says the run was canceled rather than failed.
186 if ok, err := f.srv.Runs.Finish(id, f.ownerID, 1, now.Add(time.Minute)); err != nil || !ok {
187 t.Fatalf("finish = %t, %v", ok, err)
188 }
189 resp, body = f.browserGet(t, "/"+f.repo+"/runs/"+id, cookie)
190 if resp.StatusCode != 200 || !strings.Contains(body, "canceled") ||
191 !strings.Contains(body, "Stopped while running") || !strings.Contains(body, f.ownerID) {
192 t.Fatalf("page after the stop = %d:\n%s", resp.StatusCode, body)
193 }
194}
195