feat(nextcloud-status): Benachrichtigung abonnieren und Mail bei Störung
- Glocke je Kachel (persönlich, Benutzen-Ebene), Tabelle NextcloudAlertSubscription mit RLS - Zwei-Fehlschläge-Regel und gemeldeter Zustand auf der Zeile, Anspruch vor dem Mailversand - Mail (nur Deutsch) über MailService, bis zu drei Versuche, Zugriff beim Senden erneut geprüft - Fehlercodes in der Mail als lesbarer Hinweis Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -319,6 +319,45 @@ describe('MailService.sendReminderEmail (quick-260929-if2, E-04/E-07, T-IF2-05)'
|
||||
});
|
||||
});
|
||||
|
||||
describe('MailService.sendNextcloudAlertEmail (quick-261002-kxc, L-04/L-05)', () => {
|
||||
const input = {
|
||||
kind: 'down' as const,
|
||||
customerName: 'Kunde A',
|
||||
baseUrl: 'https://cloud.a.de',
|
||||
rating: { level: 'red' as const, reason: 'unreachable' as const, updateTo: null, eolDate: null, cycle: null },
|
||||
errorKind: 'network',
|
||||
errorDetail: 'ECONNREFUSED',
|
||||
at: new Date('2026-10-02T12:30:00.000Z'),
|
||||
};
|
||||
|
||||
function make() {
|
||||
return new MailService(makeFakeSettings({ t1: configA }) as any, makeFakeConfig({}) as any);
|
||||
}
|
||||
|
||||
it('sendet Betreff, Text mit Berliner Zeit und Modul-Link ueber den Transport des Mandanten; true bei Erfolg', async () => {
|
||||
const ok = await make().sendNextcloudAlertEmail('t1', 'alice@a.example.invalid', input);
|
||||
expect(ok).toBe(true);
|
||||
const sent = mockSendMail.mock.calls[0][0] as any;
|
||||
expect(sent.to).toBe('alice@a.example.invalid');
|
||||
expect(sent.from).toBe('noreply@a.example.invalid');
|
||||
expect(sent.subject).toBe('Nextcloud Kunde A: nicht erreichbar');
|
||||
expect(sent.html).toBeUndefined();
|
||||
expect(sent.text).toContain('14:30 Uhr');
|
||||
expect(sent.text).toContain('Verbindung abgelehnt');
|
||||
expect(sent.text).toContain('http://localhost:3000/modules/nextcloud-status');
|
||||
expect(mockClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('gibt false zurueck (wirft nicht), wenn der Transport scheitert', async () => {
|
||||
mockSendMail = vi.fn(async () => {
|
||||
throw new Error('SMTP down');
|
||||
});
|
||||
const ok = await make().sendNextcloudAlertEmail('t1', 'a@a.example.invalid', input);
|
||||
expect(ok).toBe(false);
|
||||
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);
|
||||
|
||||
@@ -4,6 +4,10 @@ import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
import type SMTPTransport from 'nodemailer/lib/smtp-transport';
|
||||
import {
|
||||
buildNextcloudAlertMail,
|
||||
type NextcloudAlertMailInput,
|
||||
} from '../nextcloud-status/nextcloud-alert-mail';
|
||||
import { SettingsService } from '../settings/settings.service';
|
||||
|
||||
/**
|
||||
@@ -406,4 +410,29 @@ export class MailService {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Benachrichtigung des Moduls Nextcloud-Status (quick-261002-kxc, L-04/L-05):
|
||||
* Stoerung oder "wieder in Ordnung" einer Cloud. Text und Betreff baut der
|
||||
* reine Baustein `buildNextcloudAlertMail` (nur Deutsch, wie die
|
||||
* Erinnerungsmails). Liefert true bei Erfolg, false (und ein Protokolleintrag)
|
||||
* bei Transportfehlern — der Aufrufer entscheidet ueber Wiederholungen.
|
||||
*/
|
||||
async sendNextcloudAlertEmail(
|
||||
tenantId: string,
|
||||
to: string,
|
||||
input: NextcloudAlertMailInput,
|
||||
): Promise<boolean> {
|
||||
const { subject, text } = buildNextcloudAlertMail(input, this.appUrl);
|
||||
try {
|
||||
await this.deliver(tenantId, { to, subject, text }, 'NextcloudAlert');
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to send NextcloudAlert email to ${to}`,
|
||||
error instanceof Error ? error.stack : String(error),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user