ScuttleBot

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

Keyboard Shortcuts

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