expria-backend/src/lib/__tests__/stripeWebhookEvents.test.ts
Hermann_Kitio 6671bac347 feat(billing): TD-13 webhook idempotency + Stripe Customer Portal + doc cleanup
- Table stripe_webhook_events + helpers isEventProcessed/markEventProcessed
- POST /stripe/customer-portal (auth + stripe_customer_id check)
- ARCHITECTURE-backend.md: suppression POST /plans/upgrade (duplication doc)
- TD-13 fermé dans TECH_DEBT-backend.md
- Tests: 261 → 278 verts (+17)
2026-04-26 04:15:46 +03:00

101 lines
3.8 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
// ─── Mocks ───────────────────────────────────────────────────────────────────
const { fromMock, selectMock, eqMock, maybeSingleMock, insertMock } =
vi.hoisted(() => ({
fromMock: vi.fn(),
selectMock: vi.fn(),
eqMock: vi.fn(),
maybeSingleMock: vi.fn(),
insertMock: vi.fn(),
}));
vi.mock("../supabase", () => ({
supabase: { from: fromMock },
}));
beforeEach(() => {
fromMock.mockReset();
selectMock.mockReset();
eqMock.mockReset();
maybeSingleMock.mockReset();
insertMock.mockReset();
fromMock.mockImplementation((table: string) => {
if (table !== "stripe_webhook_events") return {};
return {
select: selectMock,
insert: insertMock,
};
});
selectMock.mockReturnValue({ eq: eqMock });
eqMock.mockReturnValue({ maybeSingle: maybeSingleMock });
});
// ─── Tests ───────────────────────────────────────────────────────────────────
describe("isEventProcessed", () => {
it("retourne true quand l'event est déjà journalisé", async () => {
maybeSingleMock.mockResolvedValue({ data: { id: "evt_123" }, error: null });
const { isEventProcessed } = await import("../stripeWebhookEvents");
const result = await isEventProcessed("evt_123");
expect(result).toBe(true);
expect(eqMock).toHaveBeenCalledWith("id", "evt_123");
});
it("retourne false quand l'event est absent", async () => {
maybeSingleMock.mockResolvedValue({ data: null, error: null });
const { isEventProcessed } = await import("../stripeWebhookEvents");
const result = await isEventProcessed("evt_456");
expect(result).toBe(false);
});
it("retourne false sur erreur de lecture (privilégie disponibilité)", async () => {
maybeSingleMock.mockResolvedValue({
data: null,
error: { message: "DB unreachable" },
});
const { isEventProcessed } = await import("../stripeWebhookEvents");
const result = await isEventProcessed("evt_789");
expect(result).toBe(false);
});
it("retourne false pour un eventId vide sans toucher Supabase", async () => {
const { isEventProcessed } = await import("../stripeWebhookEvents");
const result = await isEventProcessed("");
expect(result).toBe(false);
expect(fromMock).not.toHaveBeenCalled();
});
});
describe("markEventProcessed", () => {
it("insère l'event quand il n'existe pas", async () => {
insertMock.mockResolvedValue({ error: null });
const { markEventProcessed } = await import("../stripeWebhookEvents");
await markEventProcessed("evt_new");
expect(insertMock).toHaveBeenCalledWith({ id: "evt_new" });
});
it("avale silencieusement un conflit unique (livraison concurrente)", async () => {
insertMock.mockResolvedValue({
error: { code: "23505", message: "duplicate key" },
});
const { markEventProcessed } = await import("../stripeWebhookEvents");
await expect(markEventProcessed("evt_dup")).resolves.toBeUndefined();
});
it("ne throw pas sur erreur DB inattendue (webhook doit toujours répondre 200)", async () => {
insertMock.mockResolvedValue({
error: { code: "08006", message: "connection failure" },
});
const { markEventProcessed } = await import("../stripeWebhookEvents");
await expect(markEventProcessed("evt_fail")).resolves.toBeUndefined();
});
it("no-op pour un eventId vide", async () => {
const { markEventProcessed } = await import("../stripeWebhookEvents");
await markEventProcessed("");
expect(insertMock).not.toHaveBeenCalled();
});
});