package api_test import ( "context" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "gobuz/internal/api" ) func TestAuthWithToken_FreeAccount(t *testing.T) { srv := newMockServer(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]interface{}{ "user": map[string]interface{}{ "credential": map[string]interface{}{ "parameters": nil, }, }, }) }) defer srv.Close() c := clientForServer(t, srv) err := c.AuthWithToken(context.Background(), "42", "tok") if err == nil { t.Fatal("expected IneligibleError for free account") } if _, ok := err.(*api.IneligibleError); !ok { t.Errorf("expected IneligibleError, got %T: %v", err, err) } } func TestAuthWithToken_BadShape(t *testing.T) { srv := newMockServer(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]interface{}{ "user": "notamap", }) }) defer srv.Close() c := clientForServer(t, srv) err := c.AuthWithToken(context.Background(), "42", "tok") if err == nil { t.Fatal("expected error for bad response shape") } if _, ok := err.(*api.AuthenticationError); !ok { t.Errorf("expected AuthenticationError, got %T: %v", err, err) } } func TestDoGet_401_ReturnsAuthError(t *testing.T) { srv := newMockServer(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(401) json.NewEncoder(w).Encode(map[string]interface{}{ "code": 401, "message": "User authentication is required.", }) }) defer srv.Close() c := clientForServer(t, srv) err := c.AuthWithToken(context.Background(), "99", "tok") if err == nil { t.Fatal("expected error, got nil") } if _, ok := err.(*api.AuthenticationError); !ok { t.Errorf("expected AuthenticationError, got %T: %v", err, err) } } func TestErrorTypes(t *testing.T) { errs := []error{ &api.AuthenticationError{}, &api.IneligibleError{}, &api.InvalidAppIDError{}, &api.InvalidAppSecretError{}, &api.InvalidQualityError{}, } for _, err := range errs { if err.Error() == "" { t.Errorf("%T.Error() returned empty string", err) } } } func TestGetAlbum_MockServer(t *testing.T) { srv := newMockServer(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api.json/0.2/album/get" { t.Errorf("unexpected path: %s", r.URL.Path) } if r.URL.Query().Get("album_id") != "abc123" { t.Errorf("unexpected album_id: %s", r.URL.Query().Get("album_id")) } json.NewEncoder(w).Encode(map[string]interface{}{ "id": "abc123", "title": "Test Album", }) }) defer srv.Close() c := clientForServer(t, srv) meta, err := c.GetAlbum(context.Background(), "abc123") if err != nil { t.Fatalf("GetAlbum: %v", err) } if meta.Title != "Test Album" { t.Errorf("title = %q", meta.Title) } if meta.ID != "abc123" { t.Errorf("id = %q", meta.ID) } } func TestGetTrack_MockServer(t *testing.T) { srv := newMockServer(func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]interface{}{ "id": "t1", "title": "My Track", }) }) defer srv.Close() c := clientForServer(t, srv) meta, err := c.GetTrack(context.Background(), "t1") if err != nil { t.Fatalf("GetTrack: %v", err) } if meta.Title != "My Track" { t.Errorf("title = %q", meta.Title) } if meta.ID != "t1" { t.Errorf("id = %q", meta.ID) } } func TestAuthWithToken_MockServer(t *testing.T) { srv := newMockServer(func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() if q.Get("user_id") != "99" || q.Get("user_auth_token") != "mytoken" { w.WriteHeader(401) json.NewEncoder(w).Encode(map[string]interface{}{"code": 401}) return } json.NewEncoder(w).Encode(map[string]interface{}{ "user_auth_token": "mytoken", "user": map[string]interface{}{ "id": "99", "credential": map[string]interface{}{ "parameters": map[string]interface{}{ "short_label": "Sublime", }, }, }, }) }) defer srv.Close() c := clientForServer(t, srv) if err := c.AuthWithToken(context.Background(), "99", "mytoken"); err != nil { t.Fatalf("AuthWithToken: %v", err) } if c.Label != "Sublime" { t.Errorf("Label = %q", c.Label) } if c.UAT != "mytoken" { t.Errorf("UAT = %q", c.UAT) } } func TestAuthWithToken_WrongToken_Returns401(t *testing.T) { srv := newMockServer(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(401) json.NewEncoder(w).Encode(map[string]interface{}{"code": 401}) }) defer srv.Close() c := clientForServer(t, srv) err := c.AuthWithToken(context.Background(), "1", "badtoken") if err == nil { t.Fatal("expected error for 401") } if _, ok := err.(*api.AuthenticationError); !ok { t.Errorf("expected AuthenticationError, got %T", err) } } func TestSearchAlbums_MockServer(t *testing.T) { srv := newMockServer(func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("query") != "radiohead" { t.Errorf("unexpected query: %s", r.URL.Query().Get("query")) } json.NewEncoder(w).Encode(map[string]interface{}{ "albums": map[string]interface{}{ "total": 1, "items": []interface{}{ map[string]interface{}{"id": "alb1", "title": "OK Computer"}, }, }, }) }) defer srv.Close() c := clientForServer(t, srv) results, err := c.SearchAlbums(context.Background(), "radiohead", 5) if err != nil { t.Fatalf("SearchAlbums: %v", err) } if len(results.Items) != 1 { t.Errorf("expected 1 result, got %d", len(results.Items)) } if results.Items[0].Title != "OK Computer" { t.Errorf("title = %q, want %q", results.Items[0].Title, "OK Computer") } } func TestSearchArtists_MockServer(t *testing.T) { srv := newMockServer(func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("query") != "radiohead" { t.Errorf("unexpected query: %s", r.URL.Query().Get("query")) } json.NewEncoder(w).Encode(map[string]interface{}{ "artists": map[string]interface{}{ "total": 1, "items": []interface{}{ map[string]interface{}{"id": 123, "name": "Radiohead"}, }, }, }) }) defer srv.Close() c := clientForServer(t, srv) results, err := c.SearchArtists(context.Background(), "radiohead", 5) if err != nil { t.Fatalf("SearchArtists: %v", err) } if len(results.Items) != 1 { t.Errorf("expected 1 result, got %d", len(results.Items)) } if results.Items[0].Name != "Radiohead" { t.Errorf("name = %q, want %q", results.Items[0].Name, "Radiohead") } } func TestGetTrackURL_InvalidQuality(t *testing.T) { c := api.New("123", nil) _, err := c.GetTrackURL(context.Background(), "t1", 99) if err == nil { t.Fatal("expected error for invalid quality") } if _, ok := err.(*api.InvalidQualityError); !ok { t.Errorf("expected InvalidQualityError, got %T", err) } } func TestGetTrackURL_MockServer(t *testing.T) { srv := newMockServer(func(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() if q.Get("track_id") == "" || q.Get("format_id") == "" { t.Error("missing required params") } if q.Get("request_sig") == "" { t.Error("missing request_sig") } json.NewEncoder(w).Encode(map[string]interface{}{ "url": "https://cdn.example.com/track.flac", }) }) defer srv.Close() c := clientForServer(t, srv) c.Secret = "mysecret" result, err := c.GetTrackURL(context.Background(), "t1", 6) if err != nil { t.Fatalf("GetTrackURL: %v", err) } if result.URL != "https://cdn.example.com/track.flac" { t.Errorf("url = %q", result.URL) } } func TestHTTP500_ReturnsError(t *testing.T) { srv := newMockServer(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) w.Write([]byte("internal error")) }) defer srv.Close() c := clientForServer(t, srv) _, err := c.GetAlbum(context.Background(), "any") if err == nil { t.Fatal("expected error for 500") } } type roundTripFunc func(req *http.Request) (*http.Response, error) func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) } func TestGetArtistPage_Signing(t *testing.T) { var gotPath, gotArtistID, gotSort, gotTS, gotSig string hc := &http.Client{ Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) { gotPath = r.URL.Path gotArtistID = r.URL.Query().Get("artist_id") gotSort = r.URL.Query().Get("sort") gotTS = r.URL.Query().Get("request_ts") gotSig = r.URL.Query().Get("request_sig") body := `{"id": 12345, "name": {"display": "Radiohead"}}` return &http.Response{ StatusCode: 200, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header), }, nil }), } c := api.NewWithHTTP("123456789", []string{"testsecret"}, hc) res, err := c.GetArtistPage(context.Background(), "12345", "release_date") if err != nil { t.Fatalf("GetArtistPage failed: %v", err) } if !strings.HasSuffix(gotPath, "/artist/page") { t.Errorf("expected path to end in /artist/page, got %s", gotPath) } if gotArtistID != "12345" { t.Errorf("expected artist_id 12345, got %s", gotArtistID) } if gotSort != "release_date" { t.Errorf("expected sort release_date, got %s", gotSort) } if gotTS == "" { t.Errorf("expected non-empty request_ts") } if gotSig == "" { t.Errorf("expected non-empty request_sig") } if res.ID != "12345" { t.Errorf("expected response to have id 12345, got %s", res.ID) } if res.Name.Display != "Radiohead" { t.Errorf("expected response to have name Radiohead, got %s", res.Name.Display) } } func TestGetArtistReleasesGrid_Signing(t *testing.T) { var gotPath, gotArtistID, gotType, gotLimit, gotOffset, gotSig string hc := &http.Client{ Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) { gotPath = r.URL.Path gotArtistID = r.URL.Query().Get("artist_id") gotType = r.URL.Query().Get("release_type") gotLimit = r.URL.Query().Get("limit") gotOffset = r.URL.Query().Get("offset") gotSig = r.URL.Query().Get("request_sig") body := `{"has_more": false, "items": []}` return &http.Response{ StatusCode: 200, Body: io.NopCloser(strings.NewReader(body)), Header: make(http.Header), }, nil }), } c := api.NewWithHTTP("123456789", []string{"testsecret"}, hc) res, err := c.GetArtistReleasesGrid(context.Background(), "12345", "album", 20, 0, "release_date") if err != nil { t.Fatalf("GetArtistReleasesGrid failed: %v", err) } if !strings.HasSuffix(gotPath, "/artist/getReleasesGrid") { t.Errorf("expected path to end in /artist/getReleasesGrid, got %s", gotPath) } if gotArtistID != "12345" { t.Errorf("expected artist_id 12345, got %s", gotArtistID) } if gotType != "album" { t.Errorf("expected release_type album, got %s", gotType) } if gotLimit != "20" { t.Errorf("expected limit 20, got %s", gotLimit) } if gotOffset != "0" { t.Errorf("expected offset 0, got %s", gotOffset) } if gotSig == "" { t.Errorf("expected non-empty request_sig") } if res.HasMore != false { t.Errorf("expected has_more false, got %v", res.HasMore) } } func TestGetArtist_MockServer(t *testing.T) { srv := newMockServer(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api.json/0.2/artist/get" { t.Errorf("unexpected path: %s", r.URL.Path) } if r.URL.Query().Get("artist_id") != "123" { t.Errorf("unexpected artist_id: %s", r.URL.Query().Get("artist_id")) } json.NewEncoder(w).Encode(map[string]interface{}{ "id": 123, "name": "Radiohead", "albums_count": 9, }) }) defer srv.Close() c := clientForServer(t, srv) artist, err := c.GetArtist(context.Background(), "123") if err != nil { t.Fatalf("GetArtist: %v", err) } if artist.Name != "Radiohead" { t.Errorf("artist.Name = %q, want Radiohead", artist.Name) } if artist.AlbumsCount != 9 { t.Errorf("artist.AlbumsCount = %d, want 9", artist.AlbumsCount) } } func TestCatalogSearch_MockServer(t *testing.T) { srv := newMockServer(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api.json/0.2/catalog/search" { t.Errorf("unexpected path: %s", r.URL.Path) } if r.URL.Query().Get("query") != "radiohead" { t.Errorf("unexpected query: %s", r.URL.Query().Get("query")) } json.NewEncoder(w).Encode(map[string]interface{}{ "albums": map[string]interface{}{ "total": 1, "items": []interface{}{ map[string]interface{}{"id": "alb1", "title": "OK Computer"}, }, }, "tracks": map[string]interface{}{ "total": 1, "items": []interface{}{ map[string]interface{}{"id": 101, "title": "Karma Police"}, }, }, }) }) defer srv.Close() c := clientForServer(t, srv) res, err := c.CatalogSearch(context.Background(), "radiohead", 10, 0) if err != nil { t.Fatalf("CatalogSearch: %v", err) } if res.Albums == nil || len(res.Albums.Items) != 1 || res.Albums.Items[0].Title != "OK Computer" { t.Errorf("unexpected albums: %v", res.Albums) } if res.Tracks == nil || len(res.Tracks.Items) != 1 || res.Tracks.Items[0].Title != "Karma Police" { t.Errorf("unexpected tracks: %v", res.Tracks) } } func TestGetRaw_MockServer(t *testing.T) { expected := `{"custom_field": "verbatim", "number": 42}` srv := newMockServer(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(expected)) }) defer srv.Close() c := clientForServer(t, srv) raw, err := c.GetRaw(context.Background(), "test/endpoint", nil) if err != nil { t.Fatalf("GetRaw failed: %v", err) } if string(raw) != expected { t.Errorf("got %s, want %s", string(raw), expected) } } func TestGetAlbumRaw_MockServer(t *testing.T) { expected := `{"id": "alb999", "title": "Verbatim Album", "extra_prop": true}` srv := newMockServer(func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("album_id") != "alb999" { t.Errorf("unexpected album_id: %s", r.URL.Query().Get("album_id")) } w.Header().Set("Content-Type", "application/json") w.Write([]byte(expected)) }) defer srv.Close() c := clientForServer(t, srv) raw, err := c.GetAlbumRaw(context.Background(), "alb999") if err != nil { t.Fatalf("GetAlbumRaw failed: %v", err) } if string(raw) != expected { t.Errorf("got %s, want %s", string(raw), expected) } } func TestGetArtistPageRaw_MockServer(t *testing.T) { expected := `{"id": 12345, "name": {"display": "Radiohead"}, "arbitrary": 123}` srv := newMockServer(func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("artist_id") != "12345" { t.Errorf("unexpected artist_id: %s", r.URL.Query().Get("artist_id")) } if r.URL.Query().Get("sort") != "release_date" { t.Errorf("unexpected sort: %s", r.URL.Query().Get("sort")) } if r.URL.Query().Get("request_sig") == "" { t.Errorf("expected request_sig") } w.Header().Set("Content-Type", "application/json") w.Write([]byte(expected)) }) defer srv.Close() c := clientForServer(t, srv) raw, err := c.GetArtistPageRaw(context.Background(), "12345", "release_date") if err != nil { t.Fatalf("GetArtistPageRaw failed: %v", err) } if string(raw) != expected { t.Errorf("got %s, want %s", string(raw), expected) } } func TestSearchArtistsRaw_MockServer(t *testing.T) { expected := `{"items": [{"id": 1, "name": "Radiohead"}]}` srv := newMockServer(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"query": "radiohead", "artists": %s}`, expected) }) defer srv.Close() c := clientForServer(t, srv) raw, err := c.SearchArtistsRaw(context.Background(), "radiohead", 5) if err != nil { t.Fatalf("SearchArtistsRaw failed: %v", err) } if string(raw) != expected { t.Errorf("got %s, want %s", string(raw), expected) } } func TestSearchAlbumsRaw_MockServer(t *testing.T) { expected := `{"items": [{"id": "alb1", "title": "OK Computer"}]}` srv := newMockServer(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"query": "radiohead", "albums": %s}`, expected) }) defer srv.Close() c := clientForServer(t, srv) raw, err := c.SearchAlbumsRaw(context.Background(), "radiohead", 5) if err != nil { t.Fatalf("SearchAlbumsRaw failed: %v", err) } if string(raw) != expected { t.Errorf("got %s, want %s", string(raw), expected) } } type mockServer struct { handler http.HandlerFunc } func (s *mockServer) Close() {} func newMockServer(h http.HandlerFunc) *mockServer { return &mockServer{handler: h} } type handlerTransport struct { handler http.HandlerFunc } func (h *handlerTransport) RoundTrip(req *http.Request) (*http.Response, error) { rec := httptest.NewRecorder() h.handler(rec, req) return rec.Result(), nil } func clientForServer(t *testing.T, srv *mockServer) *api.Client { t.Helper() return api.NewWithHTTP("123456789", []string{"testsecret"}, &http.Client{Transport: &handlerTransport{handler: srv.handler}}) }