docfast/src/__tests__/billing-coverage-fix.test.ts
OpenClaw Subagent e6638fb929 test: improve billing webhook branch coverage
Added edge case tests for:
- Line 231-233: checkout.session.completed webhook catch block when session.retrieve fails
- Line 165: /success route !customerId check with various falsy values
- Line 315: getOrCreateProPrice() else branch when no product exists

All 827 tests pass.
2026-03-19 20:12:28 +01:00

153 lines
4.8 KiB
TypeScript

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" },
});
});
});
});