spaxel/mothership/internal/api/tracks_test.go
jedarden 636f3efba2 feat: complete anomaly detection & security mode dashboard UI
- Add anomaly.css and sleep.css to dashboard includes
- Add sleep.js for sleep quality monitoring
- Implement analytics API handler (flow, dwell, corridors)
- Add tracks API and tests for time-based data queries
- Add sleep monitor tests
- AnomalyDetector initialized and running in main()
- Anomaly events broadcast via WebSocket to dashboard
- Security mode arm/disarm persists across restarts (learning_state table)
- Learning progress tracking and display
- Alert banner with acknowledge functionality
- All API endpoints wired: /api/anomalies, /api/security/*, /api/analytics/*

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 05:59:54 -04:00

167 lines
4.3 KiB
Go

package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
)
// mockTracksProvider implements TracksProvider for testing.
type mockTracksProvider struct {
blobs []TrackedBlob
}
func (m *mockTracksProvider) GetTrackedBlobs() []TrackedBlob {
return m.blobs
}
// TestListTracks_NoBlobs tests GET /api/tracks with no tracked blobs.
func TestListTracks_NoBlobs(t *testing.T) {
provider := &mockTracksProvider{blobs: []TrackedBlob{}}
handler := NewTracksHandler(provider)
r := chi.NewRouter()
handler.RegisterRoutes(r)
req := httptest.NewRequest("GET", "/api/tracks", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", w.Code)
}
var tracks []Track
if err := json.NewDecoder(w.Body).Decode(&tracks); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if len(tracks) != 0 {
t.Errorf("expected 0 tracks, got %d", len(tracks))
}
}
// TestListTracks_WithBlobs tests GET /api/tracks with tracked blobs.
func TestListTracks_WithBlobs(t *testing.T) {
blobs := []TrackedBlob{
{
ID: 1,
X: 1.5,
Y: 2.3,
Z: 0.8,
VX: 0.1,
VY: 0.2,
VZ: 0.0,
Weight: 0.95,
PersonID: "person-123",
PersonLabel: "Alice",
PersonColor: "#ff0000",
IdentityConfidence: 0.85,
IdentitySource: "ble",
Posture: "standing",
},
{
ID: 2,
X: 3.2,
Y: 4.1,
Z: 0.0,
VX: 0.0,
VY: 0.0,
VZ: 0.0,
Weight: 0.75,
PersonID: "", // No identity match
PersonLabel: "",
PersonColor: "",
IdentityConfidence: 0.0,
IdentitySource: "",
Posture: "",
},
}
provider := &mockTracksProvider{blobs: blobs}
handler := NewTracksHandler(provider)
r := chi.NewRouter()
handler.RegisterRoutes(r)
req := httptest.NewRequest("GET", "/api/tracks", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", w.Code)
}
var tracks []Track
if err := json.NewDecoder(w.Body).Decode(&tracks); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if len(tracks) != 2 {
t.Fatalf("expected 2 tracks, got %d", len(tracks))
}
// Verify first track (with identity)
if tracks[0].ID != 1 {
t.Errorf("expected ID 1, got %d", tracks[0].ID)
}
if tracks[0].X != 1.5 {
t.Errorf("expected X 1.5, got %f", tracks[0].X)
}
if tracks[0].Y != 2.3 {
t.Errorf("expected Y 2.3, got %f", tracks[0].Y)
}
if tracks[0].Z != 0.8 {
t.Errorf("expected Z 0.8, got %f", tracks[0].Z)
}
if tracks[0].PersonID != "person-123" {
t.Errorf("expected PersonID person-123, got %s", tracks[0].PersonID)
}
if tracks[0].PersonLabel != "Alice" {
t.Errorf("expected PersonLabel Alice, got %s", tracks[0].PersonLabel)
}
if tracks[0].PersonColor != "#ff0000" {
t.Errorf("expected PersonColor #ff0000, got %s", tracks[0].PersonColor)
}
if tracks[0].IdentityConfidence != 0.85 {
t.Errorf("expected IdentityConfidence 0.85, got %f", tracks[0].IdentityConfidence)
}
if tracks[0].IdentitySource != "ble" {
t.Errorf("expected IdentitySource ble, got %s", tracks[0].IdentitySource)
}
if tracks[0].Posture != "standing" {
t.Errorf("expected Posture standing, got %s", tracks[0].Posture)
}
// Verify second track (without identity)
if tracks[1].ID != 2 {
t.Errorf("expected ID 2, got %d", tracks[1].ID)
}
if tracks[1].PersonID != "" {
t.Errorf("expected empty PersonID, got %s", tracks[1].PersonID)
}
if tracks[1].IdentityConfidence != 0.0 {
t.Errorf("expected IdentityConfidence 0.0, got %f", tracks[1].IdentityConfidence)
}
}
// TestListTracks_ContentType verifies the response Content-Type header.
func TestListTracks_ContentType(t *testing.T) {
provider := &mockTracksProvider{blobs: []TrackedBlob{}}
handler := NewTracksHandler(provider)
r := chi.NewRouter()
handler.RegisterRoutes(r)
req := httptest.NewRequest("GET", "/api/tracks", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
ct := w.Header().Get("Content-Type")
if ct != "application/json" {
t.Errorf("expected Content-Type application/json, got %s", ct)
}
}