Player-UI (playerserver): - Lokale Kiosk-Seite unter /player mit orientierungsgerechtem Splash-Bild - Splash-PNGs (Portrait/Landscape) eingebettet via go:embed - Unteres-Drittel-Overlay mit erweiterbaren Sysinfo-Items (Hostname, Uptime) - /api/now-playing und /api/sysinfo JSON-Endpunkte - iframe-Overlay fuer spaetere Inhalts-URL Ansible-Rolle signage_display (neu): - Pakete: xserver-xorg-core, xinit, openbox, chromium, unclutter - Kiosk-Skript mit openbox als WM (noetig fuer korrektes --kiosk-Vollbild) - systemd-Unit mit Conflicts=getty@tty1 (behebt TTY-Blockierung beim Start) - Chromium Managed Policy: TranslateEnabled=false, Notifications/Geolocation blockiert - --lang=de Flag gegen Sprachauswahl-Dialog Ansible-Rolle signage_player (erweitert): - Legt signage_user an falls nicht vorhanden - PlayerListenAddr und PlayerContentURL in Konfiguration - journald volatile Storage (SD-Karten-Schonung) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
147 lines
4.1 KiB
Go
147 lines
4.1 KiB
Go
package playerserver
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io/fs"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func newTestServer(np NowPlaying) *Server {
|
|
return New("127.0.0.1:0", func() NowPlaying { return np })
|
|
}
|
|
|
|
func TestHandlePlayerReturnsHTML(t *testing.T) {
|
|
s := newTestServer(NowPlaying{Status: "running", Connectivity: "online"})
|
|
req := httptest.NewRequest(http.MethodGet, "/player", nil)
|
|
w := httptest.NewRecorder()
|
|
s.handlePlayer(w, req)
|
|
|
|
if ct := w.Header().Get("Content-Type"); !strings.Contains(ct, "text/html") {
|
|
t.Fatalf("Content-Type = %q, want text/html", ct)
|
|
}
|
|
body := w.Body.String()
|
|
for _, want := range []string{"<!DOCTYPE html>", "/api/now-playing", "/api/sysinfo", "splash-portrait.png", "splash-landscape.png"} {
|
|
if !strings.Contains(body, want) {
|
|
t.Fatalf("HTML missing %q", want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHandleNowPlayingWithURL(t *testing.T) {
|
|
s := newTestServer(NowPlaying{
|
|
URL: "https://example.com",
|
|
Status: "running",
|
|
Connectivity: "online",
|
|
})
|
|
req := httptest.NewRequest(http.MethodGet, "/api/now-playing", nil)
|
|
w := httptest.NewRecorder()
|
|
s.handleNowPlaying(w, req)
|
|
|
|
var got NowPlaying
|
|
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
|
|
t.Fatalf("Unmarshal() error = %v", err)
|
|
}
|
|
if got.URL != "https://example.com" {
|
|
t.Fatalf("URL = %q, want https://example.com", got.URL)
|
|
}
|
|
if got.Connectivity != "online" {
|
|
t.Fatalf("Connectivity = %q, want online", got.Connectivity)
|
|
}
|
|
}
|
|
|
|
func TestHandleNowPlayingWithoutURL(t *testing.T) {
|
|
s := newTestServer(NowPlaying{Status: "running", Connectivity: "degraded"})
|
|
req := httptest.NewRequest(http.MethodGet, "/api/now-playing", nil)
|
|
w := httptest.NewRecorder()
|
|
s.handleNowPlaying(w, req)
|
|
|
|
var got NowPlaying
|
|
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
|
|
t.Fatalf("Unmarshal() error = %v", err)
|
|
}
|
|
if got.URL != "" {
|
|
t.Fatalf("URL = %q, want empty", got.URL)
|
|
}
|
|
}
|
|
|
|
func TestHandleSysInfoReturnsItems(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/sysinfo", nil)
|
|
w := httptest.NewRecorder()
|
|
handleSysInfo(w, req)
|
|
|
|
if ct := w.Header().Get("Content-Type"); !strings.Contains(ct, "application/json") {
|
|
t.Fatalf("Content-Type = %q, want application/json", ct)
|
|
}
|
|
var got SysInfo
|
|
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
|
|
t.Fatalf("Unmarshal() error = %v", err)
|
|
}
|
|
// At least hostname should always be present.
|
|
if len(got.Items) == 0 {
|
|
t.Fatal("expected at least one sysinfo item")
|
|
}
|
|
labels := make(map[string]bool)
|
|
for _, item := range got.Items {
|
|
labels[item.Label] = true
|
|
if item.Value == "" {
|
|
t.Errorf("item %q has empty value", item.Label)
|
|
}
|
|
}
|
|
if !labels["Hostname"] {
|
|
t.Error("expected Hostname item in sysinfo")
|
|
}
|
|
}
|
|
|
|
func TestAssetsServed(t *testing.T) {
|
|
s := New("127.0.0.1:0", func() NowPlaying { return NowPlaying{} })
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
ready := make(chan string, 1)
|
|
go func() {
|
|
ln, _ := net.Listen("tcp", "127.0.0.1:0")
|
|
ready <- ln.Addr().String()
|
|
ln.Close()
|
|
}()
|
|
|
|
// Use httptest recorder to test asset handler directly via the embed FS.
|
|
sub, err := fs.Sub(assetsFS, "assets")
|
|
if err != nil {
|
|
t.Fatalf("fs.Sub error = %v", err)
|
|
}
|
|
handler := http.StripPrefix("/assets/", http.FileServer(http.FS(sub)))
|
|
|
|
for _, name := range []string{"splash-landscape.png", "splash-portrait.png"} {
|
|
req := httptest.NewRequest(http.MethodGet, "/assets/"+name, nil)
|
|
w := httptest.NewRecorder()
|
|
handler.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("GET /assets/%s = %d, want 200", name, w.Code)
|
|
}
|
|
if ct := w.Header().Get("Content-Type"); !strings.Contains(ct, "image/png") {
|
|
t.Errorf("GET /assets/%s Content-Type = %q, want image/png", name, ct)
|
|
}
|
|
}
|
|
_ = s
|
|
_ = ctx
|
|
}
|
|
|
|
func TestServerRunAndStop(t *testing.T) {
|
|
s := New("127.0.0.1:0", func() NowPlaying {
|
|
return NowPlaying{Status: "running", Connectivity: "online"}
|
|
})
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
errCh := make(chan error, 1)
|
|
go func() { errCh <- s.Run(ctx) }()
|
|
|
|
cancel()
|
|
if err := <-errCh; err != nil {
|
|
t.Fatalf("Run() error = %v", err)
|
|
}
|
|
}
|