feat(server): Verbindungstest mit Fähigkeitserkennung
Der Test liest Version, Cluster, Nodes, Storages, Bridges, VNets und die effektiven Rechte des Tokens und leitet daraus ab, welcher Vorgang per API, per SSH oder gar nicht geht – je PVE-Version: Template-Bau per API erst ab 8.4 mit import-Storage, Snippets und LXC-Exec nur per SSH. Fehlende Rechte erscheinen einzeln mit Pfad und Zweck; die Rollenliste in docs/proxmox-rechte.md prüft ein Test gegen den Code. SSH-Host-Keys werden wie Zertifikate erst nach Bestätigung genutzt, und bestätigt werden kann nur ein Schlüssel, den der letzte Test gesehen hat. Statussymbole in der GUI sind jetzt gezeichnet: ◐ und ○ fehlen im Web-Subset von JetBrains Mono. Refs #003 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { utils } from "ssh2";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { POST as hostSchluesselPost } from "@/app/api/v1/server/[name]/ssh/hostschluessel/route";
|
||||
import { POST as testPost } from "@/app/api/v1/server/[name]/test/route";
|
||||
import { verschluesseln } from "@/lib/geheimnisse";
|
||||
import { MOCK_TOKEN_GEHEIMNIS, MOCK_TOKEN_ID, ProxmoxMock } from "./proxmox-mock/mock";
|
||||
import { szenarioEinrichten } from "./proxmox-mock/szenario";
|
||||
import { SshMock } from "./ssh-mock";
|
||||
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
||||
import { angemeldet, anfrage, kontext } from "./helpers/anfragen";
|
||||
|
||||
const mock = new ProxmoxMock();
|
||||
const paar = utils.generateKeyPairSync("ed25519");
|
||||
let ssh: SshMock;
|
||||
|
||||
describe.runIf(datenbankVerfuegbar)("Verbindungstest über die API", () => {
|
||||
beforeAll(async () => {
|
||||
await mock.starten();
|
||||
szenarioEinrichten(mock, { version: "8.3.5", importStorage: true });
|
||||
ssh = await new SshMock(paar.public).starten();
|
||||
});
|
||||
afterAll(async () => {
|
||||
await mock.stoppen();
|
||||
await ssh.stoppen();
|
||||
});
|
||||
|
||||
let admin: string;
|
||||
let operator: string;
|
||||
beforeEach(async () => {
|
||||
await datenbankLeeren();
|
||||
admin = (await angemeldet("ADMIN")).cookie;
|
||||
operator = (await angemeldet("OPERATOR")).cookie;
|
||||
await testPrisma.server.create({
|
||||
data: {
|
||||
name: "rz1",
|
||||
titel: "RZ 1",
|
||||
url: mock.url,
|
||||
tokenId: MOCK_TOKEN_ID,
|
||||
tokenGeheimnis: verschluesseln(MOCK_TOKEN_GEHEIMNIS),
|
||||
fingerabdruck: mock.fingerabdruck,
|
||||
sshAktiv: true,
|
||||
sshPort: ssh.port,
|
||||
sshPrivat: verschluesseln(paar.private),
|
||||
sshOeffentlich: paar.public,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const testen = async (cookie: string) =>
|
||||
await testPost(anfrage("POST", "/api/v1/server/rz1/test", cookie), kontext({ name: "rz1" }));
|
||||
|
||||
it("speichert Bericht, Version und Erreichbarkeit; Operator darf testen", async () => {
|
||||
const antwort = await testen(operator);
|
||||
expect(antwort.status).toBe(200);
|
||||
const ergebnis = await antwort.json();
|
||||
expect(ergebnis.ok).toBe(true);
|
||||
expect(ergebnis.bericht.ssh).toBe("nicht_bestaetigt");
|
||||
const server = await testPrisma.server.findUniqueOrThrow({ where: { name: "rz1" } });
|
||||
expect(server).toMatchObject({ erreichbar: true, pveVersion: "8.3.5" });
|
||||
expect(await testPrisma.protokoll.count({ where: { aktion: "Verbindung getestet" } })).toBe(1);
|
||||
});
|
||||
|
||||
it("Host-Key bestätigen: nur der gesehene, nur durch Admins – danach geht SSH", async () => {
|
||||
const erst = await (await testen(admin)).json();
|
||||
const gesehen = erst.bericht.knoten[0].ssh.hostSchluessel as string;
|
||||
const pfad = "/api/v1/server/rz1/ssh/hostschluessel";
|
||||
|
||||
expect(
|
||||
(
|
||||
await hostSchluesselPost(
|
||||
anfrage("POST", pfad, operator, { knoten: "pve01", hostSchluessel: gesehen }),
|
||||
kontext({ name: "rz1" }),
|
||||
)
|
||||
).status,
|
||||
).toBe(403);
|
||||
|
||||
const untergeschoben = "SHA256:" + "A".repeat(43);
|
||||
const abgelehnt = await hostSchluesselPost(
|
||||
anfrage("POST", pfad, admin, { knoten: "pve01", hostSchluessel: untergeschoben }),
|
||||
kontext({ name: "rz1" }),
|
||||
);
|
||||
expect(abgelehnt.status).toBe(409);
|
||||
|
||||
const ok = await hostSchluesselPost(
|
||||
anfrage("POST", pfad, admin, { knoten: "pve01", hostSchluessel: gesehen }),
|
||||
kontext({ name: "rz1" }),
|
||||
);
|
||||
expect(ok.status).toBe(200);
|
||||
expect((await ok.json()).sshHostSchluessel).toEqual({ pve01: gesehen });
|
||||
|
||||
const dann = await (await testen(admin)).json();
|
||||
expect(dann.bericht.ssh).toBe("ok");
|
||||
expect(dann.bericht.faehigkeiten.templateBau.weg).toBe("ssh");
|
||||
});
|
||||
|
||||
it("ein nicht erreichbarer Server ist ein Ergebnis, kein Fehler", async () => {
|
||||
await testPrisma.server.update({ where: { name: "rz1" }, data: { url: "https://127.0.0.1:9" } });
|
||||
const antwort = await testen(admin);
|
||||
expect(antwort.status).toBe(200);
|
||||
const ergebnis = await antwort.json();
|
||||
expect(ergebnis).toMatchObject({ ok: false });
|
||||
expect((await testPrisma.server.findUniqueOrThrow({ where: { name: "rz1" } })).erreichbar).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user