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.

Blame · server/events_test.go

Line attribution on main · 267 line(s)

eb07ff56
Keenan Nemetz
8d ago
1 package server_test
2
3 import (
4 "encoding/json"
5 "net/url"
6 "strings"
7 "testing"
8 "time"
9
d37d301f
Keenan Nemetz
16h ago
10 "rodent.software/vole/ci/runs"
eb07ff56
Keenan Nemetz
8d ago
11 )
12
13 func TestIssueAndCommentTriggersQueueRuns(t *testing.T) {
14 t.Parallel()
15 f := newFixture(t)
16 f.publish(f.repo)
17 work, _ := f.cloneWithChange(t, workflowFile(`version: 1
18 on:
19 issue:
20 comment:
21 jobs:
22 notify:
23 image: alpine:3.22
24 steps:
25 - run: cat "$VOLE_EVENT_PATH"
26 `), "ci")
27 if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "HEAD:refs/heads/main"); err != nil {
28 t.Fatalf("push: %s", errs)
29 }
30
31 if _, errs, err := f.cli(work, "issue", "new", "--title", "Trigger me", "-m", "Issue body"); err != nil {
32 t.Fatalf("issue new: %v\n%s", err, errs)
33 }
34 opened := f.waitForRun(t, 5*time.Second, func(r *runs.Run) bool {
35 return r.Trigger == "issue" && r.TriggerAction == "opened"
36 })
37 if opened.EventPayload == "" || !json.Valid([]byte(opened.EventPayload)) {
38 t.Fatalf("issue payload = %q", opened.EventPayload)
39 }
40 if !strings.Contains(opened.EventPayload, `"title":"Trigger me"`) ||
41 !strings.Contains(opened.EventPayload, `"body":"Issue body`) {
42 t.Fatalf("issue payload does not carry the issue:\n%s", opened.EventPayload)
43 }
44
45 if _, errs, err := f.cli(work, "issue", "comment", "#1", "-m", "A comment"); err != nil {
46 t.Fatalf("issue comment: %v\n%s", err, errs)
47 }
48 comment := f.waitForRun(t, 5*time.Second, func(r *runs.Run) bool {
49 return r.Trigger == "comment" && r.TriggerAction == "created"
50 })
51 if !strings.Contains(comment.EventPayload, `"body":"A comment`) ||
52 !strings.Contains(comment.EventPayload, `"comment_on":"issue"`) {
53 t.Fatalf("comment payload:\n%s", comment.EventPayload)
54 }
55 }
56
57 func TestChangeRequestTriggerCarriesBaseAndChangedFiles(t *testing.T) {
58 t.Parallel()
59 f := newFixture(t)
60 f.publish(f.repo)
61 main := workRepo(t, workflowFile(`version: 1
62 on:
63 change-request:
64 jobs:
65 describe:
66 image: alpine:3.22
67 steps:
68 - run: echo "$VOLE_EVENT_ACTION"
69 `))
70 if _, errs, err := f.git(main, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil {
71 t.Fatalf("push main: %s", errs)
72 }
73
74 feature, _ := f.cloneWithChange(t, map[string]string{"feature.txt": "a change\n"}, "feature")
75 if _, errs, err := f.git(feature, "push", f.sshURL(f.repo), "HEAD:refs/heads/feature"); err != nil {
76 t.Fatalf("push feature: %s", errs)
77 }
78 if _, errs, err := f.cli(feature, "change", "new", "--title", "A feature", "--base", "main", "--head", "feature", "-m", "body"); err != nil {
79 t.Fatalf("change new: %v\n%s", err, errs)
80 }
81
82 run := f.waitForRun(t, 5*time.Second, func(r *runs.Run) bool {
83 return r.Trigger == "change_request" && r.TriggerAction == "opened"
84 })
85 if run.Branch != "main" {
86 t.Fatalf("event run branch = %q, want the base", run.Branch)
87 }
88 var payload struct {
89 BaseSHA string `json:"base_sha"`
90 HeadSHA string `json:"head_sha"`
91 ChangedFiles []string `json:"changed_files"`
92 }
93 if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil {
94 t.Fatalf("payload: %v\n%s", err, run.EventPayload)
95 }
96 if payload.BaseSHA == "" || payload.HeadSHA == "" || payload.BaseSHA == payload.HeadSHA {
97 t.Fatalf("base/head = %q/%q", payload.BaseSHA, payload.HeadSHA)
98 }
99 found := false
100 for _, path := range payload.ChangedFiles {
101 if path == "feature.txt" {
102 found = true
103 }
104 }
105 if !found {
106 t.Fatalf("changed files = %v", payload.ChangedFiles)
107 }
108 }
109
110 func TestWorkflowDispatchCarriesInputs(t *testing.T) {
111 t.Parallel()
112 f := newFixture(t)
113 f.publish(f.repo)
114 work := workRepo(t, workflowFile(`version: 1
115 on:
116 workflow-dispatch:
117 inputs:
118 environment:
119 type: choice
120 default: staging
121 options: [staging, production]
122 verbose:
123 type: boolean
124 jobs:
125 deploy:
126 image: alpine:3.22
127 steps:
128 - run: cat "$VOLE_EVENT_PATH"
129 `))
130 if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil {
131 t.Fatalf("push: %s", errs)
132 }
133 out, errs, err := f.cli(work, "workflow", "dispatch", "--remote", f.sshURL(f.repo),
134 "--workflow", ".vole/workflows/ci.yaml",
135 "--input", "environment=production", "--input", "verbose=true")
136 if err != nil {
137 t.Fatalf("dispatch: %v\n%s", err, errs)
138 }
139 if !strings.Contains(out, "queued 1 run") {
140 t.Fatalf("dispatch output = %q", out)
141 }
142 run := f.waitForRun(t, 5*time.Second, func(r *runs.Run) bool {
143 return r.Trigger == "workflow_dispatch" && r.TriggerAction == "dispatched"
144 })
145 var payload struct {
146 Inputs map[string]string `json:"inputs"`
147 }
148 if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil {
149 t.Fatalf("payload: %v\n%s", err, run.EventPayload)
150 }
151 if payload.Inputs["environment"] != "production" || payload.Inputs["verbose"] != "true" {
152 t.Fatalf("inputs = %v", payload.Inputs)
153 }
154 }
155
156 func TestWorkflowDispatchPageRendersTheInputs(t *testing.T) {
157 t.Parallel()
158 f := newFixture(t)
159 f.publish(f.repo)
160 work := workRepo(t, workflowFile(`version: 1
161 on:
162 workflow-dispatch:
163 inputs:
164 environment:
165 description: Where to deploy
166 type: choice
167 default: staging
168 options: [staging, production]
169 jobs:
170 deploy:
171 image: alpine:3.22
172 steps:
173 - run: echo deploy
174 `))
175 if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil {
176 t.Fatalf("push: %s", errs)
177 }
178 sess, cookie := f.browserSession(t, f.ownerID)
ed03b3d1
Keenan Nemetz
3h ago
179 resp, body := f.browserGet(t, "/"+f.repo+"/workflows/dispatch?workflow=.vole/workflows/ci.yaml", cookie)
eb07ff56
Keenan Nemetz
8d ago
180 if resp.StatusCode != 200 {
181 t.Fatalf("dispatch page = %d\n%s", resp.StatusCode, body)
182 }
183 for _, want := range []string{"Run workflow", "environment", "production", ".vole/workflows/ci.yaml"} {
184 if !strings.Contains(body, want) {
185 t.Fatalf("dispatch page does not show %q:\n%s", want, body)
186 }
187 }
ed03b3d1
Keenan Nemetz
3h ago
188 resp, _ = f.browserPost(t, "/"+f.repo+"/workflows/dispatch", cookie, url.Values{
eb07ff56
Keenan Nemetz
8d ago
189 "csrf": {sess.CSRF}, "workflow": {".vole/workflows/ci.yaml"},
190 "ref": {"main"}, "environment": {"production"},
191 })
192 if resp.StatusCode != 303 {
193 t.Fatalf("dispatch post = %d", resp.StatusCode)
194 }
195 run := f.waitForRun(t, 5*time.Second, func(r *runs.Run) bool {
196 return r.Trigger == "workflow_dispatch" && strings.Contains(r.EventPayload, `"environment":"production"`)
197 })
198 if run.Branch != "main" {
199 t.Fatalf("dispatch branch = %q", run.Branch)
200 }
201 }
727df386
Keenan Nemetz
8d ago
202
203 func TestFilteredEventTriggersQueueOnlyMatchingRuns(t *testing.T) {
204 t.Parallel()
205 f := newFixture(t)
206 f.publish(f.repo)
207 f.addWriter(t) // wilbur, the account the agent filters name
208 work, _ := f.cloneWithChange(t, workflowFile(`version: 1
209 on:
210 issue:
211 types: [opened, assigned]
212 assigned-to: [wilbur]
213 author:
214 role: maintain
215 comment:
216 types: [created]
217 mentioned: [wilbur]
218 replied-to: [wilbur]
219 author:
220 name: [keenan]
221 role: maintain
222 jobs:
223 respond:
224 image: alpine:3.22
225 steps:
226 - run: cat "$VOLE_EVENT_PATH"
227 `), "ci")
228 if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "HEAD:refs/heads/main"); err != nil {
229 t.Fatalf("push: %s", errs)
230 }
231
232 out, errs, err := f.cli(work, "issue", "new", "--title", "Assigned at creation",
233 "-m", "No mention here.", "-a", "wilbur")
234 if err != nil {
235 t.Fatalf("issue new: %v\n%s", err, errs)
236 }
237 assignedID := strings.Fields(out)[0]
238 f.waitForRun(t, 5*time.Second, func(r *runs.Run) bool {
239 return r.Trigger == "issue" && r.TriggerAction == "opened" &&
240 strings.Contains(r.EventPayload, `"id":"`+assignedID+`"`)
241 })
242
243 out, errs, err = f.cli(work, "issue", "comment", assignedID, "-m", "@wilbur please take this.")
244 if err != nil {
245 t.Fatalf("issue comment: %v\n%s", err, errs)
246 }
247 f.waitForRun(t, 5*time.Second, func(r *runs.Run) bool {
248 return r.Trigger == "comment" && r.TriggerAction == "created" &&
249 strings.Contains(r.EventPayload, `"id":"`+assignedID+`"`) &&
250 strings.Contains(r.EventPayload, `"mentions":["wilbur"]`)
251 })
252
253 out, errs, err = f.cli(work, "issue", "new", "--title", "Assigned later",
254 "-m", "Plain body.")
255 if err != nil {
256 t.Fatalf("issue new (no push): %v\n%s", err, errs)
257 }
258 plainID := strings.Fields(out)[0]
259 if _, errs, err := f.cli(work, "issue", "assign", plainID, "wilbur"); err != nil {
260 t.Fatalf("issue assign: %v\n%s", err, errs)
261 }
262 f.waitForRun(t, 5*time.Second, func(r *runs.Run) bool {
263 return r.Trigger == "issue" && r.TriggerAction == "assigned" &&
264 strings.Contains(r.EventPayload, `"id":"`+plainID+`"`) &&
265 strings.Contains(r.EventPayload, `"assignees_added":["wilbur"]`)
266 })
267 }