morz-infoboard/server/backend/internal/httpapi/integration_test.go
Jesko Anschütz 803f355220 Baue Ebene 2: PostgreSQL-Backend, Medien-Upload und Playlist-UI
- DB-Package mit pgxpool, Migrations-Runner und eingebetteten SQL-Dateien
- Schema: tenants, screens, media_assets, playlists, playlist_items
- Store-Layer: alle Repositories (TenantStore, ScreenStore, MediaStore, PlaylistStore)
- JSON-API: Screens, Medien, Playlist-CRUD, Player-Sync-Endpunkt
- Admin-UI (/admin): Screens anlegen, löschen, zur Playlist navigieren
- Playlist-UI (/manage/{slug}): Drag&Drop-Sortierung, Item-Bearbeitung,
  Medienbibliothek, Datei-Upload (Bild/Video/PDF) und Web-URL
- Router auf RouterDeps umgestellt; manage-Routen nur wenn Stores vorhanden
- parseOptionalTime akzeptiert nun RFC3339 und datetime-local HTML-Format

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

126 lines
4.6 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(RouterDeps{StatusStore: 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)
}
}