ScuttleBot

scuttlebot / cmd / scuttlectl / internal / apiclient / apiclient_test.go
Blame History Raw 307 lines
1
package apiclient_test
2
3
import (
4
"encoding/json"
5
"net/http"
6
"net/http/httptest"
7
"testing"
8
9
"github.com/conflicthq/scuttlebot/cmd/scuttlectl/internal/apiclient"
10
)
11
12
func newServer(t *testing.T, handler http.Handler) (*httptest.Server, *apiclient.Client) {
13
t.Helper()
14
srv := httptest.NewServer(handler)
15
t.Cleanup(srv.Close)
16
return srv, apiclient.New(srv.URL, "test-token")
17
}
18
19
func TestStatus(t *testing.T) {
20
srv, client := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
21
if r.URL.Path != "/v1/status" || r.Method != http.MethodGet {
22
http.NotFound(w, r)
23
return
24
}
25
assertBearer(t, r)
26
w.Header().Set("Content-Type", "application/json")
27
_, _ = w.Write([]byte(`{"status":"ok"}`))
28
}))
29
_ = srv
30
31
raw, err := client.Status()
32
if err != nil {
33
t.Fatal(err)
34
}
35
var got map[string]string
36
if err := json.Unmarshal(raw, &got); err != nil {
37
t.Fatal(err)
38
}
39
if got["status"] != "ok" {
40
t.Errorf("status: got %q", got["status"])
41
}
42
}
43
44
func TestListAgents(t *testing.T) {
45
_, client := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
46
assertBearer(t, r)
47
w.Header().Set("Content-Type", "application/json")
48
_, _ = w.Write([]byte(`{"agents":[{"nick":"claude-1"}]}`))
49
}))
50
51
raw, err := client.ListAgents()
52
if err != nil {
53
t.Fatal(err)
54
}
55
if len(raw) == 0 {
56
t.Error("expected non-empty response")
57
}
58
}
59
60
func TestGetAgent(t *testing.T) {
61
_, client := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
62
assertBearer(t, r)
63
if r.URL.Path != "/v1/agents/claude-1" {
64
http.NotFound(w, r)
65
return
66
}
67
w.Header().Set("Content-Type", "application/json")
68
_, _ = w.Write([]byte(`{"nick":"claude-1","type":"worker"}`))
69
}))
70
71
raw, err := client.GetAgent("claude-1")
72
if err != nil {
73
t.Fatal(err)
74
}
75
var got map[string]string
76
if err := json.Unmarshal(raw, &got); err != nil {
77
t.Fatal(err)
78
}
79
if got["nick"] != "claude-1" {
80
t.Errorf("nick: got %q", got["nick"])
81
}
82
}
83
84
func TestRegisterAgent(t *testing.T) {
85
var gotBody map[string]any
86
_, client := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
87
assertBearer(t, r)
88
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
89
http.Error(w, err.Error(), http.StatusBadRequest)
90
return
91
}
92
w.Header().Set("Content-Type", "application/json")
93
w.WriteHeader(http.StatusCreated)
94
_, _ = w.Write([]byte(`{"nick":"claude-1","credentials":{"passphrase":"secret"}}`))
95
}))
96
97
raw, err := client.RegisterAgent("claude-1", "worker", []string{"#general"})
98
if err != nil {
99
t.Fatal(err)
100
}
101
if raw == nil {
102
t.Error("expected response body")
103
}
104
if gotBody["nick"] != "claude-1" {
105
t.Errorf("body nick: got %v", gotBody["nick"])
106
}
107
if gotBody["type"] != "worker" {
108
t.Errorf("body type: got %v", gotBody["type"])
109
}
110
}
111
112
func TestRevokeAgent(t *testing.T) {
113
called := false
114
_, client := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
115
assertBearer(t, r)
116
if r.URL.Path == "/v1/agents/claude-1/revoke" && r.Method == http.MethodPost {
117
called = true
118
w.Header().Set("Content-Type", "application/json")
119
_, _ = w.Write([]byte(`{}`))
120
} else {
121
http.NotFound(w, r)
122
}
123
}))
124
125
if err := client.RevokeAgent("claude-1"); err != nil {
126
t.Fatal(err)
127
}
128
if !called {
129
t.Error("revoke endpoint not called")
130
}
131
}
132
133
func TestRotateAgent(t *testing.T) {
134
_, client := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
135
assertBearer(t, r)
136
w.Header().Set("Content-Type", "application/json")
137
_, _ = w.Write([]byte(`{"passphrase":"newpass"}`))
138
}))
139
140
raw, err := client.RotateAgent("claude-1")
141
if err != nil {
142
t.Fatal(err)
143
}
144
var got map[string]string
145
if err := json.Unmarshal(raw, &got); err != nil {
146
t.Fatal(err)
147
}
148
if got["passphrase"] != "newpass" {
149
t.Errorf("passphrase: got %q", got["passphrase"])
150
}
151
}
152
153
func TestDeleteAgent(t *testing.T) {
154
called := false
155
_, client := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
156
assertBearer(t, r)
157
if r.Method == http.MethodDelete && r.URL.Path == "/v1/agents/claude-1" {
158
called = true
159
w.WriteHeader(http.StatusNoContent)
160
} else {
161
http.NotFound(w, r)
162
}
163
}))
164
165
if err := client.DeleteAgent("claude-1"); err != nil {
166
t.Fatal(err)
167
}
168
if !called {
169
t.Error("delete endpoint not called")
170
}
171
}
172
173
func TestListChannels(t *testing.T) {
174
_, client := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
175
assertBearer(t, r)
176
w.Header().Set("Content-Type", "application/json")
177
_, _ = w.Write([]byte(`{"channels":["#general","#ops"]}`))
178
}))
179
180
raw, err := client.ListChannels()
181
if err != nil {
182
t.Fatal(err)
183
}
184
if len(raw) == 0 {
185
t.Error("expected non-empty response")
186
}
187
}
188
189
func TestDeleteChannel(t *testing.T) {
190
called := false
191
_, client := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
192
assertBearer(t, r)
193
if r.Method == http.MethodDelete && r.URL.Path == "/v1/channels/general" {
194
called = true
195
w.WriteHeader(http.StatusNoContent)
196
} else {
197
http.NotFound(w, r)
198
}
199
}))
200
201
// should strip the leading #
202
if err := client.DeleteChannel("#general"); err != nil {
203
t.Fatal(err)
204
}
205
if !called {
206
t.Error("delete channel endpoint not called")
207
}
208
}
209
210
func TestGetLLMBackend(t *testing.T) {
211
_, client := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
212
assertBearer(t, r)
213
w.Header().Set("Content-Type", "application/json")
214
_, _ = w.Write([]byte(`{"backends":[{"name":"anthropic","backend":"anthropic"},{"name":"ollama","backend":"ollama"}]}`))
215
}))
216
217
raw, err := client.GetLLMBackend("ollama")
218
if err != nil {
219
t.Fatal(err)
220
}
221
var got map[string]string
222
if err := json.Unmarshal(raw, &got); err != nil {
223
t.Fatal(err)
224
}
225
if got["name"] != "ollama" {
226
t.Errorf("name: got %q", got["name"])
227
}
228
}
229
230
func TestGetLLMBackendNotFound(t *testing.T) {
231
_, client := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
232
assertBearer(t, r)
233
w.Header().Set("Content-Type", "application/json")
234
_, _ = w.Write([]byte(`{"backends":[]}`))
235
}))
236
237
_, err := client.GetLLMBackend("nonexistent")
238
if err == nil {
239
t.Error("expected error for missing backend, got nil")
240
}
241
}
242
243
func TestAPIError(t *testing.T) {
244
_, client := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
245
w.Header().Set("Content-Type", "application/json")
246
w.WriteHeader(http.StatusUnauthorized)
247
_, _ = w.Write([]byte(`{"error":"invalid token"}`))
248
}))
249
250
_, err := client.Status()
251
if err == nil {
252
t.Fatal("expected error, got nil")
253
}
254
if err.Error() != "API error 401: invalid token" {
255
t.Errorf("error message: got %q", err.Error())
256
}
257
}
258
259
func TestAddAdmin(t *testing.T) {
260
var gotBody map[string]string
261
_, client := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
262
assertBearer(t, r)
263
_ = json.NewDecoder(r.Body).Decode(&gotBody)
264
w.Header().Set("Content-Type", "application/json")
265
w.WriteHeader(http.StatusCreated)
266
_, _ = w.Write([]byte(`{"username":"alice"}`))
267
}))
268
269
raw, err := client.AddAdmin("alice", "hunter2")
270
if err != nil {
271
t.Fatal(err)
272
}
273
if raw == nil {
274
t.Error("expected response")
275
}
276
if gotBody["username"] != "alice" || gotBody["password"] != "hunter2" {
277
t.Errorf("body: got %v", gotBody)
278
}
279
}
280
281
func TestRemoveAdmin(t *testing.T) {
282
called := false
283
_, client := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
284
assertBearer(t, r)
285
if r.Method == http.MethodDelete && r.URL.Path == "/v1/admins/alice" {
286
called = true
287
w.WriteHeader(http.StatusNoContent)
288
} else {
289
http.NotFound(w, r)
290
}
291
}))
292
293
if err := client.RemoveAdmin("alice"); err != nil {
294
t.Fatal(err)
295
}
296
if !called {
297
t.Error("remove admin endpoint not called")
298
}
299
}
300
301
func assertBearer(t *testing.T, r *http.Request) {
302
t.Helper()
303
if r.Header.Get("Authorization") != "Bearer test-token" {
304
t.Errorf("Authorization header: got %q", r.Header.Get("Authorization"))
305
}
306
}
307

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button