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
|
|
@ -1,7 +1,11 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
// Mock nodemailer before importing email service
|
||||
const mockSendMail = vi.fn();
|
||||
vi.unmock("../services/email.js");
|
||||
|
||||
const { mockSendMail } = vi.hoisted(() => ({
|
||||
mockSendMail: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("nodemailer", () => ({
|
||||
default: {
|
||||
createTransport: vi.fn(() => ({
|
||||
|
|
@ -10,7 +14,6 @@ vi.mock("nodemailer", () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
// Mock logger
|
||||
vi.mock("../services/logger.js", () => ({
|
||||
default: { info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() },
|
||||
}));
|
||||
|
|
@ -25,35 +28,28 @@ describe("Email Service", () => {
|
|||
describe("sendVerificationEmail", () => {
|
||||
it("constructs correct email with code", async () => {
|
||||
mockSendMail.mockResolvedValueOnce({ messageId: "test-123" });
|
||||
|
||||
const result = await sendVerificationEmail("user@example.com", "654321");
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockSendMail).toHaveBeenCalledOnce();
|
||||
|
||||
const mailOptions = mockSendMail.mock.calls[0][0];
|
||||
expect(mailOptions.to).toBe("user@example.com");
|
||||
expect(mailOptions.subject).toContain("DocFast");
|
||||
expect(mailOptions.subject).toContain("Verify");
|
||||
expect(mailOptions.text).toContain("654321");
|
||||
expect(mailOptions.html).toContain("654321");
|
||||
const opts = mockSendMail.mock.calls[0][0];
|
||||
expect(opts.to).toBe("user@example.com");
|
||||
expect(opts.subject).toContain("Verify");
|
||||
expect(opts.text).toContain("654321");
|
||||
expect(opts.html).toContain("654321");
|
||||
});
|
||||
|
||||
it("returns false when SMTP fails", async () => {
|
||||
mockSendMail.mockRejectedValueOnce(new Error("SMTP connection refused"));
|
||||
|
||||
const result = await sendVerificationEmail("user@example.com", "123456");
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("includes expiry notice in email body", async () => {
|
||||
mockSendMail.mockResolvedValueOnce({ messageId: "test-456" });
|
||||
|
||||
await sendVerificationEmail("user@example.com", "111111");
|
||||
|
||||
const mailOptions = mockSendMail.mock.calls[0][0];
|
||||
expect(mailOptions.text).toContain("15 minutes");
|
||||
const opts = mockSendMail.mock.calls[0][0];
|
||||
expect(opts.text).toContain("15 minutes");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
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