import { describe, it, expect, beforeEach, vi } from 'vitest' import type { Payload } from 'payload' import type { Tenant } from '@/payload-types' const mockSendMail = vi.fn(async () => ({ messageId: 'mocked-id' })) const mockCreateTransport = vi.fn(() => ({ sendMail: mockSendMail })) vi.mock('nodemailer', () => ({ __esModule: true, default: { createTransport: (...args: unknown[]) => mockCreateTransport(...args), }, })) import { sendTenantEmail, invalidateTenantEmailCache, invalidateGlobalEmailCache, } from '@/lib/email/tenant-email-service' describe('tenant email service', () => { let payload: Payload let mockFindByID: ReturnType beforeEach(() => { mockSendMail.mockClear() mockCreateTransport.mockClear() mockFindByID = vi.fn() payload = { findByID: mockFindByID, } as unknown as Payload process.env.SMTP_HOST = 'smtp.global.test' process.env.SMTP_PORT = '587' process.env.SMTP_SECURE = 'false' process.env.SMTP_USER = 'global-user' process.env.SMTP_PASS = 'global-pass' process.env.SMTP_FROM_ADDRESS = 'noreply@example.com' invalidateGlobalEmailCache() }) it('falls back to global SMTP configuration when tenant has no custom SMTP', async () => { const tenant = { id: 1, slug: 'tenant-a', name: 'Tenant A', email: { useCustomSmtp: false, }, } as Tenant mockFindByID.mockResolvedValue(tenant) const result = await sendTenantEmail(payload, tenant.id, { to: 'user@example.com', subject: 'Test', text: 'Hello from test', }) expect(result.success).toBe(true) expect(mockCreateTransport).toHaveBeenCalledTimes(1) expect(mockCreateTransport).toHaveBeenCalledWith({ host: 'smtp.global.test', port: 587, secure: false, auth: { user: 'global-user', pass: 'global-pass', }, }) expect(mockSendMail).toHaveBeenCalledWith( expect.objectContaining({ from: '"Tenant A" ', to: 'user@example.com', }), ) }) it('uses cached tenant-specific transporter and re-creates it after invalidation', async () => { const tenant = { id: 42, slug: 'tenant-b', name: 'Tenant B', email: { useCustomSmtp: true, fromAddress: 'info@tenant-b.de', fromName: 'Tenant B', smtp: { host: 'smtp.tenant-b.de', port: 465, secure: true, user: 'tenant-user', pass: 'tenant-pass', }, }, } as Tenant mockFindByID.mockResolvedValue(tenant) await sendTenantEmail(payload, tenant.id, { to: 'recipient@example.com', subject: 'Hi', text: 'First email', }) expect(mockCreateTransport).toHaveBeenCalledTimes(1) expect(mockCreateTransport).toHaveBeenCalledWith({ host: 'smtp.tenant-b.de', port: 465, secure: true, auth: { user: 'tenant-user', pass: 'tenant-pass', }, }) mockCreateTransport.mockClear() await sendTenantEmail(payload, tenant.id, { to: 'recipient@example.com', subject: 'Hi again', text: 'Second email', }) expect(mockCreateTransport).not.toHaveBeenCalled() invalidateTenantEmailCache(tenant.id) await sendTenantEmail(payload, tenant.id, { to: 'recipient@example.com', subject: 'After invalidation', text: 'Third email', }) expect(mockCreateTransport).toHaveBeenCalledTimes(1) }) })