259 lines
9.1 KiB
Go
259 lines
9.1 KiB
Go
package metadata
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"fmt"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"github.com/Silo-Server/silo-server/internal/models"
|
||
|
|
)
|
||
|
|
|
||
|
|
func seedRelanguageItem(t *testing.T, h *testHarness, lockedFields ...MetadataField) {
|
||
|
|
t.Helper()
|
||
|
|
locked := make([]int, 0, len(lockedFields))
|
||
|
|
for _, field := range lockedFields {
|
||
|
|
locked = append(locked, int(field))
|
||
|
|
}
|
||
|
|
if err := h.itemRepo.Upsert(context.Background(), &models.MediaItem{
|
||
|
|
ContentID: "existing-1",
|
||
|
|
Type: "movie",
|
||
|
|
Title: "Gammel Kinesisk Titel",
|
||
|
|
Overview: "Old-language overview",
|
||
|
|
Year: 2020,
|
||
|
|
Status: "matched",
|
||
|
|
DefaultMetadataLanguage: "zh",
|
||
|
|
LockedFields: locked,
|
||
|
|
Studios: []string{},
|
||
|
|
Networks: []string{},
|
||
|
|
Countries: []string{},
|
||
|
|
Genres: []string{},
|
||
|
|
}); err != nil {
|
||
|
|
t.Fatalf("upsert existing item: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func newRelanguageProvider() *capturingMetadataProvider {
|
||
|
|
return &capturingMetadataProvider{
|
||
|
|
response: &MetadataResult{
|
||
|
|
HasMetadata: true,
|
||
|
|
Title: "Ny Dansk Titel",
|
||
|
|
Overview: "Dansk oversigt",
|
||
|
|
ProviderIDs: map[string]string{"tmdb": "42"},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestProcess_AdoptLanguageRewritesBaseRowAndRestamps covers the core of
|
||
|
|
// issue #211: a refresh carrying AdoptLanguage must treat req.Language as the
|
||
|
|
// item's new canonical metadata language — replacing the base-row title and
|
||
|
|
// overview and restamping default_metadata_language — instead of pinning to
|
||
|
|
// the language stamped at first match.
|
||
|
|
func TestProcess_AdoptLanguageRewritesBaseRowAndRestamps(t *testing.T) {
|
||
|
|
h := newTestHarness()
|
||
|
|
ctx := context.Background()
|
||
|
|
seedRelanguageItem(t, h)
|
||
|
|
|
||
|
|
provider := newRelanguageProvider()
|
||
|
|
|
||
|
|
result, err := h.service.ProcessWithProviders(ctx, ProcessRequest{
|
||
|
|
ContentID: "existing-1",
|
||
|
|
Language: "da",
|
||
|
|
Mode: ModeManualRefresh,
|
||
|
|
AdoptLanguage: true,
|
||
|
|
}, []Provider{provider})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("ProcessWithProviders: %v", err)
|
||
|
|
}
|
||
|
|
if result == nil || !result.Updated {
|
||
|
|
t.Fatalf("result = %#v, want Updated=true", result)
|
||
|
|
}
|
||
|
|
|
||
|
|
item, err := h.itemRepo.GetByID(ctx, "existing-1")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("get item: %v", err)
|
||
|
|
}
|
||
|
|
if item.Title != "Ny Dansk Titel" {
|
||
|
|
t.Errorf("title = %q, want base row rewritten to %q", item.Title, "Ny Dansk Titel")
|
||
|
|
}
|
||
|
|
if item.Overview != "Dansk oversigt" {
|
||
|
|
t.Errorf("overview = %q, want base row rewritten", item.Overview)
|
||
|
|
}
|
||
|
|
if item.DefaultMetadataLanguage != "da" {
|
||
|
|
t.Errorf("default_metadata_language = %q, want restamped to da", item.DefaultMetadataLanguage)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestProcess_WithoutAdoptLanguageKeepsCanonicalPin pins the pre-existing
|
||
|
|
// behavior: a refresh in a non-canonical language without AdoptLanguage must
|
||
|
|
// NOT rewrite the base row or restamp the item (the fetch routes to the
|
||
|
|
// localization tables instead).
|
||
|
|
func TestProcess_WithoutAdoptLanguageKeepsCanonicalPin(t *testing.T) {
|
||
|
|
h := newTestHarness()
|
||
|
|
ctx := context.Background()
|
||
|
|
seedRelanguageItem(t, h)
|
||
|
|
|
||
|
|
provider := newRelanguageProvider()
|
||
|
|
|
||
|
|
result, err := h.service.ProcessWithProviders(ctx, ProcessRequest{
|
||
|
|
ContentID: "existing-1",
|
||
|
|
Language: "da",
|
||
|
|
Mode: ModeManualRefresh,
|
||
|
|
}, []Provider{provider})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("ProcessWithProviders: %v", err)
|
||
|
|
}
|
||
|
|
if result == nil || !result.Updated {
|
||
|
|
t.Fatalf("result = %#v, want Updated=true", result)
|
||
|
|
}
|
||
|
|
|
||
|
|
item, err := h.itemRepo.GetByID(ctx, "existing-1")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("get item: %v", err)
|
||
|
|
}
|
||
|
|
if item.Title != "Gammel Kinesisk Titel" {
|
||
|
|
t.Errorf("title = %q, want base row untouched", item.Title)
|
||
|
|
}
|
||
|
|
if item.DefaultMetadataLanguage != "zh" {
|
||
|
|
t.Errorf("default_metadata_language = %q, want zh (unchanged)", item.DefaultMetadataLanguage)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestAdoptableFolderLanguage covers the decision for when a folder-scoped
|
||
|
|
// refresh should adopt the folder's language as the item's new canonical
|
||
|
|
// language (issue #211). Only manual-refresh passes adopt: scheduled
|
||
|
|
// refreshes merge fill-empty, which would restamp the language without
|
||
|
|
// rewriting the text.
|
||
|
|
func TestAdoptableFolderLanguage(t *testing.T) {
|
||
|
|
cases := []struct {
|
||
|
|
name string
|
||
|
|
mode RefreshMode
|
||
|
|
stamp string
|
||
|
|
language string
|
||
|
|
want bool
|
||
|
|
}{
|
||
|
|
{"manual refresh with mismatch adopts", ModeManualRefresh, "zh", "da", true},
|
||
|
|
{"manual refresh case-insensitive match does not adopt", ModeManualRefresh, "DA", "da", false},
|
||
|
|
{"scheduled refresh never adopts", ModeScheduledRefresh, "zh", "da", false},
|
||
|
|
{"identify never adopts", ModeIdentify, "zh", "da", false},
|
||
|
|
{"initial match never adopts", ModeInitialMatch, "zh", "da", false},
|
||
|
|
{"unstamped item does not adopt", ModeManualRefresh, "", "da", false},
|
||
|
|
{"empty target language does not adopt", ModeManualRefresh, "zh", "", false},
|
||
|
|
}
|
||
|
|
for _, tc := range cases {
|
||
|
|
t.Run(tc.name, func(t *testing.T) {
|
||
|
|
if got := adoptableFolderLanguage(tc.mode, tc.stamp, tc.language); got != tc.want {
|
||
|
|
t.Fatalf("adoptableFolderLanguage(%v, %q, %q) = %v, want %v", tc.mode, tc.stamp, tc.language, got, tc.want)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestProcess_AdoptLanguageSkippedWhenBothLanguageFieldsLocked: when both
|
||
|
|
// language-bearing fields (title, overview) are locked, adoption must not
|
||
|
|
// restamp default_metadata_language — nothing would actually be rewritten,
|
||
|
|
// so the restamp would permanently mislabel the old-language text and the
|
||
|
|
// quick-refresh language-mismatch predicate would never flag the item again.
|
||
|
|
// The refresh behaves like a non-adopting refresh in that language instead.
|
||
|
|
func TestProcess_AdoptLanguageSkippedWhenBothLanguageFieldsLocked(t *testing.T) {
|
||
|
|
h := newTestHarness()
|
||
|
|
ctx := context.Background()
|
||
|
|
seedRelanguageItem(t, h, FieldName, FieldOverview)
|
||
|
|
|
||
|
|
result, err := h.service.ProcessWithProviders(ctx, ProcessRequest{
|
||
|
|
ContentID: "existing-1",
|
||
|
|
Language: "da",
|
||
|
|
Mode: ModeManualRefresh,
|
||
|
|
AdoptLanguage: true,
|
||
|
|
}, []Provider{newRelanguageProvider()})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("ProcessWithProviders: %v", err)
|
||
|
|
}
|
||
|
|
if result == nil || !result.Updated {
|
||
|
|
t.Fatalf("result = %#v, want Updated=true", result)
|
||
|
|
}
|
||
|
|
|
||
|
|
item, err := h.itemRepo.GetByID(ctx, "existing-1")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("get item: %v", err)
|
||
|
|
}
|
||
|
|
if item.Title != "Gammel Kinesisk Titel" {
|
||
|
|
t.Errorf("title = %q, want locked title untouched", item.Title)
|
||
|
|
}
|
||
|
|
if item.Overview != "Old-language overview" {
|
||
|
|
t.Errorf("overview = %q, want locked overview untouched", item.Overview)
|
||
|
|
}
|
||
|
|
if item.DefaultMetadataLanguage != "zh" {
|
||
|
|
t.Errorf("default_metadata_language = %q, want zh (stamp kept when both language fields are locked)",
|
||
|
|
item.DefaultMetadataLanguage)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestProcess_AdoptLanguageWithOnlyTitleLocked: a single locked language
|
||
|
|
// field does not block adoption — the unlocked field is rewritten in the new
|
||
|
|
// language and the stamp is adopted, while the locked field keeps its text.
|
||
|
|
func TestProcess_AdoptLanguageWithOnlyTitleLocked(t *testing.T) {
|
||
|
|
h := newTestHarness()
|
||
|
|
ctx := context.Background()
|
||
|
|
seedRelanguageItem(t, h, FieldName)
|
||
|
|
|
||
|
|
result, err := h.service.ProcessWithProviders(ctx, ProcessRequest{
|
||
|
|
ContentID: "existing-1",
|
||
|
|
Language: "da",
|
||
|
|
Mode: ModeManualRefresh,
|
||
|
|
AdoptLanguage: true,
|
||
|
|
}, []Provider{newRelanguageProvider()})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("ProcessWithProviders: %v", err)
|
||
|
|
}
|
||
|
|
if result == nil || !result.Updated {
|
||
|
|
t.Fatalf("result = %#v, want Updated=true", result)
|
||
|
|
}
|
||
|
|
|
||
|
|
item, err := h.itemRepo.GetByID(ctx, "existing-1")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("get item: %v", err)
|
||
|
|
}
|
||
|
|
if item.Title != "Gammel Kinesisk Titel" {
|
||
|
|
t.Errorf("title = %q, want locked title untouched", item.Title)
|
||
|
|
}
|
||
|
|
if item.Overview != "Dansk oversigt" {
|
||
|
|
t.Errorf("overview = %q, want base row rewritten", item.Overview)
|
||
|
|
}
|
||
|
|
if item.DefaultMetadataLanguage != "da" {
|
||
|
|
t.Errorf("default_metadata_language = %q, want restamped to da (only one field locked)",
|
||
|
|
item.DefaultMetadataLanguage)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestItemLibrariesAgreeOnLanguage covers the multi-library adoption gate: an
|
||
|
|
// item that lives in several libraries (media_item_libraries) must only adopt
|
||
|
|
// a language every containing library agrees on — otherwise libraries with
|
||
|
|
// different languages would rewrite the canonical base row back and forth on
|
||
|
|
// every refresh.
|
||
|
|
func TestItemLibrariesAgreeOnLanguage(t *testing.T) {
|
||
|
|
ctx := context.Background()
|
||
|
|
cases := []struct {
|
||
|
|
name string
|
||
|
|
languages []string
|
||
|
|
lookupErr error
|
||
|
|
target string
|
||
|
|
want bool
|
||
|
|
}{
|
||
|
|
{"single library matching target agrees", []string{"da"}, nil, "da", true},
|
||
|
|
{"case-insensitive match agrees", []string{"DA"}, nil, "da", true},
|
||
|
|
{"disagreeing libraries block adoption", []string{"da", "en"}, nil, "da", false},
|
||
|
|
{"single library with other language blocks adoption", []string{"de"}, nil, "da", false},
|
||
|
|
{"no membership rows cannot disagree", nil, nil, "da", true},
|
||
|
|
{"lookup failure blocks adoption", nil, fmt.Errorf("boom"), "da", false},
|
||
|
|
}
|
||
|
|
for _, tc := range cases {
|
||
|
|
t.Run(tc.name, func(t *testing.T) {
|
||
|
|
h := newTestHarness()
|
||
|
|
h.libraryRepo.setMetadataLanguages("existing-1", tc.languages, tc.lookupErr)
|
||
|
|
if got := h.service.itemLibrariesAgreeOnLanguage(ctx, "existing-1", tc.target); got != tc.want {
|
||
|
|
t.Fatalf("itemLibrariesAgreeOnLanguage(%v, %q) = %v, want %v", tc.languages, tc.target, got, tc.want)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|