Экран ревью теперь показывает единый список источников совпадения: строка «распознано нейронкой» наравне с кандидатами баз; выбор/переключение/снятие в пользу нейронки; ручное добавление по id или URL (TMDB/IMDb — по URL, TVDB — по числовому id); предпросмотр полей и целевых путей каждого источника до применения (место под режиссёра зарезервировано). «Раскладка» осталась отдельной секцией для активного источника, инлайн-превью неактивных — по клику. Ядро: единая деривация «источник → overrides» (sourcePins), общая для превью и коммита → preview == apply; заодно чинит латентный залипший override title/year при переключении источника. Превью считается эфемерно, без записи в БД; пользовательский URL только парсится (SSRF нет). Ревью дизайна и кода пройдены; правки ревьюеров учтены (сообщение об ошибке ручного ввода доходит до пользователя, URL без схемы принимается). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
98 lines
3.9 KiB
Go
98 lines
3.9 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"git.vakhrushev.me/av/jellybit/internal/store"
|
|
"git.vakhrushev.me/av/jellybit/internal/worker"
|
|
)
|
|
|
|
func TestProviderURL(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
provider string
|
|
id string
|
|
mtype string
|
|
want string
|
|
}{
|
|
{"tmdb movie", "tmdb", "693134", "movie", "https://www.themoviedb.org/movie/693134"},
|
|
{"tmdb series", "tmdb", "60622", "series", "https://www.themoviedb.org/tv/60622"},
|
|
{"tvdb series", "tvdb", "269613", "series", "https://www.thetvdb.com/dereferrer/series/269613"},
|
|
{"tvdb movie", "tvdb", "12345", "movie", "https://www.thetvdb.com/dereferrer/movie/12345"},
|
|
{"imdb", "imdb", "tt0111161", "movie", "https://www.imdb.com/title/tt0111161"},
|
|
{"unknown provider → пусто", "kinopoisk", "42", "movie", ""},
|
|
{"пустой id → пусто", "tmdb", "", "movie", ""},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
if got := providerURL(c.provider, c.id, c.mtype); got != c.want {
|
|
t.Errorf("providerURL(%q,%q,%q) = %q, want %q", c.provider, c.id, c.mtype, got, c.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseManualSource(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
provider string
|
|
raw string
|
|
wantProvider string
|
|
wantID string
|
|
wantErr bool
|
|
}{
|
|
{"id с провайдером", "tmdb", "60622", "tmdb", "60622", false},
|
|
{"id обрезается позже воркером", "TVDB", "269613", "tvdb", "269613", false},
|
|
{"URL TMDB со slug", "", "https://www.themoviedb.org/tv/60622-fargo", "tmdb", "60622", false},
|
|
{"URL TMDB без схемы", "", "themoviedb.org/tv/60622", "tmdb", "60622", false},
|
|
{"URL TMDB movie", "", "https://www.themoviedb.org/movie/693134", "tmdb", "693134", false},
|
|
{"URL IMDb", "", "https://www.imdb.com/title/tt0111161/", "imdb", "tt0111161", false},
|
|
{"URL TVDB dereferrer", "", "https://www.thetvdb.com/dereferrer/series/269613", "tvdb", "269613", false},
|
|
{"URL TVDB slug — без id → ошибка", "", "https://www.thetvdb.com/series/fargo", "", "", true},
|
|
{"мусорный URL → ошибка", "", "https://example.com/foo/bar", "", "", true},
|
|
{"пустой ввод → ошибка", "tmdb", " ", "", "", true},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
p, id, err := parseManualSource(c.provider, c.raw)
|
|
if c.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("ожидалась ошибка, получено %q:%q", p, id)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("неожиданная ошибка: %v", err)
|
|
}
|
|
if p != c.wantProvider || id != c.wantID {
|
|
t.Errorf("parseManualSource(%q,%q) = %q:%q, want %q:%q", c.provider, c.raw, p, id, c.wantProvider, c.wantID)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMatchURLNoLinkWhenUnbuildable — эффективный провайдер, для которого URL не
|
|
// строится и совпадающего кандидата нет, даёт пустую ссылку (транспорт покажет
|
|
// матч текстом — сценарий «URL записи неизвестен»).
|
|
func TestMatchURLNoLinkWhenUnbuildable(t *testing.T) {
|
|
rd := &worker.ReviewData{Provider: "kinopoisk", ProviderID: "42"}
|
|
if got := matchURL(rd, "movie"); got != "" {
|
|
t.Errorf("matchURL = %q, want пусто (URL не строится, кандидата нет)", got)
|
|
}
|
|
}
|
|
|
|
// TestMatchURLPrefersMatchingCandidate — URL берётся у выбранного кандидата,
|
|
// когда его provider+id совпадают с эффективными.
|
|
func TestMatchURLPrefersMatchingCandidate(t *testing.T) {
|
|
rd := &worker.ReviewData{
|
|
Provider: "tmdb", ProviderID: "693134",
|
|
Candidates: []store.MetadataCandidate{{
|
|
Provider: "tmdb", ProviderID: "693134", Chosen: true,
|
|
URL: store.NullString("https://custom.example/x"),
|
|
}},
|
|
}
|
|
if got := matchURL(rd, "movie"); got != "https://custom.example/x" {
|
|
t.Errorf("matchURL = %q, want URL выбранного кандидата", got)
|
|
}
|
|
}
|