feat(260923-dhh): Proxmox-Modul Aufgabe 3 - PBS und PMG auswerten
- proxmox-normalize.ts: nachsichtige Leser (readNumber/readText/readBool/ readList) und normalizePve/normalizePbs/normalizePmg als reine Funktionen, nie ein Wurf bei unerwarteter Form - PVE ergaenzt um je Speicherort Belegung (storages) - PBS: Belegung je Datenspeicher plus letzte Sicherung/Pruefergebnis aus bis zu 10 Folgeabfragen je Durchlauf (Deckel in proxmox.service.ts) - PMG: Tageszahlen eingehend/ausgehend/Spam/Viren - Feldnamen je Produkt als benannte Konstante (Annahmen A3/A5 der Recherche), mehrere plausible Namen je Feld moeglich - proxmox.service.ts: produktabhaengige Abfragefolge, Ticket-Erneuerung jetzt je Durchlauf statt je Aufruf (PBS-Mehrfachabfragen loggen nicht mehrfach neu ein) - proxmox-nur-lesen.spec.ts: Riegel erkennt jetzt auch den Umschlag getWithRetry als zulaessige Aufrufform Tore: api 1293/1293 (>=1240), type-check 4/4. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
listPbsDatastoreNames,
|
||||
normalizePbs,
|
||||
normalizePmg,
|
||||
normalizePve,
|
||||
readBool,
|
||||
readList,
|
||||
readNumber,
|
||||
readText,
|
||||
} from './proxmox-normalize';
|
||||
|
||||
describe('nachsichtige Leser (Aufgabe 3, <behavior>)', () => {
|
||||
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, <behavior>)', () => {
|
||||
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('<html>Anmeldeseite</html>')).not.toThrow();
|
||||
const { metrics, errorKind } = normalizePve('<html>Anmeldeseite</html>');
|
||||
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, <behavior>)', () => {
|
||||
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('<html></html>', {})).not.toThrow();
|
||||
const { metrics, errorKind } = normalizePbs('<html></html>', {});
|
||||
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, <behavior>)', () => {
|
||||
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('<html></html>')).not.toThrow();
|
||||
const { metrics, errorKind } = normalizePmg('<html></html>');
|
||||
expect(errorKind).toBe('antwortform');
|
||||
expect(metrics).toEqual({
|
||||
productType: 'pmg',
|
||||
countIn: null,
|
||||
countOut: null,
|
||||
spamCount: null,
|
||||
virusCount: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user