Files
sshkeyvault/tests/e2e/zuweisungen.spec.ts
florian.wildeandClaude Opus 5.5 be9837ba50 feat(zuweisungen): Zum Dry-Run springt zur Dry-Run-Karte (#032)
Der Link im Zell-Dialog der Zuweisungsmatrix fuehrte nur auf den Kopf
der Schluesselseite; die Dry-Run-Karte liegt weit unten, und wer aus der
Matrix kommt, will genau dorthin. Die Karte traegt jetzt id="dry-run"
(Karte nimmt dafuer eine optionale id an), der Link zeigt auf
/schluessel/<id>#dry-run. scroll-mt haelt die Karte unter der festen
Kopfleiste der schmalen Ansicht.

E2E: der Matrix-Test klickt den Link und prueft Adresse und Sichtbarkeit
der Karte.

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

156 lines
6.8 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 { randomUUID } from "node:crypto";
import { expect, test, type Page } from "@playwright/test";
import pg from "pg";
import "dotenv/config";
/**
* Zuweisungsmatrix (#025) im gebauten Stand. Kein Worker und kein SSH-Ziel noetig: die Matrix
* zeigt und plant nur. Schluessel, Hosts und Hostgruppe legt der Test direkt in der
* Entwicklungsdatenbank an (Praefix zz-e2e) und raeumt sie am Ende wieder weg.
*/
const name = `zz-e2e-mx${Date.now().toString(36)}`;
const ids = {
schluessel: randomUUID(),
gruppe: randomUUID(),
h1: randomUUID(),
h2: randomUUID(),
h3: randomUUID(),
};
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 anmelden(page: Page, rolle: "admin" | "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();
}
test.beforeAll(async () => {
await datenbank(async (client) => {
for (const [id, nr] of [
[ids.h1, 1],
[ids.h2, 2],
[ids.h3, 3],
] as const) {
await client.query(`INSERT INTO "Host" ("id","name","adresse","geaendertAm") VALUES ($1,$2,$3,now())`, [
id,
`${name}-h${nr}`,
`10.99.0.${nr}`,
]);
}
await client.query(`INSERT INTO "Hostgruppe" ("id","name") VALUES ($1,$2)`, [ids.gruppe, `${name}-g`]);
await client.query(`INSERT INTO "HostgruppeMitglied" ("hostId","hostgruppeId") VALUES ($1,$3),($2,$3)`, [
ids.h1,
ids.h2,
ids.gruppe,
]);
await client.query(
`INSERT INTO "Schluessel" ("id","name","slug","typ","kategorie","publicKey","fingerprint","kommentar","geaendertAm")
VALUES ($1,$2,$3,'ED25519','DIENST','ssh-ed25519 AAAA zz-e2e',$4,'zz-e2e',now())`,
[ids.schluessel, `${name} Backup`, name, `SHA256:${name}`],
);
await client.query(
`INSERT INTO "PublicVerwendung" ("id","schluesselId","hostgruppeId","zielbenutzer","optionen")
VALUES ($1,$2,$3,'root','restrict')`,
[randomUUID(), ids.schluessel, ids.gruppe],
);
});
});
test.afterAll(async () => {
await datenbank(async (client) => {
// Verwendungen und Mitgliedschaften haengen per Cascade daran.
await client.query(`DELETE FROM "Schluessel" WHERE "id" = $1`, [ids.schluessel]);
await client.query(`DELETE FROM "Hostgruppe" WHERE "id" = $1`, [ids.gruppe]);
await client.query(`DELETE FROM "Host" WHERE "id" = ANY($1)`, [[ids.h1, ids.h2, ids.h3]]);
});
});
test("Matrix zeigt Gruppen-Zuweisung, filtert, weist zu und exportiert", async ({ page }) => {
await anmelden(page, "admin");
// Die Suche trifft Schluessel und Hosts dieses Tests – fremde Daten bleiben draussen.
await page.goto(`/zuweisungen?q=${name}`);
const matrix = page.locator("table");
await expect(matrix.getByRole("columnheader", { name: `${name}-h3` })).toBeVisible();
// Gruppen-Zuweisung erscheint auf beiden Mitgliedern als „P·G“ mit Zustand.
for (const nr of [1, 2]) {
const zelle = matrix.getByRole("button", {
name: `${name} Backup auf ${name}-h${nr}: Public Key über Hostgruppe ${name}-g, Benutzer root, noch nicht ausgerollt`,
});
await expect(zelle).toBeVisible();
await expect(zelle).toContainText("P·G");
}
// Filter Hostgruppe: der dritte Host gehoert nicht dazu und verschwindet.
await page.getByLabel("Hostgruppe").selectOption({ label: `${name}-g` });
await page.getByRole("button", { name: "Filtern" }).click();
await expect(page).toHaveURL(/gruppe=/);
await expect(matrix.getByRole("columnheader", { name: `${name}-h1` })).toBeVisible();
await expect(matrix.getByRole("columnheader", { name: `${name}-h3` })).toHaveCount(0);
// Leere Zelle auf h3 → Dialog mit vorausgewaehltem Host → zuweisen.
await page.goto(`/zuweisungen?q=${name}`);
await matrix.getByRole("button", { name: new RegExp(`auf ${name}-h3: nicht zugewiesen`) }).click();
const dialog = page.getByRole("dialog");
await expect(dialog.getByLabel("Ziel", { exact: true })).toHaveValue(`host:${ids.h3}`);
await dialog.getByLabel("Zielbenutzer").fill("deploy");
await dialog.getByRole("button", { name: "Zuweisen" }).click();
await expect(dialog.getByText("Public Key direkt")).toBeVisible();
await expect(dialog.getByRole("link", { name: "Zum Dry-Run" })).toBeVisible();
const angelegt = await datenbank((client) =>
client.query(
`SELECT "zielbenutzer" FROM "PublicVerwendung" WHERE "schluesselId" = $1 AND "hostId" = $2`,
[ids.schluessel, ids.h3],
),
);
expect(angelegt.rows).toEqual([{ zielbenutzer: "deploy" }]);
await dialog.getByRole("button", { name: "Schließen" }).click();
await expect(
matrix.getByRole("button", { name: new RegExp(`auf ${name}-h3: Public Key direkt`) }),
).toBeVisible();
// CSV mit derselben Sitzung: Gruppen-Zeile je Mitglied und die neue direkte Zuweisung.
const csv = await page.request.get(`/api/zuweisungen.csv?q=${name}`);
expect(csv.status()).toBe(200);
const text = await csv.text();
expect(text).toContain(
`"${name} Backup";"SHA256:${name}";"${name}-h2";"root";"Public Key";"${name}-g";"restrict";"noch nicht ausgerollt"`,
);
expect(text).toContain(`"${name}-h3";"deploy";"Public Key";""`);
// „Zum Dry-Run“ springt direkt zur Dry-Run-Karte der Schluesselseite (#032).
await matrix.getByRole("button", { name: new RegExp(`auf ${name}-h3: Public Key direkt`) }).click();
await dialog.getByRole("link", { name: "Zum Dry-Run" }).click();
await expect(page).toHaveURL(new RegExp(`/schluessel/${ids.schluessel}#dry-run$`));
const karte = page.locator("#dry-run");
await expect(karte).toContainText("Dry-Run-Vorschau");
await expect(karte).toBeInViewport();
});
test("Leser sieht die Matrix, aber keinen Zuweisen-Knopf", async ({ page }) => {
await anmelden(page, "reader");
await page.goto(`/zuweisungen?q=${name}`);
const matrix = page.locator("table");
await matrix
.getByRole("button", { name: new RegExp(`auf ${name}-h1: Public Key über Hostgruppe`) })
.click();
const dialog = page.getByRole("dialog");
await expect(dialog.getByText("Public Key über Hostgruppe")).toBeVisible();
await expect(dialog.getByRole("button", { name: "Zuweisen" })).toHaveCount(0);
await expect(dialog.getByRole("button", { name: "Zuweisung entfernen" })).toHaveCount(0);
await dialog.getByRole("button", { name: "Schließen" }).click();
// Leere Zellen sind fuer Leser keine Knoepfe.
await expect(matrix.getByRole("button", { name: /nicht zugewiesen/ })).toHaveCount(0);
});