import { describe, expect, it } from 'vitest'; import { listPbsDatastoreNames, normalizePbs, normalizePmg, normalizePve, readBool, readList, readNumber, readText, } from './proxmox-normalize'; describe('nachsichtige Leser (Aufgabe 3, )', () => { it('readNumber: Zahl, umwandelbare Zeichenkette, sonst null', () => { expect(readNumber(42)).toBe(42); expect(readNumber('42')).toBe(42); expect(readNumber('0.37')).toBe(0.37); expect(readNumber('nicht-umwandelbar')).toBeNull(); expect(readNumber(undefined)).toBeNull(); expect(readNumber(null)).toBeNull(); expect(readNumber(Number.NaN)).toBeNull(); }); it('readText: nichtleere Zeichenkette oder Zahl, sonst null', () => { expect(readText('hallo')).toBe('hallo'); expect(readText(42)).toBe('42'); expect(readText('')).toBeNull(); expect(readText(null)).toBeNull(); expect(readText(undefined)).toBeNull(); }); it('readBool: boolesch oder gaengige Wahr/Falsch-Formen, sonst null', () => { expect(readBool(true)).toBe(true); expect(readBool('true')).toBe(true); expect(readBool(1)).toBe(true); expect(readBool(false)).toBe(false); expect(readBool('false')).toBe(false); expect(readBool('irgendwas')).toBeNull(); }); it('readList: alles, was kein Array ist, wird eine leere Liste', () => { expect(readList([1, 2])).toEqual([1, 2]); expect(readList('kein-array')).toEqual([]); expect(readList(null)).toEqual([]); expect(readList(undefined)).toEqual([]); expect(readList({})).toEqual([]); }); }); describe('normalizePve (Aufgabe 3, )', () => { it('Knotenzahl, laufende/gestoppte Gaeste, je Knoten Prozessorlast/Speicher, je Speicherort Belegung', () => { const body = { data: [ { type: 'node', node: 'pve1', cpu: 0.25, maxcpu: 8, mem: 4_000_000_000, maxmem: 16_000_000_000 }, { type: 'node', node: 'pve2', cpu: 0.1, maxcpu: 4, mem: 1_000_000_000, maxmem: 8_000_000_000 }, { type: 'qemu', node: 'pve1', status: 'running' }, { type: 'qemu', node: 'pve1', status: 'stopped' }, { type: 'lxc', node: 'pve2', status: 'running' }, { type: 'storage', node: 'pve1', storage: 'local-lvm', disk: 100, maxdisk: 500 }, ], }; const { metrics, errorKind } = normalizePve(body); expect(errorKind).toBeNull(); expect(metrics.nodeCount).toBe(2); expect(metrics.guestsRunning).toBe(2); expect(metrics.guestsStopped).toBe(1); expect(metrics.nodes).toEqual([ { node: 'pve1', cpu: 0.25, maxcpu: 8, mem: 4_000_000_000, maxmem: 16_000_000_000 }, { node: 'pve2', cpu: 0.1, maxcpu: 4, mem: 1_000_000_000, maxmem: 8_000_000_000 }, ]); expect(metrics.storages).toEqual([ { storage: 'local-lvm', node: 'pve1', disk: 100, maxdisk: 500 }, ]); }); it('Feld fehlt -> null, nie 0/Wurf', () => { const body = { data: [{ type: 'node', node: 'pve1' }] }; expect(() => normalizePve(body)).not.toThrow(); const { metrics } = normalizePve(body); expect(metrics.nodes[0]).toEqual({ node: 'pve1', cpu: null, maxcpu: null, mem: null, maxmem: null }); }); it('Zahl kommt als Zeichenkette -> wird als Zahl gelesen', () => { const body = { data: [{ type: 'node', node: 'pve1', cpu: '0.5', maxcpu: '4', mem: '100', maxmem: '200' }] }; const { metrics } = normalizePve(body); expect(metrics.nodes[0]).toEqual({ node: 'pve1', cpu: 0.5, maxcpu: 4, mem: 100, maxmem: 200 }); }); it('Antwort ist HTML statt JSON-Objekt (hier: eine Zeichenkette) -> leeres Messwertobjekt, errorKind antwortform, kein Wurf', () => { expect(() => normalizePve('Anmeldeseite')).not.toThrow(); const { metrics, errorKind } = normalizePve('Anmeldeseite'); expect(errorKind).toBe('antwortform'); expect(metrics).toEqual({ productType: 'pve', nodeCount: 0, guestsRunning: 0, guestsStopped: 0, nodes: [], storages: [], }); }); it('Antwort ist ein Array statt eines Objekts -> antwortform, kein Wurf', () => { const { errorKind } = normalizePve([1, 2, 3]); expect(errorKind).toBe('antwortform'); }); it('Antwort ist null -> antwortform, kein Wurf', () => { const { errorKind } = normalizePve(null); expect(errorKind).toBe('antwortform'); }); }); describe('normalizePbs (Aufgabe 3, )', () => { it('je Datenspeicher Gesamt/Belegt/Frei, letzter Sicherungszeitpunkt und letztes Pruefergebnis', () => { const usage = { data: [{ store: 'backup-store', total: 1000, used: 400, avail: 600 }], }; const snapshotsByStore = { 'backup-store': { data: [ { 'backup-time': 1000, verification: { state: 'ok' } }, { 'backup-time': 2000, verification: { state: 'failed' } }, ], }, }; const { metrics, errorKind } = normalizePbs(usage, snapshotsByStore); expect(errorKind).toBeNull(); expect(metrics.datastores).toEqual([ { name: 'backup-store', total: 1000, used: 400, free: 600, lastBackupAt: 2000, lastVerifyState: 'failed', }, ]); }); it('ein Datenspeicher ohne Sicherungen ergibt null (Frontend zeigt "noch keine Sicherung") und keinen Fehler', () => { const usage = { data: [{ store: 'leer', total: 10, used: 0, avail: 10 }] }; const { metrics, errorKind } = normalizePbs(usage, { leer: { data: [] } }); expect(errorKind).toBeNull(); expect(metrics.datastores[0]).toMatchObject({ lastBackupAt: null, lastVerifyState: null }); }); it('Feld fehlt -> null, nie 0/Wurf', () => { const usage = { data: [{ store: 'x' }] }; expect(() => normalizePbs(usage, {})).not.toThrow(); const { metrics } = normalizePbs(usage, {}); expect(metrics.datastores[0]).toEqual({ name: 'x', total: null, used: null, free: null, lastBackupAt: null, lastVerifyState: null, }); }); it('Zahl kommt als Zeichenkette -> wird als Zahl gelesen', () => { const usage = { data: [{ store: 'x', total: '1000', used: '400', avail: '600' }] }; const { metrics } = normalizePbs(usage, {}); expect(metrics.datastores[0]).toMatchObject({ total: 1000, used: 400, free: 600 }); }); it('Antwort ist HTML statt JSON -> leeres Messwertobjekt, errorKind antwortform, kein Wurf', () => { expect(() => normalizePbs('', {})).not.toThrow(); const { metrics, errorKind } = normalizePbs('', {}); expect(errorKind).toBe('antwortform'); expect(metrics.datastores).toEqual([]); }); it('ein PBS-Server mit vielen Datenspeichern: listPbsDatastoreNames liefert alle Namen (Deckel lebt in proxmox.service.ts)', () => { const usage = { data: Array.from({ length: 15 }, (_, i) => ({ store: `store-${i}` })) }; expect(listPbsDatastoreNames(usage)).toHaveLength(15); }); }); describe('normalizePmg (Aufgabe 3, )', () => { it('Tageszahlen eingehend, ausgehend, Spam, Viren', () => { const body = { data: { count_in: 100, count_out: 50, spamcount_in: 10, spamcount_out: 2, viruscount_in: 1, viruscount_out: 0, }, }; const { metrics, errorKind } = normalizePmg(body); expect(errorKind).toBeNull(); expect(metrics).toEqual({ productType: 'pmg', countIn: 100, countOut: 50, spamCount: 12, virusCount: 1, }); }); it('Feld fehlt -> null, nie 0/Wurf', () => { const body = { data: {} }; expect(() => normalizePmg(body)).not.toThrow(); const { metrics } = normalizePmg(body); expect(metrics).toEqual({ productType: 'pmg', countIn: null, countOut: null, spamCount: null, virusCount: null, }); }); it('Zahl kommt als Zeichenkette -> wird als Zahl gelesen', () => { const body = { data: { count_in: '100', count_out: '50' } }; const { metrics } = normalizePmg(body); expect(metrics.countIn).toBe(100); expect(metrics.countOut).toBe(50); }); it('Antwort ist HTML statt JSON -> leeres Messwertobjekt, errorKind antwortform, kein Wurf', () => { expect(() => normalizePmg('')).not.toThrow(); const { metrics, errorKind } = normalizePmg(''); expect(errorKind).toBe('antwortform'); expect(metrics).toEqual({ productType: 'pmg', countIn: null, countOut: null, spamCount: null, virusCount: null, }); }); it('nur eine Haelfte vorhanden -> null (Spam, nur spamcount_in)', () => { const body = { data: { spamcount_in: 10 } }; const { metrics } = normalizePmg(body); expect(metrics.spamCount).toBeNull(); }); it('nur eine Haelfte vorhanden -> null (Spam, nur spamcount_out)', () => { const body = { data: { spamcount_out: 2 } }; const { metrics } = normalizePmg(body); expect(metrics.spamCount).toBeNull(); }); it('nur eine Haelfte vorhanden -> null (Viren, nur viruscount_in)', () => { const body = { data: { viruscount_in: 1 } }; const { metrics } = normalizePmg(body); expect(metrics.virusCount).toBeNull(); }); it('nur eine Haelfte vorhanden -> null (Viren, nur viruscount_out)', () => { const body = { data: { viruscount_out: 3 } }; const { metrics } = normalizePmg(body); expect(metrics.virusCount).toBeNull(); }); it('eine Haelfte ist nicht lesbar -> null (Spam, spamcount_out ist Text)', () => { const body = { data: { spamcount_in: 10, spamcount_out: 'abc' } }; const { metrics } = normalizePmg(body); expect(metrics.spamCount).toBeNull(); }); it('Unabhaengigkeit der Paare: Spam unvollstaendig, Viren vollstaendig -> spamCount null, virusCount 1, countIn/countOut unberuehrt', () => { const body = { data: { count_in: 100, count_out: 50, spamcount_in: 10, viruscount_in: 1, viruscount_out: 0, }, }; const { metrics } = normalizePmg(body); expect(metrics.spamCount).toBeNull(); expect(metrics.virusCount).toBe(1); expect(metrics.countIn).toBe(100); expect(metrics.countOut).toBe(50); }); });