feat(api): GET /api/albums/{id}/cover with sidecar fallback

This commit is contained in:
2026-04-21 22:37:53 -04:00
parent 5af6d7fac6
commit 945b773cbd
3 changed files with 192 additions and 0 deletions
+140
View File
@@ -1,7 +1,11 @@
package api
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
@@ -147,3 +151,139 @@ func fetchAlbum(t *testing.T, pool *pgxpool.Pool, id pgtype.UUID) dbq.Album {
}
return a
}
func TestHandleGetCover_SidecarHappyPath(t *testing.T) {
h, pool := testHandlers(t)
truncateLibrary(t, pool)
artist := seedArtist(t, pool, "A")
album := seedAlbum(t, pool, artist.ID, "X", 0)
_, dir := seedTrackWithFile(t, pool, album.ID, artist.ID, "t", []byte("audio"), "mp3")
coverBytes := []byte{0xFF, 0xD8, 0xFF, 0xE0, 0x01, 0x02, 0x03} // small JPEG-ish payload
if err := os.WriteFile(filepath.Join(dir, "cover.jpg"), coverBytes, 0o644); err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodGet, "/api/albums/"+uuidToString(album.ID)+"/cover", 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())
}
if ct := w.Header().Get("Content-Type"); ct != "image/jpeg" {
t.Errorf("Content-Type = %q", ct)
}
if !bytes.Equal(w.Body.Bytes(), coverBytes) {
t.Errorf("body = %x, want %x", w.Body.Bytes(), coverBytes)
}
}
func TestHandleGetCover_ExplicitPathHappyPath(t *testing.T) {
h, pool := testHandlers(t)
truncateLibrary(t, pool)
artist := seedArtist(t, pool, "A")
album := seedAlbum(t, pool, artist.ID, "X", 0)
dir := t.TempDir()
explicit := filepath.Join(dir, "art.png")
pngBytes := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A} // PNG magic
if err := os.WriteFile(explicit, pngBytes, 0o644); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(context.Background(),
`UPDATE albums SET cover_art_path = $1 WHERE id = $2`, explicit, album.ID); err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodGet, "/api/albums/"+uuidToString(album.ID)+"/cover", 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())
}
if ct := w.Header().Get("Content-Type"); ct != "image/png" {
t.Errorf("Content-Type = %q", ct)
}
if !bytes.Equal(w.Body.Bytes(), pngBytes) {
t.Errorf("body mismatch")
}
}
func TestHandleGetCover_ExplicitMissingFallsThroughToSidecar(t *testing.T) {
h, pool := testHandlers(t)
truncateLibrary(t, pool)
artist := seedArtist(t, pool, "A")
album := seedAlbum(t, pool, artist.ID, "X", 0)
_, dir := seedTrackWithFile(t, pool, album.ID, artist.ID, "t", []byte("audio"), "mp3")
sidecarBytes := []byte("sidecar")
if err := os.WriteFile(filepath.Join(dir, "cover.jpg"), sidecarBytes, 0o644); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(context.Background(),
`UPDATE albums SET cover_art_path = $1 WHERE id = $2`,
filepath.Join(dir, "does-not-exist.jpg"), album.ID); err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodGet, "/api/albums/"+uuidToString(album.ID)+"/cover", nil)
w := httptest.NewRecorder()
newLibraryRouter(h).ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("status = %d", w.Code)
}
if !bytes.Equal(w.Body.Bytes(), sidecarBytes) {
t.Errorf("body = %q, want %q", w.Body.Bytes(), sidecarBytes)
}
}
func TestHandleGetCover_NoArt(t *testing.T) {
h, pool := testHandlers(t)
truncateLibrary(t, pool)
artist := seedArtist(t, pool, "A")
album := seedAlbum(t, pool, artist.ID, "X", 0)
seedTrack(t, pool, album.ID, artist.ID, "t", 1, 1000) // synthetic path, no sidecar
req := httptest.NewRequest(http.MethodGet, "/api/albums/"+uuidToString(album.ID)+"/cover", nil)
w := httptest.NewRecorder()
newLibraryRouter(h).ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Fatalf("status = %d", w.Code)
}
var body errorBody
_ = json.Unmarshal(w.Body.Bytes(), &body)
if body.Error.Code != "not_found" || body.Error.Message != "cover not found" {
t.Errorf("error body = %+v", body)
}
}
func TestHandleGetCover_BadUUID(t *testing.T) {
h, _ := testHandlers(t)
req := httptest.NewRequest(http.MethodGet, "/api/albums/not-a-uuid/cover", nil)
w := httptest.NewRecorder()
newLibraryRouter(h).ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want 400", w.Code)
}
}
func TestHandleGetCover_UnknownAlbum(t *testing.T) {
h, pool := testHandlers(t)
truncateLibrary(t, pool)
req := httptest.NewRequest(http.MethodGet,
"/api/albums/00000000-0000-0000-0000-000000000001/cover", nil)
w := httptest.NewRecorder()
newLibraryRouter(h).ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Fatalf("status = %d", w.Code)
}
var body errorBody
_ = json.Unmarshal(w.Body.Bytes(), &body)
if body.Error.Message != "album not found" {
t.Errorf("message = %q, want \"album not found\"", body.Error.Message)
}
}