e10da76259
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>
152 lines
6.2 KiB
TypeScript
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);
|
|
});
|
|
});
|