test(actions): add unit tests for shop voucher redemption
This commit is contained in:
1 parent
62f4861705
commit
f762db9ef9
1 file changed
+161
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user