Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
320 lines
9.2 KiB
Go
320 lines
9.2 KiB
Go
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)(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())(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())(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"
|
|
}`)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewReader(body))
|
|
w := httptest.NewRecorder()
|
|
|
|
handlePlayerStatus(store)(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())(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())(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())(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())(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())(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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
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)
|
|
}
|
|
}
|