import { describe, it, expect, vi, beforeAll, afterAll, beforeEach } from "vitest"; import express from "express"; import request from "supertest"; // Mock Stripe ONCE before any imports const mockStripe = { checkout: { sessions: { create: vi.fn(), retrieve: vi.fn(), }, }, webhooks: { constructEvent: vi.fn(), }, products: { search: vi.fn(), create: vi.fn(), }, prices: { list: vi.fn(), create: vi.fn(), }, subscriptions: { retrieve: vi.fn(), }, }; vi.mock("stripe", () => { return { default: vi.fn(function() { return mockStripe; }) }; }); // Mock keys service vi.mock("../services/keys.js", () => ({ createProKey: vi.fn().mockResolvedValue({ key: "pro-key-123", tier: "pro", email: "test@test.com", createdAt: new Date().toISOString() }), findKeyByCustomerId: vi.fn().mockResolvedValue(null), downgradeByCustomer: vi.fn().mockResolvedValue(undefined), updateEmailByCustomer: vi.fn().mockResolvedValue(true), })); // Set env BEFORE importing billing router (NO module reset!) process.env.STRIPE_SECRET_KEY = "sk_test_fake_no_reset"; process.env.STRIPE_WEBHOOK_SECRET = "whsec_test_fake_no_reset"; // Import billing router ONCE import { billingRouter } from "../routes/billing.js"; import { createProKey, findKeyByCustomerId } from "../services/keys.js"; let app: express.Express; beforeAll(() => { // Setup Express app once app = express(); app.use("/v1/billing/webhook", express.raw({ type: "application/json" })); app.use(express.json()); app.use("/v1/billing", billingRouter); }); beforeEach(() => { // Only clear mock calls, DON'T reset modules vi.clearAllMocks(); // Reset default mock implementations mockStripe.products.search.mockResolvedValue({ data: [{ id: "prod_TygeG8tQPtEAdE" }] }); mockStripe.prices.list.mockResolvedValue({ data: [{ id: "price_existing_123" }] }); vi.mocked(createProKey).mockResolvedValue({ key: "pro-key-123", tier: "pro", email: "test@test.com", createdAt: new Date().toISOString() } as any); vi.mocked(findKeyByCustomerId).mockResolvedValue(null); }); describe("Billing Coverage Fix - NO MODULE RESET", () => { describe("Line 231-233: checkout.session.completed webhook catch block", () => { it("should gracefully handle Stripe API error when retrieving session", async () => { mockStripe.webhooks.constructEvent.mockReturnValue({ type: "checkout.session.completed", data: { object: { id: "cs_api_error", customer: "cus_api_error", customer_details: { email: "error@test.com" }, }, }, }); // Trigger the catch block: session.retrieve throws mockStripe.checkout.sessions.retrieve.mockRejectedValueOnce(new Error("Stripe API is down")); const res = await request(app) .post("/v1/billing/webhook") .set("content-type", "application/json") .set("stripe-signature", "valid_sig") .send(JSON.stringify({ type: "checkout.session.completed" })); expect(res.status).toBe(200); expect(res.body.received).toBe(true); // Key should NOT be provisioned due to error expect(createProKey).not.toHaveBeenCalled(); }); }); describe("Line 165: /success route !customerId branch", () => { it("should return 400 when customer is null", async () => { mockStripe.checkout.sessions.retrieve.mockResolvedValueOnce({ id: "cs_null_cust", customer: null, customer_details: { email: "test@test.com" }, }); const res = await request(app).get("/v1/billing/success?session_id=cs_null_cust"); expect(res.status).toBe(400); expect(res.body.error).toContain("No customer found"); }); }); describe("Line 315: getOrCreateProPrice() else branch - create new product", () => { it("should create new product and price when none exist", async () => { // Clear cache by calling checkout with empty product list mockStripe.products.search.mockResolvedValueOnce({ data: [] }); mockStripe.products.create.mockResolvedValueOnce({ id: "prod_new_123" }); mockStripe.prices.create.mockResolvedValueOnce({ id: "price_new_456" }); mockStripe.checkout.sessions.create.mockResolvedValueOnce({ id: "cs_new", url: "https://checkout.stripe.com/pay/cs_new" }); const res = await request(app) .post("/v1/billing/checkout") .send({}); expect(res.status).toBe(200); expect(mockStripe.products.create).toHaveBeenCalledWith({ name: "DocFast Pro", description: "5,000 PDFs / month via API. HTML, Markdown, and URL to PDF.", }); expect(mockStripe.prices.create).toHaveBeenCalledWith({ product: "prod_new_123", unit_amount: 900, currency: "eur", recurring: { interval: "month" }, }); }); }); });