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) } } }