App v0.2.26 and gateway 0.1.20
Client: seek controls, Bazarr subtitle download and cast panel in the player; MDBList ratings strip; episode and schedule detail pages; series pace estimate; what's new panel; install-permission onboarding step; synced per-profile preferences; Emby outage banner. Gateway: rebuilt admin console (one fragment per page), preference history and restore, merged Continue Watching, Emby health probe, subtitle selection and Bazarr download, structured request logging with per-request identity, and embedded build version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2675e6d82b
commit
4a4df7a73c
@@ -3,7 +3,11 @@ package api
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/store"
|
||||
)
|
||||
|
||||
func TestRequestLogLevel(t *testing.T) {
|
||||
@@ -15,6 +19,8 @@ func TestRequestLogLevel(t *testing.T) {
|
||||
{"/healthz", http.StatusOK, slog.LevelDebug},
|
||||
{"/v1/status", http.StatusOK, slog.LevelDebug},
|
||||
{"/v1/images/123/primary", http.StatusOK, slog.LevelDebug},
|
||||
{"/admin/api/status", http.StatusOK, slog.LevelDebug},
|
||||
{"/admin/api/status", http.StatusUnauthorized, slog.LevelWarn},
|
||||
{"/v1/home", http.StatusOK, slog.LevelInfo},
|
||||
{"/v1/images/123/primary", http.StatusNotFound, slog.LevelWarn},
|
||||
{"/v1/home", http.StatusServiceUnavailable, slog.LevelError},
|
||||
@@ -26,6 +32,98 @@ func TestRequestLogLevel(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestComponentNamesThePartOfTheAppARouteBelongsTo(t *testing.T) {
|
||||
tests := map[string]string{
|
||||
"/healthz": "health",
|
||||
"/v1/status": "status",
|
||||
"/v1/home": "home",
|
||||
"/v1/preferences": "settings",
|
||||
"/v1/screensaver": "screensaver",
|
||||
"/v1/auth/login": "auth",
|
||||
"/v1/auth/devices/tv-1": "devices",
|
||||
"/v1/search": "search",
|
||||
"/v1/items/42": "details",
|
||||
"/v1/items/42/related": "details",
|
||||
"/v1/items/42/playback": "playback",
|
||||
"/v1/items/42/next": "playback",
|
||||
"/v1/items/42/subtitles/search": "playback",
|
||||
"/v1/playback/started": "playback",
|
||||
"/v1/images/42/primary": "artwork",
|
||||
"/v1/recommendations": "recommendations",
|
||||
"/v1/for-you": "recommendations",
|
||||
"/v1/my-shows": "my-shows",
|
||||
"/admin/api/status": "admin",
|
||||
"/hooks/radarr": "webhooks",
|
||||
"/install": "installer",
|
||||
"/updates/latest.apk": "updates",
|
||||
"/something-nobody-has-written": "api",
|
||||
}
|
||||
for path, want := range tests {
|
||||
if got := componentFor(path); got != want {
|
||||
t.Errorf("componentFor(%q) = %q, want %q", path, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentifyNamesTheViewerAndTelevision(t *testing.T) {
|
||||
request := httptest.NewRequest(http.MethodGet, "/v1/home", nil)
|
||||
request.Header.Set("X-Memby-Version", "0.1.60")
|
||||
request, identity := withRequestIdentity(request)
|
||||
|
||||
identify(request.Context(), store.Session{
|
||||
Username: "matt", DeviceName: "Living room", ClientProtocol: "1",
|
||||
})
|
||||
|
||||
if identity.component != "home" || identity.user != "matt" || identity.device != "Living room" {
|
||||
t.Fatalf("identity was not filled in: %+v", identity)
|
||||
}
|
||||
// The header the TV actually sent must survive a session that has no version yet.
|
||||
if identity.client != "0.1.60" || identity.protocol != "1" {
|
||||
t.Fatalf("client identity was lost: %+v", identity)
|
||||
}
|
||||
}
|
||||
|
||||
// A device with no name at all must still be identifiable, or an old APK's traffic
|
||||
// becomes anonymous exactly when someone is trying to work out which television it is.
|
||||
func TestIdentifyFallsBackToTheDeviceID(t *testing.T) {
|
||||
request, identity := withRequestIdentity(httptest.NewRequest(http.MethodGet, "/v1/home", nil))
|
||||
identify(request.Context(), store.Session{DeviceID: "tv-1"})
|
||||
if identity.device != "tv-1" {
|
||||
t.Fatalf("device = %q", identity.device)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaybackTitlesNamesReportsAndStaysBounded(t *testing.T) {
|
||||
var titles playbackTitles
|
||||
titles.remember("42", "Dune")
|
||||
if got := titles.name("42"); got != "Dune" {
|
||||
t.Fatalf("remembered title = %q", got)
|
||||
}
|
||||
// An unknown item reports as itself rather than as an empty field.
|
||||
if got := titles.name("99"); got != "99" {
|
||||
t.Fatalf("unknown title = %q", got)
|
||||
}
|
||||
for i := range rememberedTitles + 10 {
|
||||
titles.remember(strconv.Itoa(1000+i), "Title")
|
||||
}
|
||||
if len(titles.titles) > rememberedTitles {
|
||||
t.Fatalf("title memory grew to %d entries", len(titles.titles))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchedPercentReadsAsProgress(t *testing.T) {
|
||||
if got := watchedPercent(30_000, 60_000); got != "50%" {
|
||||
t.Errorf("half watched = %q", got)
|
||||
}
|
||||
// Emby's reported position can overshoot a runtime by a frame or two.
|
||||
if got := watchedPercent(61_000, 60_000); got != "100%" {
|
||||
t.Errorf("overshoot = %q", got)
|
||||
}
|
||||
if got := watchedPercent(30_000, 0); got != "unknown" {
|
||||
t.Errorf("unknown runtime = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientLogValueIsNeverBlank(t *testing.T) {
|
||||
if got := clientLogValue(""); got != "unknown" {
|
||||
t.Fatalf("blank identity logged as %q", got)
|
||||
|
||||
Reference in New Issue
Block a user