Files
ecommerce-platform/test/unit/lib/email.test.ts
T
davide 5428eeccc1 test: add unit tests for lib/ (validate, auth, storage, email, rate-limit)
49 tests for all 10 Zod schemas in validate.ts, 26 tests for auth (hashPassword,
verifyPassword, createSession, getSession, getCurrentUser, deleteSession), 11 for
storage (magic-byte validation, saveImage, deleteImageFile), 9 for email (sendMail
scenarios), and 6 for rate limiting logic.
2026-05-19 14:07:38 +02:00

110 lines
4.0 KiB
TypeScript

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: '<p>Hello</p>' })
expect(mockSendMail).toHaveBeenCalledWith(
expect.objectContaining({
to: 'user@example.com',
subject: 'Test',
html: '<p>Hello</p>',
})
)
})
it('includes optional text field when provided', async () => {
await sendEmail({ to: 'u@e.com', subject: 'S', html: '<p>h</p>', 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: '<p>h</p>' })).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/)
})
})