Go · 6503 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 t.Fatalf("request: %#v", req)
29 }
30 w.Header().Set("Content-Type", "application/json")
31 _, _ = w.Write([]byte(`{
32 "token":"job-token",
33 "expires_at":"2026-05-10T21:00:00Z",
34 "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}]}
35 }`))
36 }))
37 defer srv.Close()
38
39 client, err := New(Config{BaseURL: srv.URL, RunnerToken: "runner-token", HTTPClient: srv.Client()})
40 if err != nil {
41 t.Fatalf("New: %v", err)
42 }
43 claim, err := client.Heartbeat(t.Context(), HeartbeatRequest{Labels: []string{"self-hosted", "linux"}, Capacity: 2})
44 if err != nil {
45 t.Fatalf("Heartbeat: %v", err)
46 }
47 if claim.Token != "job-token" || claim.Job.ID != 10 || claim.Job.Steps[0].Run != "echo hi" {
48 t.Fatalf("claim: %#v", claim)
49 }
50 }
51
52 func TestHeartbeat_NoJob(t *testing.T) {
53 t.Parallel()
54 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
55 w.WriteHeader(http.StatusNoContent)
56 }))
57 defer srv.Close()
58 client, err := New(Config{BaseURL: srv.URL, RunnerToken: "runner-token", HTTPClient: srv.Client()})
59 if err != nil {
60 t.Fatalf("New: %v", err)
61 }
62 claim, err := client.Heartbeat(t.Context(), HeartbeatRequest{Capacity: 1})
63 if err != nil {
64 t.Fatalf("Heartbeat: %v", err)
65 }
66 if claim != nil {
67 t.Fatalf("claim: %#v", claim)
68 }
69 }
70
71 func TestUpdateStatus_UsesJobTokenAndParsesNextToken(t *testing.T) {
72 t.Parallel()
73 started := time.Date(2026, 5, 10, 21, 0, 0, 123, time.UTC)
74 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
75 if r.URL.Path != "/api/v1/jobs/10/status" {
76 t.Fatalf("path: %s", r.URL.Path)
77 }
78 if got := r.Header.Get("Authorization"); got != "Bearer job-token" {
79 t.Fatalf("Authorization: %q", got)
80 }
81 var body map[string]string
82 if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
83 t.Fatalf("Decode: %v", err)
84 }
85 if body["status"] != "running" || !strings.HasPrefix(body["started_at"], "2026-05-10T21:00:00.") {
86 t.Fatalf("body: %#v", body)
87 }
88 w.Header().Set("Content-Type", "application/json")
89 _, _ = w.Write([]byte(`{"status":"running","conclusion":null,"next_token":"next","next_token_expires_at":"2026-05-10T21:15:00Z"}`))
90 }))
91 defer srv.Close()
92 client, err := New(Config{BaseURL: srv.URL, RunnerToken: "runner-token", HTTPClient: srv.Client()})
93 if err != nil {
94 t.Fatalf("New: %v", err)
95 }
96 resp, err := client.UpdateStatus(t.Context(), 10, "job-token", StatusRequest{Status: "running", StartedAt: started})
97 if err != nil {
98 t.Fatalf("UpdateStatus: %v", err)
99 }
100 if resp.NextToken != "next" {
101 t.Fatalf("NextToken: %q", resp.NextToken)
102 }
103 }
104
105 func TestUpdateStepStatus_UsesStepPathAndToken(t *testing.T) {
106 t.Parallel()
107 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
108 if r.URL.Path != "/api/v1/jobs/10/steps/20/status" {
109 t.Fatalf("path: %s", r.URL.Path)
110 }
111 if got := r.Header.Get("Authorization"); got != "Bearer job-token" {
112 t.Fatalf("Authorization: %q", got)
113 }
114 w.Header().Set("Content-Type", "application/json")
115 _, _ = w.Write([]byte(`{"status":"completed","conclusion":"success","next_token":"next","next_token_expires_at":"2026-05-10T21:15:00Z"}`))
116 }))
117 defer srv.Close()
118 client, err := New(Config{BaseURL: srv.URL, RunnerToken: "runner-token", HTTPClient: srv.Client()})
119 if err != nil {
120 t.Fatalf("New: %v", err)
121 }
122 resp, err := client.UpdateStepStatus(t.Context(), 10, 20, "job-token", StatusRequest{
123 Status: "completed",
124 Conclusion: "success",
125 })
126 if err != nil {
127 t.Fatalf("UpdateStepStatus: %v", err)
128 }
129 if resp.NextToken != "next" {
130 t.Fatalf("NextToken: %q", resp.NextToken)
131 }
132 }
133
134 func TestAppendLog_Base64EncodesChunk(t *testing.T) {
135 t.Parallel()
136 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
137 var body map[string]any
138 if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
139 t.Fatalf("Decode: %v", err)
140 }
141 if body["chunk"] != "aGkK" || body["seq"].(float64) != 7 {
142 t.Fatalf("body: %#v", body)
143 }
144 w.Header().Set("Content-Type", "application/json")
145 _, _ = w.Write([]byte(`{"accepted":true,"next_token":"next","next_token_expires_at":"2026-05-10T21:15:00Z"}`))
146 }))
147 defer srv.Close()
148 client, err := New(Config{BaseURL: srv.URL, RunnerToken: "runner-token", HTTPClient: srv.Client()})
149 if err != nil {
150 t.Fatalf("New: %v", err)
151 }
152 if _, err := client.AppendLog(t.Context(), 10, "job-token", LogRequest{Seq: 7, Chunk: []byte("hi\n")}); err != nil {
153 t.Fatalf("AppendLog: %v", err)
154 }
155 }
156
157 func TestCancelCheck_UsesJobTokenAndParsesResponse(t *testing.T) {
158 t.Parallel()
159 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
160 if r.URL.Path != "/api/v1/jobs/10/cancel-check" {
161 t.Fatalf("path: %s", r.URL.Path)
162 }
163 if got := r.Header.Get("Authorization"); got != "Bearer job-token" {
164 t.Fatalf("Authorization: %q", got)
165 }
166 w.Header().Set("Content-Type", "application/json")
167 _, _ = w.Write([]byte(`{"cancelled":true,"next_token":"next","next_token_expires_at":"2026-05-10T21:15:00Z"}`))
168 }))
169 defer srv.Close()
170 client, err := New(Config{BaseURL: srv.URL, RunnerToken: "runner-token", HTTPClient: srv.Client()})
171 if err != nil {
172 t.Fatalf("New: %v", err)
173 }
174 resp, err := client.CancelCheck(t.Context(), 10, "job-token")
175 if err != nil {
176 t.Fatalf("CancelCheck: %v", err)
177 }
178 if !resp.Cancelled || resp.NextToken != "next" {
179 t.Fatalf("response: %#v", resp)
180 }
181 }
182