package sonarr import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" ) const lookupJSON = `[ {"id":0,"title":"Breaking Bad","year":2008,"tvdbId":81189,"titleSlug":"breaking-bad","network":"AMC", "status":"ended","runtime":47,"genres":["Crime","Drama"],"seriesType":"standard", "ratings":{"votes":100,"value":9.3}, "seasons":[{"seasonNumber":0,"monitored":false},{"seasonNumber":1,"monitored":false},{"seasonNumber":2,"monitored":false}], "images":[{"coverType":"poster","remoteUrl":"https://img/p.jpg"}]}, {"id":12,"title":"Better Call Saul","year":2015,"tvdbId":273181} ]` func newTestClient(t *testing.T, h http.HandlerFunc) *Client { t.Helper() srv := httptest.NewServer(h) t.Cleanup(srv.Close) return NewClient(srv.URL, "secret") } func TestLookup(t *testing.T) { c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v3/series/lookup" || r.URL.Query().Get("term") != "breaking" { t.Errorf("unexpected request %s", r.URL) } if r.Header.Get("X-Api-Key") != "secret" { t.Error("missing api key header") } w.Write([]byte(lookupJSON)) }) series, err := c.Lookup(context.Background(), "breaking") if err != nil { t.Fatal(err) } if len(series) != 2 || series[0].InLibrary() || !series[1].InLibrary() { t.Fatalf("series = %+v", series) } if series[0].SeasonCount() != 2 { t.Errorf("SeasonCount = %d, want 2 (specials excluded)", series[0].SeasonCount()) } if series[0].Ratings.Value != 9.3 || series[0].Network != "AMC" { t.Errorf("fields not decoded: %+v", series[0]) } } func TestAddSeries(t *testing.T) { var got map[string]any c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || r.URL.Path != "/api/v3/series" { t.Errorf("unexpected request %s %s", r.Method, r.URL) } json.NewDecoder(r.Body).Decode(&got) w.WriteHeader(http.StatusCreated) w.Write([]byte(`{"id":3,"title":"Breaking Bad","tvdbId":81189}`)) }) s := Series{ Title: "Breaking Bad", Year: 2008, TVDBID: 81189, SeriesType: "standard", Seasons: []Season{{SeasonNumber: 1}, {SeasonNumber: 2}}, } created, err := c.AddSeries(context.Background(), s, AddOptions{ QualityProfileID: 4, RootFolderPath: "/tv", Monitored: true, Monitor: MonitorFirstSeason, SearchNow: true, }) if err != nil { t.Fatal(err) } if created.ID != 3 { t.Errorf("id = %d", created.ID) } if got["tvdbId"] != float64(81189) || got["qualityProfileId"] != float64(4) || got["rootFolderPath"] != "/tv" || got["monitored"] != true || got["seasonFolder"] != true { t.Errorf("request body = %v", got) } ao, _ := got["addOptions"].(map[string]any) if ao["monitor"] != "firstSeason" || ao["searchForMissingEpisodes"] != true { t.Errorf("addOptions = %v", ao) } if seasons, _ := got["seasons"].([]any); len(seasons) != 2 { t.Errorf("seasons = %v", got["seasons"]) } } func TestAddSeriesDefaultMonitor(t *testing.T) { var got map[string]any c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { json.NewDecoder(r.Body).Decode(&got) w.WriteHeader(http.StatusCreated) w.Write([]byte(`{}`)) }) if _, err := c.AddSeries(context.Background(), Series{}, AddOptions{}); err != nil { t.Fatal(err) } if ao, _ := got["addOptions"].(map[string]any); ao["monitor"] != "all" { t.Errorf("addOptions = %v", ao) } } func TestSearchSeries(t *testing.T) { var got map[string]any c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { json.NewDecoder(r.Body).Decode(&got) w.WriteHeader(http.StatusCreated) w.Write([]byte(`{}`)) }) if err := c.SearchSeries(context.Background(), 3); err != nil { t.Fatal(err) } if got["name"] != "SeriesSearch" || got["seriesId"] != float64(3) { t.Errorf("body = %v", got) } } func TestEpisodesAndReleaseQueries(t *testing.T) { var queries []string c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { queries = append(queries, r.URL.Path+"?"+r.URL.RawQuery) if r.URL.Path == "/api/v3/episode" { w.Write([]byte(`[{"id":10,"seriesId":3,"seasonNumber":1,"episodeNumber":2,"title":"Two","hasFile":true}]`)) return } w.Write([]byte(`[]`)) }) ctx := context.Background() eps, err := c.Episodes(ctx, 3) if err != nil || len(eps) != 1 || eps[0].ID != 10 || !eps[0].HasFile || eps[0].EpisodeNumber != 2 { t.Fatalf("episodes = %+v, %v", eps, err) } if _, err := c.EpisodeReleases(ctx, 10); err != nil { t.Fatal(err) } if _, err := c.SeasonReleases(ctx, 3, 1); err != nil { t.Fatal(err) } want := []string{ "/api/v3/episode?seriesId=3", "/api/v3/release?episodeId=10", "/api/v3/release?seasonNumber=1&seriesId=3", } for i := range want { if queries[i] != want[i] { t.Errorf("query %d = %q, want %q", i, queries[i], want[i]) } } } func TestEpisodeFiles(t *testing.T) { c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v3/episodefile" || r.URL.Query().Get("seriesId") != "4" { t.Errorf("unexpected request %s", r.URL) } w.Write([]byte(`[{"size":5,"quality":{"quality":{"name":"WEBDL-1080p","resolution":1080}},"mediaInfo":{"videoCodec":"x264"}}]`)) }) files, err := c.EpisodeFiles(context.Background(), 4) if err != nil || len(files) != 1 { t.Fatalf("files=%v err=%v", files, err) } if a := files[0].Attrs(); a.Resolution != 1080 || a.Codec != "x264" { t.Errorf("attrs = %+v", a) } } func TestDeleteSeries(t *testing.T) { c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete || r.URL.Path != "/api/v3/series/3" { t.Errorf("unexpected request %s %s", r.Method, r.URL) } q := r.URL.Query() if q.Get("deleteFiles") != "false" || q.Get("addImportListExclusion") != "true" { t.Errorf("query = %v", q) } }) if err := c.DeleteSeries(context.Background(), 3, false, true); err != nil { t.Fatal(err) } }