From f92d08810afb83dced6af8e88ea7db4e6d0e3939 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 16 Dec 2025 21:20:19 +0000 Subject: [PATCH] stripe service test --- client/src/pages/uptime/UptimeMonitors.tsx | 6 +- .../services/system/StripeService.test.js | 111 ++++++++++++++++++ 2 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 server/tests/services/system/StripeService.test.js diff --git a/client/src/pages/uptime/UptimeMonitors.tsx b/client/src/pages/uptime/UptimeMonitors.tsx index 692e1c67f..93335fd35 100644 --- a/client/src/pages/uptime/UptimeMonitors.tsx +++ b/client/src/pages/uptime/UptimeMonitors.tsx @@ -72,7 +72,7 @@ const UptimeMonitors = () => { ].filter(Boolean); const monitorRequestUrl = `/monitors?${requestParams.join("&")}`; - const { response, loading, isValidating, error, refetch } = useGet< + const { response, loading, error, refetch } = useGet< ApiResponse >( monitorRequestUrl, @@ -119,7 +119,7 @@ const UptimeMonitors = () => { return ( { onStatusesChange={setSelectedStatuses} searchString={searchString} onSearchStringChange={setSearchString} - loading={isValidating} + loading={loading} /> ({ + Org: { findOneAndUpdate: jest.fn() }, + Monitor: { countDocuments: jest.fn(), find: jest.fn(), deleteMany: jest.fn() }, + StatusPage: { countDocuments: jest.fn(), find: jest.fn(), deleteMany: jest.fn() }, + NotificationChannel: { countDocuments: jest.fn(), find: jest.fn(), deleteMany: jest.fn() }, +})); +import { Org, Monitor, StatusPage, NotificationChannel } from "@/db/models/index.js"; + +jest.mock("@/config/index.js", () => ({ + config: { + STRIPE_SECRET: "sk_test_mock", + STRIPE_WEBHOOK_SECRET: "whsec_mock", + }, +})); + +// Mock Stripe constructor and webhooks +jest.mock("stripe", () => { + const mockConstructEvent = jest.fn(); + const mockStripe = function () { + return { + webhooks: { constructEvent: mockConstructEvent }, + }; + }; + mockStripe.__constructEvent = mockConstructEvent; + return { __esModule: true, default: mockStripe }; +}); +import Stripe from "stripe"; + +const buildSub = (overrides = {}) => ({ + id: "sub_1", + metadata: { orgId: "org1" }, + status: "active", + items: { data: [{ price: { id: "price_1", lookup_key: "pro" }, current_period_start: 1, current_period_end: 2 }] }, + ...overrides, +}); + +describe("StripeService", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("returns null when constructEvent fails", async () => { + (Stripe.__constructEvent).mockImplementation(() => { + throw new Error("bad sig"); + }); + const svc = new StripeService(); + const res = await svc.webhook("evt", "sig"); + expect(res).toBeNull(); + }); + + it("handles subscription created/updated and updates org + prunes resources", async () => { + (Stripe.__constructEvent).mockReturnValue({ + type: "customer.subscription.created", + data: { object: buildSub() }, + }); + + Monitor.countDocuments.mockResolvedValue(30); + Monitor.find.mockReturnValue({ sort: () => ({ limit: () => ({ select: () => Promise.resolve([{ _id: "m1" }]) }) }) }); + Monitor.deleteMany.mockResolvedValue({}); + NotificationChannel.countDocuments.mockResolvedValue(0); + StatusPage.countDocuments.mockResolvedValue(0); + + const svc = new StripeService(); + const res = await svc.webhook("evt", "sig"); + expect(Org.findOneAndUpdate).toHaveBeenCalledWith( + { _id: "org1" }, + expect.objectContaining({ $set: expect.objectContaining({ planKey: "pro" }) }), + { new: true } + ); + expect(Monitor.deleteMany).toHaveBeenCalled(); + expect(res).toMatchObject({ status: "active" }); + }); + + it("handles subscription deleted and resets org to free", async () => { + (Stripe.__constructEvent).mockReturnValue({ + type: "customer.subscription.deleted", + data: { object: buildSub() }, + }); + const svc = new StripeService(); + const res = await svc.webhook("evt", "sig"); + expect(Org.findOneAndUpdate).toHaveBeenCalledWith( + { _id: "org1" }, + expect.objectContaining({ $set: expect.objectContaining({ planKey: "free" }), $unset: expect.any(Object) }), + { new: true } + ); + expect(res).toMatchObject({ status: "active" }); + }); + + it("returns subscription for updated event", async () => { + (Stripe.__constructEvent).mockReturnValue({ + type: "customer.subscription.updated", + data: { object: buildSub({ status: "past_due" }) }, + }); + const svc = new StripeService(); + const res = await svc.webhook("evt", "sig"); + expect(res).toMatchObject({ status: "past_due" }); + }); + + it("returns null for other events", async () => { + (Stripe.__constructEvent).mockReturnValue({ + type: "invoice.payment_succeeded", + data: { object: {} }, + }); + const svc = new StripeService(); + const res = await svc.webhook("evt", "sig"); + expect(res).toBeNull(); + }); +});