import { createHash } from "node:crypto"; import { beforeEach, describe, expect, it, vi } from "vitest"; const limit = vi.fn(); const where = vi.fn(() => ({ limit })); const from = vi.fn(() => ({ where })); const select = vi.fn(() => ({ from })); const get = vi.fn(); const set = vi.fn(); const redis = { get, set }; vi.mock("server-only", () => ({})); vi.mock("@/db", () => ({ getDb: () => ({ select }) })); vi.mock("@/lib/redis/client", () => ({ getRedisClient: async () => redis })); const { authorizeMobileSlip, createMobileSlipLink, mobileSlipStatus } = await import("./mobile-slip"); const checkout = { id: "checkout-1", userId: "customer-1", amountBaht: 150, createdAt: new Date(Date.now() - 60_000) }; describe("commission mobile slip links", () => { beforeEach(() => { vi.clearAllMocks(); set.mockResolvedValue("OK"); }); it("requires checkout ownership before issuing a link", async () => { limit.mockResolvedValueOnce([]); await expect(createMobileSlipLink("checkout-1", "wrong-user")).rejects.toMatchObject({ status: 404 }); expect(set).not.toHaveBeenCalled(); }); it("issues a ten-minute link and rejects an expired or replaced token", async () => { limit.mockResolvedValueOnce([checkout]).mockResolvedValueOnce([]); const { token, digest } = await createMobileSlipLink(checkout.id, checkout.userId); expect(token).toMatch(/^[A-Za-z0-9_-]{43}$/); expect(digest).toBe(createHash("sha256").update(token).digest("hex")); expect(set).toHaveBeenCalledWith(expect.stringContaining(":token:"), JSON.stringify({ checkoutId: checkout.id, userId: checkout.userId }), "EX", 600); expect(set).toHaveBeenCalledWith(expect.stringContaining(":active:"), digest, "EX", 600); get.mockResolvedValueOnce(null); await expect(authorizeMobileSlip(token)).rejects.toMatchObject({ status: 404 }); get.mockResolvedValueOnce(JSON.stringify({ checkoutId: checkout.id, userId: checkout.userId })) .mockResolvedValueOnce("another-digest"); await expect(authorizeMobileSlip(token)).rejects.toMatchObject({ status: 404 }); }); it("accepts the active token only for its stored checkout", async () => { const token = "x".repeat(43); const digest = createHash("sha256").update(token).digest("hex"); get.mockResolvedValueOnce(JSON.stringify({ checkoutId: checkout.id, userId: checkout.userId })) .mockResolvedValueOnce(digest); limit.mockResolvedValueOnce([checkout]).mockResolvedValueOnce([]); expect(await authorizeMobileSlip(token)).toEqual({ checkout, ticketId: null, digest }); }); it("does not issue a link for an expired checkout", async () => { limit.mockResolvedValueOnce([{ ...checkout, createdAt: new Date(Date.now() - 3_600_000) }]) .mockResolvedValueOnce([]); await expect(createMobileSlipLink(checkout.id, checkout.userId)) .rejects.toMatchObject({ status: 410, message: "checkout-expired" }); expect(set).not.toHaveBeenCalled(); }); it("caps a mobile link at the checkout deadline", async () => { limit.mockResolvedValueOnce([{ ...checkout, createdAt: new Date(Date.now() - 3_570_000) }]) .mockResolvedValueOnce([]); const { expiresAt } = await createMobileSlipLink(checkout.id, checkout.userId); expect(expiresAt).toBeLessThanOrEqual(Date.now() + 30_000); expect(set).toHaveBeenCalledWith(expect.stringContaining(":token:"), expect.any(String), "EX", 30); }); it("rejects an active phone link once its checkout expires", async () => { const token = "x".repeat(43); const digest = createHash("sha256").update(token).digest("hex"); get.mockResolvedValueOnce(JSON.stringify({ checkoutId: checkout.id, userId: checkout.userId })) .mockResolvedValueOnce(digest); limit.mockResolvedValueOnce([{ ...checkout, createdAt: new Date(Date.now() - 3_600_000) }]) .mockResolvedValueOnce([]); await expect(authorizeMobileSlip(token)) .rejects.toMatchObject({ status: 410, message: "checkout-expired" }); }); it("reports expired unpaid checkouts while preserving completed tickets", async () => { const expired = { ...checkout, createdAt: new Date(Date.now() - 3_600_000) }; limit.mockResolvedValueOnce([expired]).mockResolvedValueOnce([]); expect(await mobileSlipStatus(checkout.id, checkout.userId, "a".repeat(64))) .toEqual({ state: "expired" }); limit.mockResolvedValueOnce([expired]).mockResolvedValueOnce([{ id: "ticket-1" }]); expect(await mobileSlipStatus(checkout.id, checkout.userId, "a".repeat(64))) .toEqual({ state: "complete", ticketId: "ticket-1" }); }); });