diff --git a/docs/anleitung/bilder/abgleich-desktop.png b/docs/anleitung/bilder/abgleich-desktop.png new file mode 100644 index 0000000..85869d4 Binary files /dev/null and b/docs/anleitung/bilder/abgleich-desktop.png differ diff --git a/tests/e2e/abgleich.spec.ts b/tests/e2e/abgleich.spec.ts new file mode 100644 index 0000000..1a3d389 --- /dev/null +++ b/tests/e2e/abgleich.spec.ts @@ -0,0 +1,74 @@ +import { execFileSync } from "node:child_process"; +import { expect, test } from "@playwright/test"; +import "dotenv/config"; +import { schluesselpaarErzeugen } from "../../src/lib/ssh/openssh"; + +/** + * Abgleich und Uebernahme im Browser (#021, #022), gebauter Stand + Worker, Testziel B: + * fremder Key → „Jetzt abgleichen“ zeigt ihn → übernehmen → Dry-Run → anwenden → synchron. + */ +const ROOT_PASSWORT = process.env["SSHKV_TEST_ROOT_PASSWORT"] || "sshkv-test"; +const PORT_B = (process.env["SSHKV_TEST_ZIEL_B"] ?? "127.0.0.1:2222").split(":")[1] ?? "2222"; + +test.setTimeout(180_000); + +function aufZielB(befehl: string): string { + return execFileSync( + "docker", + ["compose", "-f", "docker-compose.test.yml", "exec", "-T", "ziel-b", "sh", "-c", befehl], + { + encoding: "utf8", + }, + ); +} + +test.beforeAll(() => { + aufZielB( + "userdel -r sshkv 2>/dev/null; userdel -r deploy 2>/dev/null; rm -f /etc/sudoers.d/sshkv*; rm -rf /root/.ssh; true", + ); +}); + +test("fremden Schlüssel finden und übernehmen", async ({ page }) => { + const name = `zz-e2e-${Date.now().toString(36)}`; + 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 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_B); + 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 }); + + const fremd = await schluesselpaarErzeugen("ED25519", { kommentar: `${name}@laptop` }); + aufZielB(`mkdir -p /root/.ssh && echo '${fremd.publicKeyZeile}' >> /root/.ssh/authorized_keys`); + + await page.getByRole("button", { name: "Jetzt abgleichen" }).click(); + await expect(page.getByText(/● Unbekannter Schlüssel/)).toBeVisible({ timeout: 30_000 }); + await expect(page.getByText("unbekannt", { exact: true })).toBeVisible(); + await page.screenshot({ path: "docs/anleitung/bilder/abgleich-desktop.png", fullPage: true }); + + await page.getByRole("button", { name: "Übernehmen", exact: true }).click(); + const dialog = page.getByRole("dialog", { name: "Schlüssel übernehmen" }); + await dialog.getByLabel("Zweck / Name").fill(`${name} Laptop`); + await dialog.getByRole("button", { name: "Übernehmen" }).click(); + await expect(dialog).toBeHidden(); + + await page.getByRole("button", { name: "Dry-Run starten" }).click(); + await expect(page.getByText(`root@${name}:/root/.ssh/authorized_keys`)).toBeVisible({ timeout: 30_000 }); + await page.getByRole("button", { name: /Änderung anwenden/ }).click(); + await expect(page.getByText("Ausgerollt.")).toBeVisible({ timeout: 60_000 }); + expect(aufZielB("grep -c laptop /root/.ssh/authorized_keys").trim()).toBe("1"); + + await page.getByRole("button", { name: "Jetzt abgleichen" }).click(); + await expect(page.getByText("Keine offenen Abweichungen.")).toBeVisible({ timeout: 30_000 }); + + await page.getByRole("button", { name: "Host archivieren" }).click(); + await page.getByRole("dialog").getByRole("button", { name: "Archivieren", exact: true }).click(); +});