import { createTransport } from 'nodemailer'; import { RecoveryMailer } from '../src/identity/recovery-mailer'; import { parseEnvironment } from '../src/config/environment'; jest.mock('nodemailer', () => ({ createTransport: jest.fn() })); describe('recovery SMTP adapter', () => { const sendMail = jest.fn(); const close = jest.fn(); const env = parseEnvironment({ DATABASE_URL: 'postgresql://localhost/mani', SMTP_HOST: 'smtp.example.com', SMTP_USER: 'user', SMTP_PASSWORD: 'secret', SMTP_FROM: 'support@example.com', RECOVERY_URL: 'https://shop.example.com/reset', }); beforeEach(() => { jest.clearAllMocks(); jest .mocked(createTransport) .mockReturnValue({ sendMail, close } as unknown as ReturnType< typeof createTransport >); }); it('fails explicitly when recovery is not configured', () => { expect(() => new RecoveryMailer( parseEnvironment({ DATABASE_URL: 'postgresql://localhost/mani' }), ).assertConfigured(), ).toThrow('not configured'); }); it('requires TLS and delivers a fragment-based reset link', async () => { sendMail.mockResolvedValue({}); await new RecoveryMailer(env).send('person@example.com', 'token'); expect(createTransport).toHaveBeenCalledWith( expect.objectContaining({ requireTLS: true, secure: false, disableFileAccess: true, disableUrlAccess: true, }), ); expect(sendMail).toHaveBeenCalledWith( expect.objectContaining({ to: 'person@example.com', text: expect.stringContaining( 'https://shop.example.com/reset#token=token', ), }), ); expect(close).toHaveBeenCalled(); }); it('closes transport even when sending fails and supports implicit TLS', async () => { sendMail.mockRejectedValue(new Error('delivery failed')); await expect( new RecoveryMailer({ ...env, SMTP_PORT: 465 }).send( 'person@example.com', 'token', ), ).rejects.toThrow(); expect(createTransport).toHaveBeenCalledWith( expect.objectContaining({ secure: true }), ); expect(close).toHaveBeenCalled(); }); });