test: improve billing.ts and demo.ts branch coverage
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 21m45s
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 21m45s
This commit is contained in:
parent
3aae96fd8a
commit
1363c61e39
2 changed files with 553 additions and 0 deletions
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
223
src/__tests__/demo-branch-coverage.test.ts
Normal file
223
src/__tests__/demo-branch-coverage.test.ts
Normal file
|
|
@ -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 </body> tag", async () => {
|
||||||
|
const { renderPdf } = await import("../services/browser.js");
|
||||||
|
// Send full HTML (with <html>) but without </body> to hit the fallback branch
|
||||||
|
const htmlWithoutClosingBody = `
|
||||||
|
<html>
|
||||||
|
<head><title>Test Page</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello</h1>
|
||||||
|
<p>Content here</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 </body> to replace)
|
||||||
|
const lastBodyCloseIndex = calledHtml.lastIndexOf("</body>");
|
||||||
|
const watermarkIndex = calledHtml.indexOf("Generated by DocFast");
|
||||||
|
// If there's a </body> 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 </body>", async () => {
|
||||||
|
const { renderPdf } = await import("../services/browser.js");
|
||||||
|
const res = await request(app)
|
||||||
|
.post("/v1/demo/html")
|
||||||
|
.set("content-type", "application/json")
|
||||||
|
.send({ html: "<div>Simple fragment</div>" });
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
const calledHtml = vi.mocked(renderPdf).mock.calls[0][0];
|
||||||
|
expect(calledHtml).toContain("<div>Simple fragment</div>");
|
||||||
|
expect(calledHtml).toContain("DEMO");
|
||||||
|
expect(calledHtml).toContain("position:fixed;bottom:0;left:0;right:0;");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle markdown that results in HTML without </body> 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 </body> (replace branch)", async () => {
|
||||||
|
const { renderPdf } = await import("../services/browser.js");
|
||||||
|
const fullHtml = `
|
||||||
|
<html>
|
||||||
|
<head><title>Test</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Complete HTML</h1>
|
||||||
|
<p>With closing body tag</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post("/v1/demo/html")
|
||||||
|
.set("content-type", "application/json")
|
||||||
|
.send({ html: fullHtml });
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
const calledHtml = vi.mocked(renderPdf).mock.calls[0][0];
|
||||||
|
// When </body> exists, watermark should be injected before it
|
||||||
|
expect(calledHtml).toContain("</body>");
|
||||||
|
expect(calledHtml).toContain("DEMO");
|
||||||
|
|
||||||
|
// The watermark should be between the content and closing </body>
|
||||||
|
const watermarkIndex = calledHtml.indexOf("Generated by DocFast");
|
||||||
|
const closingBodyIndex = calledHtml.indexOf("</body>");
|
||||||
|
expect(watermarkIndex).toBeGreaterThan(-1);
|
||||||
|
expect(closingBodyIndex).toBeGreaterThan(-1);
|
||||||
|
expect(watermarkIndex).toBeLessThan(closingBodyIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reject empty HTML input with 400 error", async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post("/v1/demo/html")
|
||||||
|
.set("content-type", "application/json")
|
||||||
|
.send({ html: "" });
|
||||||
|
|
||||||
|
// Empty HTML is rejected by validation
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
expect(res.body.error).toContain("html");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle HTML with multiple </body> tags (uses first)</body>", async () => {
|
||||||
|
const { renderPdf } = await import("../services/browser.js");
|
||||||
|
const htmlWithMultipleBodies = `
|
||||||
|
<html>
|
||||||
|
<body>First body</body>
|
||||||
|
<body>Second body</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post("/v1/demo/html")
|
||||||
|
.set("content-type", "application/json")
|
||||||
|
.send({ html: htmlWithMultipleBodies });
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
const calledHtml = vi.mocked(renderPdf).mock.calls[0][0];
|
||||||
|
// replace only replaces the first occurrence
|
||||||
|
expect(calledHtml).toContain("First body");
|
||||||
|
expect(calledHtml).toContain("DEMO");
|
||||||
|
expect(calledHtml).toContain("</body>");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Watermark content verification", () => {
|
||||||
|
it("should include demo watermark with exact styling", async () => {
|
||||||
|
const { renderPdf } = await import("../services/browser.js");
|
||||||
|
const res = await request(app)
|
||||||
|
.post("/v1/demo/html")
|
||||||
|
.set("content-type", "application/json")
|
||||||
|
.send({ html: "<h1>Test</h1>" });
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
const calledHtml = vi.mocked(renderPdf).mock.calls[0][0];
|
||||||
|
// Verify watermark styling
|
||||||
|
expect(calledHtml).toContain("background:rgba(52,211,153,0.92);color:#0b0d11");
|
||||||
|
expect(calledHtml).toContain("z-index:999999");
|
||||||
|
expect(calledHtml).toContain("pointer-events:none");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve user CSS when injecting watermark", async () => {
|
||||||
|
const { renderPdf } = await import("../services/browser.js");
|
||||||
|
const customCss = "body { background: blue; }";
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post("/v1/demo/html")
|
||||||
|
.set("content-type", "application/json")
|
||||||
|
.send({ html: "<h1>Test</h1>", css: customCss });
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
|
||||||
|
const calledHtml = vi.mocked(renderPdf).mock.calls[0][0];
|
||||||
|
// Both watermark and user CSS should be present
|
||||||
|
expect(calledHtml).toContain("DEMO");
|
||||||
|
expect(calledHtml).toContain("background: blue");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Branch coverage for attachment headers", () => {
|
||||||
|
it("should set Content-Disposition to attachment for HTML", async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post("/v1/demo/html")
|
||||||
|
.set("content-type", "application/json")
|
||||||
|
.send({ html: "<h1>Hello</h1>" });
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.headers["content-disposition"]).toMatch(/^attachment/);
|
||||||
|
expect(res.headers["content-disposition"]).toMatch(/filename="demo\.pdf"/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set Content-Disposition to attachment for markdown", async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post("/v1/demo/markdown")
|
||||||
|
.set("content-type", "application/json")
|
||||||
|
.send({ markdown: "# Hello" });
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.headers["content-disposition"]).toMatch(/^attachment/);
|
||||||
|
expect(res.headers["content-disposition"]).toMatch(/filename="demo\.pdf"/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue