import { describe, it, expect, vi, afterEach } from "vitest"; vi.mock("@/lib/musicbrainz", () => ({ browseReleaseGroups: vi.fn() })); import { browseReleaseGroups } from "@/lib/musicbrainz"; import { prisma } from "@/lib/db"; import { GET, POST } from "./route"; const mockBrowse = vi.mocked(browseReleaseGroups); afterEach(() => mockBrowse.mockReset()); function listReq(qs = "") { return new Request(`http://localhost/api/artists${qs}`); } function postReq(body: unknown) { return new Request("http://localhost/api/artists", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }); } describe("artists collection API", () => { it("follows an artist and persists the discography unmonitored", async () => { mockBrowse.mockResolvedValue([ { rgMbid: "rg1", title: "Continuum", primaryType: "Album", secondaryTypes: [], firstReleaseDate: "2006-09-12" }, { rgMbid: "rg2", title: "Battle Studies", primaryType: "Album", secondaryTypes: [], firstReleaseDate: "2009" }, ]); const res = await POST(postReq({ mbid: "a1", name: "John Mayer" })); expect(res.status).toBe(201); expect((await res.json()).releaseCount).toBe(2); const rels = await prisma.monitoredRelease.findMany({ where: { artistMbid: "a1" } }); expect(rels).toHaveLength(2); expect(rels.every((r) => r.monitored === false && r.state === "wanted")).toBe(true); }); it("rejects a bad body and a duplicate follow", async () => { expect((await POST(postReq({ mbid: "a1" }))).status).toBe(400); mockBrowse.mockResolvedValue([]); expect((await POST(postReq({ mbid: "a1", name: "John Mayer" }))).status).toBe(201); expect((await POST(postReq({ mbid: "a1", name: "John Mayer" }))).status).toBe(409); }); it("502s when the discography fetch fails and creates nothing", async () => { mockBrowse.mockRejectedValue(new Error("boom")); expect((await POST(postReq({ mbid: "a9", name: "X" }))).status).toBe(502); expect(await prisma.watchedArtist.findUnique({ where: { mbid: "a9" } })).toBeNull(); }); it("lists followed artists with counts", async () => { mockBrowse.mockResolvedValue([ { rgMbid: "rg1", title: "A", primaryType: "Album", secondaryTypes: [], firstReleaseDate: "2001" }, ]); await POST(postReq({ mbid: "a1", name: "John Mayer" })); // mark the one release as monitored + have, to exercise the counts await prisma.monitoredRelease.updateMany({ where: { artistMbid: "a1" }, data: { monitored: true, currentQualityClass: 2 } }); const res = await GET(listReq()); expect(res.status).toBe(200); const a = (await res.json()).artists[0]; expect(a).toMatchObject({ mbid: "a1", name: "John Mayer", releaseCount: 1, monitoredCount: 1, haveCount: 1 }); }); it("counts only core releases unless showAllTypes is set", async () => { await prisma.watchedArtist.create({ data: { mbid: "a2", name: "Live Band", releases: { create: [ { artistMbid: "a2", artistName: "Live Band", rgMbid: "rg-core", album: "Studio Album", primaryType: "Album", secondaryTypes: [], firstReleaseDate: "2010", monitored: true, state: "grabbed", currentQualityClass: 1 }, { artistMbid: "a2", artistName: "Live Band", rgMbid: "rg-live", album: "Live at the Roxy", primaryType: "Album", secondaryTypes: ["Live"], firstReleaseDate: "2011", monitored: true, state: "wanted", currentQualityClass: 2 }, ], }, }, }); const before = (await (await GET(listReq())).json()).artists.find((a: { mbid: string }) => a.mbid === "a2"); expect(before).toMatchObject({ showAllTypes: false, releaseCount: 1, monitoredCount: 1, haveCount: 1 }); await prisma.watchedArtist.update({ where: { mbid: "a2" }, data: { showAllTypes: true } }); const after = (await (await GET(listReq())).json()).artists.find((a: { mbid: string }) => a.mbid === "a2"); expect(after).toMatchObject({ showAllTypes: true, releaseCount: 2, monitoredCount: 2, haveCount: 2 }); }); it("GET lists owned artists that are not followed (case-insensitive)", async () => { // one owned + followed, one owned + not followed, one owned matching a followed name by case await prisma.watchedArtist.create({ data: { mbid: "wf", name: "Followed Artist" } }); for (const [artist, album] of [["Followed Artist", "A"], ["Unfollowed Artist", "B"], ["followed artist", "C"]] as const) { await prisma.libraryItem.create({ data: { artist, album, path: `/${album}`, source: "scan", format: "FLAC", qualityClass: 2 }, }); } const body = await (await GET(listReq())).json(); expect(body.ownedNotFollowed).toContain("Unfollowed Artist"); expect(body.ownedNotFollowed).not.toContain("Followed Artist"); expect(body.ownedNotFollowed).not.toContain("followed artist"); // case-insensitive match }); it("GET matches owned artists to the watched roster by MBID despite a name mismatch", async () => { // followed under the canonical MB name, but the library row's name differs (feat./locale) await prisma.watchedArtist.create({ data: { mbid: "mb-beyonce", name: "Beyoncé" } }); await prisma.libraryItem.create({ data: { artist: "Beyonce", album: "Lemonade", artistMbid: "mb-beyonce", path: "/l", source: "scan", format: "FLAC", qualityClass: 2 }, }); const body = await (await GET(listReq())).json(); expect(body.ownedNotFollowed).not.toContain("Beyonce"); // matched by MBID → not "unfollowed" }); it("GET ?mbid= returns the follow state for a single artist", async () => { await prisma.watchedArtist.create({ data: { mbid: "followed-1", name: "Followed" } }); expect((await (await GET(listReq("?mbid=followed-1"))).json()).followed).toBe(true); expect((await (await GET(listReq("?mbid=nope"))).json()).followed).toBe(false); }); });