fceceeee4b
- SMTP: SMTP_HOST/PORT/USER/FROM/TLS now all have ENV override support (previously only SMTP_PASSWORD was env-aware). ENV takes priority over DB. - docker-compose.yml: forward all SMTP_* env vars to app container + add Mailhog service (ports 1025 SMTP / 8025 HTTP, always available in dev) - Password reset: PasswordResetToken Prisma model + authRouter with requestPasswordReset (timing-safe, no email enumeration) + resetPassword - UI: /auth/forgot-password, /auth/reset-password/[token] pages + "Forgot password?" link on sign-in page - E2E: Mailhog helpers (getLatestEmailTo, clearMailhog, extractUrlFromEmail) + invite-flow.spec.ts + password-reset.spec.ts Co-Authored-By: claude-flow <ruv@ruv.net>
161 lines
4.7 KiB
TypeScript
161 lines
4.7 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import nodemailer from "nodemailer";
|
|
|
|
const { findUnique, sendMail, createTransport } = vi.hoisted(() => {
|
|
const sendMail = vi.fn().mockResolvedValue({ messageId: "test" });
|
|
return {
|
|
findUnique: vi.fn(),
|
|
sendMail,
|
|
createTransport: vi.fn(() => ({ sendMail })),
|
|
};
|
|
});
|
|
|
|
vi.mock("@capakraken/db", () => ({
|
|
prisma: {
|
|
systemSettings: { findUnique },
|
|
},
|
|
}));
|
|
|
|
vi.mock("nodemailer", () => ({
|
|
default: { createTransport },
|
|
}));
|
|
|
|
vi.mock("../lib/logger.js", () => ({
|
|
logger: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() },
|
|
}));
|
|
|
|
// Re-import after mocks are set up
|
|
const { sendEmail } = await import("../lib/email.js");
|
|
|
|
describe("SMTP ENV overrides", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
// Default DB row — used as fallback
|
|
findUnique.mockResolvedValue({
|
|
smtpHost: "db-smtp.example.com",
|
|
smtpPort: 587,
|
|
smtpUser: "db-user@example.com",
|
|
smtpPassword: "db-password",
|
|
smtpFrom: "db-from@example.com",
|
|
smtpTls: true,
|
|
});
|
|
sendMail.mockResolvedValue({ messageId: "ok" });
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
it("SMTP_HOST env overrides DB smtpHost", async () => {
|
|
vi.stubEnv("SMTP_HOST", "env-smtp.example.com");
|
|
|
|
await sendEmail({ to: "user@test.com", subject: "Test", text: "Hello" });
|
|
|
|
expect(nodemailer.createTransport).toHaveBeenCalledWith(
|
|
expect.objectContaining({ host: "env-smtp.example.com" }),
|
|
);
|
|
});
|
|
|
|
it("SMTP_PORT env overrides DB smtpPort (parsed as integer)", async () => {
|
|
vi.stubEnv("SMTP_PORT", "1025");
|
|
|
|
await sendEmail({ to: "user@test.com", subject: "Test", text: "Hello" });
|
|
|
|
expect(nodemailer.createTransport).toHaveBeenCalledWith(
|
|
expect.objectContaining({ port: 1025 }),
|
|
);
|
|
});
|
|
|
|
it("SMTP_TLS=false sets secure: false", async () => {
|
|
vi.stubEnv("SMTP_TLS", "false");
|
|
|
|
await sendEmail({ to: "user@test.com", subject: "Test", text: "Hello" });
|
|
|
|
expect(nodemailer.createTransport).toHaveBeenCalledWith(
|
|
expect.objectContaining({ secure: false }),
|
|
);
|
|
});
|
|
|
|
it("SMTP_TLS=true sets secure: true", async () => {
|
|
vi.stubEnv("SMTP_TLS", "true");
|
|
|
|
await sendEmail({ to: "user@test.com", subject: "Test", text: "Hello" });
|
|
|
|
expect(nodemailer.createTransport).toHaveBeenCalledWith(
|
|
expect.objectContaining({ secure: true }),
|
|
);
|
|
});
|
|
|
|
it("no SMTP_USER → auth uses DB user; with SMTP_USER env → auth uses env user", async () => {
|
|
vi.stubEnv("SMTP_USER", "env-user@example.com");
|
|
|
|
await sendEmail({ to: "user@test.com", subject: "Test", text: "Hello" });
|
|
|
|
expect(nodemailer.createTransport).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
auth: expect.objectContaining({ user: "env-user@example.com" }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("no SMTP_USER and no SMTP_PASSWORD → auth is undefined (Mailhog scenario)", async () => {
|
|
// Clear all user/password from both ENV and DB
|
|
vi.stubEnv("SMTP_HOST", "mailhog");
|
|
vi.stubEnv("SMTP_PORT", "1025");
|
|
vi.stubEnv("SMTP_TLS", "false");
|
|
// Explicitly no SMTP_USER / SMTP_PASSWORD
|
|
findUnique.mockResolvedValue({
|
|
smtpHost: null,
|
|
smtpPort: null,
|
|
smtpUser: null,
|
|
smtpPassword: null,
|
|
smtpFrom: "noreply@capakraken.app",
|
|
smtpTls: null,
|
|
});
|
|
|
|
await sendEmail({ to: "user@test.com", subject: "Test", text: "Hello" });
|
|
|
|
expect(nodemailer.createTransport).toHaveBeenCalledWith(
|
|
expect.objectContaining({ auth: undefined }),
|
|
);
|
|
});
|
|
|
|
it("DB values are used when no ENV overrides are set", async () => {
|
|
await sendEmail({ to: "user@test.com", subject: "Test", text: "Hello" });
|
|
|
|
expect(nodemailer.createTransport).toHaveBeenCalledWith({
|
|
host: "db-smtp.example.com",
|
|
port: 587,
|
|
secure: true,
|
|
auth: { user: "db-user@example.com", pass: "db-password" },
|
|
});
|
|
});
|
|
|
|
it("ENV overrides take priority over DB for all SMTP fields simultaneously", async () => {
|
|
vi.stubEnv("SMTP_HOST", "mailhog");
|
|
vi.stubEnv("SMTP_PORT", "1025");
|
|
vi.stubEnv("SMTP_USER", ""); // empty string = no override
|
|
vi.stubEnv("SMTP_TLS", "false");
|
|
vi.stubEnv("SMTP_PASSWORD", ""); // empty = no override
|
|
|
|
findUnique.mockResolvedValue({
|
|
smtpHost: "db-smtp.example.com",
|
|
smtpPort: 587,
|
|
smtpUser: "db-user@example.com",
|
|
smtpPassword: "db-password",
|
|
smtpFrom: "from@example.com",
|
|
smtpTls: true,
|
|
});
|
|
|
|
await sendEmail({ to: "user@test.com", subject: "Test", text: "Hello" });
|
|
|
|
expect(nodemailer.createTransport).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
host: "mailhog",
|
|
port: 1025,
|
|
secure: false,
|
|
}),
|
|
);
|
|
});
|
|
});
|