diff --git a/internal/api/library.go b/internal/api/library.go index 655d476d..bee2af24 100644 --- a/internal/api/library.go +++ b/internal/api/library.go @@ -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. diff --git a/internal/api/library_test.go b/internal/api/library_test.go index c939f5eb..6a66f7aa 100644 --- a/internal/api/library_test.go +++ b/internal/api/library_test.go @@ -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) + } +}