import { Test, TestingModule } from '@nestjs/testing'; import { MailService, ValidationAccountEmailKind, isTransientSmtpError } from './mail.service'; import { AppConfigService } from '../config/config.service'; describe('isTransientSmtpError', () => { it('détecte les erreurs SMTP temporaires (454, coupure auth, timeout)', () => { expect(isTransientSmtpError(new Error('Invalid login: 454 4.7.0 Temporary authentication failure'))).toBe(true); expect(isTransientSmtpError(new Error('Connection lost to authentication server'))).toBe(true); expect(isTransientSmtpError(Object.assign(new Error('timeout'), { code: 'ETIMEDOUT' }))).toBe(true); expect(isTransientSmtpError(Object.assign(new Error('auth failed'), { responseCode: 454 }))).toBe(true); }); it('ignore les erreurs permanentes (mauvaise adresse, refus définitif)', () => { expect(isTransientSmtpError(new Error('Invalid login: 535 Authentication failed'))).toBe(false); expect(isTransientSmtpError(new Error('Mailbox unavailable'))).toBe(false); }); }); describe('MailService (ticket #28)', () => { let service: MailService; let sendEmailSpy: jest.SpyInstance; const mockConfigGet = jest.fn((key: string, defaultValue?: unknown) => { const values: Record = { app_name: 'TestApp', app_url: 'https://app.test/', smtp_host: '127.0.0.1', smtp_port: 1025, smtp_secure: false, smtp_auth_required: false, smtp_user: '', smtp_password: '', email_from_name: 'Test', email_from_address: 'noreply@test', }; if (key in values) return values[key]; return defaultValue; }); beforeEach(async () => { jest.clearAllMocks(); const module: TestingModule = await Test.createTestingModule({ providers: [ MailService, { provide: AppConfigService, useValue: { get: mockConfigGet }, }, ], }).compile(); service = module.get(MailService); sendEmailSpy = jest.spyOn(service, 'sendEmail').mockResolvedValue(undefined); }); afterEach(() => { sendEmailSpy.mockRestore(); }); it.each<[ValidationAccountEmailKind, string]>([ ['parent', 'Compte parent validé'], ['am', 'Inscription validée'], ])('sendValidatedAccountPasswordSetupEmail (%s) utilise Handlebars + lien token', async (kind, subjectPart) => { const token = '11111111-2222-3333-4444-555555555555'; await service.sendValidatedAccountPasswordSetupEmail( { email: 'user@test.fr', prenom: 'Jean', nom: 'Dupont', token, numeroDossier: '2025-000001', }, kind, ); expect(sendEmailSpy).toHaveBeenCalledTimes(1); const [, subject, html] = sendEmailSpy.mock.calls[0]; expect(subject).toContain('TestApp'); expect(subject).toContain(subjectPart); expect(html).toContain('Jean'); expect(html).toContain('Dupont'); expect(html).toContain('2025-000001'); expect(html).toContain(`https://app.test/create-password?token=${encodeURIComponent(token)}`); }); it('sendPasswordResetEmail utilise Handlebars + lien /reset-password', async () => { const token = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; await service.sendPasswordResetEmail({ email: 'u@test.fr', prenom: 'Marie', nom: 'Curie', token, }); expect(sendEmailSpy).toHaveBeenCalledTimes(1); const [, subject, html] = sendEmailSpy.mock.calls[0]; expect(subject).toContain('Réinitialisation'); expect(html).toContain('Marie'); expect(html).toContain(`https://app.test/reset-password?token=${encodeURIComponent(token)}`); expect(html).not.toContain('create-password'); }); it('sendResoumissionPendingEmail — accusé resoumission avec n° dossier', async () => { await service.sendResoumissionPendingEmail( 'parent@test.fr', 'Claire', 'MARTIN', '2026-000024', ); expect(sendEmailSpy).toHaveBeenCalledTimes(1); const [to, subject, html] = sendEmailSpy.mock.calls[0]; expect(to).toBe('parent@test.fr'); expect(subject).toContain('resoumis'); expect(subject).toContain('2026-000024'); expect(html).toContain('Claire'); expect(html).toContain('2026-000024'); expect(html).toContain('en attente de validation'); }); });