ScuttleBot

scuttlebot / internal / api / api_test.go
Source Blame History 268 lines
2d8a379… lmata 1 package api_test
2d8a379… lmata 2
2d8a379… lmata 3 import (
2d8a379… lmata 4 "bytes"
2d8a379… lmata 5 "encoding/json"
2d8a379… lmata 6 "fmt"
2d8a379… lmata 7 "net/http"
2d8a379… lmata 8 "net/http/httptest"
2d8a379… lmata 9 "sync"
2d8a379… lmata 10 "testing"
2d8a379… lmata 11
2d8a379… lmata 12 "github.com/conflicthq/scuttlebot/internal/api"
68677f9… noreply 13 "github.com/conflicthq/scuttlebot/internal/auth"
2d8a379… lmata 14 "github.com/conflicthq/scuttlebot/internal/registry"
2d8a379… lmata 15 "log/slog"
2d8a379… lmata 16 "os"
2d8a379… lmata 17 )
2d8a379… lmata 18
2d8a379… lmata 19 var testLog = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
2d8a379… lmata 20
2d8a379… lmata 21 // mockProvisioner for registry tests.
2d8a379… lmata 22 type mockProvisioner struct {
2d8a379… lmata 23 mu sync.Mutex
2d8a379… lmata 24 accounts map[string]string
2d8a379… lmata 25 }
2d8a379… lmata 26
2d8a379… lmata 27 func newMock() *mockProvisioner {
2d8a379… lmata 28 return &mockProvisioner{accounts: make(map[string]string)}
2d8a379… lmata 29 }
2d8a379… lmata 30
2d8a379… lmata 31 func (m *mockProvisioner) RegisterAccount(name, pass string) error {
2d8a379… lmata 32 m.mu.Lock()
2d8a379… lmata 33 defer m.mu.Unlock()
2d8a379… lmata 34 if _, ok := m.accounts[name]; ok {
2d8a379… lmata 35 return fmt.Errorf("ACCOUNT_EXISTS")
2d8a379… lmata 36 }
2d8a379… lmata 37 m.accounts[name] = pass
2d8a379… lmata 38 return nil
2d8a379… lmata 39 }
2d8a379… lmata 40
2d8a379… lmata 41 func (m *mockProvisioner) ChangePassword(name, pass string) error {
2d8a379… lmata 42 m.mu.Lock()
2d8a379… lmata 43 defer m.mu.Unlock()
2d8a379… lmata 44 if _, ok := m.accounts[name]; !ok {
2d8a379… lmata 45 return fmt.Errorf("ACCOUNT_DOES_NOT_EXIST")
2d8a379… lmata 46 }
2d8a379… lmata 47 m.accounts[name] = pass
2d8a379… lmata 48 return nil
2d8a379… lmata 49 }
2d8a379… lmata 50
2d8a379… lmata 51 const testToken = "test-api-token-abc123"
2d8a379… lmata 52
2d8a379… lmata 53 func newTestServer(t *testing.T) *httptest.Server {
2d8a379… lmata 54 t.Helper()
2d8a379… lmata 55 reg := registry.New(newMock(), []byte("test-signing-key"))
68677f9… noreply 56 srv := api.New(reg, auth.TestStore(testToken), nil, nil, nil, nil, nil, nil, "", testLog)
2d8a379… lmata 57 return httptest.NewServer(srv.Handler())
2d8a379… lmata 58 }
2d8a379… lmata 59
2d8a379… lmata 60 func authHeader() http.Header {
2d8a379… lmata 61 h := http.Header{}
2d8a379… lmata 62 h.Set("Authorization", "Bearer "+testToken)
2d8a379… lmata 63 return h
2d8a379… lmata 64 }
2d8a379… lmata 65
2d8a379… lmata 66 func do(t *testing.T, srv *httptest.Server, method, path string, body any, headers http.Header) *http.Response {
2d8a379… lmata 67 t.Helper()
2d8a379… lmata 68 var buf bytes.Buffer
2d8a379… lmata 69 if body != nil {
2d8a379… lmata 70 if err := json.NewEncoder(&buf).Encode(body); err != nil {
2d8a379… lmata 71 t.Fatalf("encode body: %v", err)
2d8a379… lmata 72 }
2d8a379… lmata 73 }
2d8a379… lmata 74 req, err := http.NewRequest(method, srv.URL+path, &buf)
2d8a379… lmata 75 if err != nil {
2d8a379… lmata 76 t.Fatalf("new request: %v", err)
2d8a379… lmata 77 }
2d8a379… lmata 78 req.Header.Set("Content-Type", "application/json")
2d8a379… lmata 79 for k, vs := range headers {
2d8a379… lmata 80 for _, v := range vs {
2d8a379… lmata 81 req.Header.Set(k, v)
2d8a379… lmata 82 }
2d8a379… lmata 83 }
2d8a379… lmata 84 resp, err := http.DefaultClient.Do(req)
2d8a379… lmata 85 if err != nil {
2d8a379… lmata 86 t.Fatalf("do request: %v", err)
2d8a379… lmata 87 }
2d8a379… lmata 88 return resp
2d8a379… lmata 89 }
2d8a379… lmata 90
2d8a379… lmata 91 func TestAuthRequired(t *testing.T) {
2d8a379… lmata 92 srv := newTestServer(t)
2d8a379… lmata 93 defer srv.Close()
2d8a379… lmata 94
2d8a379… lmata 95 endpoints := []struct{ method, path string }{
2d8a379… lmata 96 {"GET", "/v1/status"},
2d8a379… lmata 97 {"GET", "/v1/agents"},
2d8a379… lmata 98 {"POST", "/v1/agents/register"},
2d8a379… lmata 99 }
2d8a379… lmata 100 for _, e := range endpoints {
2d8a379… lmata 101 t.Run(e.method+" "+e.path, func(t *testing.T) {
2d8a379… lmata 102 resp := do(t, srv, e.method, e.path, nil, nil)
2d8a379… lmata 103 defer resp.Body.Close()
2d8a379… lmata 104 if resp.StatusCode != http.StatusUnauthorized {
2d8a379… lmata 105 t.Errorf("expected 401, got %d", resp.StatusCode)
2d8a379… lmata 106 }
2d8a379… lmata 107 })
2d8a379… lmata 108 }
2d8a379… lmata 109 }
2d8a379… lmata 110
2d8a379… lmata 111 func TestInvalidToken(t *testing.T) {
2d8a379… lmata 112 srv := newTestServer(t)
2d8a379… lmata 113 defer srv.Close()
2d8a379… lmata 114
2d8a379… lmata 115 h := http.Header{}
2d8a379… lmata 116 h.Set("Authorization", "Bearer wrong-token")
2d8a379… lmata 117 resp := do(t, srv, "GET", "/v1/status", nil, h)
2d8a379… lmata 118 defer resp.Body.Close()
2d8a379… lmata 119 if resp.StatusCode != http.StatusUnauthorized {
2d8a379… lmata 120 t.Errorf("expected 401, got %d", resp.StatusCode)
2d8a379… lmata 121 }
2d8a379… lmata 122 }
2d8a379… lmata 123
2d8a379… lmata 124 func TestStatus(t *testing.T) {
2d8a379… lmata 125 srv := newTestServer(t)
2d8a379… lmata 126 defer srv.Close()
2d8a379… lmata 127
2d8a379… lmata 128 resp := do(t, srv, "GET", "/v1/status", nil, authHeader())
2d8a379… lmata 129 defer resp.Body.Close()
2d8a379… lmata 130 if resp.StatusCode != http.StatusOK {
2d8a379… lmata 131 t.Errorf("expected 200, got %d", resp.StatusCode)
2d8a379… lmata 132 }
2d8a379… lmata 133
2d8a379… lmata 134 var body map[string]any
2d8a379… lmata 135 if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
2d8a379… lmata 136 t.Fatalf("decode: %v", err)
2d8a379… lmata 137 }
2d8a379… lmata 138 if body["status"] != "ok" {
2d8a379… lmata 139 t.Errorf("status: got %q, want ok", body["status"])
2d8a379… lmata 140 }
2d8a379… lmata 141 }
2d8a379… lmata 142
2d8a379… lmata 143 func TestRegisterAndGet(t *testing.T) {
2d8a379… lmata 144 srv := newTestServer(t)
2d8a379… lmata 145 defer srv.Close()
2d8a379… lmata 146
2d8a379… lmata 147 // Register
2d8a379… lmata 148 resp := do(t, srv, "POST", "/v1/agents/register", map[string]any{
2d8a379… lmata 149 "nick": "claude-01",
2d8a379… lmata 150 "type": "worker",
2d8a379… lmata 151 "channels": []string{"#fleet"},
2d8a379… lmata 152 }, authHeader())
2d8a379… lmata 153 defer resp.Body.Close()
2d8a379… lmata 154 if resp.StatusCode != http.StatusCreated {
2d8a379… lmata 155 t.Errorf("register: expected 201, got %d", resp.StatusCode)
2d8a379… lmata 156 }
2d8a379… lmata 157
2d8a379… lmata 158 var regBody map[string]any
2d8a379… lmata 159 if err := json.NewDecoder(resp.Body).Decode(&regBody); err != nil {
2d8a379… lmata 160 t.Fatalf("decode: %v", err)
2d8a379… lmata 161 }
2d8a379… lmata 162 if regBody["credentials"] == nil {
2d8a379… lmata 163 t.Error("credentials missing from response")
2d8a379… lmata 164 }
2d8a379… lmata 165 if regBody["payload"] == nil {
2d8a379… lmata 166 t.Error("payload missing from response")
2d8a379… lmata 167 }
2d8a379… lmata 168
2d8a379… lmata 169 // Get
2d8a379… lmata 170 resp2 := do(t, srv, "GET", "/v1/agents/claude-01", nil, authHeader())
2d8a379… lmata 171 defer resp2.Body.Close()
2d8a379… lmata 172 if resp2.StatusCode != http.StatusOK {
2d8a379… lmata 173 t.Errorf("get: expected 200, got %d", resp2.StatusCode)
2d8a379… lmata 174 }
2d8a379… lmata 175 }
2d8a379… lmata 176
2d8a379… lmata 177 func TestRegisterDuplicate(t *testing.T) {
2d8a379… lmata 178 srv := newTestServer(t)
2d8a379… lmata 179 defer srv.Close()
2d8a379… lmata 180
2d8a379… lmata 181 body := map[string]any{"nick": "agent-dup", "type": "worker"}
2d8a379… lmata 182 do(t, srv, "POST", "/v1/agents/register", body, authHeader()).Body.Close()
2d8a379… lmata 183
2d8a379… lmata 184 resp := do(t, srv, "POST", "/v1/agents/register", body, authHeader())
2d8a379… lmata 185 defer resp.Body.Close()
2d8a379… lmata 186 if resp.StatusCode != http.StatusConflict {
2d8a379… lmata 187 t.Errorf("expected 409 on duplicate, got %d", resp.StatusCode)
2d8a379… lmata 188 }
2d8a379… lmata 189 }
2d8a379… lmata 190
2d8a379… lmata 191 func TestListAgents(t *testing.T) {
2d8a379… lmata 192 srv := newTestServer(t)
2d8a379… lmata 193 defer srv.Close()
2d8a379… lmata 194
2d8a379… lmata 195 for _, nick := range []string{"a1", "a2", "a3"} {
2d8a379… lmata 196 do(t, srv, "POST", "/v1/agents/register", map[string]any{"nick": nick}, authHeader()).Body.Close()
2d8a379… lmata 197 }
2d8a379… lmata 198
2d8a379… lmata 199 resp := do(t, srv, "GET", "/v1/agents", nil, authHeader())
2d8a379… lmata 200 defer resp.Body.Close()
2d8a379… lmata 201 if resp.StatusCode != http.StatusOK {
2d8a379… lmata 202 t.Errorf("expected 200, got %d", resp.StatusCode)
2d8a379… lmata 203 }
2d8a379… lmata 204
2d8a379… lmata 205 var body map[string]any
2d8a379… lmata 206 if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
2d8a379… lmata 207 t.Fatalf("decode: %v", err)
2d8a379… lmata 208 }
2d8a379… lmata 209 agents := body["agents"].([]any)
2d8a379… lmata 210 if len(agents) != 3 {
2d8a379… lmata 211 t.Errorf("expected 3 agents, got %d", len(agents))
2d8a379… lmata 212 }
2d8a379… lmata 213 }
2d8a379… lmata 214
2d8a379… lmata 215 func TestRotate(t *testing.T) {
2d8a379… lmata 216 srv := newTestServer(t)
2d8a379… lmata 217 defer srv.Close()
2d8a379… lmata 218
2d8a379… lmata 219 do(t, srv, "POST", "/v1/agents/register", map[string]any{"nick": "rot-agent"}, authHeader()).Body.Close()
2d8a379… lmata 220
2d8a379… lmata 221 resp := do(t, srv, "POST", "/v1/agents/rot-agent/rotate", nil, authHeader())
2d8a379… lmata 222 defer resp.Body.Close()
2d8a379… lmata 223 if resp.StatusCode != http.StatusOK {
2d8a379… lmata 224 t.Errorf("rotate: expected 200, got %d", resp.StatusCode)
2d8a379… lmata 225 }
2d8a379… lmata 226 }
2d8a379… lmata 227
2d8a379… lmata 228 func TestRevoke(t *testing.T) {
2d8a379… lmata 229 srv := newTestServer(t)
2d8a379… lmata 230 defer srv.Close()
2d8a379… lmata 231
2d8a379… lmata 232 do(t, srv, "POST", "/v1/agents/register", map[string]any{"nick": "rev-agent"}, authHeader()).Body.Close()
2d8a379… lmata 233
2d8a379… lmata 234 resp := do(t, srv, "POST", "/v1/agents/rev-agent/revoke", nil, authHeader())
2d8a379… lmata 235 defer resp.Body.Close()
2d8a379… lmata 236 if resp.StatusCode != http.StatusNoContent {
2d8a379… lmata 237 t.Errorf("revoke: expected 204, got %d", resp.StatusCode)
2d8a379… lmata 238 }
2d8a379… lmata 239
2d8a379… lmata 240 // Now get should 404.
2d8a379… lmata 241 resp2 := do(t, srv, "GET", "/v1/agents/rev-agent", nil, authHeader())
2d8a379… lmata 242 defer resp2.Body.Close()
2d8a379… lmata 243 if resp2.StatusCode != http.StatusNotFound {
2d8a379… lmata 244 t.Errorf("get revoked: expected 404, got %d", resp2.StatusCode)
2d8a379… lmata 245 }
2d8a379… lmata 246 }
2d8a379… lmata 247
2d8a379… lmata 248 func TestGetUnknown(t *testing.T) {
2d8a379… lmata 249 srv := newTestServer(t)
2d8a379… lmata 250 defer srv.Close()
2d8a379… lmata 251
2d8a379… lmata 252 resp := do(t, srv, "GET", "/v1/agents/nobody", nil, authHeader())
2d8a379… lmata 253 defer resp.Body.Close()
2d8a379… lmata 254 if resp.StatusCode != http.StatusNotFound {
2d8a379… lmata 255 t.Errorf("expected 404, got %d", resp.StatusCode)
2d8a379… lmata 256 }
2d8a379… lmata 257 }
2d8a379… lmata 258
2d8a379… lmata 259 func TestRegisterMissingNick(t *testing.T) {
2d8a379… lmata 260 srv := newTestServer(t)
2d8a379… lmata 261 defer srv.Close()
2d8a379… lmata 262
2d8a379… lmata 263 resp := do(t, srv, "POST", "/v1/agents/register", map[string]any{"type": "worker"}, authHeader())
2d8a379… lmata 264 defer resp.Body.Close()
2d8a379… lmata 265 if resp.StatusCode != http.StatusBadRequest {
2d8a379… lmata 266 t.Errorf("expected 400, got %d", resp.StatusCode)
2d8a379… lmata 267 }
2d8a379… lmata 268 }

Keyboard Shortcuts

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