From 1363c61e396d791eb0f607da53a993ff76d8daeb Mon Sep 17 00:00:00 2001
From: OpenClaw Subagent
Date: Sat, 14 Mar 2026 17:13:36 +0100
Subject: [PATCH] test: improve billing.ts and demo.ts branch coverage
---
src/__tests__/billing-branch-coverage.test.ts | 330 ++++++++++++++++++
src/__tests__/demo-branch-coverage.test.ts | 223 ++++++++++++
2 files changed, 553 insertions(+)
create mode 100644 src/__tests__/demo-branch-coverage.test.ts
diff --git a/src/__tests__/billing-branch-coverage.test.ts b/src/__tests__/billing-branch-coverage.test.ts
index ad2cac9..ba951c0 100644
--- a/src/__tests__/billing-branch-coverage.test.ts
+++ b/src/__tests__/billing-branch-coverage.test.ts
@@ -257,4 +257,334 @@ describe("Billing Branch Coverage", () => {
);
});
});
+
+ describe("Success route - customerId branch (line 163)", () => {
+ it("should return 400 when session.customer is null (not a string)", async () => {
+ mockStripe.checkout.sessions.retrieve.mockResolvedValue({
+ id: "cs_null_customer",
+ customer: null, // Explicitly null, not falsy string
+ customer_details: { email: "test@example.com" },
+ });
+
+ const res = await request(app).get("/v1/billing/success?session_id=cs_null_customer");
+
+ expect(res.status).toBe(400);
+ expect(res.body.error).toContain("No customer found");
+ });
+
+ it("should return 400 when customer is empty string", async () => {
+ mockStripe.checkout.sessions.retrieve.mockResolvedValue({
+ id: "cs_empty_customer",
+ customer: "", // Empty string is falsy
+ customer_details: { email: "test@example.com" },
+ });
+
+ const res = await request(app).get("/v1/billing/success?session_id=cs_empty_customer");
+
+ expect(res.status).toBe(400);
+ expect(res.body.error).toContain("No customer found");
+ });
+
+ it("should return 400 when customer is undefined", async () => {
+ mockStripe.checkout.sessions.retrieve.mockResolvedValue({
+ id: "cs_undef_customer",
+ customer: undefined,
+ customer_details: { email: "test@example.com" },
+ });
+
+ const res = await request(app).get("/v1/billing/success?session_id=cs_undef_customer");
+
+ expect(res.status).toBe(400);
+ expect(res.body.error).toContain("No customer found");
+ });
+ });
+
+ describe("Webhook checkout.session.completed - hasDocfastProduct branch (line 223)", () => {
+ it("should skip webhook event when line_items is undefined", async () => {
+ mockStripe.webhooks.constructEvent.mockReturnValue({
+ type: "checkout.session.completed",
+ data: {
+ object: {
+ id: "cs_no_items",
+ customer: "cus_no_items",
+ customer_details: { email: "test@example.com" },
+ },
+ },
+ });
+
+ // Mock: line_items is undefined
+ mockStripe.checkout.sessions.retrieve.mockResolvedValue({
+ id: "cs_no_items",
+ line_items: undefined,
+ });
+
+ 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).not.toHaveBeenCalled();
+ });
+
+ it("should skip webhook event when line_items.data is empty", async () => {
+ mockStripe.webhooks.constructEvent.mockReturnValue({
+ type: "checkout.session.completed",
+ data: {
+ object: {
+ id: "cs_empty_items",
+ customer: "cus_empty_items",
+ customer_details: { email: "test@example.com" },
+ },
+ },
+ });
+
+ mockStripe.checkout.sessions.retrieve.mockResolvedValue({
+ id: "cs_empty_items",
+ line_items: { data: [] }, // Empty array - no items
+ });
+
+ 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).not.toHaveBeenCalled();
+ });
+
+ it("should skip webhook event when price is null", async () => {
+ mockStripe.webhooks.constructEvent.mockReturnValue({
+ type: "checkout.session.completed",
+ data: {
+ object: {
+ id: "cs_null_price",
+ customer: "cus_null_price",
+ customer_details: { email: "test@example.com" },
+ },
+ },
+ });
+
+ mockStripe.checkout.sessions.retrieve.mockResolvedValue({
+ id: "cs_null_price",
+ line_items: {
+ data: [{ price: null }], // Null price
+ },
+ });
+
+ 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).not.toHaveBeenCalled();
+ });
+
+ it("should skip webhook event when product is null", async () => {
+ mockStripe.webhooks.constructEvent.mockReturnValue({
+ type: "checkout.session.completed",
+ data: {
+ object: {
+ id: "cs_null_product",
+ customer: "cus_null_product",
+ customer_details: { email: "test@example.com" },
+ },
+ },
+ });
+
+ mockStripe.checkout.sessions.retrieve.mockResolvedValue({
+ id: "cs_null_product",
+ line_items: {
+ data: [{ price: { product: null } }], // Null product
+ },
+ });
+
+ 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).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("Webhook customer.updated event (line 284-303)", () => {
+ it("should sync email when both customerId and newEmail exist", async () => {
+ mockStripe.webhooks.constructEvent.mockReturnValue({
+ type: "customer.updated",
+ data: {
+ object: {
+ id: "cus_email_update",
+ email: "newemail@example.com",
+ },
+ },
+ });
+
+ const { updateEmailByCustomer } = await import("../services/keys.js");
+ vi.mocked(updateEmailByCustomer).mockResolvedValue(true);
+
+ const res = await request(app)
+ .post("/v1/billing/webhook")
+ .set("content-type", "application/json")
+ .set("stripe-signature", "valid_sig")
+ .send(JSON.stringify({ type: "customer.updated" }));
+
+ expect(res.status).toBe(200);
+ expect(updateEmailByCustomer).toHaveBeenCalledWith("cus_email_update", "newemail@example.com");
+ });
+
+ it("should not sync email when customerId is missing", async () => {
+ mockStripe.webhooks.constructEvent.mockReturnValue({
+ type: "customer.updated",
+ data: {
+ object: {
+ id: undefined, // Missing customerId
+ email: "newemail@example.com",
+ },
+ },
+ });
+
+ const { updateEmailByCustomer } = 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.updated" }));
+
+ expect(res.status).toBe(200);
+ expect(updateEmailByCustomer).not.toHaveBeenCalled();
+ });
+
+ it("should not sync email when email is missing", async () => {
+ mockStripe.webhooks.constructEvent.mockReturnValue({
+ type: "customer.updated",
+ data: {
+ object: {
+ id: "cus_no_email",
+ email: null, // Missing email
+ },
+ },
+ });
+
+ const { updateEmailByCustomer } = 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.updated" }));
+
+ expect(res.status).toBe(200);
+ expect(updateEmailByCustomer).not.toHaveBeenCalled();
+ });
+
+ it("should not sync email when email is undefined", async () => {
+ mockStripe.webhooks.constructEvent.mockReturnValue({
+ type: "customer.updated",
+ data: {
+ object: {
+ id: "cus_no_email_2",
+ email: undefined, // Undefined email
+ },
+ },
+ });
+
+ const { updateEmailByCustomer } = 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.updated" }));
+
+ expect(res.status).toBe(200);
+ expect(updateEmailByCustomer).not.toHaveBeenCalled();
+ });
+
+ it("should log when email update returns false", async () => {
+ mockStripe.webhooks.constructEvent.mockReturnValue({
+ type: "customer.updated",
+ data: {
+ object: {
+ id: "cus_no_update",
+ email: "newemail@example.com",
+ },
+ },
+ });
+
+ const { updateEmailByCustomer } = await import("../services/keys.js");
+ vi.mocked(updateEmailByCustomer).mockResolvedValue(false); // Update returns false
+
+ const res = await request(app)
+ .post("/v1/billing/webhook")
+ .set("content-type", "application/json")
+ .set("stripe-signature", "valid_sig")
+ .send(JSON.stringify({ type: "customer.updated" }));
+
+ expect(res.status).toBe(200);
+ expect(updateEmailByCustomer).toHaveBeenCalledWith("cus_no_update", "newemail@example.com");
+ // The if (updated) branch should not be executed when false
+ });
+ });
+
+ describe("Webhook default case", () => {
+ it("should handle unknown webhook event type", async () => {
+ mockStripe.webhooks.constructEvent.mockReturnValue({
+ type: "unknown.event.type",
+ data: { object: {} },
+ });
+
+ const res = await request(app)
+ .post("/v1/billing/webhook")
+ .set("content-type", "application/json")
+ .set("stripe-signature", "valid_sig")
+ .send(JSON.stringify({ type: "unknown.event.type" }));
+
+ expect(res.status).toBe(200);
+ expect(res.body.received).toBe(true);
+ });
+
+ it("should handle payment_intent.succeeded webhook event", async () => {
+ mockStripe.webhooks.constructEvent.mockReturnValue({
+ type: "payment_intent.succeeded",
+ data: { object: {} },
+ });
+
+ const res = await request(app)
+ .post("/v1/billing/webhook")
+ .set("content-type", "application/json")
+ .set("stripe-signature", "valid_sig")
+ .send(JSON.stringify({ type: "payment_intent.succeeded" }));
+
+ expect(res.status).toBe(200);
+ expect(res.body.received).toBe(true);
+ });
+
+ it("should handle invoice.payment_succeeded webhook event", async () => {
+ mockStripe.webhooks.constructEvent.mockReturnValue({
+ type: "invoice.payment_succeeded",
+ data: { object: {} },
+ });
+
+ const res = await request(app)
+ .post("/v1/billing/webhook")
+ .set("content-type", "application/json")
+ .set("stripe-signature", "valid_sig")
+ .send(JSON.stringify({ type: "invoice.payment_succeeded" }));
+
+ expect(res.status).toBe(200);
+ expect(res.body.received).toBe(true);
+ });
+ });
});
diff --git a/src/__tests__/demo-branch-coverage.test.ts b/src/__tests__/demo-branch-coverage.test.ts
new file mode 100644
index 0000000..05befe4
--- /dev/null
+++ b/src/__tests__/demo-branch-coverage.test.ts
@@ -0,0 +1,223 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import express from "express";
+import request from "supertest";
+
+vi.mock("../services/browser.js", () => ({
+ renderPdf: vi.fn(),
+ renderUrlPdf: vi.fn(),
+}));
+
+let app: express.Express;
+
+beforeEach(async () => {
+ vi.clearAllMocks();
+ vi.resetModules();
+
+ const { renderPdf } = await import("../services/browser.js");
+ vi.mocked(renderPdf).mockResolvedValue({ pdf: Buffer.from("%PDF-1.4 mock"), durationMs: 10 });
+
+ const { demoRouter } = await import("../routes/demo.js");
+ app = express();
+ app.use(express.json({ limit: "500kb" }));
+ app.use("/v1/demo", demoRouter);
+});
+
+describe("Demo Branch Coverage", () => {
+ describe("injectWatermark fallback branch (line 19)", () => {
+ it("should append watermark when full HTML document doesn't contain
+ Hello
+ Content here
+ `;
+
+ const res = await request(app)
+ .post("/v1/demo/html")
+ .set("content-type", "application/json")
+ .send({ html: htmlWithoutClosingBody });
+
+ expect(res.status).toBe(200);
+ expect(res.headers["content-type"]).toMatch(/application\/pdf/);
+
+ // Verify watermark was appended (not replaced)
+ const calledHtml = vi.mocked(renderPdf).mock.calls[0][0];
+
+ // The fallback should append the watermark at the end
+ expect(calledHtml).toContain("Hello");
+ expect(calledHtml).toContain("Content here");
+ expect(calledHtml).toContain("DEMO");
+ expect(calledHtml).toContain("Generated by DocFast");
+
+ // Ensure the original HTML is preserved before the watermark
+ expect(calledHtml.indexOf("Hello")).toBeLessThan(calledHtml.indexOf("DEMO"));
+
+ // Ensure watermark is appended at the end (since there's no
+ Complete HTML
+ With closing body tag
+ tag", async () => {
+ const { renderPdf } = await import("../services/browser.js");
+ // Send full HTML (with ) but without to hit the fallback branch
+ const htmlWithoutClosingBody = `
+
+
to replace)
+ const lastBodyCloseIndex = calledHtml.lastIndexOf("");
+ const watermarkIndex = calledHtml.indexOf("Generated by DocFast");
+ // If there's a at the very end (from wrapping), the watermark should be before it
+ if (lastBodyCloseIndex > -1) {
+ expect(watermarkIndex).toBeLessThan(lastBodyCloseIndex);
+ }
+ });
+
+ it("should append watermark to plain HTML fragment without ", async () => {
+ const { renderPdf } = await import("../services/browser.js");
+ const res = await request(app)
+ .post("/v1/demo/html")
+ .set("content-type", "application/json")
+ .send({ html: "
" });
+
+ expect(res.status).toBe(200);
+
+ const calledHtml = vi.mocked(renderPdf).mock.calls[0][0];
+ expect(calledHtml).toContain("
");
+ expect(calledHtml).toContain("DEMO");
+ expect(calledHtml).toContain("position:fixed;bottom:0;left:0;right:0;");
+ });
+
+ it("should handle markdown that results in HTML without and injects watermark", async () => {
+ const { renderPdf } = await import("../services/browser.js");
+ const res = await request(app)
+ .post("/v1/demo/markdown")
+ .set("content-type", "application/json")
+ .send({ markdown: "# Just a heading\n\nSome text" });
+
+ expect(res.status).toBe(200);
+
+ const calledHtml = vi.mocked(renderPdf).mock.calls[0][0];
+ // Should contain watermark
+ expect(calledHtml).toContain("DEMO");
+ expect(calledHtml).toContain("Generated by DocFast");
+ expect(calledHtml).toContain("Upgrade to Pro for clean PDFs");
+ });
+
+ it("should still work correctly when HTML contains (replace branch)", async () => {
+ const { renderPdf } = await import("../services/browser.js");
+ const fullHtml = `
+
+