package musicbrainz import ( "context" "errors" "fmt" "net/http" "net/http/httptest" "strconv" "sync/atomic" "testing" "time" ) // testClient talks to srv with no pacing and fast retries. func testClient(srv *httptest.Server) *Client { return &Client{BaseURL: srv.URL, Pace: &Pacer{}, Backoff: time.Millisecond} } func TestEscape_KeepsWhatWasTyped(t *testing.T) { for in, want := range map[string]string{ "Radiohead": "Radiohead", "AC/DC": `AC\/DC`, "Sunn O)))": `Sunn O\)\)\)`, `"Weird" Al`: `\"Weird\" Al`, } { if got := Escape(in); got != want { t.Errorf("Escape(%q) = %q, want %q", in, got, want) } } } func TestSearchReleaseGroups_AsksTitleAndArtistAndReadsCredits(t *testing.T) { var query, agent atomic.Value srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { query.Store(r.URL.Query().Get("query")) agent.Store(r.Header.Get("User-Agent")) _, _ = w.Write([]byte(`{"release-groups": [{ "id": "rg1", "title": "OK Computer", "primary-type": "Album", "secondary-types": [], "first-release-date": "1997-05-21", "artist-credit": [{"name": "Radiohead", "joinphrase": "", "artist": {"id": "a1", "name": "Radiohead"}}] }]}`)) })) defer srv.Close() got, err := testClient(srv).SearchReleaseGroups(context.Background(), "ok computer", 10) if err != nil { t.Fatal(err) } if q := query.Load(); q != "releasegroup:(ok computer) OR artist:(ok computer)" { t.Errorf("query = %q", q) } if agent.Load() != UserAgent { t.Errorf("User-Agent = %q", agent.Load()) } if len(got) != 1 || got[0].MBID != "rg1" || got[0].Year() != "1997" || got[0].ArtistCredit != "Radiohead" || got[0].ArtistMBIDs[0] != "a1" { t.Errorf("got %+v", got) } } func TestSearchRecordings_FilesASongUnderItsAlbum(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(`{"recordings": [{ "id": "rec1", "title": "Karma Police", "length": 264000, "artist-credit": [{"name": "Radiohead", "artist": {"id": "a1"}}], "releases": [ {"id": "r1", "status": "Official", "release-group": {"id": "rg-live", "title": "Live", "primary-type": "Album", "secondary-types": ["Live"]}}, {"id": "r2", "status": "Official", "release-group": {"id": "rg1", "title": "OK Computer", "primary-type": "Album"}} ] }]}`)) })) defer srv.Close() got, err := testClient(srv).SearchRecordings(context.Background(), "karma police", 5) if err != nil { t.Fatal(err) } if len(got) != 1 || got[0].ReleaseGroup.MBID != "rg1" || got[0].LengthMs != 264000 { t.Errorf("got %+v, want it filed under the studio album", got) } } func TestArtistReleaseGroups_PagesUntilTheCount(t *testing.T) { var calls atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1) offset, _ := strconv.Atoi(r.URL.Query().Get("offset")) if r.URL.Query().Get("artist") != "a1" { t.Errorf("artist = %q", r.URL.Query().Get("artist")) } // 3 in all, two per page. var items string for i := offset; i < offset+2 && i < 3; i++ { if items != "" { items += "," } items += fmt.Sprintf(`{"id": "rg%d", "title": "T%d", "primary-type": "Album"}`, i, i) } _, _ = fmt.Fprintf(w, `{"release-group-count": 3, "release-groups": [%s]}`, items) })) defer srv.Close() got, err := testClient(srv).ArtistReleaseGroups(context.Background(), "a1") if err != nil { t.Fatal(err) } if len(got) != 3 || calls.Load() != 2 { t.Errorf("got %d groups in %d calls, want 3 in 2", len(got), calls.Load()) } } func TestReleaseGroupReleases_ReadsMediaAndTracks(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("inc") != "media recordings" { t.Errorf("inc = %q", r.URL.Query().Get("inc")) } _, _ = w.Write([]byte(`{"release-count": 1, "releases": [{ "id": "r1", "title": "OK Computer", "status": "Official", "date": "1997-06-16", "country": "GB", "media": [ {"position": 1, "format": "CD", "tracks": [ {"position": 1, "number": "1", "title": "Airbag", "length": 284000, "recording": {"id": "rec-a"}}, {"position": 2, "number": "2", "title": "Paranoid Android", "length": null, "recording": {"id": "rec-b", "length": 383000}} ]}, {"position": 2, "format": "CD", "tracks": []} ] }]}`)) })) defer srv.Close() got, err := testClient(srv).ReleaseGroupReleases(context.Background(), "rg1") if err != nil { t.Fatal(err) } if len(got) != 1 { t.Fatalf("got %+v", got) } r := got[0] if r.TrackCount() != 2 || r.Formats() != "2×CD" { t.Errorf("tracks %d, formats %q", r.TrackCount(), r.Formats()) } if tr := r.Media[0].Tracks[1]; tr.LengthMs != 383000 || tr.RecordingMBID != "rec-b" { t.Errorf("a track with no length of its own should take the recording's: %+v", tr) } } func TestGet_ThrottleIsRetriedThenUnreachable(t *testing.T) { var calls atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { if calls.Add(1) == 1 { w.WriteHeader(http.StatusServiceUnavailable) return } _, _ = w.Write([]byte(`{"artists": [{"id": "a1", "name": "Radiohead"}]}`)) })) defer srv.Close() got, err := testClient(srv).SearchArtists(context.Background(), "radiohead", 5) if err != nil || len(got) != 1 || calls.Load() != 2 { t.Fatalf("a throttled first answer should be retried: %v, %+v, %d calls", err, got, calls.Load()) } down := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadGateway) })) defer down.Close() if _, err := testClient(down).SearchArtists(context.Background(), "x", 5); !errors.Is(err, ErrUnreachable) { t.Errorf("err = %v, want ErrUnreachable", err) } } func TestGet_NotFoundAndNoServer(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) })) if _, err := testClient(srv).Artist(context.Background(), "nope"); !errors.Is(err, ErrNotFound) { t.Errorf("err = %v, want ErrNotFound", err) } srv.Close() c := testClient(srv) c.Retries = -1 if _, err := c.Artist(context.Background(), "a1"); !errors.Is(err, ErrUnreachable) { t.Errorf("a closed server: err = %v, want ErrUnreachable", err) } } func TestPacer_SpacesCallers(t *testing.T) { p := &Pacer{Interval: 30 * time.Millisecond} start := time.Now() for range 3 { if err := p.Wait(context.Background()); err != nil { t.Fatal(err) } } if elapsed := time.Since(start); elapsed < 60*time.Millisecond { t.Errorf("three waits took %v, want at least two intervals", elapsed) } ctx, cancel := context.WithCancel(context.Background()) cancel() if err := p.Wait(ctx); err == nil { t.Error("a cancelled wait should return the context's error") } }