test: improve branch coverage for billing.ts and keys.ts
Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Failing after 17m26s

billing.ts branches: 78.66% → 82.66%
- isDocFastSubscription expanded product object (not just string)
- isDocFastSubscription error handling path
- getOrCreateProPrice: new product + price creation
- getOrCreateProPrice: existing product, no active prices

keys.ts:
- createProKey UPSERT ON CONFLICT path (DB-only key)
- downgradeByCustomer customer not found in cache or DB
- updateKeyEmail DB fallback not-found path
- updateEmailByCustomer DB fallback not-found path

14 new tests, all 743 passing.
This commit is contained in:
OpenClaw Subagent 2026-03-14 11:09:01 +01:00
parent 2bdf93d09f
commit f5ec837e20
2 changed files with 482 additions and 0 deletions

View file

@ -0,0 +1,260 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import express from "express";
import request from "supertest";
// Mock Stripe before importing billing router
vi.mock("stripe", () => {
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(),
},
};
return { default: vi.fn(function() { return mockStripe; }), __mockStripe: mockStripe };
});
let app: express.Express;
let mockStripe: any;
beforeEach(async () => {
vi.clearAllMocks();
vi.resetModules();
process.env.STRIPE_SECRET_KEY = "sk_test_fake";
process.env.STRIPE_WEBHOOK_SECRET = "whsec_test_fake";
const stripeMod = await import("stripe");
mockStripe = (stripeMod as any).__mockStripe;
// Default: product search returns existing product+price
mockStripe.products.search.mockResolvedValue({ data: [{ id: "prod_TygeG8tQPtEAdE" }] });
mockStripe.prices.list.mockResolvedValue({ data: [{ id: "price_123" }] });
const { createProKey, findKeyByCustomerId, downgradeByCustomer, updateEmailByCustomer } = await import("../services/keys.js");
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);
vi.mocked(downgradeByCustomer).mockResolvedValue(undefined as any);
vi.mocked(updateEmailByCustomer).mockResolvedValue(true as any);
const { billingRouter } = await import("../routes/billing.js");
app = express();
app.use("/v1/billing/webhook", express.raw({ type: "application/json" }));
app.use(express.json());
app.use("/v1/billing", billingRouter);
});
describe("Billing Branch Coverage", () => {
describe("isDocFastSubscription - expanded product object (lines 63-67)", () => {
it("should handle expanded product object instead of string", async () => {
// Test the branch where price.product is an expanded Stripe.Product object
mockStripe.webhooks.constructEvent.mockReturnValue({
type: "checkout.session.completed",
data: {
object: {
id: "cs_expanded_product",
customer: "cus_expanded",
customer_details: { email: "expanded@test.com" },
},
},
});
// Mock: line_items has price.product as an object (not a string)
mockStripe.checkout.sessions.retrieve.mockResolvedValue({
id: "cs_expanded_product",
line_items: {
data: [
{
price: {
product: { id: "prod_TygeG8tQPtEAdE" } // Expanded object, not string
}
}
],
},
});
const { createProKey } = await import("../services/keys.js");
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(createProKey).toHaveBeenCalledWith("expanded@test.com", "cus_expanded");
});
it("should handle expanded product object in subscription.deleted webhook", async () => {
mockStripe.webhooks.constructEvent.mockReturnValue({
type: "customer.subscription.deleted",
data: {
object: { id: "sub_expanded", customer: "cus_expanded_del" },
},
});
// subscription.retrieve returns expanded product object
mockStripe.subscriptions.retrieve.mockResolvedValue({
items: {
data: [
{
price: {
product: { id: "prod_TygeG8tQPtEAdE" } // Expanded object
}
}
]
},
});
const { downgradeByCustomer } = await import("../services/keys.js");
const res = await request(app)
.post("/v1/billing/webhook")
.set("content-type", "application/json")
.set("stripe-signature", "valid_sig")
.send(JSON.stringify({ type: "customer.subscription.deleted" }));
expect(res.status).toBe(200);
expect(downgradeByCustomer).toHaveBeenCalledWith("cus_expanded_del");
});
});
describe("isDocFastSubscription - error handling (lines 70-71)", () => {
it("should return false when subscriptions.retrieve throws an error", async () => {
mockStripe.webhooks.constructEvent.mockReturnValue({
type: "customer.subscription.deleted",
data: {
object: { id: "sub_error", customer: "cus_error" },
},
});
// Mock: subscriptions.retrieve throws an error
mockStripe.subscriptions.retrieve.mockRejectedValue(new Error("Stripe API error"));
const { downgradeByCustomer } = await import("../services/keys.js");
const res = await request(app)
.post("/v1/billing/webhook")
.set("content-type", "application/json")
.set("stripe-signature", "valid_sig")
.send(JSON.stringify({ type: "customer.subscription.deleted" }));
// Should succeed but NOT downgrade (because isDocFastSubscription returns false on error)
expect(res.status).toBe(200);
expect(downgradeByCustomer).not.toHaveBeenCalled();
});
it("should return false when subscriptions.retrieve throws in subscription.updated", async () => {
mockStripe.webhooks.constructEvent.mockReturnValue({
type: "customer.subscription.updated",
data: {
object: {
id: "sub_update_error",
customer: "cus_update_error",
status: "canceled",
cancel_at_period_end: false,
},
},
});
mockStripe.subscriptions.retrieve.mockRejectedValue(new Error("Network timeout"));
const { downgradeByCustomer } = await import("../services/keys.js");
const res = await request(app)
.post("/v1/billing/webhook")
.set("content-type", "application/json")
.set("stripe-signature", "valid_sig")
.send(JSON.stringify({ type: "customer.subscription.updated" }));
expect(res.status).toBe(200);
expect(downgradeByCustomer).not.toHaveBeenCalled();
});
});
describe("getOrCreateProPrice - no existing product (lines 316-331)", () => {
it("should create new product and price when none exists", async () => {
// Mock: no existing product found
mockStripe.products.search.mockResolvedValue({ data: [] });
// Mock: product.create returns new product
mockStripe.products.create.mockResolvedValue({ id: "prod_new_123" });
// Mock: price.create returns new price
mockStripe.prices.create.mockResolvedValue({ id: "price_new_456" });
// Mock: checkout.sessions.create succeeds
mockStripe.checkout.sessions.create.mockResolvedValue({
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" },
});
expect(mockStripe.checkout.sessions.create).toHaveBeenCalledWith(
expect.objectContaining({
line_items: [{ price: "price_new_456", quantity: 1 }],
})
);
});
it("should create new price when product exists but has no active prices", async () => {
// Mock: product exists
mockStripe.products.search.mockResolvedValue({ data: [{ id: "prod_existing" }] });
// Mock: no active prices found
mockStripe.prices.list.mockResolvedValue({ data: [] });
// Mock: price.create returns new price
mockStripe.prices.create.mockResolvedValue({ id: "price_new_789" });
// Mock: checkout.sessions.create succeeds
mockStripe.checkout.sessions.create.mockResolvedValue({
id: "cs_existing",
url: "https://checkout.stripe.com/pay/cs_existing"
});
const res = await request(app)
.post("/v1/billing/checkout")
.send({});
expect(res.status).toBe(200);
expect(mockStripe.products.create).not.toHaveBeenCalled(); // Product exists, don't create
expect(mockStripe.prices.create).toHaveBeenCalledWith({
product: "prod_existing",
unit_amount: 900,
currency: "eur",
recurring: { interval: "month" },
});
expect(mockStripe.checkout.sessions.create).toHaveBeenCalledWith(
expect.objectContaining({
line_items: [{ price: "price_new_789", quantity: 1 }],
})
);
});
});
});