Files
memby/server/internal/bazarr/client_test.go
ponzischeme89andClaude Opus 5 4a4df7a73c App v0.2.26 and gateway 0.1.20
Client: seek controls, Bazarr subtitle download and cast panel in the
player; MDBList ratings strip; episode and schedule detail pages; series
pace estimate; what's new panel; install-permission onboarding step;
synced per-profile preferences; Emby outage banner.

Gateway: rebuilt admin console (one fragment per page), preference
history and restore, merged Continue Watching, Emby health probe,
subtitle selection and Bazarr download, structured request logging with
per-request identity, and embedded build version.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 22:33:56 +12:00

185 lines
6.1 KiB
Go

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