Bisher war vorgaengerId eindeutig: nach einer abgebrochenen Rotation passte kein zweiter Nachfolger in die Kette, und der Schlüssel ließ sich nur über Neuanlage, Zuweisen und Widerruf ersetzen. Jetzt ist die Relation 1:n. Ein partieller Index erlaubt je Vorgänger nur einen Nachfolger, der weder widerrufen ist noch gerade widerrufen wird. Maßgeblich ist der jüngste. Erst wenn der Abbruch abgeschlossen ist, beginnt die Rotation wieder bei Schritt 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
122 lines
5.9 KiB
TypeScript
122 lines
5.9 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
||
import {
|
||
anmelden,
|
||
archivieren,
|
||
aufZiel,
|
||
hostEinrichten,
|
||
kernAufbauen,
|
||
PORT,
|
||
zieleZuruecksetzen,
|
||
} from "./hilfen";
|
||
|
||
/**
|
||
* Gefuehrte Rotation im Browser (#023), gebauter Stand + Worker: Ausgangslage ist der
|
||
* Kern-Anwendungsfall. Neuer Schluessel → Zuweisungen uebernehmen → ausrollen → Verbindung
|
||
* testen → alten widerrufen. `ssh zu-b` von A funktioniert vorher und nachher.
|
||
*/
|
||
test.setTimeout(300_000);
|
||
|
||
test.beforeAll(() => zieleZuruecksetzen());
|
||
|
||
test("Rotation mit Schrittanzeige", async ({ page }) => {
|
||
const kennung = Date.now().toString(36);
|
||
const hostA = `zz-e2e-a-${kennung}`;
|
||
const hostB = `zz-e2e-b-${kennung}`;
|
||
const zweck = `zz-e2e Rotation ${kennung}`;
|
||
|
||
await anmelden(page);
|
||
await hostEinrichten(page, hostA, PORT.a);
|
||
await hostEinrichten(page, hostB, PORT.b);
|
||
await kernAufbauen(page, zweck, hostA, hostB);
|
||
const altDateien = aufZiel("ziel-a", "ls /root/.ssh");
|
||
await page.reload();
|
||
|
||
await page.getByRole("link", { name: "Rotieren" }).click();
|
||
await expect(page.getByRole("heading", { name: `„${zweck}“ rotieren` })).toBeVisible();
|
||
await page.getByRole("button", { name: "Neuen Schlüssel erzeugen und Zuweisungen übernehmen" }).click();
|
||
|
||
// Schritt 3: neuen Schluessel ausrollen.
|
||
await expect(page.getByRole("button", { name: "Dry-Run starten" })).toBeVisible({ timeout: 60_000 });
|
||
await page.getByRole("button", { name: "Dry-Run starten" }).click();
|
||
// Derselbe Alias, neue Datei: nur die IdentityFile-Zeile aendert sich.
|
||
await expect(page.getByText(/IdentityFile ~\/\.ssh\/.*-r\d{8}$/)).toBeVisible({ timeout: 30_000 });
|
||
await page.screenshot({ path: "docs/anleitung/bilder/rotation-desktop.png", fullPage: true });
|
||
await page.getByRole("button", { name: /Änderung anwenden/ }).click();
|
||
|
||
// Schritt 4: Verbindungstest mit dem neuen Schluessel.
|
||
await page.getByRole("button", { name: "Verbindungen testen" }).click({ timeout: 60_000 });
|
||
|
||
// Schritt 5: alten Schluessel widerrufen.
|
||
await page.getByRole("button", { name: "Alten Schlüssel widerrufen" }).click({ timeout: 60_000 });
|
||
await page.getByRole("button", { name: "Dry-Run starten" }).click({ timeout: 30_000 });
|
||
await page.getByRole("button", { name: /Änderung anwenden/ }).click({ timeout: 30_000 });
|
||
await expect(page.getByRole("heading", { name: "Rotation abgeschlossen" })).toBeVisible({
|
||
timeout: 60_000,
|
||
});
|
||
|
||
expect(aufZiel("ziel-a", "ssh -o BatchMode=yes zu-b id -un").trim()).toBe("root");
|
||
const neueDateien = aufZiel("ziel-a", "ls /root/.ssh");
|
||
const alterKey = altDateien.split("\n").find((d) => d.startsWith("id_ed25519") && !d.endsWith(".pub"))!;
|
||
expect(neueDateien.split("\n")).not.toContain(alterKey);
|
||
expect(neueDateien).toMatch(/-r\d{8}\n/);
|
||
|
||
// Kette auf der Seite des neuen Schluessels.
|
||
await page.getByRole("link", { name: /\(rotiert / }).click();
|
||
const kette = page.getByRole("list", { name: "Rotationskette" });
|
||
await expect(kette.getByRole("link", { name: zweck })).toBeVisible();
|
||
await expect(kette.getByText("rotiert", { exact: true })).toBeVisible();
|
||
|
||
await archivieren(page, hostA);
|
||
await archivieren(page, hostB);
|
||
});
|
||
|
||
test("Rotation abbrechen, nachdem der neue Schlüssel ausgerollt ist (#032)", async ({ page }) => {
|
||
const kennung = Date.now().toString(36);
|
||
const hostA = `zz-e2e-a-${kennung}`;
|
||
const hostB = `zz-e2e-b-${kennung}`;
|
||
const zweck = `zz-e2e Abbruch ${kennung}`;
|
||
|
||
zieleZuruecksetzen();
|
||
await anmelden(page);
|
||
await hostEinrichten(page, hostA, PORT.a);
|
||
await hostEinrichten(page, hostB, PORT.b);
|
||
const schluesselSeite = await kernAufbauen(page, zweck, hostA, hostB);
|
||
await page.reload();
|
||
|
||
await page.getByRole("link", { name: "Rotieren" }).click();
|
||
await page.getByRole("button", { name: "Neuen Schlüssel erzeugen und Zuweisungen übernehmen" }).click();
|
||
await page.getByRole("button", { name: "Dry-Run starten" }).click({ timeout: 60_000 });
|
||
await page.getByRole("button", { name: /Änderung anwenden/ }).click({ timeout: 30_000 });
|
||
await expect(page.getByRole("button", { name: "Verbindungen testen" })).toBeVisible({ timeout: 60_000 });
|
||
expect(aufZiel("ziel-a", "cat /root/.ssh/config")).toMatch(/IdentityFile ~\/\.ssh\/.*-r\d{8}$/m);
|
||
|
||
// Abbrechen: Rueckfrage, dann Widerruf des neuen Schluessels mit Dry-Run.
|
||
await page.getByRole("button", { name: "Rotation abbrechen" }).click();
|
||
const rueckfrage = page.getByRole("group", { name: "Rotation abbrechen?" });
|
||
await expect(rueckfrage.getByText(`„${zweck}“ bleibt aktiv und unverändert`)).toBeVisible();
|
||
await rueckfrage.getByRole("button", { name: "Neuen Schlüssel widerrufen" }).click();
|
||
await expect(page.getByRole("heading", { name: "Rotation wird abgebrochen" })).toBeVisible();
|
||
await page.getByRole("button", { name: "Dry-Run starten" }).click();
|
||
await page.getByRole("button", { name: /Änderung anwenden/ }).click({ timeout: 30_000 });
|
||
await expect(page.getByRole("heading", { name: "Rotation abgebrochen" })).toBeVisible({ timeout: 60_000 });
|
||
|
||
// Auf den Hosts: wie vorher. Der Alias geht wieder mit dem alten Schluessel.
|
||
expect(aufZiel("ziel-a", "ls /root/.ssh")).not.toMatch(/-r\d{8}/);
|
||
expect(aufZiel("ziel-a", "cat /root/.ssh/config")).not.toMatch(/-r\d{8}/);
|
||
expect(aufZiel("ziel-a", "ssh -o BatchMode=yes zu-b id -un").trim()).toBe("root");
|
||
|
||
// Der alte bleibt aktiv; die Kette zeigt den widerrufenen Nachfolger – und er laesst sich
|
||
// erneut rotieren (#033).
|
||
await page.goto(schluesselSeite);
|
||
const kette = page.getByRole("list", { name: "Rotationskette" });
|
||
await expect(kette.getByText("widerrufen", { exact: true })).toBeVisible();
|
||
await expect(page.getByText(/Die Rotation zu „.*“ wurde abgebrochen.*erneut rotieren/)).toBeVisible();
|
||
await page.getByRole("link", { name: "Rotieren" }).click();
|
||
await expect(
|
||
page.getByRole("button", { name: "Neuen Schlüssel erzeugen und Zuweisungen übernehmen" }),
|
||
).toBeVisible();
|
||
|
||
await archivieren(page, hostA);
|
||
await archivieren(page, hostB);
|
||
});
|