Files
florian.wildeandClaude Opus 5.5 8056b8a0c9 test(ausrollen): Dry-Run und Ausrollen im Browser (#016)
Gebauter Stand mit Worker gegen das Testziel: Host einrichten, Dry-Run
zeigt das Diff, auf dem Host aendert sich noch nichts, nach dem
Anwenden steht der Key mit seinen Einschraenkungen im Block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:21:54 +02:00

104 lines
4.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { execFileSync } from "node:child_process";
import { generateKeyPairSync, randomUUID } from "node:crypto";
import { expect, test, type Page } from "@playwright/test";
import pg from "pg";
import "dotenv/config";
import { fingerprintSha256 } from "../../src/lib/ssh/openssh";
/**
* Public Key ausrollen ueber die Hostseite (#016), gebauter Stand + Worker, Testziel B.
* Schluessel und Zuweisung legt der Test direkt in der Entwicklungsdatenbank an (Praefix zz-),
* damit er nur den Ausroll-Weg prueft.
*/
const ZIEL_PORT = (process.env["SSHKV_TEST_ZIEL_B"] ?? "127.0.0.1:2222").split(":")[1] ?? "2222";
const ROOT_PASSWORT = process.env["SSHKV_TEST_ROOT_PASSWORT"] || "sshkv-test";
test.setTimeout(150_000);
function aufZielB(befehl: string): string {
return execFileSync(
"docker",
["compose", "-f", "docker-compose.test.yml", "exec", "-T", "ziel-b", "sh", "-c", befehl],
{
encoding: "utf8",
},
);
}
function publicKey(kommentar: string) {
const { publicKey: schluessel } = generateKeyPairSync("ed25519");
const roh = schluessel.export({ format: "der", type: "spki" }).subarray(-32);
const typ = Buffer.from("ssh-ed25519");
const blob = Buffer.concat([Buffer.from([0, 0, 0, typ.length]), typ, Buffer.from([0, 0, 0, 32]), roh]);
return {
zeile: `ssh-ed25519 ${blob.toString("base64")} ${kommentar}`,
fingerprint: fingerprintSha256(blob),
};
}
async function anmelden(page: Page) {
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();
}
test.beforeAll(() => {
aufZielB("userdel -r sshkv 2>/dev/null; rm -f /etc/sudoers.d/sshkv /root/.ssh/authorized_keys*; true");
});
test("Public Key ueber die Hostseite ausrollen", async ({ page }) => {
const name = `zz-e2e-${Date.now().toString(36)}`;
await anmelden(page);
// Host einrichten wie in host-einrichten.spec.ts.
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(ZIEL_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 });
const hostId = page.url().split("/").pop()!;
// Schluessel und Zuweisung direkt anlegen.
const key = publicKey(`sshkv:${name}`);
const client = new pg.Client({ connectionString: process.env["DATABASE_URL"] });
await client.connect();
try {
const schluesselId = randomUUID();
await client.query(
`INSERT INTO "Schluessel" ("id","name","slug","typ","kategorie","publicKey","fingerprint","kommentar","geaendertAm")
VALUES ($1,$2,$3,'ED25519','DIENST',$4,$5,$6,now())`,
[schluesselId, `${name} Backup`, name, key.zeile, key.fingerprint, `sshkv:${name}`],
);
await client.query(
`INSERT INTO "PublicVerwendung" ("id","schluesselId","hostId","zielbenutzer","optionen") VALUES ($1,$2,$3,'root',$4)`,
[randomUUID(), schluesselId, hostId, 'restrict,from="10.0.0.12"'],
);
} finally {
await client.end();
}
await page.reload();
await expect(page.getByText("1 Zuweisung(en) noch nicht ausgerollt.")).toBeVisible();
await page.getByRole("button", { name: "Dry-Run starten" }).click();
await expect(page.getByText(`root@${name}:/root/.ssh/authorized_keys`)).toBeVisible({ timeout: 30_000 });
await expect(page.getByText(`restrict,from="10.0.0.12" ${key.zeile}`)).toBeVisible();
expect(aufZielB("cat /root/.ssh/authorized_keys 2>/dev/null || true")).not.toContain(key.fingerprint);
await page.screenshot({ path: "docs/anleitung/bilder/dry-run-desktop.png", fullPage: true });
await page.getByRole("button", { name: /Änderung anwenden/ }).click();
await expect(page.getByText("Ausgerollt.")).toBeVisible({ timeout: 30_000 });
await expect(page.getByText("synchron").first()).toBeVisible();
expect(aufZielB("cat /root/.ssh/authorized_keys")).toContain(`restrict,from="10.0.0.12" ${key.zeile}`);
await page.getByRole("button", { name: "Host archivieren" }).click();
await page.getByRole("dialog").getByRole("button", { name: "Archivieren", exact: true }).click();
await expect(page).toHaveURL(/\/hosts$/);
});