From a7889231c0d05f45b1216c2ac308ad63a1a5fc77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesko=20Ansch=C3=BCtz?= Date: Sun, 22 Mar 2026 19:50:01 +0100 Subject: [PATCH] Validiere server_connectivity und stale als Query-Parameter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bisher wurden ungueltige Werte fuer server_connectivity und stale im Listing-Endpunkt und auf der Statusseite stillschweigend ignoriert bzw. fuehrten zu leeren Ergebnissen ohne Fehlermeldung. Beide Parameter werden jetzt explizit auf erlaubte Werte geprueft und liefern bei ungueltiger Eingabe einen 400-Fehler mit beschreibendem error_code – konsistent mit der bestehenden Validierung fuer updated_since und limit. Neue Tests (playerstatus_test.go): - RejectsInvalidServerConnectivity - RejectsInvalidStale - RejectsInvalidUpdatedSince - RejectsInvalidLimit Neue Tests (router_test.go): - StatusPageRejectsInvalidQueryParams (table-driven, alle 4 Faelle) Co-Authored-By: Claude Sonnet 4.6 --- .../backend/internal/httpapi/playerstatus.go | 25 ++++++++++- .../internal/httpapi/playerstatus_test.go | 44 +++++++++++++++++++ .../backend/internal/httpapi/router_test.go | 26 +++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) 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 {