features
This commit is contained in:
100
apps/api/src/mail/portal-mail.service.spec.ts
Normal file
100
apps/api/src/mail/portal-mail.service.spec.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user