290 lines
8.8 KiB
Go
290 lines
8.8 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
|
)
|
|
|
|
func TestAudioContentType(t *testing.T) {
|
|
cases := map[string]string{
|
|
"mp3": "audio/mpeg",
|
|
"MP3": "audio/mpeg",
|
|
"flac": "audio/flac",
|
|
"ogg": "audio/ogg",
|
|
"opus": "audio/ogg",
|
|
"m4a": "audio/mp4",
|
|
"aac": "audio/aac",
|
|
"wav": "audio/wav",
|
|
"unknown": "application/octet-stream",
|
|
"": "application/octet-stream",
|
|
}
|
|
for in, want := range cases {
|
|
if got := audioContentType(in); got != want {
|
|
t.Errorf("audioContentType(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestImageContentType(t *testing.T) {
|
|
cases := map[string]string{
|
|
"/a/cover.jpg": "image/jpeg",
|
|
"/a/cover.JPG": "image/jpeg",
|
|
"/a/cover.jpeg": "image/jpeg",
|
|
"/a/cover.png": "image/png",
|
|
"/a/cover.webp": "image/webp",
|
|
"/a/cover.gif": "image/gif",
|
|
"/a/cover.bmp": "application/octet-stream",
|
|
"/a/cover": "application/octet-stream",
|
|
}
|
|
for in, want := range cases {
|
|
if got := imageContentType(in); got != want {
|
|
t.Errorf("imageContentType(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFindSidecarCover_PrefersCoverOverFolder(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "cover.jpg"), []byte("c"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "folder.jpg"), []byte("f"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := findSidecarCover(filepath.Join(dir, "any-track.flac"))
|
|
if got != filepath.Join(dir, "cover.jpg") {
|
|
t.Errorf("got %q, want cover.jpg path", got)
|
|
}
|
|
}
|
|
|
|
func TestFindSidecarCover_FallsBackToFolder(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(dir, "folder.png"), []byte("f"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := findSidecarCover(filepath.Join(dir, "t.flac"))
|
|
if got != filepath.Join(dir, "folder.png") {
|
|
t.Errorf("got %q, want folder.png path", got)
|
|
}
|
|
}
|
|
|
|
func TestFindSidecarCover_NoneFound(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if got := findSidecarCover(filepath.Join(dir, "t.flac")); got != "" {
|
|
t.Errorf("got %q, want empty string", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveAlbumCoverPath_ExplicitPresent(t *testing.T) {
|
|
_, 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, "explicit.jpg")
|
|
if err := os.WriteFile(explicit, []byte("e"), 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.Fatalf("UPDATE albums: %v", err)
|
|
}
|
|
got := resolveAlbumCoverPath(context.Background(), dbq.New(pool), fetchAlbum(t, pool, album.ID))
|
|
if got != explicit {
|
|
t.Errorf("got %q, want %q", got, explicit)
|
|
}
|
|
}
|
|
|
|
func TestResolveAlbumCoverPath_ExplicitMissingFallsThroughToSidecar(t *testing.T) {
|
|
_, 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")
|
|
sidecar := filepath.Join(dir, "cover.jpg")
|
|
if err := os.WriteFile(sidecar, []byte("s"), 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.Fatalf("UPDATE albums: %v", err)
|
|
}
|
|
|
|
got := resolveAlbumCoverPath(context.Background(), dbq.New(pool), fetchAlbum(t, pool, album.ID))
|
|
if got != sidecar {
|
|
t.Errorf("got %q, want %q", got, sidecar)
|
|
}
|
|
}
|
|
|
|
func TestResolveAlbumCoverPath_NoArt(t *testing.T) {
|
|
_, 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)
|
|
got := resolveAlbumCoverPath(context.Background(), dbq.New(pool), album)
|
|
if got != "" {
|
|
t.Errorf("got %q, want empty string", got)
|
|
}
|
|
}
|
|
|
|
// fetchAlbum reloads the album row so tests see updates made via raw SQL.
|
|
func fetchAlbum(t *testing.T, pool *pgxpool.Pool, id pgtype.UUID) dbq.Album {
|
|
t.Helper()
|
|
a, err := dbq.New(pool).GetAlbumByID(context.Background(), id)
|
|
if err != nil {
|
|
t.Fatalf("GetAlbumByID: %v", err)
|
|
}
|
|
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)
|
|
}
|
|
}
|