e994aae613
Wire the fsnotify watcher and a fixed 12h safety-net delta walk in main; remove the configurable scan scheduler (scheduler.go, scan_schedule table via migration 0033, GET/PATCH /api/admin/scan/schedule, and the server/api plumbing). Manual scan + scan status are unchanged.
592 lines
19 KiB
Go
592 lines
19 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/config"
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/eventbus"
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/playevents"
|
|
)
|
|
|
|
// newLibraryRouter builds a test-only chi router with the library handlers
|
|
// mounted at their path-style routes. Tests hit this router rather than
|
|
// calling handler methods directly so chi URL params populate correctly.
|
|
// RequireUser is NOT applied to most routes — library handlers don't read
|
|
// user context.
|
|
//
|
|
// The stream route is wrapped with a synthetic-user middleware because
|
|
// handleGetStream now enforces its own auth check (streamAuthOk) after the
|
|
// UPnP slice moved it out of the authed.Group. Real traffic carries either
|
|
// a session cookie (resolved by auth.OptionalUser) or a signed query
|
|
// token; tests get a fake user-in-context so the cookie path of
|
|
// streamAuthOk succeeds without seeding a real session row.
|
|
func newLibraryRouter(h *handlers) chi.Router {
|
|
r := chi.NewRouter()
|
|
r.Get("/api/artists", h.handleListArtists)
|
|
r.Get("/api/artists/{id}", h.handleGetArtist)
|
|
r.Get("/api/albums/{id}", h.handleGetAlbum)
|
|
r.Get("/api/albums/{id}/cover", h.handleGetCover)
|
|
r.Get("/api/tracks/{id}", h.handleGetTrack)
|
|
r.With(injectFakeUserForTest).Get("/api/tracks/{id}/stream", h.handleGetStream)
|
|
r.Get("/api/search", h.handleSearch)
|
|
return r
|
|
}
|
|
|
|
// injectFakeUserForTest attaches an empty dbq.User to request context via
|
|
// auth.UserCtxKeyForTest(), letting handleGetStream's streamAuthOk succeed
|
|
// on the session path without the test having to seed a real session row.
|
|
// Production traffic uses auth.OptionalUser instead; this is the test-only
|
|
// equivalent that bypasses the DB lookup.
|
|
func injectFakeUserForTest(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := context.WithValue(r.Context(), auth.UserCtxKeyForTest(), dbq.User{})
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
func TestHandleGetTrack_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)
|
|
track := seedTrack(t, pool, album.ID, artist.ID, "Something", 3, 183_000)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/tracks/"+uuidToString(track.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 TrackRef
|
|
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
|
|
t.Fatalf("decode: %v body=%s", err, w.Body.String())
|
|
}
|
|
if got.Title != "Something" || got.AlbumTitle != "Abbey Road" || got.ArtistName != "Beatles" {
|
|
t.Errorf("ref = %+v", got)
|
|
}
|
|
if got.TrackNumber != 3 || got.DurationSec != 183 {
|
|
t.Errorf("positions = %+v", got)
|
|
}
|
|
want := "/api/tracks/" + uuidToString(track.ID) + "/stream"
|
|
if got.StreamURL != want {
|
|
t.Errorf("stream_url = %q, want %q", got.StreamURL, want)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetTrack_BadUUID(t *testing.T) {
|
|
h, _ := testHandlers(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/tracks/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 TestHandleGetTrack_NotFound(t *testing.T) {
|
|
h, pool := testHandlers(t)
|
|
truncateLibrary(t, pool)
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/tracks/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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetArtist_HappyPath(t *testing.T) {
|
|
h, pool := testHandlers(t)
|
|
truncateLibrary(t, pool)
|
|
artist := seedArtist(t, pool, "Beatles")
|
|
album1 := seedAlbum(t, pool, artist.ID, "Abbey Road", 1969)
|
|
album2 := seedAlbum(t, pool, artist.ID, "Let It Be", 1970)
|
|
seedTrack(t, pool, album1.ID, artist.ID, "Something", 3, 183_000)
|
|
seedTrack(t, pool, album2.ID, artist.ID, "Get Back", 1, 189_000)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/artists/"+uuidToString(artist.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 ArtistDetail
|
|
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
|
|
t.Fatalf("decode: %v body=%s", err, w.Body.String())
|
|
}
|
|
if got.Name != "Beatles" {
|
|
t.Errorf("name = %q", got.Name)
|
|
}
|
|
if got.AlbumCount != 2 {
|
|
t.Errorf("album_count = %d, want 2", got.AlbumCount)
|
|
}
|
|
if len(got.Albums) != 2 {
|
|
t.Fatalf("albums len = %d, want 2", len(got.Albums))
|
|
}
|
|
// ListAlbumsByArtist orders by release_date then sort_title; Abbey Road (1969) first.
|
|
if got.Albums[0].Title != "Abbey Road" {
|
|
t.Errorf("first album = %q, want Abbey Road", got.Albums[0].Title)
|
|
}
|
|
for _, a := range got.Albums {
|
|
if a.ArtistName != "Beatles" {
|
|
t.Errorf("album.artist_name = %q", a.ArtistName)
|
|
}
|
|
if a.CoverURL == "" {
|
|
t.Error("album missing cover_url")
|
|
}
|
|
if a.TrackCount != 1 {
|
|
t.Errorf("album.track_count = %d, want 1", a.TrackCount)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHandleGetArtist_NoAlbums(t *testing.T) {
|
|
h, pool := testHandlers(t)
|
|
truncateLibrary(t, pool)
|
|
artist := seedArtist(t, pool, "Solo")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/artists/"+uuidToString(artist.ID), nil)
|
|
w := httptest.NewRecorder()
|
|
newLibraryRouter(h).ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d", w.Code)
|
|
}
|
|
var got ArtistDetail
|
|
_ = json.Unmarshal(w.Body.Bytes(), &got)
|
|
if got.Albums == nil {
|
|
t.Error("albums = nil, want []")
|
|
}
|
|
if got.AlbumCount != 0 {
|
|
t.Errorf("album_count = %d, want 0", got.AlbumCount)
|
|
}
|
|
}
|
|
|
|
func TestHandleGetArtist_BadUUID(t *testing.T) {
|
|
h, _ := testHandlers(t)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/artists/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 TestHandleGetArtist_NotFound(t *testing.T) {
|
|
h, pool := testHandlers(t)
|
|
truncateLibrary(t, pool)
|
|
req := httptest.NewRequest(http.MethodGet,
|
|
"/api/artists/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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// TestRoutesRegisteredInMount verifies the production Mount() wires every
|
|
// library route. We hit each path through the real Mount (no RequireUser
|
|
// stripped — we expect 401 from the middleware, which is fine — that
|
|
// proves the route reached the authenticated group).
|
|
func TestRoutesRegisteredInMount(t *testing.T) {
|
|
h, _ := testHandlers(t)
|
|
r := chi.NewRouter()
|
|
w := playevents.NewWriter(h.pool, slog.New(slog.NewTextHandler(io.Discard, nil)),
|
|
30*time.Minute, 0.5, 30000)
|
|
Mount(r, h.pool, h.logger, w, config.RecommendationConfig{RadioSize: 50, RadioSizeMax: 200, RecentlyPlayedHours: 1}, h.lidarrCfg, h.lidarrRequests, h.lidarrQuarantine, h.tracks, h.playlists, h.coverart, h.coverSettings, h.scanner, h.scanCfg, h.dataDir, nil, eventbus.New(), nil, nil)
|
|
|
|
paths := []string{
|
|
"/api/artists",
|
|
"/api/artists/00000000-0000-0000-0000-000000000001",
|
|
"/api/albums/00000000-0000-0000-0000-000000000001",
|
|
"/api/albums/00000000-0000-0000-0000-000000000001/cover",
|
|
"/api/tracks/00000000-0000-0000-0000-000000000001",
|
|
"/api/tracks/00000000-0000-0000-0000-000000000001/stream",
|
|
"/api/search?q=x",
|
|
}
|
|
for _, p := range paths {
|
|
req := httptest.NewRequest(http.MethodGet, p, nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
// Unauthenticated → 401. What we must NOT see is 404 (route missing).
|
|
if w.Code == http.StatusNotFound {
|
|
t.Errorf("path %s returned 404 — route not registered", p)
|
|
}
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("path %s status = %d, want 401 (unauth)", p, w.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetArtistTracks_HappyPath(t *testing.T) {
|
|
h, pool := testHandlers(t)
|
|
user := seedUser(t, pool, "alice", "pw", false)
|
|
q := dbq.New(pool)
|
|
artist, _ := q.UpsertArtist(context.Background(), dbq.UpsertArtistParams{
|
|
Name: "ArtistA", SortName: "ArtistA",
|
|
})
|
|
album, _ := q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{
|
|
Title: "AlbumA", SortTitle: "AlbumA", ArtistID: artist.ID,
|
|
})
|
|
_, _ = q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
|
|
Title: "T1", AlbumID: album.ID, ArtistID: artist.ID,
|
|
DurationMs: 1000, FilePath: "/tmp/m6a-T1.mp3",
|
|
FileSize: 1, FileFormat: "mp3",
|
|
})
|
|
_, _ = q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
|
|
Title: "T2", AlbumID: album.ID, ArtistID: artist.ID,
|
|
DurationMs: 1000, FilePath: "/tmp/m6a-T2.mp3",
|
|
FileSize: 1, FileFormat: "mp3",
|
|
})
|
|
|
|
r := chi.NewRouter()
|
|
r.Get("/api/artists/{id}/tracks", h.handleGetArtistTracks)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/artists/"+uuidToString(artist.ID)+"/tracks", nil)
|
|
req = withUser(req, user)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, body = %s", w.Code, w.Body.String())
|
|
}
|
|
var got []TrackRef
|
|
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("len = %d, want 2", len(got))
|
|
}
|
|
if got[0].ArtistName != "ArtistA" || got[0].AlbumTitle != "AlbumA" {
|
|
t.Errorf("got = %+v", got[0])
|
|
}
|
|
}
|
|
|
|
func TestGetArtistTracks_HonorsQuarantine(t *testing.T) {
|
|
h, pool := testHandlers(t)
|
|
user := seedUser(t, pool, "alice", "pw", false)
|
|
q := dbq.New(pool)
|
|
artist, _ := q.UpsertArtist(context.Background(), dbq.UpsertArtistParams{
|
|
Name: "ArtistB", SortName: "ArtistB",
|
|
})
|
|
album, _ := q.UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{
|
|
Title: "AlbumB", SortTitle: "AlbumB", ArtistID: artist.ID,
|
|
})
|
|
track, _ := q.UpsertTrack(context.Background(), dbq.UpsertTrackParams{
|
|
Title: "Q", AlbumID: album.ID, ArtistID: artist.ID,
|
|
DurationMs: 1000, FilePath: "/tmp/m6a-Q.mp3",
|
|
FileSize: 1, FileFormat: "mp3",
|
|
})
|
|
if _, err := pool.Exec(context.Background(),
|
|
`INSERT INTO lidarr_quarantine (user_id, track_id, reason) VALUES ($1, $2, 'bad_rip')`,
|
|
user.ID, track.ID,
|
|
); err != nil {
|
|
t.Fatalf("quarantine insert: %v", err)
|
|
}
|
|
|
|
r := chi.NewRouter()
|
|
r.Get("/api/artists/{id}/tracks", h.handleGetArtistTracks)
|
|
req := httptest.NewRequest(http.MethodGet, "/api/artists/"+uuidToString(artist.ID)+"/tracks", nil)
|
|
req = withUser(req, user)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("status = %d", w.Code)
|
|
}
|
|
var got []TrackRef
|
|
_ = json.Unmarshal(w.Body.Bytes(), &got)
|
|
if len(got) != 0 {
|
|
t.Errorf("len = %d, want 0 (quarantined)", len(got))
|
|
}
|
|
}
|
|
|
|
func TestGetArtistTracks_NotFound(t *testing.T) {
|
|
h, pool := testHandlers(t)
|
|
user := seedUser(t, pool, "alice", "pw", false)
|
|
|
|
r := chi.NewRouter()
|
|
r.Get("/api/artists/{id}/tracks", h.handleGetArtistTracks)
|
|
// Random UUID — no such artist exists.
|
|
req := httptest.NewRequest(http.MethodGet, "/api/artists/00000000-0000-0000-0000-000000000000/tracks", nil)
|
|
req = withUser(req, user)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("status = %d, want 404", w.Code)
|
|
}
|
|
_ = pool
|
|
}
|