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>
101 lines
4.5 KiB
TypeScript
101 lines
4.5 KiB
TypeScript
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$/);
|
||
});
|