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>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user