import { execFileSync } from "node:child_process"; import { expect, test, type Page } from "@playwright/test"; import "dotenv/config"; /** * Kern-Anwendungsfall im Browser (#012, #016, #017, #020), gebauter Stand + Worker: * Schluessel erzeugen → Private Key auf Ziel A (mit ~/.ssh/config und known_hosts) → * Public Key auf Ziel B → Dry-Run → anwenden → Verbindung von A nach B testen. * Beide Hosts heissen zz-e2e-… und werden am Ende archiviert. */ const ROOT_PASSWORT = process.env["SSHKV_TEST_ROOT_PASSWORT"] || "sshkv-test"; const PORT = { a: (process.env["SSHKV_TEST_ZIEL_A"] ?? "127.0.0.1:2221").split(":")[1] ?? "2221", b: (process.env["SSHKV_TEST_ZIEL_B"] ?? "127.0.0.1:2222").split(":")[1] ?? "2222", }; test.setTimeout(240_000); function aufZiel(ziel: "ziel-a" | "ziel-b", befehl: string): string { return execFileSync( "docker", ["compose", "-f", "docker-compose.test.yml", "exec", "-T", ziel, "sh", "-c", befehl], { encoding: "utf8", }, ); } async function hostEinrichten(page: Page, name: string, port: string): Promise { await page.goto("/hosts/neu"); await page.getByLabel("Name").fill(name); await page.getByLabel("Adresse").fill("127.0.0.1"); await page.getByLabel("Port").fill(port); await page.getByRole("button", { name: "Host anlegen" }).click(); await page.getByRole("button", { name: "Host-Key abrufen" }).click(); await page.getByRole("button", { name: "Fingerprint stimmt – vertrauen" }).click({ timeout: 30_000 }); await page.getByLabel("Passwort", { exact: true }).fill(ROOT_PASSWORT); await page.getByRole("button", { name: "Zugang einrichten" }).click(); await expect(page.getByRole("button", { name: "Verbindung prüfen" })).toBeVisible({ timeout: 60_000 }); } async function archivieren(page: Page, name: string): Promise { await page.goto("/hosts"); await page.getByRole("link", { name }).first().click(); await page.getByRole("button", { name: "Host archivieren" }).click(); await page.getByRole("dialog").getByRole("button", { name: "Archivieren", exact: true }).click(); await expect(page).toHaveURL(/\/hosts$/); } test.beforeAll(() => { const reset = "userdel -r sshkv 2>/dev/null; rm -f /etc/sudoers.d/sshkv; rm -rf /root/.ssh; true"; aufZiel("ziel-a", reset); aufZiel("ziel-b", reset); }); test("Backup A → B: erzeugen, ausrollen, Verbindung testen", async ({ page }) => { const kennung = Date.now().toString(36); const hostA = `zz-e2e-a-${kennung}`; const hostB = `zz-e2e-b-${kennung}`; const zweck = `zz-e2e Backup ${kennung}`; await page.goto("/anmelden"); await page.getByRole("link", { name: /Mit Mock-OIDC anmelden/ }).click(); await page.locator('button[data-rolle="admin"]').click(); await page.getByRole("button", { name: "Anmelden", exact: true }).click(); await expect(page.getByRole("heading", { name: "Übersicht" })).toBeVisible(); await hostEinrichten(page, hostA, PORT.a); await hostEinrichten(page, hostB, PORT.b); // 1. Schluessel erzeugen await page.goto("/schluessel/neu"); await page.getByLabel("Zweck").fill(zweck); await page.getByRole("button", { name: "Schlüssel erzeugen" }).click(); await expect(page.getByRole("heading", { name: zweck })).toBeVisible({ timeout: 30_000 }); // 2a. Private Key auf A, mit config-Eintrag und Host-Key von B await page.getByRole("button", { name: "Ausrollen" }).first().click(); const privat = page.getByRole("dialog", { name: "Private Key ausrollen" }); await privat.getByLabel("Quell-Host (bekommt den Private Key)").selectOption({ label: hostA }); await privat.getByLabel("Ziel-Host (für known_hosts)").selectOption({ label: hostB }); await privat.getByRole("textbox", { name: "Alias" }).fill("zu-b"); // A erreicht B im Docker-Netz unter dem Dienstnamen auf Port 22. await privat.getByRole("textbox", { name: "HostName" }).fill("ziel-b"); await privat.getByRole("spinbutton", { name: "Port" }).fill("22"); await privat.getByRole("button", { name: "Zuweisen" }).click(); await expect(privat).toBeHidden(); await expect(page.getByText(`root@${hostA}`)).toBeVisible(); // 2b. Public Key auf B await page.getByRole("button", { name: "Ausrollen" }).nth(1).click(); const oeffentlich = page.getByRole("dialog", { name: "Public Key zuweisen" }); await oeffentlich.getByLabel("Ziel", { exact: true }).selectOption({ label: hostB }); await oeffentlich.getByLabel(/Einschränkungen/).fill("no-pty,no-agent-forwarding"); await oeffentlich.getByRole("button", { name: "Zuweisen" }).click(); await expect(oeffentlich).toBeHidden(); await expect(page.getByText(`root@${hostB}`)).toBeVisible(); // 3. Dry-Run und anwenden await page.getByRole("button", { name: "Dry-Run starten" }).click(); await expect(page.getByText(`root@${hostB}:/root/.ssh/authorized_keys`)).toBeVisible({ timeout: 30_000 }); await expect(page.getByText("Host zu-b").first()).toBeVisible(); // Kein Private Key im Browser. expect(await page.content()).not.toContain("PRIVATE KEY"); await page.screenshot({ path: "docs/anleitung/bilder/kern-dry-run-desktop.png", fullPage: true }); await page.getByRole("button", { name: /Änderung anwenden/ }).click(); await expect(page.getByText("Ausgerollt.")).toBeVisible({ timeout: 60_000 }); // 4. Verbindungstest von A nach B await page.getByRole("button", { name: "Verbindung testen" }).click(); await expect(page.getByText("Erledigt.")).toBeVisible({ timeout: 30_000 }); expect(aufZiel("ziel-a", "ssh -o BatchMode=yes zu-b id -un").trim()).toBe("root"); await page.screenshot({ path: "docs/anleitung/bilder/kern-ausgerollt-desktop.png", fullPage: true }); await archivieren(page, hostA); await archivieren(page, hostB); });