feat(api): GET /api/albums/{id} with nested tracks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+43
-2
@@ -45,9 +45,50 @@ func (h *handlers) handleGetTrack(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, trackRefFrom(track, album.Title, artist.Name))
|
||||
}
|
||||
|
||||
// handleGetAlbum is implemented in Task 7.
|
||||
// handleGetAlbum implements GET /api/albums/{id}. Returns the album plus its
|
||||
// tracks (ordered by disc/track number via the underlying query) with
|
||||
// duration summed from the track list — keeps one source of truth.
|
||||
func (h *handlers) handleGetAlbum(w http.ResponseWriter, r *http.Request) {
|
||||
writeErr(w, http.StatusNotImplemented, "not_implemented", "stub")
|
||||
id, ok := parseUUID(chi.URLParam(r, "id"))
|
||||
if !ok {
|
||||
writeErr(w, http.StatusBadRequest, "bad_request", "invalid album id")
|
||||
return
|
||||
}
|
||||
q := dbq.New(h.pool)
|
||||
album, err := q.GetAlbumByID(r.Context(), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
writeErr(w, http.StatusNotFound, "not_found", "album not found")
|
||||
return
|
||||
}
|
||||
h.logger.Error("api: get album failed", "err", err)
|
||||
writeErr(w, http.StatusInternalServerError, "server_error", "lookup failed")
|
||||
return
|
||||
}
|
||||
artist, err := q.GetArtistByID(r.Context(), album.ArtistID)
|
||||
if err != nil {
|
||||
h.logger.Error("api: get album artist failed", "err", err)
|
||||
writeErr(w, http.StatusInternalServerError, "server_error", "lookup failed")
|
||||
return
|
||||
}
|
||||
tracks, err := q.ListTracksByAlbum(r.Context(), id)
|
||||
if err != nil {
|
||||
h.logger.Error("api: list tracks failed", "err", err)
|
||||
writeErr(w, http.StatusInternalServerError, "server_error", "lookup failed")
|
||||
return
|
||||
}
|
||||
refs := make([]TrackRef, 0, len(tracks))
|
||||
durSec := 0
|
||||
for _, t := range tracks {
|
||||
ref := trackRefFrom(t, album.Title, artist.Name)
|
||||
refs = append(refs, ref)
|
||||
durSec += ref.DurationSec
|
||||
}
|
||||
detail := AlbumDetail{
|
||||
AlbumRef: albumRefFrom(album, artist.Name, len(tracks), durSec),
|
||||
Tracks: refs,
|
||||
}
|
||||
writeJSON(w, http.StatusOK, detail)
|
||||
}
|
||||
|
||||
// handleGetArtist is implemented in Task 8.
|
||||
|
||||
@@ -74,3 +74,93 @@ func TestHandleGetTrack_NotFound(t *testing.T) {
|
||||
t.Errorf("status = %d, want 404", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleGetAlbum_HappyPath(t *testing.T) {
|
||||
h, pool := testHandlers(t)
|
||||
truncateLibrary(t, pool)
|
||||
artist := seedArtist(t, pool, "Beatles")
|
||||
album := seedAlbum(t, pool, artist.ID, "Abbey Road", 1969)
|
||||
seedTrack(t, pool, album.ID, artist.ID, "Come Together", 1, 259_000)
|
||||
seedTrack(t, pool, album.ID, artist.ID, "Something", 2, 183_000)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/albums/"+uuidToString(album.ID), 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 AlbumDetail
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
|
||||
t.Fatalf("decode: %v body=%s", err, w.Body.String())
|
||||
}
|
||||
if got.Title != "Abbey Road" || got.ArtistName != "Beatles" || got.Year != 1969 {
|
||||
t.Errorf("album = %+v", got)
|
||||
}
|
||||
if len(got.Tracks) != 2 {
|
||||
t.Fatalf("tracks len = %d, want 2", len(got.Tracks))
|
||||
}
|
||||
if got.Tracks[0].Title != "Come Together" || got.Tracks[1].Title != "Something" {
|
||||
t.Errorf("tracks = %+v", got.Tracks)
|
||||
}
|
||||
if got.TrackCount != 2 || got.DurationSec != 442 { // 259 + 183
|
||||
t.Errorf("counts = track=%d duration=%d", got.TrackCount, got.DurationSec)
|
||||
}
|
||||
want := "/api/albums/" + uuidToString(album.ID) + "/cover"
|
||||
if got.CoverURL != want {
|
||||
t.Errorf("cover_url = %q", got.CoverURL)
|
||||
}
|
||||
// StreamURL on nested tracks must be set
|
||||
if got.Tracks[0].StreamURL == "" {
|
||||
t.Error("nested track missing stream_url")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleGetAlbum_NoTracks(t *testing.T) {
|
||||
h, pool := testHandlers(t)
|
||||
truncateLibrary(t, pool)
|
||||
artist := seedArtist(t, pool, "Beatles")
|
||||
album := seedAlbum(t, pool, artist.ID, "Empty", 0)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/albums/"+uuidToString(album.ID), 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 AlbumDetail
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &got)
|
||||
if got.Tracks == nil {
|
||||
t.Error("tracks = nil, want [] for JSON encoding")
|
||||
}
|
||||
if len(got.Tracks) != 0 {
|
||||
t.Errorf("tracks len = %d, want 0", len(got.Tracks))
|
||||
}
|
||||
// Year omitempty: releaseYear=0 => Year=0 => field should be absent from JSON
|
||||
if got.Year != 0 {
|
||||
t.Errorf("year = %d, want 0", got.Year)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleGetAlbum_BadUUID(t *testing.T) {
|
||||
h, _ := testHandlers(t)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/albums/not-a-uuid", nil)
|
||||
w := httptest.NewRecorder()
|
||||
newLibraryRouter(h).ServeHTTP(w, req)
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("status = %d, want 400", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleGetAlbum_NotFound(t *testing.T) {
|
||||
h, pool := testHandlers(t)
|
||||
truncateLibrary(t, pool)
|
||||
req := httptest.NewRequest(http.MethodGet,
|
||||
"/api/albums/00000000-0000-0000-0000-000000000001", nil)
|
||||
w := httptest.NewRecorder()
|
||||
newLibraryRouter(h).ServeHTTP(w, req)
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("status = %d, want 404", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user