Files
ldap-identidy/apps/api/src/mail/portal-mail.service.spec.ts
Bastian Wagner 45a41c2077 features
2026-07-17 14:07:04 +02:00

101 lines
4.7 KiB
TypeScript

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<string, string> = {
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 <script>alert(1)</script>,',
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&#x3D;abc');
expect(html).toContain('&lt;script&gt;alert(1)&lt;/script&gt;');
});
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('<a');
});
it('supports missing optional display name', async () => {
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);
});
});