add verification service and email service tests (13 new tests)
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 12m26s
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 12m26s
This commit is contained in:
parent
9dcc473e78
commit
1a37765f41
2 changed files with 133 additions and 17 deletions
120
src/__tests__/verification.test.ts
Normal file
120
src/__tests__/verification.test.ts
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
// Unmock verification service (setup.ts mocks it globally)
|
||||
vi.unmock("../services/verification.js");
|
||||
|
||||
const { mockQueryWithRetry } = vi.hoisted(() => ({
|
||||
mockQueryWithRetry: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../services/db.js", () => ({
|
||||
default: { on: vi.fn(), end: vi.fn() },
|
||||
pool: { on: vi.fn(), end: vi.fn() },
|
||||
queryWithRetry: mockQueryWithRetry,
|
||||
connectWithRetry: vi.fn(),
|
||||
initDatabase: vi.fn(),
|
||||
cleanupStaleData: vi.fn(),
|
||||
isTransientError: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../services/logger.js", () => ({
|
||||
default: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() },
|
||||
}));
|
||||
|
||||
import { verifyCode, createPendingVerification } from "../services/verification.js";
|
||||
|
||||
describe("Verification Service", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("verifyCode", () => {
|
||||
it('returns "invalid" for non-existent email', async () => {
|
||||
mockQueryWithRetry.mockResolvedValueOnce({ rows: [], rowCount: 0 });
|
||||
const result = await verifyCode("nobody@example.com", "123456");
|
||||
expect(result.status).toBe("invalid");
|
||||
});
|
||||
|
||||
it('returns "expired" for expired codes', async () => {
|
||||
const pastDate = new Date(Date.now() - 20 * 60 * 1000).toISOString();
|
||||
mockQueryWithRetry
|
||||
.mockResolvedValueOnce({
|
||||
rows: [{ email: "test@example.com", code: "123456", created_at: pastDate, expires_at: pastDate, attempts: 0 }],
|
||||
rowCount: 1,
|
||||
})
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 0 });
|
||||
const result = await verifyCode("test@example.com", "123456");
|
||||
expect(result.status).toBe("expired");
|
||||
});
|
||||
|
||||
it('returns "max_attempts" after 3 wrong attempts', async () => {
|
||||
const future = new Date(Date.now() + 10 * 60 * 1000).toISOString();
|
||||
mockQueryWithRetry
|
||||
.mockResolvedValueOnce({
|
||||
rows: [{ email: "test@example.com", code: "123456", created_at: new Date().toISOString(), expires_at: future, attempts: 3 }],
|
||||
rowCount: 1,
|
||||
})
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 0 });
|
||||
const result = await verifyCode("test@example.com", "999999");
|
||||
expect(result.status).toBe("max_attempts");
|
||||
});
|
||||
|
||||
it('returns "ok" for correct code (timing-safe comparison)', async () => {
|
||||
const future = new Date(Date.now() + 10 * 60 * 1000).toISOString();
|
||||
mockQueryWithRetry
|
||||
.mockResolvedValueOnce({
|
||||
rows: [{ email: "test@example.com", code: "654321", created_at: new Date().toISOString(), expires_at: future, attempts: 0 }],
|
||||
rowCount: 1,
|
||||
})
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 0 })
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 0 });
|
||||
const result = await verifyCode("test@example.com", "654321");
|
||||
expect(result.status).toBe("ok");
|
||||
});
|
||||
|
||||
it('returns "invalid" for wrong code', async () => {
|
||||
const future = new Date(Date.now() + 10 * 60 * 1000).toISOString();
|
||||
mockQueryWithRetry
|
||||
.mockResolvedValueOnce({
|
||||
rows: [{ email: "test@example.com", code: "654321", created_at: new Date().toISOString(), expires_at: future, attempts: 0 }],
|
||||
rowCount: 1,
|
||||
})
|
||||
.mockResolvedValueOnce({ rows: [], rowCount: 0 });
|
||||
const result = await verifyCode("test@example.com", "000000");
|
||||
expect(result.status).toBe("invalid");
|
||||
});
|
||||
|
||||
it("normalizes email to lowercase and trims whitespace", async () => {
|
||||
mockQueryWithRetry.mockResolvedValueOnce({ rows: [], rowCount: 0 });
|
||||
await verifyCode(" Test@Example.COM ", "123456");
|
||||
expect(mockQueryWithRetry).toHaveBeenCalledWith(expect.stringContaining("SELECT"), ["test@example.com"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createPendingVerification", () => {
|
||||
it("generates a 6-digit code", async () => {
|
||||
mockQueryWithRetry.mockResolvedValueOnce({ rows: [], rowCount: 0 }).mockResolvedValueOnce({ rows: [], rowCount: 0 });
|
||||
const result = await createPendingVerification("test@example.com");
|
||||
expect(result.code).toMatch(/^\d{6}$/);
|
||||
});
|
||||
|
||||
it("replaces existing pending verification for same email", async () => {
|
||||
mockQueryWithRetry.mockResolvedValueOnce({ rows: [], rowCount: 0 }).mockResolvedValueOnce({ rows: [], rowCount: 0 });
|
||||
await createPendingVerification("test@example.com");
|
||||
expect(mockQueryWithRetry).toHaveBeenCalledWith(expect.stringContaining("DELETE FROM pending_verifications"), ["test@example.com"]);
|
||||
});
|
||||
|
||||
it("sets expiry 15 minutes in the future", async () => {
|
||||
mockQueryWithRetry.mockResolvedValueOnce({ rows: [], rowCount: 0 }).mockResolvedValueOnce({ rows: [], rowCount: 0 });
|
||||
const result = await createPendingVerification("test@example.com");
|
||||
const diff = new Date(result.expiresAt).getTime() - new Date(result.createdAt).getTime();
|
||||
expect(diff).toBe(15 * 60 * 1000);
|
||||
});
|
||||
|
||||
it("initializes attempts to 0", async () => {
|
||||
mockQueryWithRetry.mockResolvedValueOnce({ rows: [], rowCount: 0 }).mockResolvedValueOnce({ rows: [], rowCount: 0 });
|
||||
const result = await createPendingVerification("test@example.com");
|
||||
expect(result.attempts).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue