Merge branch 'phase/025-zuweisungsmatrix'
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
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";""`);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user