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>
156 lines
6.8 KiB
TypeScript
156 lines
6.8 KiB
TypeScript
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);
|
||
});
|