{"subject":"fix(jellycompat): stabilize signed image tags across restarts","body":"- Sign library poster and episode parent series image tags from canonical paths/thumbhashes instead of presigned URLs so tags survive restarts\n- Accept signed canonical tags in the image handler without a session and fall back to legacy URL-derived cache tags\n- Always fetch series detail for episodes to build stable parent image tags"}
188 lines
6.6 KiB
Go
188 lines
6.6 KiB
Go
package jellycompat
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/config"
|
|
)
|
|
|
|
func TestItemImageTagsUseStableCanonicalSeed(t *testing.T) {
|
|
m := newMapper(NewResourceIDCodec(), &config.Config{})
|
|
updatedAt := time.Date(2026, 5, 12, 12, 0, 0, 0, time.UTC)
|
|
item := upstreamListItem{
|
|
ContentID: "movie-1",
|
|
Type: "movie",
|
|
Title: "Movie",
|
|
PosterURL: "https://cdn.example.test/poster.jpg?sig=one",
|
|
PosterPath: "metadb://poster/movie-1",
|
|
PosterThumbhash: "thumbhash",
|
|
UpdatedAt: updatedAt,
|
|
}
|
|
|
|
first := m.itemFromList(item, false, nil, nil)
|
|
item.PosterURL = "https://cdn.example.test/poster.jpg?sig=two"
|
|
second := m.itemFromList(item, false, nil, nil)
|
|
|
|
if first.ImageTags["Primary"] == "" {
|
|
t.Fatal("primary image tag is empty")
|
|
}
|
|
if first.ImageTags["Primary"] != second.ImageTags["Primary"] {
|
|
t.Fatalf("image tag changed when only signed URL changed: %q vs %q", first.ImageTags["Primary"], second.ImageTags["Primary"])
|
|
}
|
|
|
|
item.UpdatedAt = updatedAt.Add(time.Second)
|
|
third := m.itemFromList(item, false, nil, nil)
|
|
if third.ImageTags["Primary"] == first.ImageTags["Primary"] {
|
|
t.Fatalf("image tag did not change after UpdatedAt changed: %q", third.ImageTags["Primary"])
|
|
}
|
|
}
|
|
|
|
func TestItemImageTagsFallbackToURLWhenCanonicalSeedMissing(t *testing.T) {
|
|
m := newMapper(NewResourceIDCodec(), &config.Config{})
|
|
item := upstreamListItem{
|
|
ContentID: "movie-1",
|
|
Type: "movie",
|
|
Title: "Movie",
|
|
PosterURL: "https://cdn.example.test/poster.jpg?sig=one",
|
|
}
|
|
|
|
first := m.itemFromList(item, false, nil, nil)
|
|
item.PosterURL = "https://cdn.example.test/poster.jpg?sig=two"
|
|
second := m.itemFromList(item, false, nil, nil)
|
|
|
|
if first.ImageTags["Primary"] == second.ImageTags["Primary"] {
|
|
t.Fatalf("fallback image tag did not change with URL: %q", first.ImageTags["Primary"])
|
|
}
|
|
}
|
|
|
|
func TestItemImageTagsUseConfiguredSecret(t *testing.T) {
|
|
item := upstreamListItem{
|
|
ContentID: "movie-1",
|
|
Type: "movie",
|
|
Title: "Movie",
|
|
PosterURL: "https://cdn.example.test/poster.jpg?sig=one",
|
|
PosterPath: "metadb://poster/movie-1",
|
|
PosterThumbhash: "thumbhash",
|
|
UpdatedAt: time.Date(2026, 5, 12, 12, 0, 0, 0, time.UTC),
|
|
}
|
|
|
|
first := newMapper(NewResourceIDCodec(), &config.Config{
|
|
Auth: config.AuthConfig{JWTSecret: "secret-one"},
|
|
}).itemFromList(item, false, nil, nil)
|
|
second := newMapper(NewResourceIDCodec(), &config.Config{
|
|
Auth: config.AuthConfig{JWTSecret: "secret-two"},
|
|
}).itemFromList(item, false, nil, nil)
|
|
|
|
if first.ImageTags["Primary"] == second.ImageTags["Primary"] {
|
|
t.Fatalf("signed image tag did not change with configured secret: %q", first.ImageTags["Primary"])
|
|
}
|
|
}
|
|
|
|
func TestEpisodeListImageTagsUseStillThumbhash(t *testing.T) {
|
|
secret := "image-secret"
|
|
updatedAt := time.Date(2026, 5, 12, 12, 0, 0, 0, time.UTC)
|
|
item := upstreamListItem{
|
|
ContentID: "episode-1",
|
|
Type: "episode",
|
|
Title: "Episode",
|
|
PosterURL: "https://cdn.example.test/still.jpg?sig=one",
|
|
PosterPath: "metadb://still/episode-1",
|
|
PosterThumbhash: "poster-thumbhash",
|
|
StillPath: "metadb://still/episode-1",
|
|
StillThumbhash: "still-thumbhash",
|
|
UpdatedAt: updatedAt,
|
|
}
|
|
|
|
dto := newMapper(NewResourceIDCodec(), &config.Config{
|
|
Auth: config.AuthConfig{JWTSecret: secret},
|
|
}).itemFromList(item, false, nil, nil)
|
|
expected := newImageTagSigner(secret).Tag(
|
|
imageTagSeed(item.ContentID, "Primary", compatCardImageSize, item.StillPath, item.StillThumbhash, updatedAt),
|
|
item.PosterURL,
|
|
)
|
|
|
|
if dto.ImageTags["Primary"] != expected {
|
|
t.Fatalf("primary tag = %q, want still-thumbhash seed %q", dto.ImageTags["Primary"], expected)
|
|
}
|
|
}
|
|
|
|
func TestLibraryImageTagsUseStablePosterPath(t *testing.T) {
|
|
secret := "image-secret"
|
|
codec := NewResourceIDCodec()
|
|
library := upstreamUserLibrary{
|
|
ID: 1,
|
|
Name: "Movies",
|
|
Type: "movies",
|
|
PosterURL: "https://cdn.example.test/library.jpg?sig=one",
|
|
PosterPath: "library-posters/1/original.jpg",
|
|
}
|
|
|
|
first := newMapper(codec, &config.Config{
|
|
Auth: config.AuthConfig{JWTSecret: secret},
|
|
}).viewFromLibrary(library)
|
|
library.PosterURL = "https://cdn.example.test/library.jpg?sig=two"
|
|
second := newMapper(codec, &config.Config{
|
|
Auth: config.AuthConfig{JWTSecret: secret},
|
|
}).viewFromLibrary(library)
|
|
|
|
routeID := codec.EncodeIntID(EncodedIDLibrary, int64(library.ID))
|
|
expected := newImageTagSigner(secret).Tag(
|
|
imageTagSeed(routeID, "Primary", compatCardImageSize, library.PosterPath, "", time.Time{}),
|
|
library.PosterURL,
|
|
)
|
|
|
|
if first.ImageTags["Primary"] == "" {
|
|
t.Fatal("library primary image tag is empty")
|
|
}
|
|
if first.ImageTags["Primary"] != second.ImageTags["Primary"] {
|
|
t.Fatalf("library tag changed when only signed URL changed: %q vs %q", first.ImageTags["Primary"], second.ImageTags["Primary"])
|
|
}
|
|
if second.ImageTags["Primary"] != expected {
|
|
t.Fatalf("library tag = %q, want %q", second.ImageTags["Primary"], expected)
|
|
}
|
|
}
|
|
|
|
func TestApplySeriesImagesUsesCanonicalSeriesSeeds(t *testing.T) {
|
|
secret := "image-secret"
|
|
codec := NewResourceIDCodec()
|
|
seriesContentID := "series-1"
|
|
seriesRouteID := codec.EncodeStringID(EncodedIDItem, seriesContentID)
|
|
updatedAt := time.Date(2026, 5, 12, 12, 0, 0, 0, time.UTC)
|
|
dto := baseItemDTO{SeriesID: seriesRouteID}
|
|
series := seriesImageSet{
|
|
ContentID: seriesContentID,
|
|
PosterURL: "https://cdn.example.test/poster.jpg?sig=one",
|
|
PosterPath: "metadb://poster/series-1",
|
|
PosterThumbhash: "poster-thumbhash",
|
|
BackdropURL: "https://cdn.example.test/backdrop.jpg?sig=one",
|
|
BackdropPath: "metadb://backdrop/series-1",
|
|
BackdropThumbhash: "backdrop-thumbhash",
|
|
UpdatedAt: updatedAt,
|
|
}
|
|
|
|
newMapper(codec, &config.Config{
|
|
Auth: config.AuthConfig{JWTSecret: secret},
|
|
}).applySeriesImages(&dto, series)
|
|
|
|
signer := newImageTagSigner(secret)
|
|
expectedPrimary := signer.Tag(
|
|
imageTagSeed(series.ContentID, "Primary", compatCardImageSize, series.PosterPath, series.PosterThumbhash, updatedAt),
|
|
series.PosterURL,
|
|
)
|
|
expectedBackdrop := signer.Tag(
|
|
imageTagSeed(series.ContentID, "Backdrop", compatCardImageSize, series.BackdropPath, series.BackdropThumbhash, updatedAt),
|
|
series.BackdropURL,
|
|
)
|
|
|
|
if dto.SeriesPrimaryImageTag != expectedPrimary {
|
|
t.Fatalf("SeriesPrimaryImageTag = %q, want %q", dto.SeriesPrimaryImageTag, expectedPrimary)
|
|
}
|
|
if len(dto.ParentBackdropImageTags) != 1 || dto.ParentBackdropImageTags[0] != expectedBackdrop {
|
|
t.Fatalf("ParentBackdropImageTags = %#v, want [%q]", dto.ParentBackdropImageTags, expectedBackdrop)
|
|
}
|
|
if dto.ParentThumbImageTag != expectedBackdrop {
|
|
t.Fatalf("ParentThumbImageTag = %q, want %q", dto.ParentThumbImageTag, expectedBackdrop)
|
|
}
|
|
}
|