Files
tessera-ctl/apps/api/src/proxmox/proxmox-normalize.spec.ts
T
2026-09-23 15:30:31 +02:00

288 lines
9.9 KiB
TypeScript

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,
});
});
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);
});
});