101 lines
4.7 KiB
TypeScript
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=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('<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);
|
|
});
|
|
});
|