package httpapi import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "time" ) func TestHandlePlayerStatusAccepted(t *testing.T) { store := newInMemoryPlayerStatusStore() body := []byte(`{ "screen_id": "info01-dev", "ts": "2026-03-22T16:00:00Z", "status": "running", "server_connectivity": "online", "server_url": "http://127.0.0.1:8080", "mqtt_broker": "tcp://127.0.0.1:1883", "heartbeat_every_seconds": 30, "started_at": "2026-03-22T15:59:30Z", "last_heartbeat_at": "2026-03-22T16:00:00Z" }`) req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body)) w := httptest.NewRecorder() handlePlayerStatus(store, nil, "", "", "")(w, req) if got, want := w.Code, http.StatusOK; got != want { t.Fatalf("status = %d, want %d", got, want) } var response struct { Status string `json:"status"` } if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Unmarshal() error = %v", err) } if got, want := response.Status, "accepted"; got != want { t.Fatalf("response status = %q, want %q", got, want) } stored, ok := store.Get("info01-dev") if !ok { t.Fatal("store.Get() ok = false, want true") } if got, want := stored.ScreenID, "info01-dev"; got != want { t.Fatalf("stored.ScreenID = %q, want %q", got, want) } if got, want := stored.ServerConnectivity, "online"; got != want { t.Fatalf("stored.ServerConnectivity = %q, want %q", got, want) } if stored.ReceivedAt == "" { t.Fatal("stored.ReceivedAt = empty, want server receive timestamp") } } func TestHandlePlayerStatusRejectsInvalidJSON(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewBufferString("{")) w := httptest.NewRecorder() handlePlayerStatus(newInMemoryPlayerStatusStore(), nil, "", "", "")(w, req) if got, want := w.Code, http.StatusBadRequest; got != want { t.Fatalf("status = %d, want %d", got, want) } } func TestHandlePlayerStatusRejectsMissingScreenID(t *testing.T) { body := []byte(`{ "screen_id": " ", "ts": "2026-03-22T16:00:00Z", "status": "running" }`) req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body)) w := httptest.NewRecorder() handlePlayerStatus(newInMemoryPlayerStatusStore(), nil, "", "", "")(w, req) if got, want := w.Code, http.StatusBadRequest; got != want { t.Fatalf("status = %d, want %d", got, want) } } func TestHandlePlayerStatusStoresNormalizedScreenID(t *testing.T) { store := newInMemoryPlayerStatusStore() body := []byte(`{ "screen_id": " info01-dev ", "ts": "2026-03-22T16:00:00Z", "status": "running", "heartbeat_every_seconds": 30 }`) req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body)) w := httptest.NewRecorder() handlePlayerStatus(store, nil, "", "", "")(w, req) if got, want := w.Code, http.StatusOK; got != want { t.Fatalf("status = %d, want %d", got, want) } if _, ok := store.Get("info01-dev"); !ok { t.Fatal("store.Get(normalized) ok = false, want true") } } func TestHandlePlayerStatusRejectsMissingTimestamp(t *testing.T) { body := []byte(`{ "screen_id": "info01-dev", "status": "running" }`) req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body)) w := httptest.NewRecorder() handlePlayerStatus(newInMemoryPlayerStatusStore(), nil, "", "", "")(w, req) if got, want := w.Code, http.StatusBadRequest; got != want { t.Fatalf("status = %d, want %d", got, want) } } func TestHandlePlayerStatusRejectsMissingStatus(t *testing.T) { body := []byte(`{ "screen_id": "info01-dev", "ts": "2026-03-22T16:00:00Z" }`) req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body)) w := httptest.NewRecorder() handlePlayerStatus(newInMemoryPlayerStatusStore(), nil, "", "", "")(w, req) if got, want := w.Code, http.StatusBadRequest; got != want { t.Fatalf("status = %d, want %d", got, want) } } func TestHandlePlayerStatusRejectsUnknownStatus(t *testing.T) { body := []byte(`{ "screen_id": "info01-dev", "ts": "2026-03-22T16:00:00Z", "status": "broken" }`) req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body)) w := httptest.NewRecorder() handlePlayerStatus(newInMemoryPlayerStatusStore(), nil, "", "", "")(w, req) if got, want := w.Code, http.StatusBadRequest; got != want { t.Fatalf("status = %d, want %d", got, want) } } func TestHandlePlayerStatusRejectsUnknownServerConnectivity(t *testing.T) { body := []byte(`{ "screen_id": "info01-dev", "ts": "2026-03-22T16:00:00Z", "status": "running", "server_connectivity": "maybe" }`) req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body)) w := httptest.NewRecorder() handlePlayerStatus(newInMemoryPlayerStatusStore(), nil, "", "", "")(w, req) if got, want := w.Code, http.StatusBadRequest; got != want { t.Fatalf("status = %d, want %d", got, want) } } func TestHandlePlayerStatusRejectsNonPositiveHeartbeatInterval(t *testing.T) { body := []byte(`{ "screen_id": "info01-dev", "ts": "2026-03-22T16:00:00Z", "status": "running", "heartbeat_every_seconds": 0 }`) req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body)) w := httptest.NewRecorder() handlePlayerStatus(newInMemoryPlayerStatusStore(), nil, "", "", "")(w, req) if got, want := w.Code, http.StatusBadRequest; got != want { t.Fatalf("status = %d, want %d", got, want) } } func TestHandlePlayerStatusRejectsMalformedTimestamps(t *testing.T) { body := []byte(`{ "screen_id": "info01-dev", "ts": "not-a-time", "status": "running" }`) req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body)) w := httptest.NewRecorder() handlePlayerStatus(newInMemoryPlayerStatusStore(), nil, "", "", "")(w, req) if got, want := w.Code, http.StatusBadRequest; got != want { t.Fatalf("status = %d, want %d", got, want) } } func TestHandlePlayerStatusRejectsMalformedStartedAt(t *testing.T) { body := []byte(`{ "screen_id": "info01-dev", "ts": "2026-03-22T16:00:00Z", "status": "running", "started_at": "not-a-time" }`) req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body)) w := httptest.NewRecorder() handlePlayerStatus(newInMemoryPlayerStatusStore(), nil, "", "", "")(w, req) if got, want := w.Code, http.StatusBadRequest; got != want { t.Fatalf("status = %d, want %d", got, want) } } func TestHandlePlayerStatusRejectsMalformedLastHeartbeatAt(t *testing.T) { body := []byte(`{ "screen_id": "info01-dev", "ts": "2026-03-22T16:00:00Z", "status": "running", "last_heartbeat_at": "not-a-time" }`) req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body)) w := httptest.NewRecorder() handlePlayerStatus(newInMemoryPlayerStatusStore(), nil, "", "", "")(w, req) if got, want := w.Code, http.StatusBadRequest; got != want { t.Fatalf("status = %d, want %d", got, want) } } func TestHandleGetLatestPlayerStatus(t *testing.T) { store := newInMemoryPlayerStatusStore() store.now = func() time.Time { return time.Date(2026, 3, 22, 16, 1, 0, 0, time.UTC) } store.Save(playerStatusRecord{ ScreenID: "info01-dev", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "degraded", ReceivedAt: "2026-03-22T16:00:05Z", ServerURL: "http://127.0.0.1:8080", MQTTBroker: "tcp://127.0.0.1:1883", HeartbeatEverySeconds: 30, StartedAt: "2026-03-22T15:59:30Z", LastHeartbeatAt: "2026-03-22T16:00:00Z", }) req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/info01-dev/status", nil) req.SetPathValue("screenId", "info01-dev") w := httptest.NewRecorder() handleGetLatestPlayerStatus(store)(w, req) if got, want := w.Code, http.StatusOK; got != want { t.Fatalf("status = %d, want %d", got, want) } var response playerStatusRecord if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Unmarshal() error = %v", err) } if got, want := response.ScreenID, "info01-dev"; got != want { t.Fatalf("response.ScreenID = %q, want %q", got, want) } if got, want := response.ServerConnectivity, "degraded"; got != want { t.Fatalf("response.ServerConnectivity = %q, want %q", got, want) } if got, want := response.Stale, false; got != want { t.Fatalf("response.Stale = %v, want %v", got, want) } if got, want := response.DerivedState, "degraded"; got != want { t.Fatalf("response.DerivedState = %q, want %q", got, want) } } func TestHandleGetLatestPlayerStatusMarksStaleRecords(t *testing.T) { store := newInMemoryPlayerStatusStore() store.now = func() time.Time { return time.Date(2026, 3, 22, 16, 10, 0, 0, time.UTC) } store.Save(playerStatusRecord{ ScreenID: "stale-screen", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "offline", ReceivedAt: "2026-03-22T16:00:00Z", HeartbeatEverySeconds: 30, }) req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/stale-screen/status", nil) req.SetPathValue("screenId", "stale-screen") w := httptest.NewRecorder() handleGetLatestPlayerStatus(store)(w, req) if got, want := w.Code, http.StatusOK; got != want { t.Fatalf("status = %d, want %d", got, want) } var response playerStatusRecord if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Unmarshal() error = %v", err) } if got, want := response.Stale, true; got != want { t.Fatalf("response.Stale = %v, want %v", got, want) } if got, want := response.DerivedState, "offline"; got != want { t.Fatalf("response.DerivedState = %q, want %q", got, want) } } func TestHandleGetLatestPlayerStatusUsesHeartbeatIntervalForFreshness(t *testing.T) { store := newInMemoryPlayerStatusStore() store.now = func() time.Time { return time.Date(2026, 3, 22, 16, 3, 30, 0, time.UTC) } store.Save(playerStatusRecord{ ScreenID: "slow-screen", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "online", ReceivedAt: "2026-03-22T16:00:00Z", HeartbeatEverySeconds: 120, }) req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/slow-screen/status", nil) req.SetPathValue("screenId", "slow-screen") w := httptest.NewRecorder() handleGetLatestPlayerStatus(store)(w, req) var response playerStatusRecord if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Unmarshal() error = %v", err) } if got, want := response.Stale, false; got != want { t.Fatalf("response.Stale = %v, want %v", got, want) } } func TestHandleGetLatestPlayerStatusDerivesOnlineState(t *testing.T) { store := newInMemoryPlayerStatusStore() store.now = func() time.Time { return time.Date(2026, 3, 22, 16, 0, 30, 0, time.UTC) } store.Save(playerStatusRecord{ ScreenID: "online-screen", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "online", ReceivedAt: "2026-03-22T16:00:00Z", HeartbeatEverySeconds: 30, }) req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/online-screen/status", nil) req.SetPathValue("screenId", "online-screen") w := httptest.NewRecorder() handleGetLatestPlayerStatus(store)(w, req) var response playerStatusRecord if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Unmarshal() error = %v", err) } if got, want := response.DerivedState, "online"; got != want { t.Fatalf("response.DerivedState = %q, want %q", got, want) } } func TestHandleGetLatestPlayerStatusNotFound(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/missing/status", nil) req.SetPathValue("screenId", "missing") w := httptest.NewRecorder() handleGetLatestPlayerStatus(newInMemoryPlayerStatusStore())(w, req) if got, want := w.Code, http.StatusNotFound; got != want { t.Fatalf("status = %d, want %d", got, want) } } func TestHandleListLatestPlayerStatuses(t *testing.T) { store := newInMemoryPlayerStatusStore() store.Save(playerStatusRecord{ScreenID: "screen-b", Timestamp: "2026-03-22T16:00:01Z", Status: "running", ServerConnectivity: "online"}) store.Save(playerStatusRecord{ScreenID: "screen-a", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "degraded"}) req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status", nil) w := httptest.NewRecorder() handleListLatestPlayerStatuses(store)(w, req) if got, want := w.Code, http.StatusOK; got != want { t.Fatalf("status = %d, want %d", got, want) } var response struct { Summary struct { Total int `json:"total"` Online int `json:"online"` Degraded int `json:"degraded"` Offline int `json:"offline"` Stale int `json:"stale"` } `json:"summary"` Screens []playerStatusRecord `json:"screens"` } if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Unmarshal() error = %v", err) } if got, want := len(response.Screens), 2; got != want { t.Fatalf("len(response.Screens) = %d, want %d", got, want) } if got, want := response.Screens[0].ScreenID, "screen-a"; got != want { t.Fatalf("response.Screens[0].ScreenID = %q, want %q", got, want) } if got, want := response.Screens[1].ScreenID, "screen-b"; got != want { t.Fatalf("response.Screens[1].ScreenID = %q, want %q", got, want) } if got, want := response.Summary.Total, 2; got != want { t.Fatalf("response.Summary.Total = %d, want %d", got, want) } } func TestHandleListLatestPlayerStatusesOrdersProblematicScreensFirst(t *testing.T) { store := newInMemoryPlayerStatusStore() store.now = func() time.Time { return time.Date(2026, 3, 22, 16, 10, 0, 0, time.UTC) } store.Save(playerStatusRecord{ScreenID: "screen-online", Timestamp: "2026-03-22T16:09:30Z", Status: "running", ServerConnectivity: "online", ReceivedAt: "2026-03-22T16:09:30Z", HeartbeatEverySeconds: 30}) store.Save(playerStatusRecord{ScreenID: "screen-offline", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "offline", ReceivedAt: "2026-03-22T16:00:00Z", HeartbeatEverySeconds: 30}) store.Save(playerStatusRecord{ScreenID: "screen-degraded", Timestamp: "2026-03-22T16:09:00Z", Status: "running", ServerConnectivity: "degraded", ReceivedAt: "2026-03-22T16:09:00Z", HeartbeatEverySeconds: 30}) req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status", nil) w := httptest.NewRecorder() handleListLatestPlayerStatuses(store)(w, req) var response struct { Summary struct { Total int `json:"total"` Online int `json:"online"` Degraded int `json:"degraded"` Offline int `json:"offline"` Stale int `json:"stale"` } `json:"summary"` Screens []playerStatusRecord `json:"screens"` } if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Unmarshal() error = %v", err) } if got, want := response.Screens[0].ScreenID, "screen-offline"; got != want { t.Fatalf("response.Screens[0].ScreenID = %q, want %q", got, want) } if got, want := response.Screens[1].ScreenID, "screen-degraded"; got != want { t.Fatalf("response.Screens[1].ScreenID = %q, want %q", got, want) } if got, want := response.Screens[2].ScreenID, "screen-online"; got != want { t.Fatalf("response.Screens[2].ScreenID = %q, want %q", got, want) } } func TestHandleListLatestPlayerStatusesFiltersByConnectivityAndStale(t *testing.T) { store := newInMemoryPlayerStatusStore() store.now = func() time.Time { return time.Date(2026, 3, 22, 16, 10, 0, 0, time.UTC) } store.Save(playerStatusRecord{ScreenID: "screen-online", Timestamp: "2026-03-22T16:09:30Z", Status: "running", ServerConnectivity: "online", ReceivedAt: "2026-03-22T16:09:30Z", HeartbeatEverySeconds: 30}) store.Save(playerStatusRecord{ScreenID: "screen-offline", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "offline", ReceivedAt: "2026-03-22T16:00:00Z", HeartbeatEverySeconds: 30}) req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?server_connectivity=offline&stale=true", nil) w := httptest.NewRecorder() handleListLatestPlayerStatuses(store)(w, req) if got, want := w.Code, http.StatusOK; got != want { t.Fatalf("status = %d, want %d", got, want) } var response struct { Summary struct { Total int `json:"total"` Online int `json:"online"` Degraded int `json:"degraded"` Offline int `json:"offline"` Stale int `json:"stale"` } `json:"summary"` Screens []playerStatusRecord `json:"screens"` } if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Unmarshal() error = %v", err) } if got, want := len(response.Screens), 1; got != want { t.Fatalf("len(response.Screens) = %d, want %d", got, want) } if got, want := response.Screens[0].ScreenID, "screen-offline"; got != want { t.Fatalf("response.Screens[0].ScreenID = %q, want %q", got, want) } if got, want := response.Summary.Offline, 1; got != want { t.Fatalf("response.Summary.Offline = %d, want %d", got, want) } if got, want := response.Summary.Online, 1; got != want { t.Fatalf("response.Summary.Online = %d, want %d", got, want) } } func TestHandleListLatestPlayerStatusesAppliesLimit(t *testing.T) { store := newInMemoryPlayerStatusStore() store.now = func() time.Time { return time.Date(2026, 3, 22, 16, 10, 0, 0, time.UTC) } store.Save(playerStatusRecord{ScreenID: "screen-online", Timestamp: "2026-03-22T16:09:30Z", Status: "running", ServerConnectivity: "online", ReceivedAt: "2026-03-22T16:09:30Z", HeartbeatEverySeconds: 30}) store.Save(playerStatusRecord{ScreenID: "screen-offline", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "offline", ReceivedAt: "2026-03-22T16:00:00Z", HeartbeatEverySeconds: 30}) store.Save(playerStatusRecord{ScreenID: "screen-degraded", Timestamp: "2026-03-22T16:09:00Z", Status: "running", ServerConnectivity: "degraded", ReceivedAt: "2026-03-22T16:09:00Z", HeartbeatEverySeconds: 30}) req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?limit=2", nil) w := httptest.NewRecorder() handleListLatestPlayerStatuses(store)(w, req) var response struct { Screens []playerStatusRecord `json:"screens"` } if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Unmarshal() error = %v", err) } if got, want := len(response.Screens), 2; got != want { t.Fatalf("len(response.Screens) = %d, want %d", got, want) } if got, want := response.Screens[0].ScreenID, "screen-offline"; got != want { t.Fatalf("response.Screens[0].ScreenID = %q, want %q", got, want) } if got, want := response.Screens[1].ScreenID, "screen-degraded"; got != want { t.Fatalf("response.Screens[1].ScreenID = %q, want %q", got, want) } } func TestHandleListLatestPlayerStatusesFiltersByScreenIDSubstring(t *testing.T) { store := newInMemoryPlayerStatusStore() store.Save(playerStatusRecord{ScreenID: "info01-dev", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "online"}) store.Save(playerStatusRecord{ScreenID: "info02-dev", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "online"}) store.Save(playerStatusRecord{ScreenID: "lobby-main", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "online"}) req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?q=info", nil) w := httptest.NewRecorder() handleListLatestPlayerStatuses(store)(w, req) var response struct { Screens []playerStatusRecord `json:"screens"` } if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Unmarshal() error = %v", err) } if got, want := len(response.Screens), 2; got != want { t.Fatalf("len(response.Screens) = %d, want %d", got, want) } } func TestHandleListLatestPlayerStatusesScreenIDFilterIsCaseInsensitive(t *testing.T) { store := newInMemoryPlayerStatusStore() store.Save(playerStatusRecord{ScreenID: "INFO01-DEV", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "online"}) store.Save(playerStatusRecord{ScreenID: "lobby-main", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "online"}) req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?q=info01", nil) w := httptest.NewRecorder() handleListLatestPlayerStatuses(store)(w, req) var response struct { Screens []playerStatusRecord `json:"screens"` } if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Unmarshal() error = %v", err) } if got, want := len(response.Screens), 1; got != want { t.Fatalf("len(response.Screens) = %d, want %d", got, want) } if got, want := response.Screens[0].ScreenID, "INFO01-DEV"; got != want { t.Fatalf("response.Screens[0].ScreenID = %q, want %q", got, want) } } func TestHandleListLatestPlayerStatusesFiltersByDerivedState(t *testing.T) { store := newInMemoryPlayerStatusStore() store.now = func() time.Time { return time.Date(2026, 3, 22, 16, 10, 0, 0, time.UTC) } store.Save(playerStatusRecord{ScreenID: "screen-online", Timestamp: "2026-03-22T16:09:30Z", Status: "running", ServerConnectivity: "online", ReceivedAt: "2026-03-22T16:09:30Z", HeartbeatEverySeconds: 30}) store.Save(playerStatusRecord{ScreenID: "screen-offline", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "offline", ReceivedAt: "2026-03-22T16:00:00Z", HeartbeatEverySeconds: 30}) store.Save(playerStatusRecord{ScreenID: "screen-degraded", Timestamp: "2026-03-22T16:09:00Z", Status: "running", ServerConnectivity: "degraded", ReceivedAt: "2026-03-22T16:09:00Z", HeartbeatEverySeconds: 30}) req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?derived_state=online", nil) w := httptest.NewRecorder() handleListLatestPlayerStatuses(store)(w, req) var response struct { Screens []playerStatusRecord `json:"screens"` } if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Unmarshal() error = %v", err) } if got, want := len(response.Screens), 1; got != want { t.Fatalf("len(response.Screens) = %d, want %d", got, want) } if got, want := response.Screens[0].ScreenID, "screen-online"; got != want { t.Fatalf("response.Screens[0].ScreenID = %q, want %q", got, want) } } func TestHandleListLatestPlayerStatusesRejectsInvalidDerivedState(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?derived_state=unknown", nil) w := httptest.NewRecorder() handleListLatestPlayerStatuses(newInMemoryPlayerStatusStore())(w, req) if got, want := w.Code, http.StatusBadRequest; got != want { t.Fatalf("status = %d, want %d", got, want) } } func TestHandleListLatestPlayerStatusesRejectsInvalidServerConnectivity(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?server_connectivity=garbage", nil) w := httptest.NewRecorder() handleListLatestPlayerStatuses(newInMemoryPlayerStatusStore())(w, req) if got, want := w.Code, http.StatusBadRequest; got != want { t.Fatalf("status = %d, want %d", got, want) } } func TestHandleListLatestPlayerStatusesRejectsInvalidStale(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?stale=maybe", nil) w := httptest.NewRecorder() handleListLatestPlayerStatuses(newInMemoryPlayerStatusStore())(w, req) if got, want := w.Code, http.StatusBadRequest; got != want { t.Fatalf("status = %d, want %d", got, want) } } func TestHandleListLatestPlayerStatusesRejectsInvalidUpdatedSince(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?updated_since=not-a-time", nil) w := httptest.NewRecorder() handleListLatestPlayerStatuses(newInMemoryPlayerStatusStore())(w, req) if got, want := w.Code, http.StatusBadRequest; got != want { t.Fatalf("status = %d, want %d", got, want) } } func TestHandleListLatestPlayerStatusesRejectsInvalidLimit(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?limit=0", nil) w := httptest.NewRecorder() handleListLatestPlayerStatuses(newInMemoryPlayerStatusStore())(w, req) if got, want := w.Code, http.StatusBadRequest; got != want { t.Fatalf("status = %d, want %d", got, want) } } func TestHandleListLatestPlayerStatusesFiltersByUpdatedSince(t *testing.T) { store := newInMemoryPlayerStatusStore() store.Save(playerStatusRecord{ScreenID: "screen-old", Timestamp: "2026-03-22T16:00:00Z", Status: "running", ServerConnectivity: "online", ReceivedAt: "2026-03-22T16:00:00Z", HeartbeatEverySeconds: 30}) store.Save(playerStatusRecord{ScreenID: "screen-new", Timestamp: "2026-03-22T16:06:00Z", Status: "running", ServerConnectivity: "online", ReceivedAt: "2026-03-22T16:06:00Z", HeartbeatEverySeconds: 30}) req := httptest.NewRequest(http.MethodGet, "/api/v1/screens/status?updated_since=2026-03-22T16:05:00Z", nil) w := httptest.NewRecorder() handleListLatestPlayerStatuses(store)(w, req) var response struct { Screens []playerStatusRecord `json:"screens"` } if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Unmarshal() error = %v", err) } if got, want := len(response.Screens), 1; got != want { t.Fatalf("len(response.Screens) = %d, want %d", got, want) } if got, want := response.Screens[0].ScreenID, "screen-new"; got != want { t.Fatalf("response.Screens[0].ScreenID = %q, want %q", got, want) } } func TestHandleDeletePlayerStatusOK(t *testing.T) { store := newInMemoryPlayerStatusStore() store.Save(playerStatusRecord{ScreenID: "info01-dev", Timestamp: "2026-03-22T16:00:00Z", Status: "running"}) req := httptest.NewRequest(http.MethodDelete, "/api/v1/screens/info01-dev/status", nil) req.SetPathValue("screenId", "info01-dev") w := httptest.NewRecorder() handleDeletePlayerStatus(store)(w, req) if got, want := w.Code, http.StatusOK; got != want { t.Fatalf("status = %d, want %d", got, want) } var response map[string]string if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Unmarshal() error = %v", err) } if got, want := response["status"], "deleted"; got != want { t.Fatalf("status = %q, want %q", got, want) } if _, ok := store.Get("info01-dev"); ok { t.Fatal("record still present after delete") } } func TestHandleDeletePlayerStatusNotFound(t *testing.T) { store := newInMemoryPlayerStatusStore() req := httptest.NewRequest(http.MethodDelete, "/api/v1/screens/missing/status", nil) req.SetPathValue("screenId", "missing") w := httptest.NewRecorder() handleDeletePlayerStatus(store)(w, req) if got, want := w.Code, http.StatusNotFound; got != want { t.Fatalf("status = %d, want %d", got, want) } }