Go · 6629 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package api
4
5 import (
6 "encoding/json"
7 "net/http"
8 "net/http/httptest"
9 "strings"
10 "testing"
11 "time"
12 )
13
14 func TestHeartbeat_ClaimsJob(t *testing.T) {
15 t.Parallel()
16 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
17 if r.URL.Path != "/api/v1/runners/heartbeat" {
18 t.Fatalf("path: %s", r.URL.Path)
19 }
20 if got := r.Header.Get("Authorization"); got != "Bearer runner-token" {
21 t.Fatalf("Authorization: %q", got)
22 }
23 var req HeartbeatRequest
24 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
25 t.Fatalf("Decode: %v", err)
26 }
27 if req.Capacity != 2 || strings.Join(req.Labels, ",") != "self-hosted,linux" ||
28 req.HostName != "runner-host" || req.Version != "dev-test" {
29 t.Fatalf("request: %#v", req)
30 }
31 w.Header().Set("Content-Type", "application/json")
32 _, _ = w.Write([]byte(`{
33 "token":"job-token",
34 "expires_at":"2026-05-10T21:00:00Z",
35 "job":{"id":10,"run_id":20,"repo_id":30,"run_index":1,"workflow_file":"ci.yml","workflow_name":"CI","head_sha":"abc","head_ref":"trunk","event":"push","job_key":"test","job_name":"test","runs_on":"ubuntu-latest","needs":[],"if":"","timeout_minutes":30,"permissions":{},"env":{"A":"B"},"steps":[{"id":40,"index":0,"step_id":"s1","name":"Run","if":"","run":"echo hi","uses":"","working_directory":"","env":{},"with":{},"continue_on_error":false}]}
36 }`))
37 }))
38 defer srv.Close()
39
40 client, err := New(Config{BaseURL: srv.URL, RunnerToken: "runner-token", HTTPClient: srv.Client()})
41 if err != nil {
42 t.Fatalf("New: %v", err)
43 }
44 claim, err := client.Heartbeat(t.Context(), HeartbeatRequest{
45 Labels: []string{"self-hosted", "linux"},
46 Capacity: 2,
47 HostName: "runner-host",
48 Version: "dev-test",
49 })
50 if err != nil {
51 t.Fatalf("Heartbeat: %v", err)
52 }
53 if claim.Token != "job-token" || claim.Job.ID != 10 || claim.Job.Steps[0].Run != "echo hi" {
54 t.Fatalf("claim: %#v", claim)
55 }
56 }
57
58 func TestHeartbeat_NoJob(t *testing.T) {
59 t.Parallel()
60 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
61 w.WriteHeader(http.StatusNoContent)
62 }))
63 defer srv.Close()
64 client, err := New(Config{BaseURL: srv.URL, RunnerToken: "runner-token", HTTPClient: srv.Client()})
65 if err != nil {
66 t.Fatalf("New: %v", err)
67 }
68 claim, err := client.Heartbeat(t.Context(), HeartbeatRequest{Capacity: 1})
69 if err != nil {
70 t.Fatalf("Heartbeat: %v", err)
71 }
72 if claim != nil {
73 t.Fatalf("claim: %#v", claim)
74 }
75 }
76
77 func TestUpdateStatus_UsesJobTokenAndParsesNextToken(t *testing.T) {
78 t.Parallel()
79 started := time.Date(2026, 5, 10, 21, 0, 0, 123, time.UTC)
80 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
81 if r.URL.Path != "/api/v1/jobs/10/status" {
82 t.Fatalf("path: %s", r.URL.Path)
83 }
84 if got := r.Header.Get("Authorization"); got != "Bearer job-token" {
85 t.Fatalf("Authorization: %q", got)
86 }
87 var body map[string]string
88 if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
89 t.Fatalf("Decode: %v", err)
90 }
91 if body["status"] != "running" || !strings.HasPrefix(body["started_at"], "2026-05-10T21:00:00.") {
92 t.Fatalf("body: %#v", body)
93 }
94 w.Header().Set("Content-Type", "application/json")
95 _, _ = w.Write([]byte(`{"status":"running","conclusion":null,"next_token":"next","next_token_expires_at":"2026-05-10T21:15:00Z"}`))
96 }))
97 defer srv.Close()
98 client, err := New(Config{BaseURL: srv.URL, RunnerToken: "runner-token", HTTPClient: srv.Client()})
99 if err != nil {
100 t.Fatalf("New: %v", err)
101 }
102 resp, err := client.UpdateStatus(t.Context(), 10, "job-token", StatusRequest{Status: "running", StartedAt: started})
103 if err != nil {
104 t.Fatalf("UpdateStatus: %v", err)
105 }
106 if resp.NextToken != "next" {
107 t.Fatalf("NextToken: %q", resp.NextToken)
108 }
109 }
110
111 func TestUpdateStepStatus_UsesStepPathAndToken(t *testing.T) {
112 t.Parallel()
113 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
114 if r.URL.Path != "/api/v1/jobs/10/steps/20/status" {
115 t.Fatalf("path: %s", r.URL.Path)
116 }
117 if got := r.Header.Get("Authorization"); got != "Bearer job-token" {
118 t.Fatalf("Authorization: %q", got)
119 }
120 w.Header().Set("Content-Type", "application/json")
121 _, _ = w.Write([]byte(`{"status":"completed","conclusion":"success","next_token":"next","next_token_expires_at":"2026-05-10T21:15:00Z"}`))
122 }))
123 defer srv.Close()
124 client, err := New(Config{BaseURL: srv.URL, RunnerToken: "runner-token", HTTPClient: srv.Client()})
125 if err != nil {
126 t.Fatalf("New: %v", err)
127 }
128 resp, err := client.UpdateStepStatus(t.Context(), 10, 20, "job-token", StatusRequest{
129 Status: "completed",
130 Conclusion: "success",
131 })
132 if err != nil {
133 t.Fatalf("UpdateStepStatus: %v", err)
134 }
135 if resp.NextToken != "next" {
136 t.Fatalf("NextToken: %q", resp.NextToken)
137 }
138 }
139
140 func TestAppendLog_Base64EncodesChunk(t *testing.T) {
141 t.Parallel()
142 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
143 var body map[string]any
144 if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
145 t.Fatalf("Decode: %v", err)
146 }
147 if body["chunk"] != "aGkK" || body["seq"].(float64) != 7 {
148 t.Fatalf("body: %#v", body)
149 }
150 w.Header().Set("Content-Type", "application/json")
151 _, _ = w.Write([]byte(`{"accepted":true,"next_token":"next","next_token_expires_at":"2026-05-10T21:15:00Z"}`))
152 }))
153 defer srv.Close()
154 client, err := New(Config{BaseURL: srv.URL, RunnerToken: "runner-token", HTTPClient: srv.Client()})
155 if err != nil {
156 t.Fatalf("New: %v", err)
157 }
158 if _, err := client.AppendLog(t.Context(), 10, "job-token", LogRequest{Seq: 7, Chunk: []byte("hi\n")}); err != nil {
159 t.Fatalf("AppendLog: %v", err)
160 }
161 }
162
163 func TestCancelCheck_UsesJobTokenAndParsesResponse(t *testing.T) {
164 t.Parallel()
165 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
166 if r.URL.Path != "/api/v1/jobs/10/cancel-check" {
167 t.Fatalf("path: %s", r.URL.Path)
168 }
169 if got := r.Header.Get("Authorization"); got != "Bearer job-token" {
170 t.Fatalf("Authorization: %q", got)
171 }
172 w.Header().Set("Content-Type", "application/json")
173 _, _ = w.Write([]byte(`{"cancelled":true,"next_token":"next","next_token_expires_at":"2026-05-10T21:15:00Z"}`))
174 }))
175 defer srv.Close()
176 client, err := New(Config{BaseURL: srv.URL, RunnerToken: "runner-token", HTTPClient: srv.Client()})
177 if err != nil {
178 t.Fatalf("New: %v", err)
179 }
180 resp, err := client.CancelCheck(t.Context(), 10, "job-token")
181 if err != nil {
182 t.Fatalf("CancelCheck: %v", err)
183 }
184 if !resp.Cancelled || resp.NextToken != "next" {
185 t.Fatalf("response: %#v", resp)
186 }
187 }
188