import { describe, it, expect, vi, beforeEach } from 'vitest' const { mockSendMail, mockCreateTransport } = vi.hoisted(() => { const mockSendMail = vi.fn().mockResolvedValue({ messageId: 'mock-id' }) const mockCreateTransport = vi.fn().mockReturnValue({ sendMail: mockSendMail }) return { mockSendMail, mockCreateTransport } }) vi.mock('nodemailer', () => ({ default: { createTransport: mockCreateTransport }, })) import { sendEmail, sendOrderConfirmationEmail, sendPasswordResetEmail } from '@/lib/email' beforeEach(() => { vi.clearAllMocks() mockSendMail.mockResolvedValue({ messageId: 'mock-id' }) }) // ─── sendEmail ──────────────────────────────────────────────────────────────── describe('sendEmail', () => { it('calls sendMail with correct to, subject, html', async () => { await sendEmail({ to: 'user@example.com', subject: 'Test', html: '
Hello
' }) expect(mockSendMail).toHaveBeenCalledWith( expect.objectContaining({ to: 'user@example.com', subject: 'Test', html: 'Hello
', }) ) }) it('includes optional text field when provided', async () => { await sendEmail({ to: 'u@e.com', subject: 'S', html: 'h
', text: 'plain text' }) expect(mockSendMail).toHaveBeenCalledWith(expect.objectContaining({ text: 'plain text' })) }) it('propagates sendMail errors', async () => { mockSendMail.mockRejectedValueOnce(new Error('SMTP down')) await expect(sendEmail({ to: 'u@e.com', subject: 'S', html: 'h
' })).rejects.toThrow('SMTP down') }) }) // ─── sendOrderConfirmationEmail ─────────────────────────────────────────────── describe('sendOrderConfirmationEmail', () => { it('sends email with correct subject containing orderId', async () => { await sendOrderConfirmationEmail('customer@example.com', { orderId: 'order-42', grandTotal: 2999, currency: 'EUR', }) expect(mockSendMail).toHaveBeenCalledWith( expect.objectContaining({ to: 'customer@example.com', subject: expect.stringContaining('order-42'), }) ) }) it('formats price correctly (cents to euros)', async () => { await sendOrderConfirmationEmail('customer@example.com', { orderId: 'order-1', grandTotal: 2999, currency: 'EUR', }) const callArg = mockSendMail.mock.calls[0][0] expect(callArg.html).toContain('29.99') expect(callArg.html).toContain('EUR') }) it('includes orderId in the email body', async () => { await sendOrderConfirmationEmail('customer@example.com', { orderId: 'order-xyz', grandTotal: 1000, currency: 'USD', }) const callArg = mockSendMail.mock.calls[0][0] expect(callArg.html).toContain('order-xyz') }) it('includes account orders link', async () => { await sendOrderConfirmationEmail('customer@example.com', { orderId: 'order-1', grandTotal: 100, currency: 'EUR', }) const callArg = mockSendMail.mock.calls[0][0] expect(callArg.html).toContain('/account/orders') }) }) // ─── sendPasswordResetEmail ─────────────────────────────────────────────────── describe('sendPasswordResetEmail', () => { it('sends email with reset token in URL', async () => { await sendPasswordResetEmail('user@example.com', 'reset-token-abc') const callArg = mockSendMail.mock.calls[0][0] expect(callArg.html).toContain('reset-token-abc') expect(callArg.to).toBe('user@example.com') }) it('subject mentions password reset', async () => { await sendPasswordResetEmail('user@example.com', 'token') const callArg = mockSendMail.mock.calls[0][0] expect(callArg.subject).toMatch(/[Pp]assword [Rr]eset/) }) })