test(actions): add unit tests for shop voucher redemption

This commit is contained in:
openhands committed 2026-09-13 14:21:55 +02:00
1 parent 62f4861705
commit f762db9ef9
1 file changed
+161
+161
View File
@@ -0,0 +1,161 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("next/server", () => ({
NextResponse: {
json: vi.fn(),
},
}));
vi.mock("next-auth", () => ({
default: vi.fn(() => ({
handlers: {},
auth: vi.fn(),
signOut: vi.fn(),
})),
}));
vi.mock("@/lib/auth", () => ({
auth: vi.fn(),
handlers: {},
signOut: vi.fn(),
}));
const mockAuth = vi.hoisted(() => vi.fn());
vi.mock("@/lib/auth", () => ({
auth: mockAuth,
}));
const hoistedInsertValues = vi.hoisted(() => vi.fn());
const hoistedDeleteWhere = vi.hoisted(() => vi.fn());
const hoistedSelectLimit = vi.hoisted(() => vi.fn());
const hoistedSelectWhere = vi.hoisted(() =>
vi.fn(() => ({ limit: hoistedSelectLimit })),
);
const hoistedSelectFrom = vi.hoisted(() =>
vi.fn(() => ({ where: hoistedSelectWhere })),
);
const hoistedUpdateWhere = vi.hoisted(() => vi.fn());
const hoistedUpdateSetObj = vi.hoisted(() => ({
set: vi.fn(() => ({ where: hoistedUpdateWhere })),
}));
vi.mock("@/lib/db", () => ({
db: {
select: vi.fn(() => ({ from: hoistedSelectFrom })),
insert: vi.fn(() => ({ values: hoistedInsertValues })),
update: vi.fn(() => hoistedUpdateSetObj),
delete: vi.fn(() => ({ where: hoistedDeleteWhere })),
},
WebsiteShopVouchers: {
id: "voucher.id",
code: "voucher.code",
amount: "voucher.amount",
maxUses: "voucher.maxUses",
useCount: "voucher.useCount",
expiresAt: "voucher.expiresAt",
},
WebsiteUsedShopVouchers: {
id: "used.id",
userId: "used.userId",
voucherId: "used.voucherId",
},
}));
const mockSendCurrency = vi.hoisted(() => vi.fn());
vi.mock("@/lib/services/send-currency", () => ({
currencyDb: {},
sendCurrency: mockSendCurrency,
}));
const mockRateLimit = vi.hoisted(() => vi.fn());
vi.mock("@/lib/rate-limit", () => ({
clientIp: vi.fn().mockResolvedValue("127.0.0.1"),
rateLimit: mockRateLimit,
}));
const mockRevalidatePath = vi.hoisted(() => vi.fn());
vi.mock("next/cache", () => ({
revalidatePath: mockRevalidatePath,
unstable_cache: (fn: any) => fn,
}));
import { db } from "@/lib/db";
import { redeem } from "./voucher";
const fakeForm = (data: Record<string, string>) =>
({
get: (key: string) => data[key] ?? null,
}) as unknown as FormData;
beforeEach(() => {
vi.clearAllMocks();
mockAuth.mockResolvedValue({ user: { id: "1" } });
mockRateLimit.mockResolvedValue({ ok: true });
hoistedSelectLimit.mockResolvedValue([]);
hoistedInsertValues.mockResolvedValue([]);
hoistedUpdateWhere.mockResolvedValue([]);
mockSendCurrency.mockResolvedValue(undefined);
});
describe("redeem action", () => {
it("rejects if user is not signed in", async () => {
mockAuth.mockResolvedValueOnce(null);
const result = await redeem(null, fakeForm({ code: "TEST" }));
expect(result).toEqual({
ok: false,
message: "You must be signed in to redeem a voucher.",
});
});
it("rejects if rate limited", async () => {
mockRateLimit.mockResolvedValueOnce({ ok: false });
const result = await redeem(null, fakeForm({ code: "TEST" }));
expect(result?.ok).toBe(false);
expect(result?.message).toContain("too fast");
});
it("rejects empty codes", async () => {
const result = await redeem(null, fakeForm({ code: "" }));
expect(result).toEqual({
ok: false,
message: "Please enter a voucher code.",
});
});
it("rejects if voucher does not exist or is expired", async () => {
hoistedSelectLimit.mockResolvedValueOnce([]); // voucher query
const result = await redeem(null, fakeForm({ code: "INVALID" }));
expect(result).toEqual({
ok: false,
message: "No active voucher with the given code was found.",
});
});
it("successfully redeems a valid voucher", async () => {
const futureDate = new Date(Date.now() + 86400000);
// 1st call: voucher query, 2nd call: already used check, 3rd & 4th calls: bump useCount / check maxUses
hoistedSelectLimit
.mockResolvedValueOnce([
{
id: 10n,
amount: 500,
maxUses: 10,
useCount: 2,
expiresAt: futureDate,
},
])
.mockResolvedValueOnce([]) // not used yet
.mockResolvedValueOnce([{ maxUses: 10, useCount: 3 }]); // updated count
const result = await redeem(null, fakeForm({ code: "VALIDCODE" }));
expect(result?.ok).toBe(true);
expect(result?.message).toContain("500 credits");
expect(mockSendCurrency).toHaveBeenCalledWith(
expect.any(Object),
1,
"credits",
500,
);
expect(db.insert).toHaveBeenCalled();
});
});