Closes #11. Ombi builds recentlyRequested TV rows from child requests, so requestId is a child id — and upstream persists the provider id as the child PK for new-request children, so the value is provider-shaped (TVDB/TMDB) while the parent request id never appears in the payload. Consumers following target into read_requests get hit upstream 500s. TV rows now resolve through one bounded v1 parent scan: an exact child-id match against embedded childRequests (authoritative), then a provider-id fallback against tvDbId/externalProviderId covering versions that put the provider id in requestId directly. The child id is preserved as an ombi_tv_child identifier, provider ids land under tvdb/tmdb/imdb, and unresolvable rows emit target.id 0 with a warning rather than a fabricated or provider-shaped target.
335 lines
9.5 KiB
Go
335 lines
9.5 KiB
Go
package tools
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func idMap(ids []Identifier) map[string]string {
|
|
out := map[string]string{}
|
|
for _, id := range ids {
|
|
out[id.Namespace] = id.Value
|
|
}
|
|
return out
|
|
}
|
|
|
|
func countNS(ids []Identifier, ns string) int {
|
|
n := 0
|
|
for _, id := range ids {
|
|
if id.Namespace == ns {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func TestProjectSearchMediaMovieFallsBackToID(t *testing.T) {
|
|
m := projectSearchMedia(map[string]any{
|
|
"id": 348, "title": "Alien", "imdbId": "tt0078748",
|
|
}, "movie", "tmdb")
|
|
ids := idMap(m.Identifiers)
|
|
if ids["tmdb"] != "348" || ids["imdb"] != "tt0078748" {
|
|
t.Fatalf("identifiers = %v", m.Identifiers)
|
|
}
|
|
}
|
|
|
|
func TestProjectFullMovieDedupsImdbAndSkipsCollectionID(t *testing.T) {
|
|
m := projectFullMovie(map[string]any{
|
|
"id": 348, "title": "Alien", "imdbId": "tt0078748",
|
|
"externalIds": map[string]any{"imdbId": "tt0078748"},
|
|
"belongsToCollection": map[string]any{"id": 8091, "name": "Alien Collection"},
|
|
})
|
|
ids := idMap(m.Identifiers)
|
|
if ids["tmdb"] != "348" {
|
|
t.Errorf("tmdb = %q, want 348 (not collection 8091)", ids["tmdb"])
|
|
}
|
|
if _, ok := ids["tmdb"]; ok && countNS(m.Identifiers, "tmdb") != 1 {
|
|
t.Errorf("tmdb emitted %d times: %v", countNS(m.Identifiers, "tmdb"), m.Identifiers)
|
|
}
|
|
if countNS(m.Identifiers, "imdb") != 1 {
|
|
t.Errorf("imdb emitted %d times: %v", countNS(m.Identifiers, "imdb"), m.Identifiers)
|
|
}
|
|
}
|
|
|
|
func TestProjectSearchMediaTVV2IDFallbackIsTMDBNotTVMaze(t *testing.T) {
|
|
m := projectSearchMedia(map[string]any{
|
|
"id": 1668, "title": "Reacher", "seriesId": 1668,
|
|
}, "tv", "tmdb")
|
|
ids := idMap(m.Identifiers)
|
|
if ids["tmdb"] != "1668" {
|
|
t.Errorf("tmdb = %q", ids["tmdb"])
|
|
}
|
|
if _, ok := ids["tvmaze"]; ok {
|
|
t.Errorf("v2 seriesId must not be labelled tvmaze: %v", m.Identifiers)
|
|
}
|
|
if _, ok := ids["tvdb"]; ok {
|
|
t.Errorf("unexpected tvdb: %v", m.Identifiers)
|
|
}
|
|
}
|
|
|
|
func TestProjectSearchMediaTVV1KeepsTVMazeAndDoesNotInventTMDB(t *testing.T) {
|
|
m := projectSearchMedia(map[string]any{
|
|
"id": 81189, "theMovieDbId": 81189, "seriesId": 169,
|
|
"imdbId": "tt0903747", "title": "Breaking Bad",
|
|
}, "tv", "tvdb")
|
|
ids := idMap(m.Identifiers)
|
|
if ids["tvdb"] != "81189" || ids["tvmaze"] != "169" || ids["imdb"] != "tt0903747" {
|
|
t.Errorf("identifiers = %v", m.Identifiers)
|
|
}
|
|
if _, ok := ids["tmdb"]; ok {
|
|
t.Errorf("v1 id fallback must not emit tmdb: %v", m.Identifiers)
|
|
}
|
|
}
|
|
|
|
func TestProjectFullTVTMDBDoesNotEchoSeriesIdAsTVMaze(t *testing.T) {
|
|
o := &op{}
|
|
m := o.projectFullTV(map[string]any{
|
|
"id": 1206, "theMovieDbId": 1206, "seriesId": 1206,
|
|
"theTvDbId": 75150, "imdbId": "tt0227882",
|
|
"externalIds": map[string]any{"imdbId": "tt0227882", "tvdbId": 75150},
|
|
"title": "Button Moon",
|
|
}, "tmdb")
|
|
ids := idMap(m.Identifiers)
|
|
if ids["tmdb"] != "1206" || ids["tvdb"] != "75150" || ids["imdb"] != "tt0227882" {
|
|
t.Errorf("identifiers = %v", m.Identifiers)
|
|
}
|
|
if v, ok := ids["tvmaze"]; ok {
|
|
t.Errorf("bogus tvmaze %q (echoed TMDB id)", v)
|
|
}
|
|
if countNS(m.Identifiers, "imdb") != 1 {
|
|
t.Errorf("imdb emitted %d times: %v", countNS(m.Identifiers, "imdb"), m.Identifiers)
|
|
}
|
|
}
|
|
|
|
func TestProjectMultiResultArtistCaseInsensitive(t *testing.T) {
|
|
o := &op{}
|
|
m := o.projectMultiResult(map[string]any{
|
|
"id": "8dc08e7c-41f1-4a04-97dc-eb00d91d1d2f",
|
|
"mediaType": "Artist", "title": "Radiohead",
|
|
})
|
|
if m.Media != "artist" {
|
|
t.Errorf("media = %q, want artist", m.Media)
|
|
}
|
|
ids := idMap(m.Identifiers)
|
|
if ids["musicbrainz"] != "8dc08e7c-41f1-4a04-97dc-eb00d91d1d2f" {
|
|
t.Errorf("identifiers = %v", m.Identifiers)
|
|
}
|
|
if _, ok := ids["provider_unknown"]; ok {
|
|
t.Errorf("Artist mapped as provider_unknown: %v", m.Identifiers)
|
|
}
|
|
}
|
|
|
|
func TestProjectRequestPrefersRequestIDOverProviderID(t *testing.T) {
|
|
o := &op{}
|
|
r := o.projectRequest(map[string]any{
|
|
"id": 259032, "requestId": 88, "title": "Seven Up!",
|
|
"mediaId": "259032", "tvDbId": 259032,
|
|
}, "tv_parent")
|
|
if r.Target.Kind != "tv_parent" || r.Target.ID != 88 {
|
|
t.Errorf("target = %+v, want id 88", r.Target)
|
|
}
|
|
ids := idMap(r.Identifiers)
|
|
if ids["tvdb"] != "259032" {
|
|
t.Errorf("tvdb identifier = %q", ids["tvdb"])
|
|
}
|
|
if ids["tmdb"] != "259032" {
|
|
t.Errorf("mediaId tmdb identifier = %q", ids["tmdb"])
|
|
}
|
|
}
|
|
|
|
func TestProjectRequestListStillUsesEntityID(t *testing.T) {
|
|
o := &op{}
|
|
r := o.projectRequest(map[string]any{
|
|
"id": 10, "theMovieDbId": 27205, "title": "Inception",
|
|
}, "movie")
|
|
if r.Target.ID != 10 {
|
|
t.Errorf("target.id = %d, want 10", r.Target.ID)
|
|
}
|
|
}
|
|
|
|
func TestMergeTVRequestState(t *testing.T) {
|
|
it := &Media{
|
|
Seasons: []SeasonOut{
|
|
{
|
|
SeasonNumber: 1,
|
|
Episodes: []EpisodeOut{
|
|
{EpisodeNumber: 1},
|
|
{EpisodeNumber: 2},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
parent := map[string]any{
|
|
"id": 909,
|
|
"tvDbId": 81189,
|
|
"childRequests": []any{
|
|
map[string]any{
|
|
"seasonRequests": []any{
|
|
map[string]any{
|
|
"seasonNumber": 1,
|
|
"episodes": []any{
|
|
map[string]any{"episodeNumber": 1, "requested": true, "available": true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
matched := mergeTVRequestState(it, parent, 81189, "")
|
|
if !matched {
|
|
t.Fatalf("expected match")
|
|
}
|
|
if it.Requested == nil || !*it.Requested {
|
|
t.Errorf("expected media Requested=true")
|
|
}
|
|
if len(it.RequestTargets) != 1 || it.RequestTargets[0].ID != 909 {
|
|
t.Errorf("expected target ID 909")
|
|
}
|
|
if it.Seasons[0].Episodes[0].Requested == nil || !*it.Seasons[0].Episodes[0].Requested {
|
|
t.Errorf("expected ep1 Requested=true")
|
|
}
|
|
if it.Seasons[0].Episodes[0].Available == nil || !*it.Seasons[0].Episodes[0].Available {
|
|
t.Errorf("expected ep1 Available=true")
|
|
}
|
|
if it.Seasons[0].Episodes[1].Requested != nil {
|
|
t.Errorf("expected ep2 Requested=nil")
|
|
}
|
|
}
|
|
|
|
func TestMergeTVRequestStateNoMatch(t *testing.T) {
|
|
it := &Media{}
|
|
parent := map[string]any{"tvDbId": 99999}
|
|
matched := mergeTVRequestState(it, parent, 81189, "")
|
|
if matched {
|
|
t.Fatalf("expected no match")
|
|
}
|
|
if it.Requested != nil {
|
|
t.Errorf("expected Requested=nil on no match")
|
|
}
|
|
}
|
|
|
|
func TestTVParentMatchChildIDBeatsProviderID(t *testing.T) {
|
|
parent := map[string]any{
|
|
"id": 909, "tvDbId": 259032, "externalProviderId": 118680,
|
|
"childRequests": []any{
|
|
map[string]any{
|
|
"id": 88,
|
|
"seasonRequests": []any{
|
|
map[string]any{"childRequestId": 88, "seasonNumber": 1},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
if got := tvParentMatch(parent, tvRecentKey{requestID: 88, mediaID: "1396"}); got != tvMatchChild {
|
|
t.Errorf("real child id must match tier 1, got %d", got)
|
|
}
|
|
if got := tvParentMatch(parent, tvRecentKey{requestID: 259032}); got != tvMatchProvider {
|
|
t.Errorf("provider-shaped child pk falls back to tier 2 without embedded child, got %d", got)
|
|
}
|
|
if got := tvParentMatch(parent, tvRecentKey{requestID: 0, mediaID: "118680"}); got != tvMatchProvider {
|
|
t.Errorf("mediaId provider match = %d, want %d", got, tvMatchProvider)
|
|
}
|
|
if got := tvParentMatch(parent, tvRecentKey{requestID: 777, mediaID: "999"}); got != tvMatchNone {
|
|
t.Errorf("unrelated key matched: %d", got)
|
|
}
|
|
}
|
|
|
|
func TestTVParentMatchProviderShapedChildPK(t *testing.T) {
|
|
// Live rows: upstream persists the provider id as the child PK, so
|
|
// childRequests[].id equals tvDbId/externalProviderId.
|
|
parent := map[string]any{
|
|
"id": 909, "tvDbId": 259032,
|
|
"childRequests": []any{
|
|
map[string]any{"id": 259032},
|
|
},
|
|
}
|
|
if got := tvParentMatch(parent, tvRecentKey{requestID: 259032}); got != tvMatchChild {
|
|
t.Errorf("provider-shaped child id is still an exact tier-1 match, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestRefOfDeterministicValue(t *testing.T) {
|
|
o := &op{}
|
|
m := map[string]any{
|
|
"id": 53,
|
|
"name": "Thriller",
|
|
"logoPath": "/x",
|
|
"enabled": false,
|
|
}
|
|
for i := 0; i < 50; i++ {
|
|
r := o.refOf(m, []string{"id"}, []string{"name"}, nil, "genre")
|
|
if r.ID != "53" {
|
|
t.Fatalf("iteration %d: id = %q, want \"53\"", i, r.ID)
|
|
}
|
|
if r.Name != "Thriller" {
|
|
t.Fatalf("iteration %d: name = %q, want \"Thriller\"", i, r.Name)
|
|
}
|
|
if r.Value != 53 {
|
|
t.Fatalf("iteration %d: value = %v (%T), want 53", i, r.Value, r.Value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRefOfValueKeysOverride(t *testing.T) {
|
|
o := &op{}
|
|
m := map[string]any{
|
|
"id": 53,
|
|
"name": "Thriller",
|
|
"customVal": "override",
|
|
}
|
|
r := o.refOf(m, []string{"id"}, []string{"name"}, []string{"customVal"}, "genre")
|
|
if r.Value != "override" {
|
|
t.Fatalf("value = %v, want \"override\"", r.Value)
|
|
}
|
|
}
|
|
|
|
func TestRefOfCorruptedNameSkip(t *testing.T) {
|
|
o := &op{}
|
|
m := map[string]any{
|
|
"id": 1,
|
|
"english_name": "?????",
|
|
"name": "Clean",
|
|
}
|
|
r := o.refOf(m, []string{"id"}, []string{"english_name", "name"}, nil, "language")
|
|
if r.Name != "Clean" {
|
|
t.Fatalf("name = %q, want \"Clean\"", r.Name)
|
|
}
|
|
if len(o.warn) != 0 {
|
|
t.Fatalf("unexpected warnings: %v", o.warn)
|
|
}
|
|
|
|
o = &op{}
|
|
m2 := map[string]any{
|
|
"id": 2,
|
|
"english_name": "Bad\uFFFDName",
|
|
"name": "CleanAlternate",
|
|
}
|
|
r2 := o.refOf(m2, []string{"id"}, []string{"english_name", "name"}, nil, "language")
|
|
if r2.Name != "CleanAlternate" {
|
|
t.Fatalf("name = %q, want \"CleanAlternate\"", r2.Name)
|
|
}
|
|
if len(o.warn) != 0 {
|
|
t.Fatalf("unexpected warnings: %v", o.warn)
|
|
}
|
|
}
|
|
|
|
func TestRefOfAllCorruptedEmitsAndWarns(t *testing.T) {
|
|
o := &op{}
|
|
m := map[string]any{
|
|
"id": "ky",
|
|
"english_name": "?????",
|
|
"name": "?????",
|
|
}
|
|
r := o.refOf(m, []string{"id"}, []string{"english_name", "name"}, nil, "language")
|
|
if r.Name != "?????" {
|
|
t.Fatalf("name = %q, want \"?????\"", r.Name)
|
|
}
|
|
if len(o.warn) == 0 {
|
|
t.Fatalf("expected warning for corrupted name, got none")
|
|
}
|
|
expectedWarn := "upstream language label appears corrupted (id \"ky\")"
|
|
if o.warn[0] != expectedWarn {
|
|
t.Fatalf("warn = %q, want %q", o.warn[0], expectedWarn)
|
|
}
|
|
}
|