package bazarr import ( "context" "encoding/json" "net/http" "net/http/httptest" "net/url" "testing" "time" ) func TestSearchMovieSendsRadarrIDAndKeyInHeader(t *testing.T) { upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/providers/movies" { t.Errorf("path = %q", r.URL.Path) } if got := r.Header.Get("X-API-KEY"); got != "secret" { t.Errorf("X-API-KEY = %q", got) } if got := r.URL.Query().Get("radarrid"); got != "42" { t.Errorf("radarrid = %q", got) } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`[ {"language":"en","forced":false,"hearing_impaired":"False","provider":"opensubtitles", "subtitle":"opaque-token","score":97,"release_info":["Arrival.2016.1080p"],"uploader":"someone"} ]`)) })) defer upstream.Close() subtitles, err := New(upstream.URL, "secret", time.Second).SearchMovie(context.Background(), 42) if err != nil { t.Fatal(err) } if len(subtitles) != 1 { t.Fatalf("subtitles = %+v", subtitles) } got := subtitles[0] if got.Language != "en" || got.Score != 97 || got.Token != "opaque-token" { t.Fatalf("unexpected subtitle: %+v", got) } if got.HearingImpaired { t.Error(`hearing_impaired "False" decoded as true`) } } // Bazarr has shipped both shapes for these fields across versions. Decoding either is what // stops an upgrade from silently turning every result into a plain, non-forced track. func TestSubtitleDecodesBoolAndStringFlagsAndBothLanguageShapes(t *testing.T) { for _, payload := range []struct { name string json string }{ {"strings", `{"language":"it","forced":"True","hearing_impaired":"true","subtitle":"t"}`}, {"bools", `{"language":"it","forced":true,"hearing_impaired":true,"subtitle":"t"}`}, {"language object", `{"language":{"code2":"it","name":"Italian"},"forced":true,"hearing_impaired":1,"subtitle":"t"}`}, } { t.Run(payload.name, func(t *testing.T) { var subtitle Subtitle if err := json.Unmarshal([]byte(payload.json), &subtitle); err != nil { t.Fatal(err) } if subtitle.Language != "it" { t.Errorf("language = %q", subtitle.Language) } if !subtitle.Forced { t.Error("forced = false") } // `1` is a number, not one of the accepted strings, so the last case is // deliberately allowed to be false — the point is that it decodes at all. if payload.name != "language object" && !subtitle.HearingImpaired { t.Error("hearing_impaired = false") } }) } } // The result row is opaque and has to be handed back exactly as it arrived; reconstructing // it from the parsed fields is the mistake this pins against. func TestDownloadMoviePostsTheTokenVerbatim(t *testing.T) { var form url.Values upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || r.URL.Path != "/api/providers/movies" { t.Errorf("%s %s", r.Method, r.URL.Path) } if err := r.ParseForm(); err != nil { t.Fatal(err) } form = r.PostForm w.WriteHeader(http.StatusNoContent) })) defer upstream.Close() err := New(upstream.URL, "secret", time.Second).DownloadMovie(context.Background(), 42, Subtitle{ Language: "en", Provider: "opensubtitles", Token: "opaque {token} with spaces", HearingImpaired: true, }) if err != nil { t.Fatal(err) } if got := form.Get("subtitle"); got != "opaque {token} with spaces" { t.Errorf("subtitle = %q", got) } if got := form.Get("radarrid"); got != "42" { t.Errorf("radarrid = %q", got) } if got := form.Get("hi"); got != "true" { t.Errorf("hi = %q", got) } if got := form.Get("forced"); got != "false" { t.Errorf("forced = %q", got) } } func TestDownloadEpisodeCarriesBothIDs(t *testing.T) { var form url.Values upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _ = r.ParseForm() form = r.PostForm w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(``)) })) defer upstream.Close() err := New(upstream.URL, "secret", time.Second). DownloadEpisode(context.Background(), 8, 91, Subtitle{Language: "en", Token: "t"}) if err != nil { t.Fatal(err) } if form.Get("seriesid") != "8" || form.Get("episodeid") != "91" { t.Errorf("form = %v", form) } } func TestListingsUnwrapTheDataEnvelopeAndAskForEverything(t *testing.T) { upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if got := r.URL.Query().Get("length"); got != "-1" && r.URL.Path != "/api/episodes" { t.Errorf("length = %q for %s", got, r.URL.Path) } w.Header().Set("Content-Type", "application/json") switch r.URL.Path { case "/api/movies": _, _ = w.Write([]byte(`{"data":[{"radarrId":42,"title":"Arrival","year":"2016"}]}`)) case "/api/series": _, _ = w.Write([]byte(`{"data":[{"sonarrSeriesId":8,"title":"Severance","year":"2022"}]}`)) case "/api/episodes": if got := r.URL.Query().Get("seriesid[]"); got != "8" { t.Errorf("seriesid[] = %q", got) } _, _ = w.Write([]byte(`{"data":[{"sonarrEpisodeId":91,"sonarrSeriesId":8,"season":3,"episode":4}]}`)) } })) defer upstream.Close() client := New(upstream.URL, "secret", time.Second) movies, err := client.Movies(context.Background()) if err != nil || len(movies) != 1 || movies[0].RadarrID != 42 { t.Fatalf("movies = %+v, err = %v", movies, err) } series, err := client.Series(context.Background()) if err != nil || len(series) != 1 || series[0].SonarrSeriesID != 8 { t.Fatalf("series = %+v, err = %v", series, err) } episodes, err := client.Episodes(context.Background(), 8) if err != nil || len(episodes) != 1 || episodes[0].SonarrEpisodeID != 91 { t.Fatalf("episodes = %+v, err = %v", episodes, err) } } func TestUpstreamFailureCarriesTheStatus(t *testing.T) { upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "no providers enabled", http.StatusBadRequest) })) defer upstream.Close() _, err := New(upstream.URL, "secret", time.Second).SearchMovie(context.Background(), 1) apiErr, ok := err.(*APIError) if !ok { t.Fatalf("err = %T %v", err, err) } if apiErr.StatusCode != http.StatusBadRequest { t.Errorf("status = %d", apiErr.StatusCode) } }