import 'reflect-metadata'; import { BadRequestException, ValidationPipe } from '@nestjs/common'; import { DEFAULT_WELCOME_MAIL_TEXTS } from '@tessera/shared'; import { describe, expect, it, vi } from 'vitest'; import { ROLES_KEY } from '../auth/decorators/roles.decorator'; import { WelcomeMailPreviewDto, WelcomeMailTemplateDto } from './dto/welcome-mail-template.dto'; import { WelcomeMailTemplateController } from './welcome-mail-template.controller'; /** * Administrator → Willkommensmail (eigene Vorlage). * * Festgenagelt: alle Routen nur ADMIN/SUPER_ADMIN; Mandant IMMER aus dem * Token (nie aus dem Body); unbekannte Platzhalter und zu lange Texte → 400 * mit Nennung des Platzhalters; die Testmail geht an den Aufrufer selbst. */ function makeTemplates() { return { getState: vi.fn(async (..._args: unknown[]) => ({ custom: false })), save: vi.fn(async (..._args: unknown[]) => ({ custom: true })), reset: vi.fn(async (..._args: unknown[]) => ({ custom: false })), }; } function makeWelcome() { return { preview: vi.fn(async (..._args: unknown[]) => ({ subject: 's', html: '', text: 't' })), sendTest: vi.fn(async (..._args: unknown[]) => ({ to: 'ada@example.invalid' })), }; } const admin = { id: 'u-admin', username: 'ada', role: 'ADMIN', tenantId: 't1' } as any; const proto = WelcomeMailTemplateController.prototype as any; const pipe = new ValidationPipe({ whitelist: true, transform: true }); const valid = { ...DEFAULT_WELCOME_MAIL_TEXTS }; async function validateBody(body: unknown, metatype: any = WelcomeMailTemplateDto) { return pipe.transform(body, { type: 'body', metatype }); } async function messagesOf(body: unknown): Promise { try { await validateBody(body); } catch (error) { expect(error).toBeInstanceOf(BadRequestException); return ((error as BadRequestException).getResponse() as { message: string[] }).message; } throw new Error('erwartete 400'); } describe('WelcomeMailTemplateController — Rechte', () => { it.each([ 'get', 'save', 'reset', 'preview', 'sendTest', ])('%s nur fuer ADMIN und SUPER_ADMIN', (name) => { expect(Reflect.getMetadata(ROLES_KEY, proto[name])).toEqual(['ADMIN', 'SUPER_ADMIN']); }); it('haengt an Pfad welcome-mail-template', () => { expect(Reflect.getMetadata('path', WelcomeMailTemplateController)).toBe( 'welcome-mail-template', ); }); }); describe('WelcomeMailTemplateController — Mandant aus dem Token', () => { it('reicht currentUser.tenantId (und Benutzername/-kennung) weiter, nie Werte aus dem Body', async () => { const templates = makeTemplates(); const welcome = makeWelcome(); const controller = new WelcomeMailTemplateController(templates as any, welcome as any); await controller.get(admin); await controller.save(admin, { ...valid, tenantId: 'evil' } as any); await controller.reset(admin); await controller.preview(admin, { ...valid, account: 'local' }, 'https://o.example.invalid'); const result = await controller.sendTest(admin, valid as any, undefined); expect(templates.getState).toHaveBeenCalledWith('t1'); expect(templates.save).toHaveBeenCalledWith('t1', valid, 'ada'); expect(templates.reset).toHaveBeenCalledWith('t1'); expect(welcome.preview).toHaveBeenCalledWith('t1', valid, 'local', 'https://o.example.invalid'); expect(welcome.sendTest).toHaveBeenCalledWith('t1', 'u-admin', valid, undefined); expect(result).toEqual({ success: true, to: 'ada@example.invalid' }); }); it('Vorschau ohne Kontoart zeigt die Verzeichniskonto-Variante', async () => { const welcome = makeWelcome(); const controller = new WelcomeMailTemplateController(makeTemplates() as any, welcome as any); await controller.preview(admin, { ...valid }); expect(welcome.preview.mock.calls[0][2]).toBe('directory'); }); }); describe('WelcomeMailTemplateDto (globale ValidationPipe)', () => { it('Standardtexte und alle bekannten Platzhalter (auch mit Leerraum/Grossschreibung) sind gueltig', async () => { const out: any = await validateBody({ ...valid, intro: '{{name}} {{vorname}} {{benutzername}} {{email}} {{adresse}} {{firma}} {{ Name }}', tenantId: 'evil', }); expect(out).not.toHaveProperty('tenantId'); }); it('unbekannter Platzhalter → 400 mit Feld und Platzhalter', async () => { const messages = await messagesOf({ ...valid, heading: 'Hallo {{xyz}} und {{abc}}' }); expect(messages).toContain('Überschrift: Unbekannte Platzhalter {{xyz}}, {{abc}}'); }); it('ein unbekannter Platzhalter im Betreff → "Unbekannter Platzhalter {{xyz}}"', async () => { const messages = await messagesOf({ ...valid, subject: 'Hi {{xyz}}' }); expect(messages).toContain('Betreff: Unbekannter Platzhalter {{xyz}}'); }); it('Laengen: Betreff/Ueberschrift 200, Texte 4000', async () => { await expect( validateBody({ ...valid, subject: 'a'.repeat(200), intro: 'b'.repeat(4000) }), ).resolves.toBeTruthy(); await expect(messagesOf({ ...valid, subject: 'a'.repeat(201) })).resolves.toHaveLength(1); await expect(messagesOf({ ...valid, heading: 'a'.repeat(201) })).resolves.toHaveLength(1); await expect(messagesOf({ ...valid, intro: 'a'.repeat(4001) })).resolves.toHaveLength(1); await expect(messagesOf({ ...valid, closing: 'a'.repeat(4001) })).resolves.toHaveLength(1); }); it('leerer Betreff/leere Ueberschrift → 400; leere Einleitung/Abschluss erlaubt', async () => { expect(await messagesOf({ ...valid, subject: ' ' })).toContain( 'Bitte geben Sie einen Betreff ein.', ); expect(await messagesOf({ ...valid, heading: '' })).toContain( 'Bitte geben Sie eine Überschrift ein.', ); await expect(validateBody({ ...valid, intro: '', closing: '' })).resolves.toBeTruthy(); }); it('fehlendes Feld → 400', async () => { const { closing: _closing, ...rest } = valid; await expect(messagesOf(rest)).resolves.not.toHaveLength(0); }); it('Vorschau: nur directory/local als Kontoart', async () => { await expect( validateBody({ ...valid, account: 'local' }, WelcomeMailPreviewDto), ).resolves.toBeTruthy(); await expect( validateBody({ ...valid, account: 'root' }, WelcomeMailPreviewDto), ).rejects.toBeInstanceOf(BadRequestException); }); });