- 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)
101 lines
3.8 KiB
TypeScript
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();
|
|
});
|
|
});
|