feat(users): Willkommensmail mit Wellen-Kopf und Logo, Spalte Letzte Anmeldung
POST /users/:id/welcome-mail (gleiche Rechte wie Bearbeiten, jederzeit sendbar), GET /users/welcome-mail/status; HTML-Mail (Tabellenlayout, Inline-Stile, Kopfbild als CID-PNG aus assets/mail/welcome-header.svg, erzeugt mit scripts/render-mail-header.mjs) plus Textfassung. Verzeichniskonten: Hinweis auf Windows-Passwort; lokale Konten: Link Passwort festlegen (7 Tage, einmalig). Neue Spalte User.welcomeMailSentAt (Migration 20260930120000). Benutzerliste: Spalte Letzte Anmeldung, Zeilenaktionen als Symbole. Dockerfile kopiert apps/api/assets. Lokal per MailHog nachgewiesen. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
import { MailService } from './mail.service';
|
||||
import { MailService, WELCOME_HEADER_CID } from './mail.service';
|
||||
|
||||
/**
|
||||
* MailService.spec — NEU (260914-eym, Etappe 3c, WINDOWS #30). Der Bereich
|
||||
@@ -166,7 +166,11 @@ describe('MailService — Transport je Versand nach Mandant des Empfaengers (260
|
||||
const service = new MailService(settings as any, makeFakeConfig({}) as any);
|
||||
|
||||
await service.sendPasswordResetEmail('alice@a.example.invalid', 'tok-a', 't1');
|
||||
await service.sendWelcomeEmail('bob@b.example.invalid', 'bob', 't2');
|
||||
await service.sendWelcomeMail('t2', 'bob@b.example.invalid', {
|
||||
subject: 'Willkommen bei Tessera',
|
||||
text: 'Guten Tag bob,',
|
||||
html: '<p>bob</p>',
|
||||
});
|
||||
|
||||
expect(settings.getDecryptedSmtpConfig.mock.calls.map((c) => c[0])).toEqual(['t1', 't2']);
|
||||
const transports = vi.mocked(nodemailer.createTransport).mock.calls.map((c) => c[0] as any);
|
||||
@@ -314,3 +318,47 @@ describe('MailService.sendReminderEmail (quick-260929-if2, E-04/E-07, T-IF2-05)'
|
||||
expect(mockClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MailService.sendWelcomeMail / hasConfiguredTransport (Willkommensmail)', () => {
|
||||
it('haengt das Kopfbild als CID-Anhang an, reicht HTML und Text durch und wirft Transportfehler nach aussen', async () => {
|
||||
const service = new MailService(makeFakeSettings({ t1: configA }) as any, makeFakeConfig({}) as any);
|
||||
|
||||
await service.sendWelcomeMail('t1', 'neu@a.example.invalid', {
|
||||
subject: 'Willkommen bei Tessera',
|
||||
text: 'Text',
|
||||
html: `<img src="cid:${WELCOME_HEADER_CID}">`,
|
||||
});
|
||||
|
||||
const sent = mockSendMail.mock.calls[0][0] as any;
|
||||
expect(sent.from).toBe('noreply@a.example.invalid');
|
||||
expect(sent.html).toContain(`cid:${WELCOME_HEADER_CID}`);
|
||||
expect(sent.text).toBe('Text');
|
||||
expect(sent.attachments).toHaveLength(1);
|
||||
expect(sent.attachments[0].cid).toBe(WELCOME_HEADER_CID);
|
||||
expect(sent.attachments[0].contentType).toBe('image/png');
|
||||
// PNG-Signatur: das Bild aus apps/api/assets/mail ist wirklich geladen.
|
||||
expect((sent.attachments[0].content as Buffer).subarray(1, 4).toString()).toBe('PNG');
|
||||
|
||||
mockSendMail = vi.fn(async () => {
|
||||
throw new Error('ECONNREFUSED');
|
||||
});
|
||||
await expect(
|
||||
service.sendWelcomeMail('t1', 'neu@a.example.invalid', { subject: 'S', text: 'T', html: 'H' }),
|
||||
).rejects.toThrow('ECONNREFUSED');
|
||||
});
|
||||
|
||||
it('hasConfiguredTransport: SmtpConfig des Mandanten oder MAIL_HOST/TESSERA_SMTP_HOST — der Rueckfall localhost:1025 zaehlt nicht', async () => {
|
||||
expect(
|
||||
await new MailService(makeFakeSettings({ t1: configA }) as any, makeFakeConfig({}) as any).hasConfiguredTransport('t1'),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await new MailService(makeFakeSettings({}) as any, makeFakeConfig({ MAIL_HOST: 'smtp.example.invalid' }) as any).hasConfiguredTransport('t1'),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await new MailService(makeFakeSettings({}) as any, makeFakeConfig({ TESSERA_SMTP_HOST: 'legacy.example.invalid' }) as any).hasConfiguredTransport('t1'),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await new MailService(makeFakeSettings({}) as any, makeFakeConfig({}) as any).hasConfiguredTransport('t1'),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user