package api import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" ) func callRadio(h *handlers, user interface{}, query string) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodGet, "/api/radio?"+query, nil) req = req.WithContext(context.WithValue(req.Context(), userCtxKeyForTest(), user)) w := httptest.NewRecorder() h.handleRadio(w, req) return w } func TestHandleRadio_ColdStart_OnlySeedReturned(t *testing.T) { h, pool := testHandlers(t) truncateLibrary(t, pool) user := seedUser(t, pool, "alice", "x", false) artist := seedArtist(t, pool, "X") album := seedAlbum(t, pool, artist.ID, "X", 1990) seed := seedTrack(t, pool, album.ID, artist.ID, "Seed", 1, 100_000) w := callRadio(h, user, "seed_track="+uuidToString(seed.ID)) if w.Code != http.StatusOK { t.Fatalf("status = %d body=%s", w.Code, w.Body.String()) } var resp RadioResponse _ = json.Unmarshal(w.Body.Bytes(), &resp) if len(resp.Tracks) != 1 { t.Fatalf("len = %d, want 1", len(resp.Tracks)) } if resp.Tracks[0].Title != "Seed" { t.Errorf("seed not first: %v", resp.Tracks[0].Title) } } func TestHandleRadio_Typical_SeedFirstPlusPicks(t *testing.T) { h, pool := testHandlers(t) truncateLibrary(t, pool) user := seedUser(t, pool, "alice", "x", false) artist := seedArtist(t, pool, "X") album := seedAlbum(t, pool, artist.ID, "X", 1990) seed := seedTrack(t, pool, album.ID, artist.ID, "Seed", 1, 100_000) for i := 2; i <= 6; i++ { seedTrack(t, pool, album.ID, artist.ID, "T"+string(rune('0'+i)), i, 100_000) } w := callRadio(h, user, "seed_track="+uuidToString(seed.ID)) if w.Code != http.StatusOK { t.Fatalf("status = %d", w.Code) } var resp RadioResponse _ = json.Unmarshal(w.Body.Bytes(), &resp) if len(resp.Tracks) != 6 { t.Fatalf("len = %d, want 6 (seed + 5 picks)", len(resp.Tracks)) } if resp.Tracks[0].Title != "Seed" { t.Errorf("seed not at index 0: %v", resp.Tracks[0].Title) } } func TestHandleRadio_UnknownSeedIs404(t *testing.T) { h, pool := testHandlers(t) user := seedUser(t, pool, "alice", "x", false) w := callRadio(h, user, "seed_track=00000000-0000-0000-0000-000000000000") if w.Code != http.StatusNotFound { t.Errorf("status = %d", w.Code) } } func TestHandleRadio_BadSeedIs400(t *testing.T) { h, pool := testHandlers(t) user := seedUser(t, pool, "alice", "x", false) w := callRadio(h, user, "seed_track=not-a-uuid") if w.Code != http.StatusBadRequest { t.Errorf("status = %d", w.Code) } } func TestHandleRadio_MissingSeedIs400(t *testing.T) { h, pool := testHandlers(t) user := seedUser(t, pool, "alice", "x", false) w := callRadio(h, user, "") if w.Code != http.StatusBadRequest { t.Errorf("status = %d", w.Code) } } func TestHandleRadio_BadLimitIs400(t *testing.T) { h, pool := testHandlers(t) truncateLibrary(t, pool) user := seedUser(t, pool, "alice", "x", false) artist := seedArtist(t, pool, "X") album := seedAlbum(t, pool, artist.ID, "X", 1990) seed := seedTrack(t, pool, album.ID, artist.ID, "Seed", 1, 100_000) w := callRadio(h, user, "seed_track="+uuidToString(seed.ID)+"&limit=0") if w.Code != http.StatusBadRequest { t.Errorf("limit=0 status = %d", w.Code) } w = callRadio(h, user, "seed_track="+uuidToString(seed.ID)+"&limit=-1") if w.Code != http.StatusBadRequest { t.Errorf("limit=-1 status = %d", w.Code) } } func TestHandleRadio_LimitClampedToMax(t *testing.T) { h, pool := testHandlers(t) truncateLibrary(t, pool) user := seedUser(t, pool, "alice", "x", false) artist := seedArtist(t, pool, "X") album := seedAlbum(t, pool, artist.ID, "X", 1990) seed := seedTrack(t, pool, album.ID, artist.ID, "Seed", 1, 100_000) for i := 2; i <= 6; i++ { seedTrack(t, pool, album.ID, artist.ID, "T"+string(rune('0'+i)), i, 100_000) } w := callRadio(h, user, "seed_track="+uuidToString(seed.ID)+"&limit=99999") if w.Code != http.StatusOK { t.Fatalf("status = %d", w.Code) } var resp RadioResponse _ = json.Unmarshal(w.Body.Bytes(), &resp) // We only have 6 tracks; clamped limit (max 200) returns all 6. if len(resp.Tracks) != 6 { t.Errorf("len = %d, want 6", len(resp.Tracks)) } }