Der Server kannte die Standards schon für kurze CLI-Befehle, aber nichts setzte sie. Jetzt im Server-Formular (Auswahl aus dem Verbindungstest) und als Vorbelegung im Assistenten. Closes #023 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
214 lines
8.9 KiB
TypeScript
214 lines
8.9 KiB
TypeScript
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
import * as einzeln from "@/app/api/v1/server/[name]/route";
|
|
import { POST as sshPost } from "@/app/api/v1/server/[name]/ssh/route";
|
|
import { POST as pruefenPost } from "@/app/api/v1/server/pruefen/route";
|
|
import * as liste from "@/app/api/v1/server/route";
|
|
import { entschluesseln } from "@/lib/geheimnisse";
|
|
import { MOCK_TOKEN_GEHEIMNIS, MOCK_TOKEN_ID, ProxmoxMock } from "./proxmox-mock/mock";
|
|
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
|
import { angemeldet, anfrage, kontext } from "./helpers/anfragen";
|
|
|
|
const mock = new ProxmoxMock();
|
|
let admin: string;
|
|
let operator: string;
|
|
|
|
function eingabe(ueberschreiben: Record<string, unknown> = {}) {
|
|
return {
|
|
name: "rz1",
|
|
titel: "Rechenzentrum 1",
|
|
url: mock.url,
|
|
tokenId: MOCK_TOKEN_ID,
|
|
tokenGeheimnis: MOCK_TOKEN_GEHEIMNIS,
|
|
tlsPruefen: false,
|
|
fingerabdruck: mock.fingerabdruck,
|
|
...ueberschreiben,
|
|
};
|
|
}
|
|
|
|
describe.runIf(datenbankVerfuegbar)("Server-Verwaltung", () => {
|
|
beforeAll(async () => {
|
|
await mock.starten();
|
|
mock.route("GET", "/version", () => ({ data: { version: "9.2.1", release: "9.2" } }));
|
|
});
|
|
afterAll(() => mock.stoppen());
|
|
|
|
beforeEach(async () => {
|
|
await datenbankLeeren();
|
|
admin = (await angemeldet("ADMIN")).cookie;
|
|
operator = (await angemeldet("OPERATOR")).cookie;
|
|
});
|
|
|
|
it("Admin legt an; das Secret liegt verschlüsselt und kommt nie zurück", async () => {
|
|
const antwort = await liste.POST(anfrage("POST", "/api/v1/server", admin, eingabe()));
|
|
expect(antwort.status).toBe(201);
|
|
const text = await antwort.text();
|
|
expect(text).not.toContain(MOCK_TOKEN_GEHEIMNIS);
|
|
expect(JSON.parse(text)).toMatchObject({ name: "rz1", fingerabdruck: mock.fingerabdruck });
|
|
|
|
const gespeichert = await testPrisma.server.findUniqueOrThrow({ where: { name: "rz1" } });
|
|
expect(gespeichert.tokenGeheimnis).toMatch(/^v1\./);
|
|
expect(entschluesseln(gespeichert.tokenGeheimnis)).toBe(MOCK_TOKEN_GEHEIMNIS);
|
|
|
|
const lesen = await liste.GET(anfrage("GET", "/api/v1/server", operator));
|
|
expect(await lesen.text()).not.toContain("v1.");
|
|
});
|
|
|
|
it("Operator darf lesen, aber nicht anlegen, ändern oder löschen", async () => {
|
|
await liste.POST(anfrage("POST", "/api/v1/server", admin, eingabe()));
|
|
expect(
|
|
(await liste.POST(anfrage("POST", "/api/v1/server", operator, eingabe({ name: "rz2" })))).status,
|
|
).toBe(403);
|
|
expect(
|
|
(
|
|
await einzeln.PATCH(
|
|
anfrage("PATCH", "/api/v1/server/rz1", operator, eingabe()),
|
|
kontext({ name: "rz1" }),
|
|
)
|
|
).status,
|
|
).toBe(403);
|
|
expect(
|
|
(await einzeln.DELETE(anfrage("DELETE", "/api/v1/server/rz1", operator), kontext({ name: "rz1" })))
|
|
.status,
|
|
).toBe(403);
|
|
expect(
|
|
(await einzeln.GET(anfrage("GET", "/api/v1/server/rz1", operator), kontext({ name: "rz1" }))).status,
|
|
).toBe(200);
|
|
});
|
|
|
|
it("ergänzt Schema und Port und normalisiert den Fingerprint", async () => {
|
|
const ohneSchema = mock.url.replace("https://", "");
|
|
const antwort = await liste.POST(
|
|
anfrage(
|
|
"POST",
|
|
"/api/v1/server",
|
|
admin,
|
|
eingabe({ url: ohneSchema, fingerabdruck: mock.fingerabdruck.replaceAll(":", "").toLowerCase() }),
|
|
),
|
|
);
|
|
expect(await antwort.json()).toMatchObject({ url: mock.url, fingerabdruck: mock.fingerabdruck });
|
|
const nurHost = await liste.POST(
|
|
anfrage("POST", "/api/v1/server", admin, eingabe({ name: "rz2", url: "pve02.example" })),
|
|
);
|
|
expect((await nurHost.json()).url).toBe("https://pve02.example:8006");
|
|
});
|
|
|
|
it("meldet ungültige Felder einzeln", async () => {
|
|
const antwort = await liste.POST(
|
|
anfrage(
|
|
"POST",
|
|
"/api/v1/server",
|
|
admin,
|
|
eingabe({ name: "RZ 1", tokenId: "root@pam", fingerabdruck: null }),
|
|
),
|
|
);
|
|
expect(antwort.status).toBe(400);
|
|
const { fehler } = await antwort.json();
|
|
expect(Object.keys(fehler.felder)).toEqual(expect.arrayContaining(["name", "tokenId", "fingerabdruck"]));
|
|
});
|
|
|
|
it("lehnt doppelte Namen ab", async () => {
|
|
await liste.POST(anfrage("POST", "/api/v1/server", admin, eingabe()));
|
|
expect((await liste.POST(anfrage("POST", "/api/v1/server", admin, eingabe()))).status).toBe(409);
|
|
});
|
|
|
|
it("Ändern ohne Secret behält das alte; geänderte Felder stehen im Protokoll, Werte von Geheimnissen nicht", async () => {
|
|
await liste.POST(anfrage("POST", "/api/v1/server", admin, eingabe()));
|
|
const antwort = await einzeln.PATCH(
|
|
anfrage("PATCH", "/api/v1/server/rz1", admin, eingabe({ titel: "RZ eins", tokenGeheimnis: "" })),
|
|
kontext({ name: "rz1" }),
|
|
);
|
|
expect(antwort.status).toBe(200);
|
|
const server = await testPrisma.server.findUniqueOrThrow({ where: { name: "rz1" } });
|
|
expect(entschluesseln(server.tokenGeheimnis)).toBe(MOCK_TOKEN_GEHEIMNIS);
|
|
expect(server.titel).toBe("RZ eins");
|
|
|
|
const eintrag = await testPrisma.protokoll.findFirstOrThrow({ where: { aktion: "Server geändert" } });
|
|
expect(eintrag.details).toEqual({ felder: ["titel"] });
|
|
const alles = JSON.stringify(await testPrisma.protokoll.findMany(), (_, wert) =>
|
|
typeof wert === "bigint" ? wert.toString() : wert,
|
|
);
|
|
expect(alles).not.toContain(MOCK_TOKEN_GEHEIMNIS);
|
|
});
|
|
|
|
it("Standardwerte: gespeichert, leere Felder fallen weg, ganz leer ist null", async () => {
|
|
await liste.POST(anfrage("POST", "/api/v1/server", admin, eingabe()));
|
|
const setzen = async (standards: unknown) =>
|
|
await (
|
|
await einzeln.PATCH(
|
|
anfrage("PATCH", "/api/v1/server/rz1", admin, eingabe({ tokenGeheimnis: "", standards })),
|
|
kontext({ name: "rz1" }),
|
|
)
|
|
).json();
|
|
expect((await setzen({ knoten: "pve01", storage: " local-zfs ", bridge: "" })).standards).toEqual({
|
|
knoten: "pve01",
|
|
storage: "local-zfs",
|
|
});
|
|
expect((await testPrisma.server.findFirstOrThrow()).standards).toEqual({
|
|
knoten: "pve01",
|
|
storage: "local-zfs",
|
|
});
|
|
expect((await setzen({ knoten: "", storage: "", bridge: "" })).standards).toBeNull();
|
|
expect((await testPrisma.server.findFirstOrThrow()).standards).toBeNull();
|
|
});
|
|
|
|
it("löscht und protokolliert", async () => {
|
|
await liste.POST(anfrage("POST", "/api/v1/server", admin, eingabe()));
|
|
expect(
|
|
(await einzeln.DELETE(anfrage("DELETE", "/api/v1/server/rz1", admin), kontext({ name: "rz1" }))).status,
|
|
).toBe(204);
|
|
expect(await testPrisma.server.count()).toBe(0);
|
|
expect(await testPrisma.protokoll.count({ where: { aktion: "Server gelöscht" } })).toBe(1);
|
|
expect(
|
|
(await einzeln.GET(anfrage("GET", "/api/v1/server/rz1", admin), kontext({ name: "rz1" }))).status,
|
|
).toBe(404);
|
|
});
|
|
|
|
it("Prüfen: liefert das Zertifikat zum Bestätigen, dann mit Pin die PVE-Version", async () => {
|
|
const ohnePin = await (
|
|
await pruefenPost(anfrage("POST", "/api/v1/server/pruefen", admin, { url: mock.url }))
|
|
).json();
|
|
expect(ohnePin.zertifikat.fingerabdruck).toBe(mock.fingerabdruck);
|
|
expect(ohnePin.zertifikat.vertraut).toBe(false);
|
|
expect(ohnePin.verbindung).toBeNull();
|
|
|
|
const mitPin = await (
|
|
await pruefenPost(anfrage("POST", "/api/v1/server/pruefen", admin, { ...eingabe(), name: undefined }))
|
|
).json();
|
|
expect(mitPin.pinPasst).toBe(true);
|
|
expect(mitPin.verbindung).toEqual({ ok: true, version: "9.2.1", release: "9.2" });
|
|
});
|
|
|
|
it("Prüfen: falsches Secret ergibt Ursache und Hinweis statt Fehlerseite", async () => {
|
|
const ergebnis = await (
|
|
await pruefenPost(
|
|
anfrage("POST", "/api/v1/server/pruefen", admin, eingabe({ tokenGeheimnis: "falsch" })),
|
|
)
|
|
).json();
|
|
expect(ergebnis.verbindung).toMatchObject({ ok: false, art: "anmeldung" });
|
|
});
|
|
|
|
it("Prüfen: bei bestehendem Server reicht der Name statt des Secrets", async () => {
|
|
await liste.POST(anfrage("POST", "/api/v1/server", admin, eingabe()));
|
|
const ergebnis = await (
|
|
await pruefenPost(anfrage("POST", "/api/v1/server/pruefen", admin, eingabe({ tokenGeheimnis: "" })))
|
|
).json();
|
|
expect(ergebnis.verbindung).toMatchObject({ ok: true });
|
|
});
|
|
|
|
it("Prüfen ist nur für Admins (baut Verbindungen zu beliebigen Adressen auf)", async () => {
|
|
expect(
|
|
(await pruefenPost(anfrage("POST", "/api/v1/server/pruefen", operator, { url: mock.url }))).status,
|
|
).toBe(403);
|
|
});
|
|
|
|
it("erzeugt ein SSH-Schlüsselpaar; nur der öffentliche Teil ist sichtbar", async () => {
|
|
await liste.POST(anfrage("POST", "/api/v1/server", admin, eingabe()));
|
|
const antwort = await sshPost(anfrage("POST", "/api/v1/server/rz1/ssh", admin), kontext({ name: "rz1" }));
|
|
const sicht = await antwort.json();
|
|
expect(sicht.sshOeffentlich).toMatch(/^ssh-ed25519 AAAA\S+ proxdeploy@rz1$/);
|
|
expect(JSON.stringify(sicht)).not.toContain("PRIVATE");
|
|
const server = await testPrisma.server.findUniqueOrThrow({ where: { name: "rz1" } });
|
|
expect(entschluesseln(server.sshPrivat!)).toContain("BEGIN OPENSSH PRIVATE KEY");
|
|
});
|
|
});
|