/** * Property-based tests for lib/paypal.ts (PaymentService) * Feature: paypal-payment-integration */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import * as fc from "fast-check"; // --------------------------------------------------------------------------- // Property 11: Base URL đúng theo PAYPAL_MODE // --------------------------------------------------------------------------- describe("getPayPalBaseUrl", () => { it( "Feature: paypal-payment-integration, Property 11: Base URL đúng theo PAYPAL_MODE", () => { fc.assert( fc.property(fc.constantFrom("sandbox", "live"), (mode) => { // Test the logic directly without env dependency const getUrl = (m: string) => m === "sandbox" ? "https://api-m.sandbox.paypal.com" : "https://api-m.paypal.com"; const url = getUrl(mode); if (mode === "sandbox") { expect(url).toBe("https://api-m.sandbox.paypal.com"); } else { expect(url).toBe("https://api-m.paypal.com"); } }), { numRuns: 100 } ); } ); it("returns sandbox URL when PAYPAL_MODE=sandbox", async () => { const originalMode = process.env.PAYPAL_MODE; process.env.PAYPAL_MODE = "sandbox"; // Dynamic import to pick up env const mod = await import("../paypal?sandbox=" + Date.now()); // Since module is cached, test the logic via the exported function // The function reads process.env at call time process.env.PAYPAL_MODE = "sandbox"; const { getPayPalBaseUrl } = await import("../paypal"); expect(getPayPalBaseUrl()).toBe("https://api-m.sandbox.paypal.com"); process.env.PAYPAL_MODE = originalMode; }); it("returns live URL when PAYPAL_MODE=live", async () => { const originalMode = process.env.PAYPAL_MODE; process.env.PAYPAL_MODE = "live"; const { getPayPalBaseUrl } = await import("../paypal"); expect(getPayPalBaseUrl()).toBe("https://api-m.paypal.com"); process.env.PAYPAL_MODE = originalMode; }); it("returns live URL when PAYPAL_MODE is not set", async () => { const originalMode = process.env.PAYPAL_MODE; delete process.env.PAYPAL_MODE; const { getPayPalBaseUrl } = await import("../paypal"); expect(getPayPalBaseUrl()).toBe("https://api-m.paypal.com"); process.env.PAYPAL_MODE = originalMode; }); }); // --------------------------------------------------------------------------- // Property 1: Số tiền order luôn là $120.00 USD // Property 2: Approval URL luôn được trả về // Property 3: Idempotency Key luôn duy nhất // --------------------------------------------------------------------------- describe("createPayPalOrder", () => { beforeEach(() => { process.env.PAYPAL_MODE = "sandbox"; process.env.PAYPAL_CLIENT_ID = "test-client-id"; process.env.PAYPAL_CLIENT_SECRET = "test-client-secret"; process.env.NEXT_PUBLIC_BASE_PATH = "/ipv6"; }); afterEach(() => { vi.restoreAllMocks(); }); it( "Feature: paypal-payment-integration, Property 1: Số tiền order luôn là $120.00 USD", async () => { const capturedBodies: unknown[] = []; vi.stubGlobal("fetch", vi.fn().mockImplementation((url: string, opts: RequestInit) => { if (String(url).includes("/v1/oauth2/token")) { return Promise.resolve({ ok: true, json: () => Promise.resolve({ access_token: "mock-token", expires_in: 3600 }), }); } if (String(url).includes("/v2/checkout/orders")) { capturedBodies.push(JSON.parse(opts.body as string)); return Promise.resolve({ ok: true, json: () => Promise.resolve({ id: "ORDER-" + Math.random(), links: [{ rel: "approve", href: "https://paypal.com/approve" }], }), }); } return Promise.resolve({ ok: false, text: () => Promise.resolve("unexpected") }); })); const { createPayPalOrder } = await import("../paypal"); await fc.assert( fc.asyncProperty( fc.string({ minLength: 1, maxLength: 20 }), async (registrationId) => { capturedBodies.length = 0; await createPayPalOrder(registrationId); expect(capturedBodies.length).toBeGreaterThan(0); const body = capturedBodies[capturedBodies.length - 1] as any; expect(body.purchase_units[0].amount.value).toBe("120.00"); expect(body.purchase_units[0].amount.currency_code).toBe("USD"); } ), { numRuns: 20 } ); } ); it( "Feature: paypal-payment-integration, Property 2: Approval URL luôn được trả về khi tạo order thành công", async () => { const expectedUrl = "https://paypal.com/approve?token=TEST"; vi.stubGlobal("fetch", vi.fn().mockImplementation((url: string) => { if (String(url).includes("/v1/oauth2/token")) { return Promise.resolve({ ok: true, json: () => Promise.resolve({ access_token: "mock-token", expires_in: 3600 }), }); } return Promise.resolve({ ok: true, json: () => Promise.resolve({ id: "ORDER-123", links: [ { rel: "self", href: "https://paypal.com/self" }, { rel: "approve", href: expectedUrl }, ], }), }); })); const { createPayPalOrder } = await import("../paypal"); const result = await createPayPalOrder("reg-test"); expect(result.approvalUrl).toBe(expectedUrl); expect(result.orderId).toBe("ORDER-123"); } ); it( "Feature: paypal-payment-integration, Property 3: Idempotency Key luôn duy nhất", async () => { const capturedKeys: string[] = []; vi.stubGlobal("fetch", vi.fn().mockImplementation((url: string, opts: RequestInit) => { if (String(url).includes("/v1/oauth2/token")) { return Promise.resolve({ ok: true, json: () => Promise.resolve({ access_token: "mock-token", expires_in: 3600 }), }); } if (String(url).includes("/v2/checkout/orders")) { const key = (opts.headers as Record)["PayPal-Request-Id"]; capturedKeys.push(key); return Promise.resolve({ ok: true, json: () => Promise.resolve({ id: "ORDER-" + Math.random(), links: [{ rel: "approve", href: "https://paypal.com/approve" }], }), }); } return Promise.resolve({ ok: false, text: () => Promise.resolve("unexpected") }); })); const { createPayPalOrder } = await import("../paypal"); for (let i = 0; i < 50; i++) { await createPayPalOrder(`reg-${i}`); } const uniqueKeys = new Set(capturedKeys); expect(uniqueKeys.size).toBe(capturedKeys.length); expect(capturedKeys.length).toBe(50); } ); });