Validiere server_connectivity und stale als Query-Parameter

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 <noreply@anthropic.com>
This commit is contained in:
Jesko Anschütz 2026-03-22 19:50:01 +01:00
parent 0b199f9289
commit a7889231c0
3 changed files with 93 additions and 2 deletions

View file

@ -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)
}

View file

@ -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})

View file

@ -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 {