Files
sshkeyvault/tests/e2e/schluessel.spec.ts
T
florian.wildeandClaude Opus 5.5 263143134f feat(schluessel): erzeugen, importieren, exportieren (#012)
Schluessel werden jetzt verwaltet statt nur im Prototyp gezeigt: Liste
mit Filter, Anlegen (erzeugen oder importieren), Detail nach dem
Prototyp aus #002, Bearbeiten der Metadaten und Download <slug>.pub.

Warum so:
- Erzeugen und Import eines Paars laufen als Vorgang im Worker, weil
  nur er den Master-Key hat. Passphrase und importierter Private Key
  reisen wie das Bootstrap-Passwort mit SSHKV_APP_SECRET verschluesselt
  im Job, nie in Vorgang.eingabe; der Worker loescht den Job danach.
- Ein reiner Public Key ist kein Geheimnis und wird direkt im Web
  angelegt, mit verstaendlicher Meldung bei einem Duplikat.
- Das Web liest nie das Chiffrat: ob ein Private Key gespeichert ist,
  verraet masterKeyVersion (per CHECK an das Chiffrat gekoppelt). Ein
  struktureller Test verlangt fuer jede zeilenliefernde Abfrage auf
  Schluessel ausserhalb des Workers ein festes select.
- Migration: Vorgangsarten fuer Erzeugen, Import und (fuer #014) das
  Loeschen des Private Keys; CHECKs fuer den Slug (landet in
  Dateinamen) und "geloescht nur ohne Chiffrat".
- Platzhalter PublicAusrollenAktion/PrivateAusrollenAktion fuer #016/#017.

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

75 lines
3.5 KiB
TypeScript
Raw 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 { expect, test, type Page } from "@playwright/test";
import "dotenv/config";
import { schluesselpaarErzeugen } from "../../src/lib/ssh/openssh";
/**
* Schluessel im gebauten Stand mit laufendem Worker (#012–#014). Testdaten heissen zz-e2e-…
* und bleiben liegen (Schluessel werden nicht geloescht, nur spaeter widerrufen).
*/
test.setTimeout(90_000);
async function oidcAnmelden(page: Page, rolle: "admin" | "operator" | "reader") {
await page.goto("/anmelden");
await page.getByRole("link", { name: /Mit Mock-OIDC anmelden/ }).click();
await page.locator(`button[data-rolle="${rolle}"]`).click();
await page.getByRole("button", { name: "Anmelden", exact: true }).click();
await expect(page.getByRole("heading", { name: "Übersicht" })).toBeVisible();
}
function eindeutig(): string {
return `zz-e2e-${Date.now().toString(36)}`;
}
test("Operator erzeugt einen Schlüssel und landet im Detail", async ({ page }) => {
const name = `${eindeutig()} Backup`;
await oidcAnmelden(page, "operator");
await page.goto("/schluessel/neu");
await page.getByLabel("Zweck").fill(name);
await page.getByLabel("Tags").fill("e2e");
await page.getByRole("button", { name: "Schlüssel erzeugen" }).click();
await expect(page.getByRole("heading", { name })).toBeVisible({ timeout: 30_000 });
await expect(page).toHaveURL(/\/schluessel\/[^/]+$/);
await expect(page.getByText("verschlüsselt gespeichert")).toBeVisible();
await expect(page.getByText("ed25519", { exact: true })).toBeVisible();
await expect(page.getByLabel("Public Key", { exact: true })).toContainText("ssh-ed25519 AAAA");
await expect(page.getByLabel("Public Key", { exact: true })).toContainText(
`sshkv:${name.toLowerCase().replace(/ /g, "-")}`,
);
await page.goto(`/schluessel?q=${encodeURIComponent(name)}`);
await expect(page.getByRole("link", { name })).toBeVisible();
});
test("Public Key importieren, Duplikat wird erkannt", async ({ page }) => {
const name = `${eindeutig()} Laptop`;
const paar = await schluesselpaarErzeugen("ED25519", { kommentar: "e2e@laptop" });
await oidcAnmelden(page, "operator");
await page.goto("/schluessel/neu");
await page.getByRole("radio", { name: "Importieren" }).click();
await page.getByLabel("Zweck").fill(name);
await page.getByLabel("Public Key").fill(paar.publicKeyZeile);
await page.locator(`input[name="kategorie"][value="PERSON"]`).check();
await page.getByRole("button", { name: "Schlüssel importieren" }).click();
await expect(page.getByRole("heading", { name })).toBeVisible();
await expect(page.getByText("nicht gespeichert (Personen-Key)")).toBeVisible();
await expect(page.getByText(paar.fingerprint)).toBeVisible();
await page.goto("/schluessel/neu");
await page.getByRole("radio", { name: "Importieren" }).click();
await page.getByLabel("Zweck").fill(`${name} nochmal`);
await page.getByLabel("Public Key").fill(paar.publicKeyZeile);
await page.getByRole("button", { name: "Schlüssel importieren" }).click();
await expect(page.getByRole("alert").first()).toContainText(`bereits verwaltet: „${name}“`);
});
test("Leser sieht Schlüssel, darf aber keine anlegen", async ({ page }) => {
await oidcAnmelden(page, "reader");
await page.goto("/schluessel");
await expect(page.getByRole("heading", { name: "Schlüssel", exact: true })).toBeVisible();
await expect(page.getByRole("link", { name: "Schlüssel anlegen" })).toHaveCount(0);
await page.goto("/schluessel/neu");
await expect(page).toHaveURL(/\/kein-zugriff/);
});