package server import ( "encoding/json" "io" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "time" "github.com/longbin/agent-notify/internal/remote" ) func openTestStore(t *testing.T) *Store { t.Helper() db := filepath.Join(t.TempDir(), "test.db") s, err := Open(db) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = s.Close() }) return s } func TestHTTPPostWithoutToken401(t *testing.T) { store := openTestStore(t) srv := httptest.NewServer(NewHandler(store, "secret")) defer srv.Close() body := `{"hostname":"h","ips":["1.2.3.4"],"agent":"Cursor","cwd":"/x","status":"waiting","updated_at":"2026-06-02T00:00:00Z"}` resp, err := http.Post(srv.URL+"/api/v1/status", "application/json", strings.NewReader(body)) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("status: got %d want %d", resp.StatusCode, http.StatusUnauthorized) } } func TestHTTPPostValidToken200(t *testing.T) { store := openTestStore(t) srv := httptest.NewServer(NewHandler(store, "secret")) defer srv.Close() now := time.Now().UTC().Format(time.RFC3339Nano) body := `{"hostname":"h","ips":["1.2.3.4"],"agent":"Cursor","cwd":"/x","status":"waiting","updated_at":"` + now + `"}` req, err := http.NewRequest(http.MethodPost, srv.URL+"/api/v1/status", strings.NewReader(body)) if err != nil { t.Fatal(err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer secret") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("status: got %d want %d", resp.StatusCode, http.StatusOK) } var okResp map[string]bool if err := json.NewDecoder(resp.Body).Decode(&okResp); err != nil { t.Fatal(err) } if !okResp["ok"] { t.Fatalf("response: %+v", okResp) } rows, err := store.List(ListFilters{Agent: "Cursor"}) if err != nil { t.Fatal(err) } if len(rows) != 1 { t.Fatalf("expected 1 row in store, got %d", len(rows)) } if rows[0].Hostname != "h" || rows[0].Status != "waiting" { t.Fatalf("row: %+v", rows[0]) } } func TestHTTPGetAgentFilter(t *testing.T) { store := openTestStore(t) now := time.Now().UTC() for _, agent := range []string{"Cursor", "Claude"} { if err := store.Upsert(remote.StatusReport{ Hostname: "host-filter", IPs: []string{"10.0.0.1"}, Agent: agent, CWD: "/proj", Status: "waiting", UpdatedAt: now, }); err != nil { t.Fatal(err) } } srv := httptest.NewServer(NewHandler(store, "secret")) defer srv.Close() req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/v1/status?agent=Cursor", nil) if err != nil { t.Fatal(err) } req.Header.Set("Authorization", "Bearer secret") resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("status: got %d want %d", resp.StatusCode, http.StatusOK) } data, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) } var rows []SessionRow if err := json.Unmarshal(data, &rows); err != nil { t.Fatal(err) } if len(rows) != 1 { t.Fatalf("expected 1 row, got %d: %s", len(rows), data) } if rows[0].Agent != "Cursor" { t.Fatalf("agent: got %q want Cursor", rows[0].Agent) } } func TestHTTPGetRootNoAuth200(t *testing.T) { store := openTestStore(t) srv := httptest.NewServer(NewHandler(store, "secret")) defer srv.Close() resp, err := http.Get(srv.URL + "/") if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("status: got %d want %d", resp.StatusCode, http.StatusOK) } body, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) } if !strings.Contains(string(body), "agent-notify") { t.Fatalf("expected dashboard HTML, got: %q", string(body[:min(80, len(body))])) } } func TestHTTPHealthz200(t *testing.T) { store := openTestStore(t) srv := httptest.NewServer(NewHandler(store, "secret")) defer srv.Close() resp, err := http.Get(srv.URL + "/healthz") if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("status: got %d want %d", resp.StatusCode, http.StatusOK) } }