package radarr import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "git.stinnesbeck.com/nils/arr-tui/internal/arr" ) const lookupJSON = `[ {"id":0,"title":"The Matrix","year":1999,"tmdbId":603,"imdbId":"tt0133093","titleSlug":"the-matrix-603", "runtime":136,"genres":["Action","Science Fiction"],"overview":"A hacker...", "images":[{"coverType":"poster","remoteUrl":"https://img/p.jpg"}], "ratings":{"imdb":{"votes":2000000,"value":8.7},"tmdb":{"votes":1,"value":8.2}}}, {"id":42,"title":"The Matrix Reloaded","year":2003,"tmdbId":604,"ratings":{"tmdb":{"votes":1,"value":7.0}}} ]` 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/movie/lookup" || r.URL.Query().Get("term") != "the matrix" { 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)) }) movies, err := c.Lookup(context.Background(), "the matrix") if err != nil { t.Fatal(err) } if len(movies) != 2 { t.Fatalf("got %d movies", len(movies)) } if movies[0].InLibrary() || !movies[1].InLibrary() { t.Error("InLibrary wrong") } if movies[0].Rating() != 8.7 || movies[1].Rating() != 7.0 { t.Errorf("ratings: %v %v", movies[0].Rating(), movies[1].Rating()) } if movies[0].Images[0].RemoteURL == "" { t.Error("image not decoded") } } func TestAddMovie(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/movie" { t.Errorf("unexpected request %s %s", r.Method, r.URL) } json.NewDecoder(r.Body).Decode(&got) w.WriteHeader(http.StatusCreated) w.Write([]byte(`{"id":7,"title":"The Matrix","tmdbId":603}`)) }) m := Movie{Title: "The Matrix", Year: 1999, TMDBID: 603, TitleSlug: "the-matrix-603"} created, err := c.AddMovie(context.Background(), m, AddOptions{ QualityProfileID: 4, RootFolderPath: "/movies", Monitored: true, SearchNow: true, }) if err != nil { t.Fatal(err) } if created.ID != 7 { t.Errorf("id = %d", created.ID) } if got["tmdbId"] != float64(603) || got["qualityProfileId"] != float64(4) || got["rootFolderPath"] != "/movies" || got["monitored"] != true || got["minimumAvailability"] != "released" { t.Errorf("request body = %v", got) } if ao, _ := got["addOptions"].(map[string]any); ao["searchForMovie"] != true { t.Errorf("addOptions = %v", got["addOptions"]) } } func TestAPIErrors(t *testing.T) { tests := map[string]struct { status int body string want string }{ "message": {http.StatusBadRequest, `{"message":"bad thing"}`, "HTTP 400: bad thing"}, "validation": {http.StatusBadRequest, `[{"propertyName":"TmdbId","errorMessage":"This movie has already been added"}]`, "HTTP 400: This movie has already been added"}, "unauth": {http.StatusUnauthorized, ``, "unexpected response: HTTP 401"}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(tc.status) w.Write([]byte(tc.body)) }) _, err := c.AddMovie(context.Background(), Movie{}, AddOptions{}) var apiErr *arr.APIError if !errors.As(err, &apiErr) || apiErr.Status != tc.status || err.Error() != tc.want { t.Fatalf("err = %v, want %q", err, tc.want) } }) } } func TestSharedEndpoints(t *testing.T) { c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/v3/system/status": w.Write([]byte(`{"appName":"Radarr","version":"5.0.0"}`)) case "/api/v3/qualityprofile": w.Write([]byte(`[{"id":1,"name":"Any"},{"id":4,"name":"HD-1080p"}]`)) case "/api/v3/rootfolder": w.Write([]byte(`[{"id":1,"path":"/movies","freeSpace":1000,"accessible":true}]`)) default: t.Errorf("unexpected %s", r.URL) } }) ctx := context.Background() if s, err := c.SystemStatus(ctx); err != nil || s.AppName != "Radarr" { t.Errorf("status = %+v, %v", s, err) } if p, err := c.QualityProfiles(ctx); err != nil || len(p) != 2 || p[1].Name != "HD-1080p" { t.Errorf("profiles = %+v, %v", p, err) } if f, err := c.RootFolders(ctx); err != nil || len(f) != 1 || f[0].Path != "/movies" { t.Errorf("folders = %+v, %v", f, err) } } func TestSearchMovie(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.SearchMovie(context.Background(), 7); err != nil { t.Fatal(err) } if got["name"] != "MoviesSearch" { t.Errorf("body = %v", got) } } func TestReleases(t *testing.T) { c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v3/release" || r.URL.Query().Get("movieId") != "7" { t.Errorf("unexpected request %s", r.URL) } w.Write([]byte(`[{"guid":"g","title":"x","approved":true}]`)) }) rels, err := c.Releases(context.Background(), 7) if err != nil || len(rels) != 1 || rels[0].GUID != "g" { t.Fatalf("releases = %+v, %v", rels, err) } } func TestDeleteMovie(t *testing.T) { c := newTestClient(t, func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete || r.URL.Path != "/api/v3/movie/12" { t.Errorf("unexpected request %s %s", r.Method, r.URL) } q := r.URL.Query() if q.Get("deleteFiles") != "true" || q.Get("addImportExclusion") != "false" { t.Errorf("query = %v", q) } }) if err := c.DeleteMovie(context.Background(), 12, true, false); err != nil { t.Fatal(err) } }