Files

91 lines
4.5 KiB
TypeScript

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" });
});
});