go vet caught the Mount signature change: library_test.go calls it from
inside the package, so the earlier grep for "api.Mount(" missed it.
Rather than only appending the argument, the route table now includes
both admin surfaces from this arc. That test exists to prove every route
is actually registered — a 404 there means the route is missing — and
the two paths added today had no such coverage. Both are in the admin
group, so reaching the 401 is what proves they are wired.
The new service is passed as nil, matching the other optional services
in this call: the test asserts routing, never executes an admin handler,
and constructing a settings service would need a pool round-trip for
nothing.
601 lines
19 KiB
Go
601 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.recSettings, h.lidarrCfg, h.lidarrRequests, h.lidarrQuarantine, h.tracks, h.playlists, h.coverart, h.coverSettings, h.tagSettings, h.scanner, h.scanCfg, h.dataDir, nil, eventbus.New(), nil, nil, h.netSettings, 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",
|
|
// Browse indexes (#367).
|
|
"/api/library/genres",
|
|
"/api/library/years",
|
|
// Admin surfaces for the missing-file lifecycle (#2527). Both must
|
|
// 401 at the middleware rather than 404 — they live inside the
|
|
// admin group, so reaching the auth check is what proves they are
|
|
// wired.
|
|
"/api/admin/library/missing",
|
|
"/api/admin/library/reacquisition",
|
|
}
|
|
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
|
|
}
|