/** * Tests fuer die Auswertung der Rueckkehr von Stripe. Die Adresszeile ist frei * waehlbar — "bezahlt" darf deshalb nur herauskommen, wenn Stripe es sagt und * die Sitzung zu einer eigenen Bestellung gehoert. */ import { describe, test, expect, beforeEach, vi } from "vitest"; const sessionsRetrieve = vi.fn(); vi.mock("@/lib/stripe", () => ({ getStripeClient: () => ({ checkout: { sessions: { retrieve: sessionsRetrieve } } }), })); 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 db = { order: { findFirst: vi.fn() } }; vi.mock("@/lib/prisma", () => ({ prisma: db })); const { resolveCheckoutReturn } = await import("./checkout-return"); const SESSION_ID = "cs_test_a1B2c3"; const ORDER = { id: "order_1", orderNumber: "WH-2026-AABBCCDD", status: "NEU", cancelToken: "a".repeat(64), }; function stripeSession(overrides: Record = {}) { return { id: SESSION_ID, status: "complete", payment_status: "paid", client_secret: "cs_test_a1B2c3_secret_x", metadata: { orderId: ORDER.id }, ...overrides, }; } beforeEach(() => { vi.clearAllMocks(); rateLimit.mockReturnValue(true); db.order.findFirst.mockResolvedValue(ORDER); sessionsRetrieve.mockResolvedValue(stripeSession()); vi.spyOn(console, "error").mockImplementation(() => {}); }); describe("resolveCheckoutReturn", () => { test("meldet eine bezahlte Sitzung mit der Bestellnummer aus der Datenbank", async () => { await expect(resolveCheckoutReturn(SESSION_ID)).resolves.toEqual({ kind: "paid", orderNumber: ORDER.orderNumber, }); }); test("meldet eine abgeschlossene, noch nicht eingegangene Zahlung als in Bearbeitung", async () => { sessionsRetrieve.mockResolvedValue(stripeSession({ payment_status: "unpaid" })); await expect(resolveCheckoutReturn(SESSION_ID)).resolves.toEqual({ kind: "processing", orderNumber: ORDER.orderNumber, }); }); test("bietet nach abgebrochener PayPal-Zahlung dem bestellenden Browser einen erneuten Versuch an", async () => { sessionsRetrieve.mockResolvedValue(stripeSession({ status: "open", payment_status: "unpaid" })); await expect(resolveCheckoutReturn(SESSION_ID, ORDER.cancelToken)).resolves.toEqual({ kind: "open", clientSecret: "cs_test_a1B2c3_secret_x", cancelToken: ORDER.cancelToken, }); }); test("gibt Zahlungsformular und Abbruch-Token nicht an einen fremden Browser mit derselben URL", async () => { // Die session_id steht in Verlauf, Proxy-Logs und Screenshots. Allein // darf sie keine fremde Bestellung oeffnen oder abbrechen. sessionsRetrieve.mockResolvedValue(stripeSession({ status: "open", payment_status: "unpaid" })); await expect(resolveCheckoutReturn(SESSION_ID)).resolves.toEqual({ kind: "notCompleted" }); await expect(resolveCheckoutReturn(SESSION_ID, "f".repeat(64))).resolves.toEqual({ kind: "notCompleted", }); }); test("fragt bei einer fremd geformten ID weder Datenbank noch Stripe", async () => { await expect(resolveCheckoutReturn('cs_test_1"; DROP')).resolves.toEqual({ kind: "notCompleted" }); await expect(resolveCheckoutReturn("pi_123")).resolves.toEqual({ kind: "notCompleted" }); expect(db.order.findFirst).not.toHaveBeenCalled(); expect(sessionsRetrieve).not.toHaveBeenCalled(); }); test("fragt Stripe gar nicht erst, wenn es keine passende Bestellung gibt", async () => { db.order.findFirst.mockResolvedValue(null); await expect(resolveCheckoutReturn(SESSION_ID)).resolves.toEqual({ kind: "notCompleted" }); expect(sessionsRetrieve).not.toHaveBeenCalled(); }); test("glaubt keiner Sitzung, die laut Metadaten zu einer anderen Bestellung gehoert", async () => { sessionsRetrieve.mockResolvedValue(stripeSession({ metadata: { orderId: "order_other" } })); await expect(resolveCheckoutReturn(SESSION_ID)).resolves.toEqual({ kind: "notCompleted" }); }); test("bietet eine abgelaufene Sitzung nicht zum Bezahlen an", async () => { sessionsRetrieve.mockResolvedValue(stripeSession({ status: "expired", payment_status: "unpaid" })); await expect(resolveCheckoutReturn(SESSION_ID)).resolves.toEqual({ kind: "notCompleted" }); }); test("bremst viele Abfragen derselben Adresse, bevor Stripe gefragt wird", async () => { rateLimit.mockReturnValue(false); await expect(resolveCheckoutReturn(SESSION_ID)).resolves.toEqual({ kind: "notCompleted" }); expect(sessionsRetrieve).not.toHaveBeenCalled(); }); test("vertraut bei einem Stripe-Ausfall dem vom Webhook bereits verbuchten Status", async () => { db.order.findFirst.mockResolvedValue({ ...ORDER, status: "BEZAHLT" }); sessionsRetrieve.mockRejectedValue(new Error("ECONNRESET")); await expect(resolveCheckoutReturn(SESSION_ID)).resolves.toEqual({ kind: "paid", orderNumber: ORDER.orderNumber, }); }); test("behauptet bei einem Stripe-Ausfall und offener Bestellung keine Zahlung", async () => { sessionsRetrieve.mockRejectedValue(new Error("ECONNRESET")); await expect(resolveCheckoutReturn(SESSION_ID)).resolves.toEqual({ kind: "notCompleted" }); }); });