/** Abo-Abschluss: nur mit verifiziertem Konto, nur fuer verfuegbare Abo-Angebote, mit gepruefter Personalisierung. */ import { beforeEach, describe, expect, test, vi } from "vitest"; const m = vi.hoisted(() => ({ getCurrentCustomer: vi.fn(), getOfferBySlug: vi.fn(), createSubscriptionCheckout: vi.fn(), preparePendingCheckout: vi.fn(), isTrustedOrigin: vi.fn(), })); vi.mock("@/lib/auth", () => ({ getCurrentCustomer: m.getCurrentCustomer })); vi.mock("@/lib/csrf", () => ({ isTrustedOrigin: m.isTrustedOrigin })); vi.mock("@/lib/rate-limit", () => ({ rateLimit: () => true, getClientIp: async () => "127.0.0.1" })); vi.mock("@/i18n/locale", () => ({ getLocale: async () => "de" })); vi.mock("@/lib/sponsorship/offers", () => ({ getOfferBySlug: m.getOfferBySlug })); vi.mock("@/lib/sponsorship-subscription/checkout", () => ({ createSubscriptionCheckout: m.createSubscriptionCheckout })); vi.mock("@/lib/sponsorship-subscription/pending", () => ({ preparePendingCheckout: m.preparePendingCheckout })); vi.mock("@/lib/certificate/layout", () => ({ checkCertificateFits: () => ({ ok: true }) })); const { POST } = await import("./route"); const CUSTOMER = { id: "cust_1", email: "erika@example.org", emailVerifiedAt: new Date(), stripeCustomerId: null, sepaBlockedAt: null }; const PERSONALIZATION = { beeName: "Summsi", recipientName: "Erika Mustermann", occasion: "SELBST", startDate: "2030-01-01" }; function post(body: unknown): Promise { return POST( new Request("https://wabenhain.test/api/patenschaft/abo", { method: "POST", headers: { "Content-Type": "application/json", Origin: "https://wabenhain.test" }, body: JSON.stringify(body), }) ); } beforeEach(() => { vi.clearAllMocks(); vi.spyOn(console, "error").mockImplementation(() => {}); m.isTrustedOrigin.mockReturnValue(true); m.getCurrentCustomer.mockResolvedValue(CUSTOMER); m.getOfferBySlug.mockResolvedValue({ id: "offer_1", slug: "vierteljaehrlich", kind: "ABO", isAvailable: true }); m.createSubscriptionCheckout.mockResolvedValue({ clientSecret: "cs_secret", subscriptionId: "sub_local" }); m.preparePendingCheckout.mockResolvedValue({ ok: true }); }); describe("POST /api/patenschaft/abo", () => { test("liefert das client_secret und ersetzt das Startdatum durch heute", async () => { const response = await post({ offerSlug: "vierteljaehrlich", personalization: PERSONALIZATION }); expect(response.status).toBe(200); expect(await response.json()).toEqual({ clientSecret: "cs_secret" }); const input = m.createSubscriptionCheckout.mock.calls[0][0]; expect(input.personalization.startDate).toBe(new Date().toISOString().slice(0, 10)); expect(input.offerId).toBe("offer_1"); }); test("weist fremde Herkunft ab", async () => { m.isTrustedOrigin.mockReturnValue(false); expect((await post({ offerSlug: "x", personalization: PERSONALIZATION })).status).toBe(403); }); test("ohne Konto 401, ohne bestaetigte Adresse 403", async () => { m.getCurrentCustomer.mockResolvedValueOnce(null); expect((await post({ offerSlug: "vierteljaehrlich", personalization: PERSONALIZATION })).status).toBe(401); m.getCurrentCustomer.mockResolvedValueOnce({ ...CUSTOMER, emailVerifiedAt: null }); expect((await post({ offerSlug: "vierteljaehrlich", personalization: PERSONALIZATION })).status).toBe(403); expect(m.createSubscriptionCheckout).not.toHaveBeenCalled(); }); test("ungueltige Personalisierung wird abgelehnt", async () => { const response = await post({ offerSlug: "vierteljaehrlich", personalization: { ...PERSONALIZATION, beeName: "" } }); expect(response.status).toBe(400); expect((await response.json()).field).toBe("beeName"); }); test("Einmal-Angebote laufen nicht ueber den Abo-Weg", async () => { m.getOfferBySlug.mockResolvedValue({ id: "offer_2", slug: "bienenfreund", kind: "EINMALIG", isAvailable: true }); expect((await post({ offerSlug: "bienenfreund", personalization: PERSONALIZATION })).status).toBe(404); }); test("ausgebuchtes Abo (Kontingent oder Honig) nimmt keine Abschluesse an", async () => { m.getOfferBySlug.mockResolvedValue({ id: "offer_1", slug: "vierteljaehrlich", kind: "ABO", isAvailable: false }); expect((await post({ offerSlug: "vierteljaehrlich", personalization: PERSONALIZATION })).status).toBe(409); expect(m.createSubscriptionCheckout).not.toHaveBeenCalled(); }); test("ein Stripe-Fehler wird ohne Details gemeldet", async () => { m.createSubscriptionCheckout.mockRejectedValue(new Error("Invalid API Key provided: sk_test_***")); const response = await post({ offerSlug: "vierteljaehrlich", personalization: PERSONALIZATION }); expect(response.status).toBe(502); expect(JSON.stringify(await response.json())).not.toContain("sk_test"); }); test("bereitet den Abschluss vor (alter offener Abschluss desselben Angebots wird ersetzt)", async () => { await post({ offerSlug: "vierteljaehrlich", personalization: PERSONALIZATION }); expect(m.preparePendingCheckout).toHaveBeenCalledWith({ customerId: "cust_1", offerId: "offer_1" }); expect(m.preparePendingCheckout.mock.invocationCallOrder[0]).toBeLessThan(m.createSubscriptionCheckout.mock.invocationCallOrder[0]); }); test("mehr als drei offene Abschluesse je Konto: klare Ablehnung, kein neuer Checkout", async () => { m.preparePendingCheckout.mockResolvedValue({ ok: false, reason: "too_many_pending" }); const response = await post({ offerSlug: "vierteljaehrlich", personalization: PERSONALIZATION }); expect(response.status).toBe(409); expect(await response.json()).toMatchObject({ code: "too_many_pending", error: expect.stringContaining("offene Abo-Abschlüsse") }); expect(m.createSubscriptionCheckout).not.toHaveBeenCalled(); }); });