faed0d760f
- 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>
176 lines
5.7 KiB
TypeScript
176 lines
5.7 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
decideAlert,
|
|
FAILURES_FOR_RED,
|
|
planStatusWrite,
|
|
RETRY_DELAY_MS,
|
|
} from './nextcloud-alert-rules';
|
|
import { type NextcloudReference, rateNextcloud } from './nextcloud-rating';
|
|
import type { NextcloudCheckResult } from './nextcloud-status-fetch';
|
|
|
|
const NOW = new Date('2026-10-02T12:00:00Z');
|
|
const REFERENCE: NextcloudReference = {
|
|
fetchedAt: '2026-10-02T10:00:00.000Z',
|
|
cycles: [{ cycle: 35, eol: '2099-09-30', latest: '35.0.1' }],
|
|
};
|
|
|
|
const OK: NextcloudCheckResult = {
|
|
reachable: true,
|
|
maintenance: false,
|
|
needsDbUpgrade: false,
|
|
versionString: '35.0.1',
|
|
edition: 'enterprise',
|
|
productName: 'Nextcloud',
|
|
errorKind: null,
|
|
errorDetail: null,
|
|
};
|
|
const FAILED: NextcloudCheckResult = {
|
|
reachable: false,
|
|
maintenance: null,
|
|
needsDbUpgrade: null,
|
|
versionString: null,
|
|
edition: null,
|
|
productName: null,
|
|
errorKind: 'network',
|
|
errorDetail: 'ECONNREFUSED',
|
|
};
|
|
|
|
describe('Konstanten', () => {
|
|
it('zwei Fehlschlaege fuer Rot, Wiederholung nach fuenf Minuten', () => {
|
|
expect(FAILURES_FOR_RED).toBe(2);
|
|
expect(RETRY_DELAY_MS).toBe(5 * 60 * 1000);
|
|
});
|
|
});
|
|
|
|
describe('decideAlert', () => {
|
|
it.each([
|
|
['ok', 'red', 'down'],
|
|
['red', 'red', null],
|
|
['red', 'green', 'up'],
|
|
['red', 'yellow', 'up'],
|
|
['red', 'unknown', null],
|
|
['ok', 'green', null],
|
|
['ok', 'yellow', null],
|
|
['ok', 'unknown', null],
|
|
] as const)('%s + %s -> %s', (prev, level, expected) => {
|
|
expect(decideAlert(prev, level)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe('planStatusWrite', () => {
|
|
it('erster Fehlschlag: nur Zaehler und Zeitpunkt, kein Statusfeld, kein lastCheckedAt', () => {
|
|
const plan = planStatusWrite(0, FAILED, NOW);
|
|
expect(plan.outcome).toBe('pending');
|
|
expect(plan.data).toEqual({ consecutiveFailures: 1, firstFailureAt: NOW });
|
|
expect(plan.data).not.toHaveProperty('lastCheckedAt');
|
|
expect(plan.data).not.toHaveProperty('reachable');
|
|
});
|
|
|
|
it('zweiter Fehlschlag: bestaetigt, schreibt die Fehlerfelder', () => {
|
|
const plan = planStatusWrite(1, FAILED, NOW);
|
|
expect(plan.outcome).toBe('confirmed');
|
|
expect(plan.data).toMatchObject({
|
|
reachable: false,
|
|
errorKind: 'network',
|
|
errorDetail: 'ECONNREFUSED',
|
|
lastCheckedAt: NOW,
|
|
consecutiveFailures: 2,
|
|
});
|
|
expect(plan.data).not.toHaveProperty('firstFailureAt');
|
|
});
|
|
|
|
it('weitere Fehlschlaege einer roten Cloud lassen den Zaehler bei 2', () => {
|
|
const plan = planStatusWrite(2, FAILED, NOW);
|
|
expect(plan.outcome).toBe('confirmed');
|
|
expect(plan.data.consecutiveFailures).toBe(2);
|
|
});
|
|
|
|
it('jeder Erfolg setzt Zaehler und Zeitpunkt zurueck und schreibt alle Statusfelder', () => {
|
|
for (const prev of [0, 1, 2]) {
|
|
const plan = planStatusWrite(prev, OK, NOW);
|
|
expect(plan.outcome).toBe('ok');
|
|
expect(plan.data).toMatchObject({
|
|
reachable: true,
|
|
maintenance: false,
|
|
needsDbUpgrade: false,
|
|
versionString: '35.0.1',
|
|
edition: 'enterprise',
|
|
productName: 'Nextcloud',
|
|
errorKind: null,
|
|
errorDetail: null,
|
|
lastCheckedAt: NOW,
|
|
consecutiveFailures: 0,
|
|
firstFailureAt: null,
|
|
});
|
|
}
|
|
});
|
|
|
|
it('Wartungsmodus ist ein Erfolg der Pruefung (sofort rot ueber die Bewertung)', () => {
|
|
const plan = planStatusWrite(0, { ...OK, maintenance: true }, NOW);
|
|
expect(plan.outcome).toBe('ok');
|
|
expect(plan.data.maintenance).toBe(true);
|
|
});
|
|
});
|
|
|
|
/** Zustand der Zeile nach dem Schreiben, als Bewertungseingabe. */
|
|
function rateStored(stored: Record<string, unknown>) {
|
|
return rateNextcloud(
|
|
{
|
|
checkedAt: (stored.lastCheckedAt as Date) ?? null,
|
|
reachable: (stored.reachable as boolean | null) ?? null,
|
|
maintenance: (stored.maintenance as boolean | null) ?? null,
|
|
needsDbUpgrade: (stored.needsDbUpgrade as boolean | null) ?? null,
|
|
versionString: (stored.versionString as string | null) ?? null,
|
|
errorKind: (stored.errorKind as string | null) ?? null,
|
|
},
|
|
REFERENCE,
|
|
NOW,
|
|
);
|
|
}
|
|
|
|
describe('Zusammenspiel mit der Bewertung', () => {
|
|
const greenStored = {
|
|
lastCheckedAt: new Date('2026-10-02T11:00:00Z'),
|
|
reachable: true,
|
|
maintenance: false,
|
|
needsDbUpgrade: false,
|
|
versionString: '35.0.1',
|
|
errorKind: null,
|
|
};
|
|
|
|
it('gruene Cloud + ein Fehlschlag: bleibt gruen, keine Meldung', () => {
|
|
const plan = planStatusWrite(0, FAILED, NOW);
|
|
const rating = rateStored({ ...greenStored, ...plan.data });
|
|
expect(rating.level).toBe('green');
|
|
expect(decideAlert('ok', rating.level)).toBeNull();
|
|
});
|
|
|
|
it('gruene Cloud + zweiter Fehlschlag: rot -> down', () => {
|
|
const plan = planStatusWrite(1, FAILED, NOW);
|
|
const rating = rateStored({ ...greenStored, ...plan.data });
|
|
expect(rating).toMatchObject({ level: 'red', reason: 'unreachable' });
|
|
expect(decideAlert('ok', rating.level)).toBe('down');
|
|
});
|
|
|
|
it('gruene Cloud + Wartungsantwort: sofort down', () => {
|
|
const plan = planStatusWrite(0, { ...OK, maintenance: true }, NOW);
|
|
const rating = rateStored({ ...greenStored, ...plan.data });
|
|
expect(rating).toMatchObject({ level: 'red', reason: 'maintenance' });
|
|
expect(decideAlert('ok', rating.level)).toBe('down');
|
|
});
|
|
|
|
it('rote Cloud + gruene Antwort: up', () => {
|
|
const plan = planStatusWrite(2, OK, NOW);
|
|
const rating = rateStored({ ...greenStored, ...plan.data });
|
|
expect(rating.level).toBe('green');
|
|
expect(decideAlert('red', rating.level)).toBe('up');
|
|
});
|
|
|
|
it('nie geprueft + ein Fehlschlag: bleibt grau, keine Meldung', () => {
|
|
const plan = planStatusWrite(0, FAILED, NOW);
|
|
const rating = rateStored({ ...plan.data, lastCheckedAt: null });
|
|
expect(rating.level).toBe('unknown');
|
|
expect(decideAlert('ok', rating.level)).toBeNull();
|
|
});
|
|
});
|