import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("next/cache", () => ({ revalidateTag: vi.fn() })); vi.mock("@/src/lib/prisma", () => { const prisma = { marketingConsent: { findUnique: vi.fn(), upsert: vi.fn() }, marketingAuditLog: { createMany: vi.fn() }, $transaction: vi.fn(), }; prisma.$transaction.mockImplementation((fn: (tx: unknown) => unknown) => fn(prisma)); return { prisma }; }); import { Prisma } from "@/src/generated/prisma/client"; import { prisma } from "@/src/lib/prisma"; import { setMarketingConsent } from "@/src/modules/marketing/lib/consent"; import { CONSENT_TEXT_VERSION } from "@/src/modules/marketing/lib/consent-text"; beforeEach(() => { vi.clearAllMocks(); }); describe("setMarketingConsent", () => { it("speichert die Einwilligung mit Wortlaut-Version und protokolliert sie", async () => { vi.mocked(prisma.marketingConsent.findUnique).mockResolvedValue(null); const changed = await setMarketingConsent({ userId: "user-1", businessId: "biz-1", granted: true, source: "course_booking", }); expect(changed).toBe(true); const upsert = vi.mocked(prisma.marketingConsent.upsert).mock.calls[0][0]; expect(upsert.create).toMatchObject({ granted: true, textVersion: CONSENT_TEXT_VERSION, source: "course_booking", }); expect(upsert.create?.grantedAt).toBeInstanceOf(Date); const [entry] = vi.mocked(prisma.marketingAuditLog.createMany).mock.calls[0][0]!.data as { action: string; actorType: string; userId: string; }[]; expect(entry).toMatchObject({ action: "consent.granted", actorType: "customer", userId: "user-1", }); }); it("protokolliert den Widerruf", async () => { vi.mocked(prisma.marketingConsent.findUnique).mockResolvedValue({ granted: true } as never); await setMarketingConsent({ userId: "user-1", businessId: "biz-1", granted: false, source: "unsubscribe_link", }); const upsert = vi.mocked(prisma.marketingConsent.upsert).mock.calls[0][0]; expect(upsert.update).toMatchObject({ granted: false }); expect(upsert.update?.revokedAt).toBeInstanceOf(Date); const [entry] = vi.mocked(prisma.marketingAuditLog.createMany).mock.calls[0][0]!.data as { action: string; }[]; expect(entry.action).toBe("consent.revoked"); }); it("ändert nichts, wenn der Stand schon stimmt", async () => { vi.mocked(prisma.marketingConsent.findUnique).mockResolvedValue({ granted: true } as never); const changed = await setMarketingConsent({ userId: "user-1", businessId: "biz-1", granted: true, source: "profile", }); expect(changed).toBe(false); expect(prisma.marketingConsent.upsert).not.toHaveBeenCalled(); expect(prisma.marketingAuditLog.createMany).not.toHaveBeenCalled(); }); it("versucht es einmal neu, wenn eine parallele Anfrage die Zeile zuerst anlegt", async () => { // Beide Anfragen lesen "keine Zeile"; unsere scheitert beim Anlegen am Unique-Key. // Im zweiten Durchlauf ist die Zeile der anderen Anfrage da. vi.mocked(prisma.marketingConsent.findUnique) .mockResolvedValueOnce(null) .mockResolvedValueOnce({ granted: true } as never); vi.mocked(prisma.marketingConsent.upsert).mockRejectedValueOnce( new Prisma.PrismaClientKnownRequestError("Unique constraint failed", { code: "P2002", clientVersion: "test", }) ); const changed = await setMarketingConsent({ userId: "user-1", businessId: "biz-1", granted: true, source: "profile", }); expect(changed).toBe(false); expect(prisma.marketingConsent.findUnique).toHaveBeenCalledTimes(2); }); it("gibt andere Fehler weiter", async () => { vi.mocked(prisma.marketingConsent.findUnique).mockRejectedValue(new Error("DB weg")); await expect( setMarketingConsent({ userId: "u", businessId: "b", granted: true, source: "profile" }) ).rejects.toThrow("DB weg"); }); });