feat: MusicBrainz proxy API routes (artist search + discography browse)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
|
||||
vi.mock("@/lib/musicbrainz", () => ({ browseReleaseGroups: vi.fn() }));
|
||||
import { browseReleaseGroups } from "@/lib/musicbrainz";
|
||||
import { GET } from "./route";
|
||||
|
||||
const mockBrowse = vi.mocked(browseReleaseGroups);
|
||||
// NOTE: reset in afterEach, not beforeEach - see the matching note in
|
||||
// ../route.test.ts and task-2-report.md for the root cause.
|
||||
afterEach(() => mockBrowse.mockReset());
|
||||
|
||||
const ctx = (mbid: string) => ({ params: Promise.resolve({ mbid }) });
|
||||
|
||||
describe("GET /api/mb/artists/[mbid]/releases", () => {
|
||||
it("returns the discography", async () => {
|
||||
mockBrowse.mockResolvedValue([
|
||||
{ rgMbid: "rg1", title: "Continuum", primaryType: "Album", secondaryTypes: [], firstReleaseDate: "2006" },
|
||||
]);
|
||||
const res = await GET(new Request("http://localhost/x"), ctx("a1"));
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.releases[0].rgMbid).toBe("rg1");
|
||||
expect(mockBrowse).toHaveBeenCalledWith("a1");
|
||||
});
|
||||
|
||||
it("502s when MusicBrainz throws", async () => {
|
||||
mockBrowse.mockRejectedValue(new Error("boom"));
|
||||
expect((await GET(new Request("http://localhost/x"), ctx("a1"))).status).toBe(502);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { browseReleaseGroups } from "@/lib/musicbrainz";
|
||||
|
||||
export async function GET(_request: Request, { params }: { params: Promise<{ mbid: string }> }) {
|
||||
const { mbid } = await params;
|
||||
try {
|
||||
return Response.json({ releases: await browseReleaseGroups(mbid) });
|
||||
} catch {
|
||||
return Response.json({ error: "MusicBrainz unavailable" }, { status: 502 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
|
||||
vi.mock("@/lib/musicbrainz", () => ({ searchArtists: vi.fn() }));
|
||||
import { searchArtists } from "@/lib/musicbrainz";
|
||||
import { GET } from "./route";
|
||||
|
||||
const mockSearch = vi.mocked(searchArtists);
|
||||
// NOTE: reset in afterEach, not beforeEach (deviation from the brief - see task-2-report.md).
|
||||
// In this Vitest 2.1.8 + Node 22.23.1 environment, resetting a vi.mock()-produced spy in
|
||||
// beforeEach causes a later mockRejectedValue() + await/catch in the same test to be
|
||||
// misreported as a failing test, even though the rejection is handled correctly. Moving the
|
||||
// identical reset call to afterEach gives the same inter-test isolation and avoids the false
|
||||
// failure. Reproduced independent of this repo (bare vitest 2.1.8/2.1.9/3.2.4 project) and
|
||||
// independent of the Node wrapper (same result with the raw fnm-installed node binary).
|
||||
afterEach(() => mockSearch.mockReset());
|
||||
|
||||
function req(q: string | null) {
|
||||
return new Request(`http://localhost/api/mb/artists${q === null ? "" : `?q=${encodeURIComponent(q)}`}`);
|
||||
}
|
||||
|
||||
describe("GET /api/mb/artists", () => {
|
||||
it("returns hits for a query", async () => {
|
||||
mockSearch.mockResolvedValue([{ mbid: "a1", name: "John Mayer", disambiguation: "" }]);
|
||||
const res = await GET(req("john"));
|
||||
expect(res.status).toBe(200);
|
||||
expect((await res.json()).artists[0].name).toBe("John Mayer");
|
||||
});
|
||||
|
||||
it("400s a blank query", async () => {
|
||||
expect((await GET(req(" "))).status).toBe(400);
|
||||
expect((await GET(req(null))).status).toBe(400);
|
||||
});
|
||||
|
||||
it("502s when MusicBrainz throws", async () => {
|
||||
mockSearch.mockRejectedValue(new Error("MusicBrainz request failed: 503"));
|
||||
expect((await GET(req("john"))).status).toBe(502);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import { searchArtists } from "@/lib/musicbrainz";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const q = new URL(request.url).searchParams.get("q");
|
||||
if (!q || !q.trim()) {
|
||||
return Response.json({ error: "q is required" }, { status: 400 });
|
||||
}
|
||||
try {
|
||||
return Response.json({ artists: await searchArtists(q.trim()) });
|
||||
} catch {
|
||||
return Response.json({ error: "MusicBrainz unavailable" }, { status: 502 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user