feat(api): GET /api/artists with alpha/newest sort and paging

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 08:06:03 -04:00
parent f37fd782af
commit 8fb2f4e184
3 changed files with 268 additions and 2 deletions
+169
View File
@@ -254,3 +254,172 @@ func TestHandleGetArtist_NotFound(t *testing.T) {
t.Errorf("status = %d, want 404", w.Code)
}
}
func TestHandleListArtists_DefaultAlpha(t *testing.T) {
h, pool := testHandlers(t)
truncateLibrary(t, pool)
seedArtist(t, pool, "Beatles")
seedArtist(t, pool, "ABBA")
seedArtist(t, pool, "Zeppelin")
req := httptest.NewRequest(http.MethodGet, "/api/artists", nil)
w := httptest.NewRecorder()
newLibraryRouter(h).ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d body = %s", w.Code, w.Body.String())
}
var got Page[ArtistRef]
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
t.Fatalf("decode: %v", err)
}
if got.Total != 3 {
t.Errorf("total = %d, want 3", got.Total)
}
if got.Limit != 50 || got.Offset != 0 {
t.Errorf("pagination defaults = limit=%d offset=%d, want 50/0", got.Limit, got.Offset)
}
if len(got.Items) != 3 {
t.Fatalf("items len = %d", len(got.Items))
}
if got.Items[0].Name != "ABBA" || got.Items[2].Name != "Zeppelin" {
t.Errorf("alpha order wrong: %q, %q, %q", got.Items[0].Name, got.Items[1].Name, got.Items[2].Name)
}
}
func TestHandleListArtists_SortNewest(t *testing.T) {
h, pool := testHandlers(t)
truncateLibrary(t, pool)
// Seeded in this order; created_at DESC means Third first.
seedArtist(t, pool, "First")
seedArtist(t, pool, "Second")
seedArtist(t, pool, "Third")
req := httptest.NewRequest(http.MethodGet, "/api/artists?sort=newest", nil)
w := httptest.NewRecorder()
newLibraryRouter(h).ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d", w.Code)
}
var got Page[ArtistRef]
_ = json.Unmarshal(w.Body.Bytes(), &got)
if len(got.Items) != 3 {
t.Fatalf("items len = %d", len(got.Items))
}
if got.Items[0].Name != "Third" || got.Items[2].Name != "First" {
t.Errorf("newest order wrong: %q, %q, %q", got.Items[0].Name, got.Items[1].Name, got.Items[2].Name)
}
}
func TestHandleListArtists_InvalidSort(t *testing.T) {
h, pool := testHandlers(t)
truncateLibrary(t, pool)
seedArtist(t, pool, "Only")
req := httptest.NewRequest(http.MethodGet, "/api/artists?sort=garbage", nil)
w := httptest.NewRecorder()
newLibraryRouter(h).ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want 400", w.Code)
}
}
func TestHandleListArtists_Pagination(t *testing.T) {
h, pool := testHandlers(t)
truncateLibrary(t, pool)
for _, n := range []string{"A", "B", "C", "D", "E"} {
seedArtist(t, pool, n)
}
req := httptest.NewRequest(http.MethodGet, "/api/artists?limit=2&offset=2", nil)
w := httptest.NewRecorder()
newLibraryRouter(h).ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d", w.Code)
}
var got Page[ArtistRef]
_ = json.Unmarshal(w.Body.Bytes(), &got)
if got.Total != 5 {
t.Errorf("total = %d, want 5", got.Total)
}
if got.Limit != 2 || got.Offset != 2 {
t.Errorf("page = limit=%d offset=%d", got.Limit, got.Offset)
}
if len(got.Items) != 2 {
t.Fatalf("items len = %d", len(got.Items))
}
if got.Items[0].Name != "C" || got.Items[1].Name != "D" {
t.Errorf("slice = %q, %q", got.Items[0].Name, got.Items[1].Name)
}
}
func TestHandleListArtists_LimitClamped(t *testing.T) {
h, pool := testHandlers(t)
truncateLibrary(t, pool)
seedArtist(t, pool, "One")
req := httptest.NewRequest(http.MethodGet, "/api/artists?limit=9999", nil)
w := httptest.NewRecorder()
newLibraryRouter(h).ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d", w.Code)
}
var got Page[ArtistRef]
_ = json.Unmarshal(w.Body.Bytes(), &got)
if got.Limit != 200 {
t.Errorf("limit = %d, want 200 (max clamp)", got.Limit)
}
}
func TestHandleListArtists_LimitNonNumeric(t *testing.T) {
h, _ := testHandlers(t)
req := httptest.NewRequest(http.MethodGet, "/api/artists?limit=abc", nil)
w := httptest.NewRecorder()
newLibraryRouter(h).ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want 400", w.Code)
}
}
func TestHandleListArtists_AlbumCount(t *testing.T) {
h, pool := testHandlers(t)
truncateLibrary(t, pool)
artist := seedArtist(t, pool, "A")
seedAlbum(t, pool, artist.ID, "X", 0)
seedAlbum(t, pool, artist.ID, "Y", 0)
req := httptest.NewRequest(http.MethodGet, "/api/artists", nil)
w := httptest.NewRecorder()
newLibraryRouter(h).ServeHTTP(w, req)
var got Page[ArtistRef]
_ = json.Unmarshal(w.Body.Bytes(), &got)
if len(got.Items) != 1 || got.Items[0].AlbumCount != 2 {
t.Errorf("items = %+v", got.Items)
}
}
func TestHandleListArtists_EmptyDB(t *testing.T) {
h, pool := testHandlers(t)
truncateLibrary(t, pool)
req := httptest.NewRequest(http.MethodGet, "/api/artists", nil)
w := httptest.NewRecorder()
newLibraryRouter(h).ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d", w.Code)
}
var got Page[ArtistRef]
_ = json.Unmarshal(w.Body.Bytes(), &got)
if got.Items == nil {
t.Error("items = nil, want []")
}
if got.Total != 0 {
t.Errorf("total = %d", got.Total)
}
}