import { existsSync } from 'node:fs'; import { join } from 'node:path'; import { PortalMailService } from './portal-mail.service'; import { mailTemplateDir } from './mail-template.constants'; import { MailTemplateRendererService } from './mail-template-renderer.service'; import { PortalMailDeliveryError, PortalMailTemplateError } from './mail-errors'; describe('PortalMailService templates', () => { const config = { get: jest.fn((key: string) => { const values: Record = { PUBLIC_WEB_URL: 'https://portal.example.com', MAIL_PRODUCT_NAME: 'LDAP Portal', MAIL_COMPANY_NAME: 'Example Corp', MAIL_PRIMARY_COLOR: '#0f6b6e', MAIL_SUPPORT_EMAIL: 'support@example.com', }; return values[key]; }), }; function createService(sendMail = jest.fn().mockResolvedValue(undefined)) { const renderer = new MailTemplateRendererService(); return { service: new PortalMailService({ sendMail } as any, config as any, renderer), sendMail, renderer }; } it('renders password reset HTML with escaped display name and action URL', async () => { const { renderer } = createService(); const html = await renderer.renderHtml('password-reset', { branding: { productName: 'LDAP Portal', companyName: 'Example Corp', primaryColor: '#0f6b6e', supportEmail: 'support@example.com', }, title: 'Passwort zuruecksetzen', greeting: 'Hallo ,', intro: 'Intro', action: { label: 'Passwort zuruecksetzen', url: 'https://portal.example.com/reset?token=abc' }, resetUrl: 'https://portal.example.com/reset?token=abc', expiresAtText: '17.07.2026, 12:30', warningBox: { text: 'Warnung' }, }); expect(html).toContain('Passwort zuruecksetzen'); expect(html).toContain('https://portal.example.com/reset?token=abc'); expect(html).toContain('<script>alert(1)</script>'); }); it('renders password reset text with the full action URL', async () => { const { renderer } = createService(); const text = await renderer.renderText('password-reset', { branding: { supportEmail: 'support@example.com' }, title: 'Passwort zuruecksetzen', intro: 'Intro', resetUrl: 'https://portal.example.com/reset?token=abc&x=1', expiresAtText: '17.07.2026, 12:30', }); expect(text).toContain('https://portal.example.com/reset?token=abc&x=1'); expect(text).not.toContain(' { const { service, sendMail } = createService(); await service.sendPasswordResetMail({ recipient: 'maria@example.com', token: 'abc', expiresAt: new Date('2026-07-17T12:30:00Z') }); expect(sendMail).toHaveBeenCalledWith(expect.objectContaining({ template: 'password-reset', subject: 'Passwort fuer LDAP Portal zuruecksetzen', text: expect.stringContaining('Passwort zuruecksetzen'), })); }); it('adds branding and passes the expected template name to the mailer', async () => { const { service, sendMail } = createService(); await service.sendPasswordResetMail({ recipient: 'maria@example.com', token: 'abc', expiresAt: new Date('2026-07-17T12:30:00Z') }); expect(sendMail.mock.calls[0][0].context.branding.productName).toBe('LDAP Portal'); expect(sendMail.mock.calls[0][0].template).toBe('password-reset'); }); it('allows optional notification fields to be omitted', async () => { const { service, sendMail } = createService(); await service.sendNotificationMail({ recipient: 'admin@example.com', title: 'Hinweis' }); expect(sendMail).toHaveBeenCalledWith(expect.objectContaining({ template: 'generic-notification' })); }); it('keeps templates available for build asset copying', () => { expect(existsSync(join(mailTemplateDir, 'password-reset.hbs'))).toBe(true); expect(existsSync(join(mailTemplateDir, 'partials', 'button.hbs'))).toBe(true); }); it('wraps template errors separately from delivery errors', async () => { const renderer = { renderHtml: jest.fn().mockRejectedValue(new Error('missing partial')), renderText: jest.fn() }; const service = new PortalMailService({ sendMail: jest.fn() } as any, config as any, renderer as any); await expect(service.sendPasswordResetMail({ recipient: 'maria@example.com', token: 'abc' })).rejects.toBeInstanceOf(PortalMailTemplateError); }); it('wraps delivery errors without treating them as template errors', async () => { const { service } = createService(jest.fn().mockRejectedValue(new Error('smtp down'))); await expect(service.sendPasswordResetMail({ recipient: 'maria@example.com', token: 'abc' })).rejects.toBeInstanceOf(PortalMailDeliveryError); }); });