Replace raw db.execute tuple casts with queryRows/rowsFrom/execResult/ affectedRows helpers from lib/db, drop redundant mysql2 casts on typed query builders, and centralize per-test fakeForm into test/fake-form. Update db mocks in tests so helpers resolve against mocked execute.
105 lines
2.5 KiB
TypeScript
105 lines
2.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const { selectLimit, insertOnDup, mockSendMail, mockRedirect } = vi.hoisted(
|
|
() => ({
|
|
selectLimit: vi.fn(),
|
|
insertOnDup: vi.fn().mockResolvedValue({}),
|
|
mockSendMail: vi.fn(),
|
|
mockRedirect: vi.fn(),
|
|
}),
|
|
);
|
|
|
|
vi.mock("next/navigation", () => ({
|
|
redirect: (...args: unknown[]) => {
|
|
mockRedirect(...args);
|
|
throw new Error("redirect");
|
|
},
|
|
}));
|
|
|
|
vi.mock("@/lib/db", () => {
|
|
return {
|
|
db: {
|
|
select: vi.fn(() => ({
|
|
from: vi.fn(() => ({
|
|
where: vi.fn(() => ({
|
|
limit: selectLimit,
|
|
})),
|
|
})),
|
|
})),
|
|
insert: vi.fn(() => ({
|
|
values: vi.fn(() => ({
|
|
onDuplicateKeyUpdate: insertOnDup,
|
|
})),
|
|
})),
|
|
},
|
|
User: { id: "id", mail: "mail" },
|
|
PasswordReset: { email: "email", token: "token", createdAt: "createdAt" },
|
|
};
|
|
});
|
|
|
|
vi.mock("@/lib/services/email", () => ({
|
|
sendMail: mockSendMail,
|
|
}));
|
|
|
|
vi.mock("@/lib/services/captcha", () => ({
|
|
captchaConfig: vi.fn().mockResolvedValue({
|
|
provider: "none",
|
|
siteKey: "",
|
|
field: "",
|
|
}),
|
|
verifyCaptcha: vi.fn().mockResolvedValue(true),
|
|
}));
|
|
|
|
vi.mock("@/lib/rate-limit", () => ({
|
|
rateLimit: vi.fn().mockResolvedValue({ ok: true, retryAfter: 0 }),
|
|
clientIp: vi.fn().mockResolvedValue("127.0.0.1"),
|
|
}));
|
|
|
|
vi.mock("@/env", () => ({
|
|
env: { APP_URL: "http://localhost:3000", HOTEL_NAME: "TestHotel" },
|
|
}));
|
|
|
|
import { requestReset } from "./password-reset";
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
insertOnDup.mockResolvedValue({});
|
|
});
|
|
|
|
describe("requestReset", () => {
|
|
it("sends a reset email when the user exists", async () => {
|
|
selectLimit.mockResolvedValue([{ id: 1 }]);
|
|
|
|
const fd = new FormData();
|
|
fd.set("email", "[email protected]");
|
|
|
|
await expect(requestReset(fd)).rejects.toThrow("redirect");
|
|
|
|
expect(selectLimit).toHaveBeenCalled();
|
|
expect(insertOnDup).toHaveBeenCalled();
|
|
expect(mockSendMail).toHaveBeenCalledWith(
|
|
"[email protected]",
|
|
expect.stringContaining("password reset"),
|
|
expect.stringContaining("http://localhost:3000/reset"),
|
|
);
|
|
});
|
|
|
|
it("does not send email when user is not found", async () => {
|
|
selectLimit.mockResolvedValue([]);
|
|
|
|
const fd = new FormData();
|
|
fd.set("email", "[email protected]");
|
|
|
|
await expect(requestReset(fd)).rejects.toThrow("redirect");
|
|
expect(mockSendMail).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rate limits and does not throw on email without @", async () => {
|
|
const fd = new FormData();
|
|
fd.set("email", "not-an-email");
|
|
|
|
await expect(requestReset(fd)).rejects.toThrow("redirect");
|
|
expect(selectLimit).not.toHaveBeenCalled();
|
|
});
|
|
});
|