Files
tessera-ctl/apps/api/src/user/welcome-mail-template.controller.spec.ts
T
schalli e10da76259
Tessera CI/CD / Lint & Type Check (push) Successful in 50s
Tessera CI/CD / Tests (push) Successful in 1m22s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 19s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m10s
feat(admin): eigene Vorlage fuer die Willkommensmail mit Platzhaltern, Vorschau und Testmail
Administrator -> Willkommensmail: Betreff, Ueberschrift, Einleitung, Abschluss je
Mandant (Tabelle WelcomeMailTemplate, RLS je Mandant, Migration 20260930150000);
Platzhalter {{name}} {{vorname}} {{benutzername}} {{email}} {{adresse}} {{firma}},
unbekannte -> 400 bzw. Hinweis beim Tippen; Werte escaped, Vorlage reiner Text.
Live-Vorschau per API gerendert, Testmail an die eigene Adresse ohne Token,
Zuruecksetzen auf Standard. Feste Bausteine (Kopf, Zugangsdaten, Anmeldehinweis,
Knoepfe, Fusszeile) bleiben immer drin.
Kopf: Wellenzelle dunkel statt weiss, Streifen 600x40, Inhalt 24 px naeher –
keine weisse Luecke, wenn OWA das CID-Bild nicht zeigt.
Lokal nachgewiesen: Hinweis/Sperre bei {{xyz}}, Speichern, Testmail (Link nur
/login), echte Mail mit eigener Vorlage und 7-Tage-Link.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 16:49:17 +02:00

152 lines
6.2 KiB
TypeScript

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: '<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<string[]> {
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);
});
});