diff --git a/server/backend/internal/httpapi/playerstatus.go b/server/backend/internal/httpapi/playerstatus.go index 53b4838..365190f 100644 --- a/server/backend/internal/httpapi/playerstatus.go +++ b/server/backend/internal/httpapi/playerstatus.go @@ -157,8 +157,23 @@ func handleListLatestPlayerStatuses(store playerStatusStore) http.HandlerFunc { func buildScreenStatusOverview(store playerStatusStore, query url.Values) (screenStatusOverview, error) { records := store.List() + wantConnectivity := strings.TrimSpace(query.Get("server_connectivity")) + switch wantConnectivity { + case "", "online", "degraded", "offline", "unknown": + // valid + default: + return screenStatusOverview{}, errInvalidServerConnectivity + } + wantStale := strings.TrimSpace(query.Get("stale")) + switch wantStale { + case "", "true", "false": + // valid + default: + return screenStatusOverview{}, errInvalidStale + } + updatedSince, err := parseOptionalRFC3339(query.Get("updated_since")) if err != nil { return screenStatusOverview{}, errInvalidUpdatedSince @@ -222,8 +237,10 @@ func buildScreenStatusOverview(store playerStatusStore, query url.Values) (scree } var ( - errInvalidUpdatedSince = errors.New("invalid updated_since") - errInvalidLimit = errors.New("invalid limit") + errInvalidUpdatedSince = errors.New("invalid updated_since") + errInvalidLimit = errors.New("invalid limit") + errInvalidServerConnectivity = errors.New("invalid server_connectivity") + errInvalidStale = errors.New("invalid stale") ) func writeOverviewQueryError(w http.ResponseWriter, err error) { @@ -232,6 +249,10 @@ func writeOverviewQueryError(w http.ResponseWriter, err error) { writeError(w, http.StatusBadRequest, "invalid_updated_since", "updated_since ist kein gueltiger RFC3339-Zeitstempel", nil) case errInvalidLimit: writeError(w, http.StatusBadRequest, "invalid_limit", "limit muss eine positive Ganzzahl sein", nil) + case errInvalidServerConnectivity: + writeError(w, http.StatusBadRequest, "invalid_server_connectivity", "server_connectivity muss online, offline, degraded oder unknown sein", nil) + case errInvalidStale: + writeError(w, http.StatusBadRequest, "invalid_stale", "stale muss true oder false sein", nil) default: writeError(w, http.StatusBadRequest, "invalid_query", "ungueltige Query-Parameter", nil) } diff --git a/server/backend/internal/httpapi/playerstatus_test.go b/server/backend/internal/httpapi/playerstatus_test.go index 49af99a..8002ca6 100644 --- a/server/backend/internal/httpapi/playerstatus_test.go +++ b/server/backend/internal/httpapi/playerstatus_test.go @@ -579,6 +579,50 @@ func TestHandleListLatestPlayerStatusesAppliesLimit(t *testing.T) { } } +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}) diff --git a/server/backend/internal/httpapi/router_test.go b/server/backend/internal/httpapi/router_test.go index 05ea613..847ae9d 100644 --- a/server/backend/internal/httpapi/router_test.go +++ b/server/backend/internal/httpapi/router_test.go @@ -176,6 +176,32 @@ func TestRouterScreenStatusListRoute(t *testing.T) { } } +func TestRouterStatusPageRejectsInvalidQueryParams(t *testing.T) { + cases := []struct { + name string + query string + }{ + {"invalid server_connectivity", "?server_connectivity=garbage"}, + {"invalid stale", "?stale=maybe"}, + {"invalid updated_since", "?updated_since=not-a-time"}, + {"invalid limit zero", "?limit=0"}, + {"invalid limit negative", "?limit=-1"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/status"+tc.query, nil) + w := httptest.NewRecorder() + + NewRouter(newInMemoryPlayerStatusStore()).ServeHTTP(w, req) + + if got, want := w.Code, http.StatusBadRequest; got != want { + t.Fatalf("status = %d, want %d", got, want) + } + }) + } +} + func TestRouterStatusPageRoute(t *testing.T) { store := newInMemoryPlayerStatusStore() store.now = func() time.Time {