- Added comprehensive integration tests in test/acceptance/ covering all 6 acceptance scenarios from plan.md - AS-1: First-time setup in under 5 minutes - verifies PIN setup and node auto-discovery - AS-2: Person detected while walking - verifies blob detection during walker simulation - AS-3: Fall alert fires correctly - verifies fall detection with webhook integration - AS-4: BLE identity resolves to person name - verifies BLE device registration and identity matching - AS-5: OTA update succeeds / rollback on bad firmware - verifies OTA workflow and rollback - AS-6: Replay shows recorded history - verifies replay session creation, seeking, and playback Tests use spaxel-sim CLI as the test harness and verify: - API endpoint responses (/api/auth/setup, /api/nodes, /api/blobs, /api/events, /api/ble/devices, /api/replay/*) - Detection accuracy thresholds (>60% blob presence during walking) - Alert generation and webhook delivery - Firmware version updates and rollback behavior - Replay session lifecycle management All tests skip by default unless ACCEPTANCE_TEST=1 or SPAXEL_INTEGRATION_TEST=1 is set. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
167 lines
4.4 KiB
Go
167 lines
4.4 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 { //nolint:errcheck
|
|
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 { //nolint:errcheck
|
|
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)
|
|
}
|
|
}
|