diff --git a/web/src/app/api/discover/search/route.test.ts b/web/src/app/api/discover/search/route.test.ts new file mode 100644 index 0000000..467bd55 --- /dev/null +++ b/web/src/app/api/discover/search/route.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; + +vi.mock("@/lib/musicbrainz", () => ({ searchArtists: vi.fn() })); +vi.mock("@/lib/listenbrainz", () => ({ similarArtists: vi.fn() })); +import { searchArtists } from "@/lib/musicbrainz"; +import { similarArtists } from "@/lib/listenbrainz"; +import { GET } from "./route"; + +const mockSearch = vi.mocked(searchArtists); +const mockSimilar = vi.mocked(similarArtists); +afterEach(() => { mockSearch.mockReset(); mockSimilar.mockReset(); }); + +function req(artist?: string) { + const u = new URL("http://localhost/api/discover/search"); + if (artist !== undefined) u.searchParams.set("artist", artist); + return new Request(u); +} + +describe("discover search API", () => { + it("400s when artist is missing/blank", async () => { + expect((await GET(req())).status).toBe(400); + expect((await GET(req(" "))).status).toBe(400); + }); + + it("resolves the top MB hit then returns its similar artists", async () => { + mockSearch.mockResolvedValue([{ mbid: "seed", name: "Boygenius", disambiguation: "" }]); + mockSimilar.mockResolvedValue([{ mbid: "c1", name: "Cand", score: 9 }]); + const body = await (await GET(req("boygenius"))).json(); + expect(body.seed).toMatchObject({ mbid: "seed", name: "Boygenius" }); + expect(body.similar).toEqual([{ mbid: "c1", name: "Cand", score: 9 }]); + expect(mockSimilar).toHaveBeenCalledWith("seed"); + }); + + it("returns empty when MB has no hit, 502 on failures", async () => { + mockSearch.mockResolvedValue([]); + expect(await (await GET(req("nobody"))).json()).toEqual({ seed: null, similar: [] }); + mockSearch.mockRejectedValue(new Error("mb down")); + expect((await GET(req("x"))).status).toBe(502); + mockSearch.mockResolvedValue([{ mbid: "seed", name: "X", disambiguation: "" }]); + mockSimilar.mockRejectedValue(new Error("lb down")); + expect((await GET(req("x"))).status).toBe(502); + }); +}); diff --git a/web/src/app/api/discover/search/route.ts b/web/src/app/api/discover/search/route.ts new file mode 100644 index 0000000..a8cc0f2 --- /dev/null +++ b/web/src/app/api/discover/search/route.ts @@ -0,0 +1,24 @@ +import { searchArtists } from "@/lib/musicbrainz"; +import { similarArtists } from "@/lib/listenbrainz"; + +export async function GET(request: Request) { + const artist = new URL(request.url).searchParams.get("artist"); + if (!artist || !artist.trim()) { + return Response.json({ error: "artist is required" }, { status: 400 }); + } + let hits; + try { + hits = await searchArtists(artist.trim()); + } catch { + return Response.json({ error: "MusicBrainz unavailable" }, { status: 502 }); + } + if (hits.length === 0) return Response.json({ seed: null, similar: [] }); + const seed = hits[0]; + let similar; + try { + similar = await similarArtists(seed.mbid); + } catch { + return Response.json({ error: "ListenBrainz unavailable" }, { status: 502 }); + } + return Response.json({ seed, similar }); +}