import { describe, expect, it, vi } from "vitest"; import type { MailSettings } from "@/lib/mail-settings"; import { createSmtpTransport, sendContactMessage, sendMail, sendTestEmail } from "@/lib/mail"; function settings(overrides: Partial = {}): MailSettings { return { smtp: { host: "smtp.example.com", port: 587, secure: false, username: "mailer", password: "secret", ...overrides.smtp }, sender: { email: "hello@example.com", name: "Studio", ...overrides.sender }, recipients: { contact: "contact@example.com", test: "test@example.com", ...overrides.recipients }, }; } function mockTransport() { const sendMailMock = vi.fn().mockResolvedValue({}); const createTransport = vi.fn().mockReturnValue({ sendMail: sendMailMock }); return { sendMailMock, createTransport }; } describe("createSmtpTransport", () => { it("builds the transport with host, port, secure, and auth", () => { const { createTransport } = mockTransport(); createSmtpTransport(settings({ smtp: { host: "h", port: 465, secure: true, username: "u", password: "p" } }), createTransport); expect(createTransport).toHaveBeenCalledWith({ host: "h", port: 465, secure: true, auth: { user: "u", pass: "p" }, }); }); it("requires host, username and password", () => { const { createTransport } = mockTransport(); expect(() => createSmtpTransport(settings({ smtp: { host: "", port: 587, secure: false, username: "u", password: "p" } }), createTransport)).toThrow(/host/i); expect(() => createSmtpTransport(settings({ smtp: { host: "h", port: 587, secure: false, username: "", password: "p" } }), createTransport)).toThrow(/username/i); expect(() => createSmtpTransport(settings({ smtp: { host: "h", port: 587, secure: false, username: "u", password: "" } }), createTransport)).toThrow(/password/i); }); }); describe("sendMail", () => { it("formats the from header with the sender name", async () => { const { sendMailMock, createTransport } = mockTransport(); await sendMail({ to: "x@y.z", subject: "S", text: "T" }, { settings: settings(), createTransport }); expect(sendMailMock).toHaveBeenCalledWith(expect.objectContaining({ from: "Studio ", to: "x@y.z" })); }); it("omits the display name when sender name is blank", async () => { const { sendMailMock, createTransport } = mockTransport(); await sendMail({ to: "x@y.z", subject: "S", text: "T" }, { settings: settings({ sender: { email: "hello@example.com", name: "" } }), createTransport }); expect(sendMailMock).toHaveBeenCalledWith(expect.objectContaining({ from: "hello@example.com" })); }); it("requires a from email", async () => { const { createTransport } = mockTransport(); await expect( sendMail({ to: "x@y.z", subject: "S", text: "T" }, { settings: settings({ sender: { email: "", name: "" } }), createTransport }), ).rejects.toThrow(/from email/i); }); }); describe("sendContactMessage", () => { it("targets the contact recipient with reply-to and full body", async () => { const { sendMailMock, createTransport } = mockTransport(); await sendContactMessage( { locale: "en", name: "Jane", email: "jane@x.z", phone: "123", company: "Acme", message: "Hi there team." }, { settings: settings(), createTransport }, ); const call = sendMailMock.mock.calls[0][0]; expect(call).toMatchObject({ to: "contact@example.com", subject: "New contact message", replyTo: "jane@x.z" }); expect(call.text).toContain("Name: Jane"); expect(call.text).toContain("Phone: 123"); expect(call.text).toContain("Company: Acme"); expect(call.text).toContain("Hi there team."); }); it("falls back to the test recipient when contact is empty", async () => { const { sendMailMock, createTransport } = mockTransport(); await sendContactMessage( { locale: "de", name: "Jane", email: "jane@x.z", message: "Fallback works fine." }, { settings: settings({ recipients: { contact: "", test: "fallback@x.z" } }), createTransport }, ); expect(sendMailMock).toHaveBeenCalledWith(expect.objectContaining({ to: "fallback@x.z" })); }); it("renders dashes for missing optional fields", async () => { const { sendMailMock, createTransport } = mockTransport(); await sendContactMessage( { locale: "en", name: "Jane", email: "jane@x.z", message: "No phone or company." }, { settings: settings(), createTransport }, ); const call = sendMailMock.mock.calls[0][0]; expect(call.text).toContain("Phone: -"); expect(call.text).toContain("Company: -"); }); }); describe("sendTestEmail", () => { it("sends to the test recipient", async () => { const { sendMailMock, createTransport } = mockTransport(); await sendTestEmail({ settings: settings(), createTransport }); expect(sendMailMock).toHaveBeenCalledWith(expect.objectContaining({ to: "test@example.com", subject: "SMTP test email" })); }); it("falls back to the contact recipient when test is empty", async () => { const { sendMailMock, createTransport } = mockTransport(); await sendTestEmail({ settings: settings({ recipients: { contact: "c@x.z", test: "" } }), createTransport }); expect(sendMailMock).toHaveBeenCalledWith(expect.objectContaining({ to: "c@x.z" })); }); it("propagates transport failures", async () => { const createTransport = vi.fn().mockReturnValue({ sendMail: vi.fn().mockRejectedValue(new Error("Auth failed.")) }); await expect(sendTestEmail({ settings: settings(), createTransport })).rejects.toThrow("Auth failed."); }); });