package api import ( "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/enmanuel/agents/shell/process" ) // newTestServer creates a Server with a real (temp-dir) Manager and a test API key. func newTestServer(t *testing.T) *Server { t.Helper() dir := t.TempDir() mgr := process.NewManager(dir+"/run", dir+"/agents/*/config.yaml", "") return New(mgr, "test-key-abcd1234", 0, nil) } // --- Auth tests --- func TestAuthMissingHeader(t *testing.T) { s := newTestServer(t) req := httptest.NewRequest(http.MethodGet, "/agents", nil) w := httptest.NewRecorder() s.auth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Error("handler called despite missing auth") })).ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", w.Code) } } func TestAuthWrongKey(t *testing.T) { s := newTestServer(t) req := httptest.NewRequest(http.MethodGet, "/agents", nil) req.Header.Set("Authorization", "Bearer wrong-key") w := httptest.NewRecorder() s.auth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Error("handler called despite wrong key") })).ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", w.Code) } } func TestAuthCorrectKey(t *testing.T) { s := newTestServer(t) req := httptest.NewRequest(http.MethodGet, "/agents", nil) req.Header.Set("Authorization", "Bearer test-key-abcd1234") w := httptest.NewRecorder() called := false s.auth(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true w.WriteHeader(http.StatusOK) })).ServeHTTP(w, req) if !called { t.Fatal("handler not called with valid key") } if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } } // --- Health endpoint --- func TestHealthEndpoint(t *testing.T) { s := newTestServer(t) mux := http.NewServeMux() mux.HandleFunc("GET /health", s.handleHealth) req := httptest.NewRequest(http.MethodGet, "/health", nil) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } var resp map[string]string if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("decode: %v", err) } if resp["status"] != "ok" { t.Errorf("expected status=ok, got %q", resp["status"]) } } // --- List agents --- func TestListAgentsEmpty(t *testing.T) { s := newTestServer(t) mux := http.NewServeMux() mux.Handle("GET /agents", s.auth(http.HandlerFunc(s.handleListAgents))) req := httptest.NewRequest(http.MethodGet, "/agents", nil) req.Header.Set("Authorization", "Bearer test-key-abcd1234") w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } body, _ := io.ReadAll(w.Body) // With empty agents dir, should return empty JSON array. trimmed := strings.TrimSpace(string(body)) if trimmed != "null" && trimmed != "[]" { // Accept both null and [] for empty slice serialisation. t.Logf("body: %s (acceptable)", trimmed) } } // --- Bus tests --- func TestBusSubscribePublish(t *testing.T) { b := NewBus() ch := b.Subscribe("test") defer b.Unsubscribe("test", ch) b.Publish("test", "hello") select { case ev := <-ch: if ev != "hello" { t.Fatalf("expected 'hello', got %v", ev) } case <-time.After(time.Second): t.Fatal("timeout waiting for event") } } func TestBusUnsubscribe(t *testing.T) { b := NewBus() ch := b.Subscribe("test") b.Unsubscribe("test", ch) // After unsubscribe, channel should be closed. select { case _, ok := <-ch: if ok { t.Fatal("channel should be closed after unsubscribe") } case <-time.After(100 * time.Millisecond): t.Fatal("channel not closed after unsubscribe") } } func TestBusMultipleSubscribers(t *testing.T) { b := NewBus() ch1 := b.Subscribe("x") ch2 := b.Subscribe("x") defer b.Unsubscribe("x", ch1) defer b.Unsubscribe("x", ch2) b.Publish("x", 42) for _, ch := range []<-chan Event{ch1, ch2} { select { case ev := <-ch: if ev != 42 { t.Fatalf("expected 42, got %v", ev) } case <-time.After(time.Second): t.Fatal("timeout") } } } // --- Get agent not found --- func TestGetAgentNotFound(t *testing.T) { s := newTestServer(t) mux := http.NewServeMux() mux.Handle("GET /agents/{id}", s.auth(http.HandlerFunc(s.handleGetAgent))) req := httptest.NewRequest(http.MethodGet, "/agents/nonexistent", nil) req.Header.Set("Authorization", "Bearer test-key-abcd1234") w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", w.Code) } } // --- Stop agent not running --- func TestStopAgentNotRunning(t *testing.T) { s := newTestServer(t) mux := http.NewServeMux() mux.Handle("POST /agents/{id}/stop", s.auth(http.HandlerFunc(s.handleStopAgent))) req := httptest.NewRequest(http.MethodPost, "/agents/ghost/stop", nil) req.Header.Set("Authorization", "Bearer test-key-abcd1234") w := httptest.NewRecorder() mux.ServeHTTP(w, req) // stop returns Conflict when the agent is not running if w.Code != http.StatusConflict { t.Fatalf("expected 409, got %d", w.Code) } } // --- Logs endpoint --- func TestLogsEndpointNotFound(t *testing.T) { s := newTestServer(t) mux := http.NewServeMux() mux.Handle("GET /agents/{id}/logs", s.auth(http.HandlerFunc(s.handleAgentLogs))) req := httptest.NewRequest(http.MethodGet, "/agents/nonexistent/logs?n=10", nil) req.Header.Set("Authorization", "Bearer test-key-abcd1234") w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("expected 404, got %d", w.Code) } } // --- extractBearerToken --- func TestExtractBearerToken(t *testing.T) { cases := []struct { header string want string }{ {"Bearer abc123", "abc123"}, {"bearer abc123", ""}, // case sensitive {"Basic abc123", ""}, {"", ""}, {"Bearer ", ""}, } for _, tc := range cases { req := httptest.NewRequest(http.MethodGet, "/", nil) if tc.header != "" { req.Header.Set("Authorization", tc.header) } got := extractBearerToken(req) if got != tc.want { t.Errorf("header=%q: got=%q want=%q", tc.header, got, tc.want) } } }