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/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

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

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 19m ago 4b8c9e1d
history blame raw 6.3 KiB
1package ssh
2
3// The run actions the browser offers are also CLI verbs, and both go through the
4// same rules. These tests speak the runner protocol and the run-management verbs
5// over real SSH channels: a stop request reaches a running runner on its next
6// heartbeat, the stopped attempt keeps the canceled status, and a finished run can
7// be queued again.
8
9import (
10 "os"
11 "path/filepath"
12 "strings"
13 "testing"
14 "time"
15
16 "rodent.software/vole/ci/runs"
17)
18
19func TestAHeartbeatCarriesTheStopRequest(t *testing.T) {
20 t.Parallel()
21 h := newHarness(t)
22 c := h.runner(t, h.keyPath)
23 run, job := h.leaseRun(t, c, "build")
24
25 // Nobody has asked yet, so the heartbeat is only a lease renewal.
26 if rep := c.call(runnerRequest{Op: "heartbeat", Run: job.ID}); !rep.OK || rep.Cancel != "" {
27 t.Fatalf("heartbeat before the request = %+v", rep)
28 }
29
30 ok, err := h.runs.CancelRunning(run.ID, h.repoName, "maintainer", time.Now().UTC())
31 if err != nil || !ok {
32 t.Fatalf("cancel running = %t, %v", ok, err)
33 }
34 // The lease is untouched: the runner still holds the job, and that is what makes
35 // its partial attempt acceptable when it arrives.
36 rep := c.call(runnerRequest{Op: "heartbeat", Run: job.ID})
37 if !rep.OK || rep.Cancel != "maintainer" {
38 t.Fatalf("heartbeat after the request = %+v", rep)
39 }
40
41 rec := recordFor(job, h.writerID)
42 rec.Exit = 1
43 for i := range rec.Steps {
44 rec.Steps[i].Exit = 1
45 }
46 rep = c.call(runnerRequest{Op: "finish", Run: job.ID, Record: &rec})
47 if !rep.OK {
48 t.Fatalf("the stopped attempt was refused: %+v", rep)
49 }
50 got, err := h.runs.Get(run.ID)
51 if err != nil {
52 t.Fatal(err)
53 }
54 if got.Status != runs.StatusCanceled || !got.CancelRequested || got.CancelActor != "maintainer" {
55 t.Fatalf("run after the stopped attempt = %+v", got)
56 }
57}
58
59// The CLI cancel asks the instance, not a clone: the queue and the access rules are
60// both server-side. It is the same action the browser offers, over the key the
61// account pushes with, and it writes the same audit record.
62func TestTheCLICancelsARunningRun(t *testing.T) {
63 t.Parallel()
64 h := newHarness(t)
65 runner := h.runner(t, h.keyPath)
66 run, job := h.leaseRun(t, runner, "build")
67
68 client := h.dial(t, h.keyPath)
69 sess, err := client.NewSession()
70 if err != nil {
71 t.Fatalf("new session: %v", err)
72 }
73 defer sess.Close()
74 out, err := sess.CombinedOutput("vole-run-cancel /demo.git " + run.ID)
75 if err != nil {
76 t.Fatalf("cancel: %v: %s", err, out)
77 }
78 if !strings.Contains(string(out), "stop") {
79 t.Fatalf("cancel said %q, want the runner's next-heartbeat outcome", out)
80 }
81
82 got, err := h.runs.Get(run.ID)
83 if err != nil {
84 t.Fatal(err)
85 }
86 if got.Status != runs.StatusRunning || !got.CancelRequested || got.CancelActor != h.writerID {
87 t.Fatalf("run after the CLI cancel = %+v", got)
88 }
89
90 // The runner learns from the reply it already asks for.
91 if rep := runner.call(runnerRequest{Op: "heartbeat", Run: job.ID}); !rep.OK || rep.Cancel != h.writerID {
92 t.Fatalf("heartbeat after the CLI cancel = %+v", rep)
93 }
94
95 raw, err := os.ReadFile(filepath.Join(h.dir, "audit.log"))
96 if err != nil {
97 t.Fatalf("reading the audit log: %v", err)
98 }
99 audit := string(raw)
100 for _, want := range []string{"run.cancel", h.writerID, h.repoName, run.ID} {
101 if !strings.Contains(audit, want) {
102 t.Fatalf("audit log does not name %q:\n%s", want, audit)
103 }
104 }
105}
106
107// Canceling is a maintainer capability, and the CLI asks the same rule the page
108// does: a reader may look at a run and may not stop it.
109func TestTheCLIWillNotCancelAsAReader(t *testing.T) {
110 t.Parallel()
111 h := newHarness(t)
112 runner := h.runner(t, h.keyPath)
113 run, _ := h.leaseRun(t, runner, "build")
114
115 client := h.dial(t, h.roKey)
116 sess, err := client.NewSession()
117 if err != nil {
118 t.Fatalf("new session: %v", err)
119 }
120 defer sess.Close()
121 out, err := sess.CombinedOutput("vole-run-cancel /demo.git " + run.ID)
122 if err == nil {
123 t.Fatalf("a reader canceled a run: %s", out)
124 }
125 if !strings.Contains(string(out), "may not manage runs") {
126 t.Fatalf("refusal = %q, want the missing capability", out)
127 }
128 got, err := h.runs.Get(run.ID)
129 if err != nil {
130 t.Fatal(err)
131 }
132 if got.CancelRequested {
133 t.Fatalf("a reader's cancel was applied: %+v", got)
134 }
135}
136
137// Rerun is the other action on a run page, and the CLI offers it with the same
138// rule: a finished run is queued again with the job it already pinned, and the
139// history of the attempt is kept.
140func TestTheCLIRerunsAFinishedRun(t *testing.T) {
141 t.Parallel()
142 h := newHarness(t)
143 runner := h.runner(t, h.keyPath)
144 run, _ := h.leaseRun(t, runner, "build")
145 if ok, err := h.runs.Finish(run.ID, h.writerID, 1, time.Now().UTC()); err != nil || !ok {
146 t.Fatalf("finish = %t, %v", ok, err)
147 }
148
149 client := h.dial(t, h.keyPath)
150 sess, err := client.NewSession()
151 if err != nil {
152 t.Fatalf("new session: %v", err)
153 }
154 defer sess.Close()
155 out, err := sess.CombinedOutput("vole-run-rerun /demo.git " + run.ID)
156 if err != nil {
157 t.Fatalf("rerun: %v: %s", err, out)
158 }
159 if !strings.Contains(string(out), "queued") {
160 t.Fatalf("rerun said %q, want a queued outcome", out)
161 }
162
163 got, err := h.runs.Get(run.ID)
164 if err != nil {
165 t.Fatal(err)
166 }
167 if got.Status != runs.StatusQueued || got.Attempts != run.Attempts {
168 t.Fatalf("run after the CLI rerun = %+v, want queued at the same attempt", got)
169 }
170
171 raw, err := os.ReadFile(filepath.Join(h.dir, "audit.log"))
172 if err != nil {
173 t.Fatalf("reading the audit log: %v", err)
174 }
175 audit := string(raw)
176 for _, want := range []string{"run.rerun", h.writerID, h.repoName, run.ID} {
177 if !strings.Contains(audit, want) {
178 t.Fatalf("audit log does not name %q:\n%s", want, audit)
179 }
180 }
181}
182
183// A run that has not finished cannot be rerun: that would run the same job twice,
184// not queue it again.
185func TestTheCLIWillNotRerunARunningRun(t *testing.T) {
186 t.Parallel()
187 h := newHarness(t)
188 runner := h.runner(t, h.keyPath)
189 run, _ := h.leaseRun(t, runner, "build")
190
191 client := h.dial(t, h.keyPath)
192 sess, err := client.NewSession()
193 if err != nil {
194 t.Fatalf("new session: %v", err)
195 }
196 defer sess.Close()
197 out, err := sess.CombinedOutput("vole-run-rerun /demo.git " + run.ID)
198 if err == nil {
199 t.Fatalf("a running run was rerun: %s", out)
200 }
201 if !strings.Contains(string(out), "only a finished run can be rerun") {
202 t.Fatalf("refusal = %q, want the finished-run rule", out)
203 }
204 got, err := h.runs.Get(run.ID)
205 if err != nil {
206 t.Fatal(err)
207 }
208 if got.Status != runs.StatusRunning {
209 t.Fatalf("a refused rerun changed the run: %+v", got)
210 }
211}
212