Files
sshkeyvault/tests/e2e/anmeldung.spec.ts
T
florian.wildeandClaude Opus 5.5 ea6e44fced test(anmeldung): Namen gezielt im Benutzerkasten pruefen (#026)
Seit die Uebersicht echte letzte Aktionen zeigt, steht der Name des
Mock-Admins mehrfach auf der Seite.

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

101 lines
4.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 pg from "pg";
import "dotenv/config";
import { base32Dekodieren, totpCode, totpSchritt } from "../../src/lib/totp";
/**
* Anmeldung im gebauten Stand (#004): OIDC ueber den Mock-Anbieter und Notfallzugang mit
* TOTP-Einrichtung. Laeuft gegen die Entwicklungsdatenbank und setzt dort vorher das TOTP
* des Notfall-Admins zurueck – deshalb nie gegen einen Betrieb richten.
*/
const NOTFALL_PASSWORT = process.env["SSHKV_BREAKGLASS_PASSWORD"] ?? "";
async function datenbank<T>(arbeit: (client: pg.Client) => Promise<T>): Promise<T> {
const client = new pg.Client({ connectionString: process.env["DATABASE_URL"] });
await client.connect();
try {
return await arbeit(client);
} finally {
await client.end();
}
}
async function oidcAnmelden(page: Page, rolle: "admin" | "operator" | "reader" | "ohne") {
await page.goto("/");
await expect(page).toHaveURL(/\/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();
}
test.beforeAll(async () => {
await datenbank(async (client) => {
await client.query(
`UPDATE "Benutzer" SET "totpGeheimnis" = NULL, "totpEingerichtetAm" = NULL, "totpLetzterSchritt" = NULL WHERE "istNotfall"`,
);
await client.query(`DELETE FROM "Ratenbegrenzung"`);
});
});
test("ohne Sitzung geht es zur Anmeldung, mit Ziel", async ({ page }) => {
await page.goto("/hosts");
await expect(page).toHaveURL(/\/anmelden\?ziel=%2Fhosts/);
await expect(page.getByRole("heading", { name: "Anmelden" })).toBeVisible();
});
test("OIDC-Admin landet in der Übersicht und kann sich abmelden", async ({ page }) => {
await oidcAnmelden(page, "admin");
await expect(page.getByRole("heading", { name: "Übersicht" })).toBeVisible();
// Der Name steht seit #026 auch in den letzten Aktionen – gezielt im Benutzerkasten pruefen.
await expect(
page.getByText("Admin · über Mock-OIDC").locator("..").getByText("Admin (Mock)", { exact: true }),
).toBeVisible();
await expect(page.getByText("Admin · über Mock-OIDC")).toBeVisible();
await page.getByRole("button", { name: "Abmelden" }).click();
await expect(page.getByText("Sie wurden abgemeldet.")).toBeVisible();
await page.goto("/");
await expect(page).toHaveURL(/\/anmelden/);
});
test("OIDC ohne sshkv-Gruppe wird abgewiesen", async ({ page }) => {
await oidcAnmelden(page, "ohne");
await expect(page).toHaveURL(/fehler=keine-gruppe/);
await expect(page.getByText(/keiner SSHKeyVault-Gruppe zugeordnet/)).toBeVisible();
});
test("Notfall-Admin richtet TOTP ein und bestätigt sich erneut", async ({ page }) => {
test.skip(NOTFALL_PASSWORT.length < 16, "SSHKV_BREAKGLASS_PASSWORD fehlt in .env");
await page.goto("/anmelden");
await page.getByText("Notfallzugang", { exact: true }).click();
await page.getByLabel("Anmeldename").fill("notfall");
await page.getByLabel("Passwort").fill(NOTFALL_PASSWORT);
await page.getByRole("button", { name: "Mit Notfallzugang anmelden" }).click();
await expect(page).toHaveURL(/\/anmelden\/totp/);
await expect(page.getByRole("img", { name: /QR-Code/ })).toBeVisible();
const geheimnis = ((await page.locator("code").first().textContent()) ?? "").trim();
expect(geheimnis).toMatch(/^[A-Z2-7]{32}$/);
const code = () => totpCode(base32Dekodieren(geheimnis), totpSchritt(Date.now()));
await page.getByLabel("Code zur Bestätigung").fill("000000");
await page.getByRole("button", { name: /TOTP einrichten/ }).click();
await expect(page.getByText(/Der Code passt nicht/)).toBeVisible();
await page.getByLabel("Code zur Bestätigung").fill(code());
await page.getByRole("button", { name: /TOTP einrichten/ }).click();
await expect(page.getByRole("heading", { name: "Übersicht" })).toBeVisible();
await expect(page.getByText("Admin · über Notfallzugang")).toBeVisible();
// Erneute Bestaetigung: derselbe Code gilt nicht noch einmal, also auf den naechsten
// Zeitschritt warten.
await page.goto("/bestaetigen?ziel=%2Fhosts");
const schritt = totpSchritt(Date.now());
await expect
.poll(() => totpSchritt(Date.now()), { timeout: 35_000, intervals: [1000] })
.toBeGreaterThan(schritt);
await page.getByLabel("Passwort").fill(NOTFALL_PASSWORT);
await page.getByLabel("Code aus der Authenticator-App").fill(code());
await page.getByRole("button", { name: "Bestätigen" }).click();
await expect(page).toHaveURL(/\/hosts$/);
});