Files
2026-08-09 08:25:50 +12:00

178 lines
5.9 KiB
Go

package opensubtitles
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestSearchParamsPrefersItsOwnIDOverTheParents(t *testing.T) {
params := searchParams(Query{
IMDBID: "tt0903747", ParentIMDBID: "tt0999999", Season: 1, Episode: 2,
Languages: []string{"it", "en", "en"}, Type: "episode",
})
if got := params.Get("imdb_id"); got != "0903747" {
t.Fatalf("imdb_id = %q, want the tt stripped", got)
}
if params.Has("parent_imdb_id") {
t.Fatal("parent_imdb_id was sent alongside the episode's own id")
}
if got := params.Get("season_number"); got != "1" {
t.Fatalf("season_number = %q", got)
}
// Sorted and deduplicated: the API rejects the list in any other shape.
if got := params.Get("languages"); got != "en,it" {
t.Fatalf("languages = %q, want %q", got, "en,it")
}
}
func TestSearchParamsFallsBackToTheSeriesForAnEpisode(t *testing.T) {
params := searchParams(Query{ParentIMDBID: "tt0903747", Season: 5, Episode: 14})
if got := params.Get("parent_imdb_id"); got != "0903747" {
t.Fatalf("parent_imdb_id = %q", got)
}
if got := params.Get("episode_number"); got != "14" {
t.Fatalf("episode_number = %q", got)
}
}
// A film must never be sent a season number: an id plus a season is how a search comes
// back empty for a title that plainly exists.
func TestSearchParamsSendsNoSeasonForAFilm(t *testing.T) {
params := searchParams(Query{TMDBID: "550", Type: "movie"})
if params.Has("season_number") || params.Has("episode_number") {
t.Fatal("a film was searched for with episode numbers")
}
if got := params.Get("tmdb_id"); got != "550" {
t.Fatalf("tmdb_id = %q", got)
}
}
func TestSearchParamsRefusesAQueryItCannotIdentify(t *testing.T) {
if params := searchParams(Query{Languages: []string{"en"}}); len(params) != 0 {
t.Fatalf("searchParams answered %v for a query with no identity", params)
}
}
func TestSearchDropsRowsWithNoFileToFetch(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Api-Key") != "key" {
t.Errorf("api key header = %q", r.Header.Get("Api-Key"))
}
_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{
map[string]any{"attributes": map[string]any{
"language": "en", "ratings": 8.5, "download_count": 120,
"foreign_parts_only": true, "release": "BluRay",
"files": []any{map[string]any{"file_id": 42, "file_name": "x.srt"}},
}},
map[string]any{"attributes": map[string]any{"language": "it", "files": []any{}}},
}})
}))
defer server.Close()
client := New("key", "Memby/test", "", "", 5*time.Second)
client.SetBaseURL(server.URL)
found, err := client.Search(context.Background(), Query{IMDBID: "tt1", Languages: []string{"en"}})
if err != nil {
t.Fatalf("Search: %v", err)
}
if len(found) != 1 {
t.Fatalf("got %d candidates, want the one with a file", len(found))
}
if found[0].FileID != 42 || !found[0].Forced || found[0].Downloads != 120 {
t.Fatalf("candidate decoded as %+v", found[0])
}
}
func TestDownloadFollowsTheLinkWithoutTheCredentials(t *testing.T) {
var files *httptest.Server
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/login":
_ = json.NewEncoder(w).Encode(map[string]string{"token": "jwt"})
case "/download":
if r.Header.Get("Authorization") != "Bearer jwt" {
t.Errorf("download authorization = %q", r.Header.Get("Authorization"))
}
_ = json.NewEncoder(w).Encode(map[string]any{
"link": files.URL + "/f.srt", "file_name": "f.srt", "remaining": 19,
})
default:
t.Errorf("unexpected path %s", r.URL.Path)
}
}))
defer api.Close()
files = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// The CDN is not the API: neither the key nor the token belongs on this request.
if r.Header.Get("Api-Key") != "" || r.Header.Get("Authorization") != "" {
t.Error("credentials were sent to the download host")
}
_, _ = w.Write([]byte("1\n00:00:01,000 --> 00:00:02,000\nhello\n"))
}))
defer files.Close()
client := New("key", "Memby/test", "someone", "secret", 5*time.Second)
client.SetBaseURL(api.URL)
name, content, err := client.Download(context.Background(), 42)
if err != nil {
t.Fatalf("Download: %v", err)
}
if name != "f.srt" || len(content) == 0 {
t.Fatalf("Download returned %q / %d bytes", name, len(content))
}
}
// The allowance running out is the one failure a viewer can act on, so it must not be
// flattened into "the provider did not answer".
func TestDownloadReportsAnExhaustedQuota(t *testing.T) {
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotAcceptable)
_, _ = w.Write([]byte(`{"message":"quota exceeded"}`))
}))
defer api.Close()
client := New("key", "Memby/test", "", "", 5*time.Second)
client.SetBaseURL(api.URL)
_, _, err := client.Download(context.Background(), 7)
if _, ok := err.(*QuotaError); !ok {
t.Fatalf("Download error = %v, want a QuotaError", err)
}
}
func TestLoginTokenIsFetchedOnce(t *testing.T) {
logins := 0
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/login" {
logins++
_ = json.NewEncoder(w).Encode(map[string]string{"token": "jwt"})
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{}})
}))
defer api.Close()
client := New("key", "Memby/test", "someone", "secret", 5*time.Second)
client.SetBaseURL(api.URL)
for range 3 {
if _, err := client.Search(context.Background(), Query{IMDBID: "tt1"}); err != nil {
t.Fatalf("Search: %v", err)
}
}
if logins != 1 {
t.Fatalf("logged in %d times, want once", logins)
}
}
func TestFormatFromNameFallsBackToSubRip(t *testing.T) {
for name, want := range map[string]string{
"a.srt": "srt", "b.VTT": "vtt", "c.ass": "ass", "d": "srt", "e.zip": "srt",
} {
if got := formatFromName(name); got != want {
t.Errorf("formatFromName(%q) = %q, want %q", name, got, want)
}
}
}