Files
jellybit/internal/metadata/tvdb_test.go
T
avandClaude Opus 4.8 280204db18 Правки: дата на стр. загрузки, ссылка TVDB по типу, заголовки в Telegram
- Веб-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>
2026-07-04 11:50:04 +03:00

160 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
}
}