morz-infoboard/server/backend/internal/httpapi/integration_test.go
Jesko Anschütz 56635554c7 Fuege Screen-Loeschung, Meta-Update, Datei-Persistenz und Lifecycle-Test hinzu
- DELETE /api/v1/screens/{screenId}/status loescht einzelne Screen-Eintraege
- /api/v1/meta listet jetzt 5 Tools inkl. screen-status-delete und diagnostic_ui-Pfade
- filePlayerStatusStore persistiert den Status-Store atomar in einer JSON-Datei
- MORZ_INFOBOARD_STATUS_STORE_PATH aktiviert die Datei-Persistenz (leer = In-Memory)
- Integration-Test deckt den vollstaendigen Lifecycle: POST -> list -> HTML -> JSON -> DELETE -> 404 ab
- DEVELOPMENT.md beschreibt End-to-End-Entwicklungstest und neue Env-Variable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 20:34:37 +01:00

126 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package httpapi
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
// TestPlayerStatusLifecycle covers the full lifecycle of a screen status entry:
// ingest → list → HTML detail → JSON detail → delete → verify gone.
func TestPlayerStatusLifecycle(t *testing.T) {
store := newInMemoryPlayerStatusStore()
store.now = func() time.Time {
return time.Date(2026, 3, 22, 16, 10, 0, 0, time.UTC)
}
router := NewRouter(store)
// 1. POST /api/v1/player/status ingest a status report
body := `{
"screen_id": "lifecycle-screen",
"ts": "2026-03-22T16:09:30Z",
"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-22T16:00:00Z",
"last_heartbeat_at": "2026-03-22T16:09:30Z"
}`
req := httptest.NewRequest(http.MethodPost, "/api/v1/player/status", bytes.NewBufferString(body))
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if got, want := w.Code, http.StatusOK; got != want {
t.Fatalf("POST status: got %d, want %d body: %s", got, want, w.Body.String())
}
// 2. GET /api/v1/screens/status appears in list with derived_state=online
req = httptest.NewRequest(http.MethodGet, "/api/v1/screens/status", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
if got, want := w.Code, http.StatusOK; got != want {
t.Fatalf("GET list: got %d, want %d", got, want)
}
var list struct {
Summary struct{ Total int } `json:"summary"`
Screens []playerStatusRecord `json:"screens"`
}
if err := json.Unmarshal(w.Body.Bytes(), &list); err != nil {
t.Fatalf("GET list Unmarshal: %v", err)
}
if got, want := list.Summary.Total, 1; got != want {
t.Fatalf("summary.total = %d, want %d", got, want)
}
if got, want := list.Screens[0].ScreenID, "lifecycle-screen"; got != want {
t.Fatalf("Screens[0].ScreenID = %q, want %q", got, want)
}
if got, want := list.Screens[0].DerivedState, "online"; got != want {
t.Fatalf("Screens[0].DerivedState = %q, want %q", got, want)
}
// 3. GET /status/lifecycle-screen HTML detail page contains key fields
req = httptest.NewRequest(http.MethodGet, "/status/lifecycle-screen", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
if got, want := w.Code, http.StatusOK; got != want {
t.Fatalf("GET HTML detail: got %d, want %d", got, want)
}
if ct := w.Header().Get("Content-Type"); !strings.Contains(ct, "text/html") {
t.Fatalf("Content-Type = %q, want text/html", ct)
}
htmlBody := w.Body.String()
for _, want := range []string{"lifecycle-screen", "online", "running"} {
if !strings.Contains(htmlBody, want) {
t.Fatalf("HTML detail page missing %q", want)
}
}
// 4. GET /api/v1/screens/lifecycle-screen/status JSON detail
req = httptest.NewRequest(http.MethodGet, "/api/v1/screens/lifecycle-screen/status", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
if got, want := w.Code, http.StatusOK; got != want {
t.Fatalf("GET JSON detail: got %d, want %d", got, want)
}
var detail playerStatusRecord
if err := json.Unmarshal(w.Body.Bytes(), &detail); err != nil {
t.Fatalf("GET JSON detail Unmarshal: %v", err)
}
if got, want := detail.ScreenID, "lifecycle-screen"; got != want {
t.Fatalf("detail.ScreenID = %q, want %q", got, want)
}
if got, want := detail.ServerURL, "http://127.0.0.1:8080"; got != want {
t.Fatalf("detail.ServerURL = %q, want %q", got, want)
}
// 5. DELETE /api/v1/screens/lifecycle-screen/status remove the record
req = httptest.NewRequest(http.MethodDelete, "/api/v1/screens/lifecycle-screen/status", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
if got, want := w.Code, http.StatusOK; got != want {
t.Fatalf("DELETE: got %d, want %d body: %s", got, want, w.Body.String())
}
// 6. GET /api/v1/screens/lifecycle-screen/status must be 404 now
req = httptest.NewRequest(http.MethodGet, "/api/v1/screens/lifecycle-screen/status", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
if got, want := w.Code, http.StatusNotFound; got != want {
t.Fatalf("GET after delete: got %d, want %d", got, want)
}
// 7. GET /api/v1/screens/status list must be empty again
req = httptest.NewRequest(http.MethodGet, "/api/v1/screens/status", nil)
w = httptest.NewRecorder()
router.ServeHTTP(w, req)
if err := json.Unmarshal(w.Body.Bytes(), &list); err != nil {
t.Fatalf("GET list after delete Unmarshal: %v", err)
}
if got, want := list.Summary.Total, 0; got != want {
t.Fatalf("summary.total after delete = %d, want %d", got, want)
}
}