- Веб-UI: под шапкой страницы загрузки — дата добавления и относительная давность («N дней назад»), как в карточках списка. - Баг: ссылка-dereferrer TVDB для фильма вела на /series/; теперь строится по типу запроса (/movie/ либо /series/). Тест + правка спеки metadata-match. - Telegram: заголовки уведомлений (готово/ошибка/рассинхрон) берутся из display_name — консистентно с веб-UI; сезон подтягивается автоматически. - Telegram: в сообщениях об ошибке — и заголовок, и #id загрузки (ULID) для быстрого поиска по логам. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
160 lines
4.9 KiB
Go
160 lines
4.9 KiB
Go
package metadata
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"sync/atomic"
|
||
"testing"
|
||
)
|
||
|
||
// fakeTVDB — стенд v4: /login выдаёт токен, остальное требует Bearer.
|
||
func fakeTVDB(t *testing.T, logins *atomic.Int32) *httptest.Server {
|
||
t.Helper()
|
||
mux := http.NewServeMux()
|
||
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
|
||
var body map[string]string
|
||
_ = json.NewDecoder(r.Body).Decode(&body)
|
||
if body["apikey"] != "k" {
|
||
w.WriteHeader(http.StatusUnauthorized)
|
||
return
|
||
}
|
||
if logins != nil {
|
||
logins.Add(1)
|
||
}
|
||
_, _ = w.Write([]byte(`{"status":"success","data":{"token":"tok"}}`))
|
||
})
|
||
authed := func(next http.HandlerFunc) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
if r.Header.Get("Authorization") != "Bearer tok" {
|
||
w.WriteHeader(http.StatusUnauthorized)
|
||
return
|
||
}
|
||
next(w, r)
|
||
}
|
||
}
|
||
mux.HandleFunc("/search", authed(func(w http.ResponseWriter, r *http.Request) {
|
||
if r.URL.Query().Get("type") != "series" || r.URL.Query().Get("query") != "Fargo" {
|
||
t.Errorf("query = %v", r.URL.Query())
|
||
}
|
||
_, _ = w.Write([]byte(`{"data":[{"tvdb_id":"269613","name":"Fargo","year":"2014"}]}`))
|
||
}))
|
||
mux.HandleFunc("/series/269613/extended", authed(func(w http.ResponseWriter, _ *http.Request) {
|
||
_, _ = w.Write([]byte(`{"data":{"episodes":[
|
||
{"seasonNumber":1},{"seasonNumber":1},{"seasonNumber":2}
|
||
]}}`))
|
||
}))
|
||
srv := httptest.NewServer(mux)
|
||
t.Cleanup(srv.Close)
|
||
return srv
|
||
}
|
||
|
||
func newTVDB(t *testing.T, url string) *TVDB {
|
||
t.Helper()
|
||
c, err := NewTVDB(TVDBConfig{APIKey: "k", BaseURL: url}, nil)
|
||
if err != nil {
|
||
t.Fatalf("NewTVDB: %v", err)
|
||
}
|
||
return c
|
||
}
|
||
|
||
func TestTVDB_SearchAndLoginCached(t *testing.T) {
|
||
var logins atomic.Int32
|
||
srv := fakeTVDB(t, &logins)
|
||
c := newTVDB(t, srv.URL)
|
||
|
||
got, err := c.Search(context.Background(), Query{Type: Series, Title: "Fargo", Year: 2014})
|
||
if err != nil {
|
||
t.Fatalf("Search: %v", err)
|
||
}
|
||
if len(got) != 1 || got[0].ID != "269613" || got[0].Provider != "tvdb" || got[0].Year != 2014 {
|
||
t.Fatalf("candidate = %+v", got)
|
||
}
|
||
if got[0].URL != "https://www.thetvdb.com/dereferrer/series/269613" {
|
||
t.Fatalf("URL = %q", got[0].URL)
|
||
}
|
||
// Второй запрос переиспользует токен — повторного логина нет.
|
||
if _, err := c.Search(context.Background(), Query{Type: Series, Title: "Fargo"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if logins.Load() != 1 {
|
||
t.Errorf("logins = %d, want 1 (token cached)", logins.Load())
|
||
}
|
||
}
|
||
|
||
// Для фильма ссылка-dereferrer должна вести на /movie/, а не /series/.
|
||
func TestTVDB_MovieDereferrerURL(t *testing.T) {
|
||
mux := http.NewServeMux()
|
||
mux.HandleFunc("/login", func(w http.ResponseWriter, _ *http.Request) {
|
||
_, _ = w.Write([]byte(`{"data":{"token":"tok"}}`))
|
||
})
|
||
mux.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) {
|
||
if r.URL.Query().Get("type") != "movie" {
|
||
t.Errorf("type = %q, want movie", r.URL.Query().Get("type"))
|
||
}
|
||
_, _ = w.Write([]byte(`{"data":[{"tvdb_id":"3015","name":"The Last Unicorn","year":"1982"}]}`))
|
||
})
|
||
srv := httptest.NewServer(mux)
|
||
defer srv.Close()
|
||
|
||
got, err := newTVDB(t, srv.URL).Search(context.Background(), Query{Type: Movie, Title: "The Last Unicorn"})
|
||
if err != nil {
|
||
t.Fatalf("Search: %v", err)
|
||
}
|
||
if len(got) != 1 || got[0].URL != "https://www.thetvdb.com/dereferrer/movie/3015" {
|
||
t.Fatalf("candidate = %+v", got)
|
||
}
|
||
}
|
||
|
||
func TestTVDB_SeasonEpisodeCounts(t *testing.T) {
|
||
srv := fakeTVDB(t, nil)
|
||
counts, err := newTVDB(t, srv.URL).SeasonEpisodeCounts(context.Background(), "269613")
|
||
if err != nil {
|
||
t.Fatalf("SeasonEpisodeCounts: %v", err)
|
||
}
|
||
if counts[1] != 2 || counts[2] != 1 {
|
||
t.Errorf("counts = %v", counts)
|
||
}
|
||
}
|
||
|
||
func TestTVDB_ReloginOn401(t *testing.T) {
|
||
var logins atomic.Int32
|
||
var token atomic.Value
|
||
token.Store("tok")
|
||
mux := http.NewServeMux()
|
||
mux.HandleFunc("/login", func(w http.ResponseWriter, _ *http.Request) {
|
||
logins.Add(1)
|
||
_, _ = w.Write([]byte(`{"data":{"token":"tok"}}`))
|
||
})
|
||
var firstCall atomic.Bool
|
||
mux.HandleFunc("/search", func(w http.ResponseWriter, _ *http.Request) {
|
||
// Первый авторизованный запрос отдаёт 401 (токен «протух»).
|
||
if firstCall.CompareAndSwap(false, true) {
|
||
w.WriteHeader(http.StatusUnauthorized)
|
||
return
|
||
}
|
||
_, _ = w.Write([]byte(`{"data":[{"tvdb_id":"1","name":"X","year":"2000"}]}`))
|
||
})
|
||
srv := httptest.NewServer(mux)
|
||
defer srv.Close()
|
||
|
||
c := newTVDB(t, srv.URL)
|
||
got, err := c.Search(context.Background(), Query{Type: Series, Title: "X"})
|
||
if err != nil {
|
||
t.Fatalf("Search: %v", err)
|
||
}
|
||
if len(got) != 1 {
|
||
t.Fatalf("got %d", len(got))
|
||
}
|
||
if logins.Load() != 2 {
|
||
t.Errorf("logins = %d, want 2 (initial + relogin)", logins.Load())
|
||
}
|
||
}
|
||
|
||
func TestNewTVDB_RequiresKey(t *testing.T) {
|
||
if _, err := NewTVDB(TVDBConfig{}, nil); err == nil {
|
||
t.Fatal("want error without api_key")
|
||
}
|
||
}
|