package lidarr import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "os" "testing" ) // newTestClient creates an httptest.Server and a Client wired to it. func newTestClient(handler http.HandlerFunc) (*Client, *httptest.Server) { srv := httptest.NewServer(handler) return &Client{BaseURL: srv.URL, APIKey: "key123", HTTP: srv.Client()}, srv } // mustReadFixture reads a file from testdata/ or fatals the test. func mustReadFixture(t *testing.T, name string) []byte { t.Helper() b, err := os.ReadFile("testdata/" + name) if err != nil { t.Fatalf("read fixture %q: %v", name, err) } return b } // --- LookupArtist --- func TestLookupArtist_HappyPath(t *testing.T) { body := mustReadFixture(t, "lookup_artist.json") c, srv := newTestClient(func(w http.ResponseWriter, r *http.Request) { if got := r.Header.Get("X-Api-Key"); got != "key123" { t.Errorf("X-Api-Key = %q, want key123", got) } if r.URL.Path != "/api/v1/artist/lookup" { t.Errorf("path = %q, want /api/v1/artist/lookup", r.URL.Path) } if got := r.URL.Query().Get("term"); got != "boards" { t.Errorf("term = %q, want boards", got) } _, _ = w.Write(body) }) defer srv.Close() got, err := c.LookupArtist(context.Background(), "boards") if err != nil { t.Fatalf("LookupArtist: %v", err) } if len(got) != 2 { t.Fatalf("len = %d, want 2", len(got)) } // First result — rich data. if got[0].MBID != "069b64b6-7884-4f6a-94cc-e4c1d6c87a01" { t.Errorf("MBID = %q", got[0].MBID) } if got[0].Name != "Boards of Canada" { t.Errorf("Name = %q", got[0].Name) } if got[0].Secondary != "Electronic · 18 albums" { t.Errorf("Secondary = %q, want %q", got[0].Secondary, "Electronic · 18 albums") } if got[0].ImageURL != "https://example.invalid/boc.jpg" { t.Errorf("ImageURL = %q", got[0].ImageURL) } // Second result — empty genres and zero album count → empty secondary. if got[1].Secondary != "" { t.Errorf("expected empty secondary for no genres + 0 albums; got %q", got[1].Secondary) } if got[1].ImageURL != "" { t.Errorf("expected empty ImageURL; got %q", got[1].ImageURL) } } func TestLookupArtist_AuthFailed(t *testing.T) { c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnauthorized) }) defer srv.Close() _, err := c.LookupArtist(context.Background(), "boards") if !errors.Is(err, ErrAuthFailed) { t.Fatalf("err = %v, want ErrAuthFailed", err) } } func TestLookupArtist_Forbidden(t *testing.T) { c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusForbidden) }) defer srv.Close() _, err := c.LookupArtist(context.Background(), "boards") if !errors.Is(err, ErrAuthFailed) { t.Fatalf("err = %v, want ErrAuthFailed (403 must map to auth)", err) } } func TestLookupArtist_ServerError(t *testing.T) { c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) }) defer srv.Close() _, err := c.LookupArtist(context.Background(), "boards") if !errors.Is(err, ErrServerError) { t.Fatalf("err = %v, want ErrServerError", err) } } func TestLookupArtist_Unreachable(t *testing.T) { c := &Client{BaseURL: "http://127.0.0.1:1", APIKey: "x", HTTP: &http.Client{}} _, err := c.LookupArtist(context.Background(), "boards") if !errors.Is(err, ErrUnreachable) { t.Fatalf("err = %v, want ErrUnreachable", err) } } func TestLookupArtist_BadJSON(t *testing.T) { c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(`not json`)) }) defer srv.Close() _, err := c.LookupArtist(context.Background(), "boards") if !errors.Is(err, ErrInvalidPayload) { t.Fatalf("err = %v, want ErrInvalidPayload", err) } } // --- LookupAlbum --- func TestLookupAlbum_HappyPath(t *testing.T) { body := mustReadFixture(t, "lookup_album.json") c, srv := newTestClient(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v1/album/lookup" { t.Errorf("path = %q, want /api/v1/album/lookup", r.URL.Path) } if got := r.URL.Query().Get("term"); got != "geogaddi" { t.Errorf("term = %q, want geogaddi", got) } _, _ = w.Write(body) }) defer srv.Close() got, err := c.LookupAlbum(context.Background(), "geogaddi") if err != nil { t.Fatalf("LookupAlbum: %v", err) } if len(got) != 2 { t.Fatalf("len = %d, want 2", len(got)) } // First result: year from releaseDate, trackCount in secondary. if got[0].MBID != "a1b2c3d4-e5f6-7890-abcd-ef1234567890" { t.Errorf("MBID = %q", got[0].MBID) } if got[0].ArtistMBID != "069b64b6-7884-4f6a-94cc-e4c1d6c87a01" { t.Errorf("ArtistMBID = %q, want 069b64b6-7884-4f6a-94cc-e4c1d6c87a01", got[0].ArtistMBID) } if got[0].AlbumMBID != "" { t.Errorf("AlbumMBID = %q, want empty on album result", got[0].AlbumMBID) } if got[0].Name != "Music Has the Right to Children" { t.Errorf("Name = %q", got[0].Name) } want0 := "Boards of Canada · 1998 · 18 tracks" if got[0].Secondary != want0 { t.Errorf("Secondary = %q, want %q", got[0].Secondary, want0) } // Poster image from second image entry. if got[0].ImageURL != "https://example.invalid/mhtrtc-poster.jpg" { t.Errorf("ImageURL = %q", got[0].ImageURL) } // Second result. want1 := "Boards of Canada · 2002 · 23 tracks" if got[1].Secondary != want1 { t.Errorf("Secondary = %q, want %q", got[1].Secondary, want1) } if got[1].ImageURL != "" { t.Errorf("expected empty ImageURL; got %q", got[1].ImageURL) } } func TestLookupAlbum_BadJSON(t *testing.T) { c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(`{}`)) }) defer srv.Close() _, err := c.LookupAlbum(context.Background(), "x") if !errors.Is(err, ErrInvalidPayload) { t.Fatalf("err = %v, want ErrInvalidPayload (object, not array)", err) } } // --- LookupTrack --- func TestLookupTrack_HappyPath(t *testing.T) { body := mustReadFixture(t, "lookup_track.json") c, srv := newTestClient(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v1/track/lookup" { t.Errorf("path = %q, want /api/v1/track/lookup", r.URL.Path) } _, _ = w.Write(body) }) defer srv.Close() got, err := c.LookupTrack(context.Background(), "roygbiv") if err != nil { t.Fatalf("LookupTrack: %v", err) } if len(got) != 2 { t.Fatalf("len = %d, want 2", len(got)) } if got[0].Name != "Roygbiv" { t.Errorf("Name = %q", got[0].Name) } want := "Music Has the Right to Children · Boards of Canada" if got[0].Secondary != want { t.Errorf("Secondary = %q, want %q", got[0].Secondary, want) } if got[0].ArtistMBID != "069b64b6-7884-4f6a-94cc-e4c1d6c87a01" { t.Errorf("ArtistMBID = %q, want 069b64b6-7884-4f6a-94cc-e4c1d6c87a01", got[0].ArtistMBID) } if got[0].AlbumMBID != "a1b2c3d4-e5f6-7890-abcd-ef1234567890" { t.Errorf("AlbumMBID = %q, want a1b2c3d4-e5f6-7890-abcd-ef1234567890", got[0].AlbumMBID) } if got[0].ImageURL != "" { t.Errorf("tracks carry no images; ImageURL = %q", got[0].ImageURL) } } func TestLookupTrack_NoArtistName(t *testing.T) { // Verify secondary degrades gracefully when artistName is absent. c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(`[{"foreignTrackId":"abc","title":"X","albumTitle":"AlbumY","artistName":""}]`)) }) defer srv.Close() got, err := c.LookupTrack(context.Background(), "x") if err != nil { t.Fatalf("err: %v", err) } if got[0].Secondary != "AlbumY" { t.Errorf("Secondary = %q, want %q", got[0].Secondary, "AlbumY") } } // --- AddArtist --- func TestAddArtist_PostsCorrectBody(t *testing.T) { var decoded map[string]any c, srv := newTestClient(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("method = %q, want POST", r.Method) } if r.URL.Path != "/api/v1/artist" { t.Errorf("path = %q, want /api/v1/artist", r.URL.Path) } if ct := r.Header.Get("Content-Type"); ct != "application/json" { t.Errorf("Content-Type = %q, want application/json", ct) } if err := json.NewDecoder(r.Body).Decode(&decoded); err != nil { t.Errorf("decode body: %v", err) } w.WriteHeader(http.StatusCreated) }) defer srv.Close() err := c.AddArtist(context.Background(), AddArtistParams{ ForeignArtistID: "069b64b6-7884-4f6a-94cc-e4c1d6c87a01", QualityProfileID: 2, RootFolderPath: "/music", MonitorAll: true, }) if err != nil { t.Fatalf("AddArtist: %v", err) } if decoded["foreignArtistId"] != "069b64b6-7884-4f6a-94cc-e4c1d6c87a01" { t.Errorf("foreignArtistId = %v", decoded["foreignArtistId"]) } if decoded["qualityProfileId"] != float64(2) { t.Errorf("qualityProfileId = %v", decoded["qualityProfileId"]) } if decoded["rootFolderPath"] != "/music" { t.Errorf("rootFolderPath = %v", decoded["rootFolderPath"]) } if decoded["monitored"] != true { t.Errorf("monitored = %v, want true", decoded["monitored"]) } if decoded["monitor"] != "all" { t.Errorf("monitor = %v, want all (MonitorAll=true)", decoded["monitor"]) } opts, ok := decoded["addOptions"].(map[string]any) if !ok { t.Fatalf("addOptions missing or wrong type: %T", decoded["addOptions"]) } if opts["searchForMissingAlbums"] != true { t.Errorf("addOptions.searchForMissingAlbums = %v, want true", opts["searchForMissingAlbums"]) } } func TestAddArtist_MonitorFuture(t *testing.T) { var decoded map[string]any c, srv := newTestClient(func(w http.ResponseWriter, r *http.Request) { _ = json.NewDecoder(r.Body).Decode(&decoded) w.WriteHeader(http.StatusCreated) }) defer srv.Close() _ = c.AddArtist(context.Background(), AddArtistParams{MonitorAll: false}) if decoded["monitor"] != "future" { t.Errorf("monitor = %v, want future (MonitorAll=false)", decoded["monitor"]) } } func TestAddArtist_ServerError(t *testing.T) { c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) }) defer srv.Close() err := c.AddArtist(context.Background(), AddArtistParams{}) if !errors.Is(err, ErrServerError) { t.Fatalf("err = %v, want ErrServerError", err) } } // --- AddAlbum --- func TestAddAlbum_PostsCorrectBody(t *testing.T) { var decoded map[string]any c, srv := newTestClient(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("method = %q, want POST", r.Method) } if r.URL.Path != "/api/v1/album" { t.Errorf("path = %q, want /api/v1/album", r.URL.Path) } _ = json.NewDecoder(r.Body).Decode(&decoded) w.WriteHeader(http.StatusCreated) }) defer srv.Close() err := c.AddAlbum(context.Background(), AddAlbumParams{ ForeignAlbumID: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", ForeignArtistID: "069b64b6-7884-4f6a-94cc-e4c1d6c87a01", QualityProfileID: 1, RootFolderPath: "/music", }) if err != nil { t.Fatalf("AddAlbum: %v", err) } if decoded["foreignAlbumId"] != "a1b2c3d4-e5f6-7890-abcd-ef1234567890" { t.Errorf("foreignAlbumId = %v", decoded["foreignAlbumId"]) } if decoded["foreignArtistId"] != "069b64b6-7884-4f6a-94cc-e4c1d6c87a01" { t.Errorf("foreignArtistId = %v", decoded["foreignArtistId"]) } if decoded["monitored"] != true { t.Errorf("monitored = %v, want true", decoded["monitored"]) } opts, ok := decoded["addOptions"].(map[string]any) if !ok { t.Fatalf("addOptions missing or wrong type: %T", decoded["addOptions"]) } if opts["searchForNewAlbum"] != true { t.Errorf("addOptions.searchForNewAlbum = %v, want true", opts["searchForNewAlbum"]) } } func TestAddAlbum_AuthFailed(t *testing.T) { c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnauthorized) }) defer srv.Close() err := c.AddAlbum(context.Background(), AddAlbumParams{}) if !errors.Is(err, ErrAuthFailed) { t.Fatalf("err = %v, want ErrAuthFailed", err) } } // --- ListQualityProfiles --- func TestListQualityProfiles_HappyPath(t *testing.T) { body := mustReadFixture(t, "quality_profiles.json") c, srv := newTestClient(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v1/qualityprofile" { t.Errorf("path = %q, want /api/v1/qualityprofile", r.URL.Path) } _, _ = w.Write(body) }) defer srv.Close() got, err := c.ListQualityProfiles(context.Background()) if err != nil { t.Fatalf("ListQualityProfiles: %v", err) } if len(got) != 3 { t.Fatalf("len = %d, want 3", len(got)) } if got[0].ID != 1 || got[0].Name != "Any" { t.Errorf("first profile = %+v, want {ID:1 Name:Any}", got[0]) } if got[1].ID != 2 || got[1].Name != "Lossless" { t.Errorf("second profile = %+v", got[1]) } } func TestListQualityProfiles_BadJSON(t *testing.T) { c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(`not json`)) }) defer srv.Close() _, err := c.ListQualityProfiles(context.Background()) if !errors.Is(err, ErrInvalidPayload) { t.Fatalf("err = %v, want ErrInvalidPayload", err) } } // --- LookupArtist (additional) --- func TestLookupArtist_LookupFailed(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) })) defer srv.Close() c := &Client{BaseURL: srv.URL, APIKey: "x", HTTP: srv.Client()} _, err := c.LookupArtist(context.Background(), "boards") if !errors.Is(err, ErrLookupFailed) { t.Fatalf("err = %v, want ErrLookupFailed", err) } } func TestGet_BaseURLWithTrailingSlash(t *testing.T) { body, _ := os.ReadFile("testdata/lookup_artist.json") srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Verify path is /api/v1/artist/lookup, NOT //api/v1/artist/lookup if r.URL.Path != "/api/v1/artist/lookup" { t.Errorf("got path %q; trailing-slash BaseURL produced double-slash", r.URL.Path) } _, _ = w.Write(body) })) defer srv.Close() c := &Client{BaseURL: srv.URL + "/", APIKey: "k", HTTP: srv.Client()} _, err := c.LookupArtist(context.Background(), "boards") if err != nil { t.Fatalf("LookupArtist: %v", err) } } // --- AddArtist (additional) --- func TestAddArtist_PostFailed(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnprocessableEntity) })) defer srv.Close() c := &Client{BaseURL: srv.URL, APIKey: "x", HTTP: srv.Client()} err := c.AddArtist(context.Background(), AddArtistParams{ForeignArtistID: "x", QualityProfileID: 1, RootFolderPath: "/m"}) if !errors.Is(err, ErrLookupFailed) { t.Fatalf("err = %v, want ErrLookupFailed", err) } } // --- ListRootFolders --- func TestListRootFolders_HappyPath(t *testing.T) { body := mustReadFixture(t, "root_folders.json") c, srv := newTestClient(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v1/rootfolder" { t.Errorf("path = %q, want /api/v1/rootfolder", r.URL.Path) } _, _ = w.Write(body) }) defer srv.Close() got, err := c.ListRootFolders(context.Background()) if err != nil { t.Fatalf("ListRootFolders: %v", err) } if len(got) != 3 { t.Fatalf("len = %d, want 3", len(got)) } if got[0].Path != "/music" { t.Errorf("Path = %q", got[0].Path) } if !got[0].Accessible { t.Errorf("Accessible = false for /music, want true") } if got[0].FreeSpace != 107374182400 { t.Errorf("FreeSpace = %d", got[0].FreeSpace) } if got[2].Accessible { t.Errorf("expected /music-offline to be inaccessible") } } func TestListRootFolders_NullBody(t *testing.T) { c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(`null`)) }) defer srv.Close() // null decodes to a nil slice, no error — but len check should yield 0. got, err := c.ListRootFolders(context.Background()) if err != nil { t.Fatalf("unexpected error on null body: %v", err) } if len(got) != 0 { t.Errorf("expected empty slice from null body, got %d", len(got)) } } func TestListRootFolders_BadJSON(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("{not valid json")) })) defer srv.Close() c := &Client{BaseURL: srv.URL, APIKey: "x", HTTP: srv.Client()} _, err := c.ListRootFolders(context.Background()) if !errors.Is(err, ErrInvalidPayload) { t.Fatalf("err = %v, want ErrInvalidPayload", err) } } // --- Ping --- func TestPing_ReturnsVersion(t *testing.T) { c, srv := newTestClient(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v1/system/status" { t.Errorf("path = %q, want /api/v1/system/status", r.URL.Path) } _, _ = w.Write([]byte(`{"version":"2.4.3.3795","buildTime":"2024-01-15T00:00:00Z"}`)) }) defer srv.Close() res, err := c.Ping(context.Background()) if err != nil { t.Fatalf("Ping: %v", err) } if res.Version != "2.4.3.3795" { t.Errorf("Version = %q, want 2.4.3.3795", res.Version) } } func TestPing_AuthFailed(t *testing.T) { c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnauthorized) }) defer srv.Close() _, err := c.Ping(context.Background()) if !errors.Is(err, ErrAuthFailed) { t.Fatalf("err = %v, want ErrAuthFailed", err) } } func TestPing_Unreachable(t *testing.T) { c := &Client{BaseURL: "http://127.0.0.1:1", APIKey: "x", HTTP: &http.Client{}} _, err := c.Ping(context.Background()) if !errors.Is(err, ErrUnreachable) { t.Fatalf("err = %v, want ErrUnreachable", err) } } func TestPing_BadJSON(t *testing.T) { c, srv := newTestClient(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(`{bad`)) }) defer srv.Close() _, err := c.Ping(context.Background()) if !errors.Is(err, ErrInvalidPayload) { t.Fatalf("err = %v, want ErrInvalidPayload", err) } } // --- pickPosterImage --- func TestPickPosterImage_PreferRemoteURL(t *testing.T) { imgs := []lidarrImage{ {CoverType: "poster", RemoteURL: "https://remote.example/img.jpg", URL: "https://local.example/img.jpg"}, } got := pickPosterImage(imgs) if got != "https://remote.example/img.jpg" { t.Errorf("got %q, want remote URL", got) } } func TestPickPosterImage_FallsBackToLocalURL(t *testing.T) { imgs := []lidarrImage{ {CoverType: "poster", RemoteURL: "", URL: "https://local.example/img.jpg"}, } got := pickPosterImage(imgs) if got != "https://local.example/img.jpg" { t.Errorf("got %q, want local URL", got) } } func TestPickPosterImage_SkipsNonPoster(t *testing.T) { imgs := []lidarrImage{ {CoverType: "banner", RemoteURL: "https://banner.example/img.jpg"}, {CoverType: "cover", RemoteURL: "https://cover.example/img.jpg"}, } got := pickPosterImage(imgs) if got != "" { t.Errorf("got %q, want empty (no poster)", got) } } // --- UnmonitorTrack --- // unmonitorMux is the three-endpoint stub UnmonitorTrack walks. The flags it // captures are inspected by the individual tests. type unmonitorMux struct { t *testing.T wantAlbumMbid string albumStatus int // override status for album lookup; 0 → 200 with body tracksStatus int // override status for tracks list; 0 → 200 with body putStatus int // override status for PUT /track/monitor; 0 → 200 albumBody []byte // when albumStatus is 0 tracksBody []byte // when tracksStatus is 0 putBody map[string]any putHit bool } func (m *unmonitorMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/v1/album": if got := r.URL.Query().Get("foreignAlbumId"); got != m.wantAlbumMbid { m.t.Errorf("album lookup foreignAlbumId = %q, want %q", got, m.wantAlbumMbid) } if m.albumStatus != 0 { w.WriteHeader(m.albumStatus) return } _, _ = w.Write(m.albumBody) case "/api/v1/track": if got := r.URL.Query().Get("albumId"); got != "42" { m.t.Errorf("track list albumId = %q, want 42", got) } if m.tracksStatus != 0 { w.WriteHeader(m.tracksStatus) return } _, _ = w.Write(m.tracksBody) case "/api/v1/track/monitor": if r.Method != http.MethodPut { m.t.Errorf("monitor method = %q, want PUT", r.Method) } m.putHit = true if m.putStatus != 0 { w.WriteHeader(m.putStatus) return } _ = json.NewDecoder(r.Body).Decode(&m.putBody) w.WriteHeader(http.StatusAccepted) default: m.t.Errorf("unexpected path: %s", r.URL.Path) w.WriteHeader(http.StatusNotFound) } } func TestUnmonitorTrack_HappyPath(t *testing.T) { mux := &unmonitorMux{ t: t, wantAlbumMbid: "album-mbid", albumBody: []byte(`[{"id":42,"foreignAlbumId":"album-mbid","title":"X","artistId":7}]`), tracksBody: []byte(`[{"id":1001,"foreignTrackId":"other-mbid"},{"id":1002,"foreignTrackId":"track-mbid"}]`), } srv := httptest.NewServer(mux) defer srv.Close() c := &Client{BaseURL: srv.URL, APIKey: "k", HTTP: srv.Client()} if err := c.UnmonitorTrack(context.Background(), "track-mbid", "album-mbid"); err != nil { t.Fatalf("UnmonitorTrack: %v", err) } if !mux.putHit { t.Fatal("PUT /api/v1/track/monitor was not called") } if mux.putBody["monitored"] != false { t.Errorf("PUT body monitored = %v, want false", mux.putBody["monitored"]) } ids, ok := mux.putBody["trackIds"].([]any) if !ok || len(ids) != 1 { t.Fatalf("PUT body trackIds = %v, want [1002]", mux.putBody["trackIds"]) } if ids[0] != float64(1002) { t.Errorf("PUT trackIds[0] = %v, want 1002 (the matching foreignTrackId)", ids[0]) } } func TestUnmonitorTrack_EmptyMbid(t *testing.T) { c := &Client{BaseURL: "http://127.0.0.1:1", APIKey: "k", HTTP: &http.Client{}} if err := c.UnmonitorTrack(context.Background(), "", "album-mbid"); err == nil { t.Error("UnmonitorTrack with empty trackMbid: want error, got nil") } if err := c.UnmonitorTrack(context.Background(), "track-mbid", ""); err == nil { t.Error("UnmonitorTrack with empty albumMbid: want error, got nil") } } func TestUnmonitorTrack_AlbumNotInLidarr(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v1/album" { t.Errorf("unexpected path %q", r.URL.Path) } _, _ = w.Write([]byte(`[]`)) })) defer srv.Close() c := &Client{BaseURL: srv.URL, APIKey: "k", HTTP: srv.Client()} err := c.UnmonitorTrack(context.Background(), "track-mbid", "album-mbid") if !errors.Is(err, ErrNotFound) { t.Fatalf("err = %v, want ErrNotFound", err) } } func TestUnmonitorTrack_TrackNotInAlbum(t *testing.T) { mux := &unmonitorMux{ t: t, wantAlbumMbid: "album-mbid", albumBody: []byte(`[{"id":42,"foreignAlbumId":"album-mbid","title":"X","artistId":7}]`), // No track with foreignTrackId="track-mbid" tracksBody: []byte(`[{"id":1001,"foreignTrackId":"other-mbid"}]`), } srv := httptest.NewServer(mux) defer srv.Close() c := &Client{BaseURL: srv.URL, APIKey: "k", HTTP: srv.Client()} err := c.UnmonitorTrack(context.Background(), "track-mbid", "album-mbid") if !errors.Is(err, ErrNotFound) { t.Fatalf("err = %v, want ErrNotFound", err) } if mux.putHit { t.Error("PUT was called even though track wasn't found") } } func TestUnmonitorTrack_ServerError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) })) defer srv.Close() c := &Client{BaseURL: srv.URL, APIKey: "k", HTTP: srv.Client()} err := c.UnmonitorTrack(context.Background(), "track-mbid", "album-mbid") if !errors.Is(err, ErrServerError) { t.Fatalf("err = %v, want ErrServerError", err) } } func TestUnmonitorTrack_AuthFailed(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnauthorized) })) defer srv.Close() c := &Client{BaseURL: srv.URL, APIKey: "k", HTTP: srv.Client()} err := c.UnmonitorTrack(context.Background(), "track-mbid", "album-mbid") if !errors.Is(err, ErrAuthFailed) { t.Fatalf("err = %v, want ErrAuthFailed", err) } } func TestUnmonitorTrack_Unreachable(t *testing.T) { c := &Client{BaseURL: "http://127.0.0.1:1", APIKey: "k", HTTP: &http.Client{}} err := c.UnmonitorTrack(context.Background(), "track-mbid", "album-mbid") if !errors.Is(err, ErrUnreachable) { t.Fatalf("err = %v, want ErrUnreachable", err) } } func TestUnmonitorTrack_PutServerError(t *testing.T) { mux := &unmonitorMux{ t: t, wantAlbumMbid: "album-mbid", albumBody: []byte(`[{"id":42,"foreignAlbumId":"album-mbid","title":"X","artistId":7}]`), tracksBody: []byte(`[{"id":1002,"foreignTrackId":"track-mbid"}]`), putStatus: http.StatusInternalServerError, } srv := httptest.NewServer(mux) defer srv.Close() c := &Client{BaseURL: srv.URL, APIKey: "k", HTTP: srv.Client()} err := c.UnmonitorTrack(context.Background(), "track-mbid", "album-mbid") if !errors.Is(err, ErrServerError) { t.Fatalf("err = %v, want ErrServerError on PUT 5xx", err) } }