/** * Tests fuer den Abbruch einer offenen Zahlung. Kernpunkt ist die * Reihenfolge: erst bei Stripe nachsehen, dann zurueckbuchen — sonst liesse * sich eine bereits bezahlte Bestellung stornieren und doppelt verkaufen. */ import { describe, test, expect, beforeEach, vi } from "vitest"; const sessionsRetrieve = vi.fn(); const sessionsExpire = vi.fn(); vi.mock("@/lib/stripe", () => ({ getStripeClient: () => ({ checkout: { sessions: { retrieve: sessionsRetrieve, expire: sessionsExpire } }, }), })); const rateLimit = vi.fn<(key: string, limit: number, windowMs: number) => boolean>(() => true); vi.mock("@/lib/rate-limit", () => ({ rateLimit: (key: string, limit: number, windowMs: number) => rateLimit(key, limit, windowMs), getClientIp: vi.fn(async () => "203.0.113.7"), })); const releaseUnpaidOrder = vi.fn(); vi.mock("@/lib/order-release", () => ({ releaseUnpaidOrder: (...args: unknown[]) => releaseUnpaidOrder(...args), })); const db = { order: { findUnique: vi.fn() } }; vi.mock("@/lib/prisma", () => ({ prisma: db })); const { releaseCheckout } = await import("./checkout-release"); const TOKEN = "b".repeat(64); const ORDER = { id: "order_1", orderNumber: "WH-2026-AABBCCDD", status: "NEU", stripeCheckoutSessionId: "cs_test_1", }; beforeEach(() => { vi.clearAllMocks(); rateLimit.mockReturnValue(true); db.order.findUnique.mockResolvedValue(ORDER); sessionsRetrieve.mockResolvedValue({ status: "open" }); sessionsExpire.mockResolvedValue({}); releaseUnpaidOrder.mockResolvedValue({ orderNumber: ORDER.orderNumber }); vi.spyOn(console, "info").mockImplementation(() => {}); vi.spyOn(console, "error").mockImplementation(() => {}); }); describe("releaseCheckout", () => { test("schliesst eine offene Sitzung erst und gibt danach den Bestand frei", async () => { await releaseCheckout(TOKEN); expect(sessionsExpire).toHaveBeenCalledWith("cs_test_1"); expect(releaseUnpaidOrder).toHaveBeenCalledWith(ORDER.id, "delete"); expect(sessionsExpire.mock.invocationCallOrder[0]).toBeLessThan( releaseUnpaidOrder.mock.invocationCallOrder[0] ); }); test("laesst eine bei Stripe bereits bezahlte Bestellung unangetastet", async () => { sessionsRetrieve.mockResolvedValue({ status: "complete" }); await releaseCheckout(TOKEN); expect(sessionsExpire).not.toHaveBeenCalled(); expect(releaseUnpaidOrder).not.toHaveBeenCalled(); }); test("gibt nichts frei, wenn das Schliessen bei Stripe scheitert", async () => { sessionsExpire.mockRejectedValue(new Error("stripe down")); await expect(releaseCheckout(TOKEN)).resolves.toBeUndefined(); expect(releaseUnpaidOrder).not.toHaveBeenCalled(); }); test("ignoriert Tokens im falschen Format, ohne die Datenbank zu fragen", async () => { await releaseCheckout("WH-2026-AABBCCDD"); expect(db.order.findUnique).not.toHaveBeenCalled(); }); test("tut nichts bei einer nicht mehr offenen Bestellung", async () => { db.order.findUnique.mockResolvedValue({ ...ORDER, status: "BEZAHLT" }); await releaseCheckout(TOKEN); expect(sessionsRetrieve).not.toHaveBeenCalled(); expect(releaseUnpaidOrder).not.toHaveBeenCalled(); }); test("bremst viele Abbrueche derselben Adresse", async () => { rateLimit.mockReturnValue(false); await releaseCheckout(TOKEN); expect(db.order.findUnique).not.toHaveBeenCalled(); }); });