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) { 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(); }); });