feat(discovery): POST /api/discover/[id] follow/want/dismiss actions

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-12 00:38:06 +02:00
parent bf868829d8
commit c7aeab5176
2 changed files with 164 additions and 0 deletions
@@ -0,0 +1,83 @@
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 { POST } from "./route";
const mockBrowse = vi.mocked(browseReleaseGroups);
afterEach(() => mockBrowse.mockReset());
function post(id: string, body: unknown) {
return POST(
new Request(`http://localhost/api/discover/${id}`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
}),
{ params: Promise.resolve({ id }) },
);
}
async function artist(mbid = "a1") {
return prisma.discoverySuggestion.create({
data: { kind: "artist", artistMbid: mbid, artistName: "Cand", score: 1, seedCount: 1,
sources: ["listenbrainz"], secondaryTypes: [], dedupeKey: `artist:${mbid}:` },
});
}
async function album(rg = "rg1") {
return prisma.discoverySuggestion.create({
data: { kind: "album", artistMbid: "a9", artistName: "Band", rgMbid: rg, album: "Rec",
primaryType: "Album", score: 1, seedCount: 1, sources: ["listenbrainz"],
secondaryTypes: [], firstReleaseDate: "2023", dedupeKey: `album:a9:${rg}` },
});
}
describe("discover action API", () => {
it("dismiss marks the suggestion dismissed", async () => {
const s = await artist("d1");
const res = await post(s.id, { action: "dismiss" });
expect(res.status).toBe(200);
expect((await prisma.discoverySuggestion.findUnique({ where: { id: s.id } }))!.status).toBe("dismissed");
});
it("follow creates a WatchedArtist + unmonitored discography and marks followed", async () => {
mockBrowse.mockResolvedValue([
{ rgMbid: "rg1", title: "Debut", primaryType: "Album", secondaryTypes: [], firstReleaseDate: "2019" },
]);
const s = await artist("f1");
const res = await post(s.id, { action: "follow" });
expect(res.status).toBe(200);
expect(await prisma.watchedArtist.findUnique({ where: { mbid: "f1" } })).not.toBeNull();
const rel = await prisma.monitoredRelease.findUnique({ where: { rgMbid: "rg1" } });
expect(rel).toMatchObject({ monitored: false, artistName: "Cand" });
expect((await prisma.discoverySuggestion.findUnique({ where: { id: s.id } }))!.status).toBe("followed");
});
it("follow is idempotent when already following (no MB call, still marks followed)", async () => {
await prisma.watchedArtist.create({ data: { mbid: "f2", name: "Cand" } });
const s = await artist("f2");
const res = await post(s.id, { action: "follow" });
expect(res.status).toBe(200);
expect(mockBrowse).not.toHaveBeenCalled();
expect((await prisma.discoverySuggestion.findUnique({ where: { id: s.id } }))!.status).toBe("followed");
});
it("want upserts a monitored release from the suggestion and marks wanted", async () => {
const s = await album("rgW");
const res = await post(s.id, { action: "want" });
expect(res.status).toBe(200);
const rel = await prisma.monitoredRelease.findUnique({ where: { rgMbid: "rgW" } });
expect(rel).toMatchObject({ album: "Rec", monitored: true, artistName: "Band" });
expect((await prisma.discoverySuggestion.findUnique({ where: { id: s.id } }))!.status).toBe("wanted");
});
it("rejects bad action, mismatched kind, and unknown id", async () => {
const a = await artist("m1");
expect((await post(a.id, { action: "nope" })).status).toBe(400);
expect((await post(a.id, { action: "want" })).status).toBe(400); // want on artist
const al = await album("rgM");
expect((await post(al.id, { action: "follow" })).status).toBe(400); // follow on album
expect((await post("missing", { action: "dismiss" })).status).toBe(404);
});
});