Big changes

This commit is contained in:
ponzischeme89
2026-07-29 15:26:27 +12:00
parent 8d6cf2f5a1
commit 70914400b4
62 changed files with 7501 additions and 744 deletions
+119 -1
View File
@@ -5,9 +5,11 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/ponzischeme89/memby/server/internal/config"
"github.com/ponzischeme89/memby/server/internal/emby"
"github.com/ponzischeme89/memby/server/internal/store"
)
func TestBearerTokenSources(t *testing.T) {
@@ -116,6 +118,55 @@ func TestPlaybackHintAvoidsAnUpstreamItemLookup(t *testing.T) {
}
}
func TestEpisodeAfterUsesPositionNotListLength(t *testing.T) {
episode := func(id string) json.RawMessage {
return json.RawMessage(`{"Id":"` + id + `","Name":"Episode ` + id + `","SeriesName":"Westworld"}`)
}
t.Run("middle of a season", func(t *testing.T) {
items := []json.RawMessage{episode("1"), episode("2"), episode("3")}
raw, next, ok := episodeAfter(items, "2")
if !ok {
t.Fatal("expected an episode after 2")
}
if next.ID != "3" {
t.Fatalf("next = %q, want 3", next.ID)
}
if got := seriesNameOf(raw); got != "Westworld" {
t.Fatalf("series name = %q, want Westworld", got)
}
})
// Emby drops the leading entry for the first episode, so a two-item list can mean
// either "first, second" or "second-to-last, last" depending on where we are.
t.Run("first episode has no previous", func(t *testing.T) {
items := []json.RawMessage{episode("1"), episode("2")}
if _, next, ok := episodeAfter(items, "1"); !ok || next.ID != "2" {
t.Fatalf("next = %+v, ok = %v, want episode 2", next, ok)
}
})
t.Run("series finale has nothing after it", func(t *testing.T) {
items := []json.RawMessage{episode("1"), episode("2")}
if _, _, ok := episodeAfter(items, "2"); ok {
t.Fatal("the last episode must not resolve a next one")
}
})
t.Run("current episode missing from the result", func(t *testing.T) {
items := []json.RawMessage{episode("7"), episode("8")}
if _, _, ok := episodeAfter(items, "42"); ok {
t.Fatal("an unrelated result must not resolve a next episode")
}
})
t.Run("empty result", func(t *testing.T) {
if _, _, ok := episodeAfter(nil, "1"); ok {
t.Fatal("no episodes must not resolve a next one")
}
})
}
// Empty rows must serialise as [] so kotlinx.serialization can decode them into the
// client's non-null List fields.
func TestHomeResponseEncodesEmptyRowsAsArrays(t *testing.T) {
@@ -137,7 +188,22 @@ func TestHomeResponseEncodesEmptyRowsAsArrays(t *testing.T) {
}
}
// The four fixed rows must keep their order, ids and kinds: the client maps kinds onto
func TestSearchHistoryResponseEncodesEmptyQueriesAsArray(t *testing.T) {
resp := searchHistoryResponse{Queries: []string{}}
body, err := json.Marshal(resp)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var decoded map[string]any
if err := json.Unmarshal(body, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if _, ok := decoded["queries"].([]any); !ok {
t.Fatalf("queries encoded as %T, want array", decoded["queries"])
}
}
// The fixed rows must keep their order, ids and kinds: the client maps kinds onto
// card shapes and uses ids as Compose keys.
func TestBaseRowsShape(t *testing.T) {
rows := baseRows(homeResponse{
@@ -168,6 +234,29 @@ func TestBaseRowsShape(t *testing.T) {
}
}
func TestHomeForYouWindowAt(t *testing.T) {
tests := []struct {
hour int
id string
minutes int
}{
{hour: 4, id: "late-night", minutes: 60},
{hour: 5, id: "morning", minutes: 30},
{hour: 11, id: "morning", minutes: 30},
{hour: 12, id: "afternoon", minutes: 60},
{hour: 16, id: "afternoon", minutes: 60},
{hour: 17, id: "evening", minutes: 120},
{hour: 22, id: "evening", minutes: 120},
{hour: 23, id: "late-night", minutes: 60},
}
for _, test := range tests {
got := homeForYouWindowAt(time.Date(2026, time.July, 29, test.hour, 0, 0, 0, time.UTC))
if got.ID != test.id || got.Minutes != test.minutes {
t.Errorf("hour %d: got %#v, want id=%q minutes=%d", test.hour, got, test.id, test.minutes)
}
}
}
func TestRecommendationBuildsAreDeduplicatedPerUser(t *testing.T) {
var builds recommendationBuilds
@@ -200,3 +289,32 @@ func TestSummariseReadsResumePosition(t *testing.T) {
t.Fatalf("resume position = %d ms, want 3600000", got)
}
}
func TestMergeClientIdentityPersistsReportedHeaders(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/v1/status", nil)
req.Header.Set("X-Memby-Version", "0.1.60")
req.Header.Set("X-Memby-Protocol", "1")
sess := store.Session{}
if changed := mergeClientIdentity(req, &sess); !changed {
t.Fatal("reported identity should update the session")
}
if sess.ClientVersion != "0.1.60" || sess.ClientProtocol != "1" {
t.Fatalf("session identity = %q/%q", sess.ClientVersion, sess.ClientProtocol)
}
}
func TestMergeClientIdentityAttributesHeaderlessAuthenticatedRequest(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/v1/images/42/primary?t=token", nil)
sess := store.Session{ClientVersion: "0.1.60", ClientProtocol: "1"}
if changed := mergeClientIdentity(req, &sess); changed {
t.Fatal("inheriting identity should not write the session again")
}
if got := clientVersion(req); got != "0.1.60" {
t.Fatalf("inherited client version = %q", got)
}
if got := clientProtocol(req); got != "1" {
t.Fatalf("inherited client protocol = %q", got)
}
}