test: add billing edge case tests (characterization)
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 13m8s
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 13m8s
This commit is contained in:
parent
82946ffcf0
commit
9eb9b4232b
1 changed files with 194 additions and 0 deletions
|
|
@ -137,6 +137,36 @@ describe("GET /v1/billing/success", () => {
|
|||
const res = await request(app).get("/v1/billing/success?session_id=cs_err");
|
||||
expect(res.status).toBe(500);
|
||||
});
|
||||
|
||||
it("returns 400 when session has no customer", async () => {
|
||||
mockStripe.checkout.sessions.retrieve.mockResolvedValue({
|
||||
id: "cs_no_cust",
|
||||
customer: null,
|
||||
customer_details: { email: "test@test.com" },
|
||||
});
|
||||
const res = await request(app).get("/v1/billing/success?session_id=cs_no_cust");
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toMatch(/No customer found/);
|
||||
});
|
||||
|
||||
it("escapes HTML in displayed key to prevent XSS", async () => {
|
||||
mockStripe.checkout.sessions.retrieve.mockResolvedValue({
|
||||
id: "cs_xss",
|
||||
customer: "cus_xss",
|
||||
customer_details: { email: "xss@test.com" },
|
||||
});
|
||||
const { createProKey } = await import("../services/keys.js");
|
||||
vi.mocked(createProKey).mockResolvedValue({
|
||||
key: '<script>alert("xss")</script>',
|
||||
tier: "pro",
|
||||
email: "xss@test.com",
|
||||
createdAt: new Date().toISOString(),
|
||||
} as any);
|
||||
const res = await request(app).get("/v1/billing/success?session_id=cs_xss");
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.text).not.toContain('<script>alert("xss")</script>');
|
||||
expect(res.text).toContain("<script>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /v1/billing/webhook", () => {
|
||||
|
|
@ -275,6 +305,170 @@ describe("POST /v1/billing/webhook", () => {
|
|||
expect(downgradeByCustomer).toHaveBeenCalledWith("cus_cancel");
|
||||
});
|
||||
|
||||
it("does not provision key when checkout.session.completed has missing customer", async () => {
|
||||
mockStripe.webhooks.constructEvent.mockReturnValue({
|
||||
type: "checkout.session.completed",
|
||||
data: {
|
||||
object: {
|
||||
id: "cs_no_cust",
|
||||
customer: null,
|
||||
customer_details: { email: "nocust@test.com" },
|
||||
},
|
||||
},
|
||||
});
|
||||
mockStripe.checkout.sessions.retrieve.mockResolvedValue({
|
||||
id: "cs_no_cust",
|
||||
line_items: { data: [{ price: { product: "prod_TygeG8tQPtEAdE" } }] },
|
||||
});
|
||||
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("does not provision key when checkout.session.completed has missing email", async () => {
|
||||
mockStripe.webhooks.constructEvent.mockReturnValue({
|
||||
type: "checkout.session.completed",
|
||||
data: {
|
||||
object: {
|
||||
id: "cs_no_email",
|
||||
customer: "cus_no_email",
|
||||
customer_details: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
mockStripe.checkout.sessions.retrieve.mockResolvedValue({
|
||||
id: "cs_no_email",
|
||||
line_items: { data: [{ price: { product: "prod_TygeG8tQPtEAdE" } }] },
|
||||
});
|
||||
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("does not downgrade on customer.subscription.updated with non-DocFast product", async () => {
|
||||
mockStripe.webhooks.constructEvent.mockReturnValue({
|
||||
type: "customer.subscription.updated",
|
||||
data: {
|
||||
object: { id: "sub_other", customer: "cus_other", status: "canceled", cancel_at_period_end: false },
|
||||
},
|
||||
});
|
||||
mockStripe.subscriptions.retrieve.mockResolvedValue({
|
||||
items: { data: [{ price: { product: { id: "prod_OTHER" } } }] },
|
||||
});
|
||||
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();
|
||||
});
|
||||
|
||||
it("downgrades on customer.subscription.updated with past_due status", async () => {
|
||||
mockStripe.webhooks.constructEvent.mockReturnValue({
|
||||
type: "customer.subscription.updated",
|
||||
data: {
|
||||
object: { id: "sub_past", customer: "cus_past", status: "past_due", cancel_at_period_end: false },
|
||||
},
|
||||
});
|
||||
mockStripe.subscriptions.retrieve.mockResolvedValue({
|
||||
items: { data: [{ price: { product: { id: "prod_TygeG8tQPtEAdE" } } }] },
|
||||
});
|
||||
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).toHaveBeenCalledWith("cus_past");
|
||||
});
|
||||
|
||||
it("does not downgrade on customer.subscription.updated with active status and no cancel", async () => {
|
||||
mockStripe.webhooks.constructEvent.mockReturnValue({
|
||||
type: "customer.subscription.updated",
|
||||
data: {
|
||||
object: { id: "sub_ok", customer: "cus_ok", status: "active", cancel_at_period_end: false },
|
||||
},
|
||||
});
|
||||
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();
|
||||
});
|
||||
|
||||
it("does not downgrade on customer.subscription.deleted with non-DocFast product", async () => {
|
||||
mockStripe.webhooks.constructEvent.mockReturnValue({
|
||||
type: "customer.subscription.deleted",
|
||||
data: {
|
||||
object: { id: "sub_del_other", customer: "cus_del_other" },
|
||||
},
|
||||
});
|
||||
mockStripe.subscriptions.retrieve.mockResolvedValue({
|
||||
items: { data: [{ price: { product: { id: "prod_OTHER" } } }] },
|
||||
});
|
||||
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).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns 200 for unknown event type", async () => {
|
||||
mockStripe.webhooks.constructEvent.mockReturnValue({
|
||||
type: "invoice.payment_failed",
|
||||
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_failed" }));
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.received).toBe(true);
|
||||
});
|
||||
|
||||
it("returns 200 when session retrieve fails on checkout.session.completed", async () => {
|
||||
mockStripe.webhooks.constructEvent.mockReturnValue({
|
||||
type: "checkout.session.completed",
|
||||
data: {
|
||||
object: {
|
||||
id: "cs_fail_retrieve",
|
||||
customer: "cus_fail",
|
||||
customer_details: { email: "fail@test.com" },
|
||||
},
|
||||
},
|
||||
});
|
||||
mockStripe.checkout.sessions.retrieve.mockRejectedValue(new Error("Stripe retrieve failed"));
|
||||
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(res.body.received).toBe(true);
|
||||
expect(createProKey).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("syncs email on customer.updated", async () => {
|
||||
mockStripe.webhooks.constructEvent.mockReturnValue({
|
||||
type: "customer.updated",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue