test(vorlagen): Service-, E2E-Tests und Screenshots (#019)
Der Service war nur ueber die reinen Pruefregeln abgedeckt. Die DB-Tests halten fest, was die Oberflaeche voraussetzt: doppelte Namen melden sich am Feld name, Aenderungen protokollieren genau die geaenderten Felder, eingebaute Vorlagen lassen sich nicht archivieren. Der E2E-Ablauf prueft Feldfehler, Vorschau und Rechte im gebauten Stand; die Screenshots dienen der Layout-Abnahme bei 390 px. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 174 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
@@ -39,6 +39,13 @@ const SEITEN: { name: string; pfad: string; oeffentlich?: boolean; folgen?: stri
|
||||
{ name: "meine-schluessel", pfad: "/meine-schluessel" },
|
||||
{ name: "hosts", pfad: "/hosts" },
|
||||
{ name: "host-detail", pfad: "/hosts", folgen: 'main table a[href^="/hosts/"]' },
|
||||
{ name: "vorlagen", pfad: "/vorlagen" },
|
||||
// Die eingebaute „Deployment-Zugang“ zeigt Platzhalter, Laufzeit und Benutzer-Anlegen (#019).
|
||||
{
|
||||
name: "vorlage-bearbeiten",
|
||||
pfad: "/vorlagen",
|
||||
folgen: 'main a[aria-label="Deployment-Zugang bearbeiten"]',
|
||||
},
|
||||
];
|
||||
|
||||
const ANSICHTEN = [
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* Vorlagen im gebauten Stand (#019). Testvorlagen heissen zz-e2e-… und werden am Ende des
|
||||
* Ablaufs archiviert, damit die Entwicklungsdatenbank sauber bleibt.
|
||||
*/
|
||||
async function oidcAnmelden(page: Page, rolle: "admin" | "operator" | "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("Admin legt eine Vorlage an, sieht Feldfehler und archiviert sie", async ({ page }) => {
|
||||
const name = `zz-e2e-${Date.now().toString(36)}`;
|
||||
await oidcAnmelden(page, "admin");
|
||||
await page.goto("/vorlagen");
|
||||
await expect(page.getByRole("heading", { name: "Deployment-Zugang" })).toBeVisible();
|
||||
await page.getByRole("link", { name: "Vorlage anlegen" }).click();
|
||||
await expect(page).toHaveURL(/\/vorlagen\/neu$/);
|
||||
|
||||
await page.getByLabel("Name").fill(name);
|
||||
await page.getByLabel("Zielbenutzer").fill("zzdeploy");
|
||||
await page.getByRole("checkbox", { name: /Benutzer anlegen/ }).check();
|
||||
await page.getByLabel("authorized_keys-Optionen (optional)").fill('from="{{quellIp}}",no-pty');
|
||||
|
||||
// Die Vorschau meldet den Fehler schon vor dem Absenden, der Server dann am Feld.
|
||||
await expect(page.getByText("Unbekannter Platzhalter {{quellIp}}.").first()).toBeVisible();
|
||||
await page.getByRole("button", { name: "Vorlage anlegen" }).click();
|
||||
await expect(page.locator("#feld-authorizedKeysOptionen-fehler")).toHaveText(
|
||||
"Unbekannter Platzhalter {{quellIp}}.",
|
||||
);
|
||||
// Eingaben bleiben nach der Feldmeldung stehen.
|
||||
await expect(page.getByLabel("Name")).toHaveValue(name);
|
||||
|
||||
await page.getByLabel("authorized_keys-Optionen (optional)").fill('from="{{quellHostIp}}",no-pty');
|
||||
await page.getByLabel("Alias-Muster für ~/.ssh/config (optional)").fill("zz-{{zielHost}}");
|
||||
await expect(page.getByText('from="10.0.0.5",no-pty')).toBeVisible();
|
||||
await expect(page.getByText("zz-srv-web01")).toBeVisible();
|
||||
|
||||
// sudo „bestimmte Befehle“ blendet das Befehlsfeld ein, „voll“ warnt.
|
||||
await page.getByRole("radio", { name: "voll" }).check();
|
||||
await expect(page.getByRole("note")).toContainText("root");
|
||||
await page.getByRole("radio", { name: "bestimmte Befehle" }).check();
|
||||
await page.getByLabel("Erlaubte Befehle").fill("/usr/bin/systemctl restart nginx");
|
||||
await expect(page.getByLabel("sudoers-Inhalt", { exact: true })).toContainText(
|
||||
"zzdeploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx",
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "Vorlage anlegen" }).click();
|
||||
await expect(page).toHaveURL(/\/vorlagen$/);
|
||||
const karte = page.locator("article", { has: page.getByRole("heading", { name }) });
|
||||
await expect(karte).toBeVisible();
|
||||
await expect(karte.getByText('from="{{quellHostIp}}",no-pty')).toBeVisible();
|
||||
await expect(karte.getByText("wird angelegt")).toBeVisible();
|
||||
|
||||
await karte.getByRole("link", { name: `${name} bearbeiten` }).click();
|
||||
await page.getByRole("button", { name: "Vorlage archivieren" }).click();
|
||||
await page.getByRole("dialog").getByRole("button", { name: "Archivieren" }).click();
|
||||
await expect(page).toHaveURL(/\/vorlagen$/);
|
||||
await expect(page.getByRole("heading", { name })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("eingebaute Vorlagen haben keinen Archivieren-Knopf", async ({ page }) => {
|
||||
await oidcAnmelden(page, "admin");
|
||||
await page.goto("/vorlagen");
|
||||
await page.getByRole("link", { name: "Deployment-Zugang bearbeiten" }).click();
|
||||
await expect(page.getByLabel("Name")).toHaveValue("Deployment-Zugang");
|
||||
await expect(page.getByRole("button", { name: "Vorlage archivieren" })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("Leser sieht Vorlagen, darf aber keine anlegen oder bearbeiten", async ({ page }) => {
|
||||
await oidcAnmelden(page, "reader");
|
||||
await page.goto("/vorlagen");
|
||||
await expect(page.getByRole("heading", { name: "Deployment-Zugang" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Vorlage anlegen" })).toHaveCount(0);
|
||||
await expect(page.getByRole("link", { name: /bearbeiten/ })).toHaveCount(0);
|
||||
|
||||
await page.goto("/vorlagen/neu");
|
||||
await expect(page).toHaveURL(/\/kein-zugriff/);
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { VorlagenFehler, type VorlageEingabe } from "@/lib/vorlagen";
|
||||
import { vorlagenListe } from "@/server/queries/vorlagen";
|
||||
import { vorlageAnlegen, vorlageArchivieren, vorlageBearbeiten } from "@/server/services/vorlagen";
|
||||
import { datenbankLeeren } from "./hilfen/db";
|
||||
|
||||
/** Vorlagen-Service (#019) gegen die Testdatenbank: Eindeutigkeit, Protokoll, Schutz der eingebauten. */
|
||||
|
||||
let akteur: { id: string; name: string };
|
||||
|
||||
beforeEach(async () => {
|
||||
await datenbankLeeren();
|
||||
const benutzer = await prisma.benutzer.create({ data: { name: "Ada Admin", rolle: "ADMIN" } });
|
||||
akteur = { id: benutzer.id, name: benutzer.name };
|
||||
});
|
||||
|
||||
const BASIS: VorlageEingabe = {
|
||||
name: "Monitoring",
|
||||
beschreibung: " Zugang für Prometheus ",
|
||||
schluesselTyp: "ED25519",
|
||||
kategorie: "DIENST",
|
||||
standardLaufzeitTage: 30,
|
||||
zielbenutzer: "monitor",
|
||||
zielbenutzerAnlegen: true,
|
||||
sudoModus: "BEFEHLE",
|
||||
sudoBefehle: ["/usr/bin/systemctl status nginx"],
|
||||
authorizedKeysOptionen: 'from="{{quellHostIp}}",no-pty',
|
||||
sshConfigAlias: "mon-{{zielHost}}",
|
||||
knownHostsEintragen: true,
|
||||
privatNachAusrollenLoeschen: false,
|
||||
};
|
||||
|
||||
async function fehlerVon(versuch: Promise<unknown>): Promise<VorlagenFehler> {
|
||||
const fehler = await versuch.then(
|
||||
() => undefined,
|
||||
(f: unknown) => f,
|
||||
);
|
||||
expect(fehler).toBeInstanceOf(VorlagenFehler);
|
||||
return fehler as VorlagenFehler;
|
||||
}
|
||||
|
||||
describe("Vorlage anlegen", () => {
|
||||
it("speichert bereinigt, laesst Platzhalter stehen und protokolliert", async () => {
|
||||
const id = await vorlageAnlegen(BASIS, akteur);
|
||||
const v = await prisma.vorlage.findUniqueOrThrow({ where: { id } });
|
||||
expect(v.eingebaut).toBe(false);
|
||||
expect(v.beschreibung).toBe("Zugang für Prometheus");
|
||||
expect(v.sudoBefehle).toEqual(["/usr/bin/systemctl status nginx"]);
|
||||
expect(v.authorizedKeysOptionen).toBe('from="{{quellHostIp}}",no-pty');
|
||||
expect(v.sshConfigAlias).toBe("mon-{{zielHost}}");
|
||||
|
||||
const eintrag = await prisma.protokoll.findFirstOrThrow({ where: { aktion: "vorlage.angelegt" } });
|
||||
expect(eintrag.akteurId).toBe(akteur.id);
|
||||
expect(eintrag.details).toMatchObject({ name: "Monitoring" });
|
||||
expect((await vorlagenListe()).map((l) => l.name)).toEqual(["Monitoring"]);
|
||||
});
|
||||
|
||||
it("verwirft sudo-Befehle, wenn der Modus nicht BEFEHLE ist", async () => {
|
||||
const id = await vorlageAnlegen({ ...BASIS, sudoModus: "KEINER" }, akteur);
|
||||
expect((await prisma.vorlage.findUniqueOrThrow({ where: { id } })).sudoBefehle).toEqual([]);
|
||||
});
|
||||
|
||||
it("meldet einen doppelten Namen am Feld name", async () => {
|
||||
await vorlageAnlegen(BASIS, akteur);
|
||||
const fehler = await fehlerVon(vorlageAnlegen({ ...BASIS, zielbenutzer: "anders" }, akteur));
|
||||
expect(fehler.feld).toBe("name");
|
||||
expect(await prisma.vorlage.count()).toBe(1);
|
||||
});
|
||||
|
||||
it("meldet einen unbekannten Platzhalter am Feld der Optionen", async () => {
|
||||
const fehler = await fehlerVon(
|
||||
vorlageAnlegen({ ...BASIS, authorizedKeysOptionen: 'from="{{quellIp}}"' }, akteur),
|
||||
);
|
||||
expect(fehler.feld).toBe("authorizedKeysOptionen");
|
||||
expect(fehler.message).toContain("{{quellIp}}");
|
||||
expect(await prisma.vorlage.count()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Vorlage bearbeiten", () => {
|
||||
it("protokolliert genau die geaenderten Felder", async () => {
|
||||
const id = await vorlageAnlegen(BASIS, akteur);
|
||||
await vorlageBearbeiten(
|
||||
id,
|
||||
{ ...BASIS, standardLaufzeitTage: 60, sudoBefehle: ["/usr/bin/systemctl status nginx", "/usr/bin/id"] },
|
||||
akteur,
|
||||
);
|
||||
const v = await prisma.vorlage.findUniqueOrThrow({ where: { id } });
|
||||
expect(v.standardLaufzeitTage).toBe(60);
|
||||
|
||||
const eintrag = await prisma.protokoll.findFirstOrThrow({ where: { aktion: "vorlage.geaendert" } });
|
||||
const details = eintrag.details as { name: string; geaendert: string[] };
|
||||
expect(details.name).toBe("Monitoring");
|
||||
expect(details.geaendert.sort()).toEqual(["standardLaufzeitTage", "sudoBefehle"]);
|
||||
});
|
||||
|
||||
it("meldet beim Umbenennen auf einen vergebenen Namen das Feld name", async () => {
|
||||
await vorlageAnlegen(BASIS, akteur);
|
||||
const id = await vorlageAnlegen({ ...BASIS, name: "Zweite" }, akteur);
|
||||
const fehler = await fehlerVon(vorlageBearbeiten(id, BASIS, akteur));
|
||||
expect(fehler.feld).toBe("name");
|
||||
});
|
||||
|
||||
it("verweigert archivierte Vorlagen", async () => {
|
||||
const id = await vorlageAnlegen(BASIS, akteur);
|
||||
await vorlageArchivieren(id, akteur);
|
||||
await fehlerVon(vorlageBearbeiten(id, BASIS, akteur));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Vorlage archivieren", () => {
|
||||
it("archiviert eigene Vorlagen und blendet sie aus der Liste aus", async () => {
|
||||
const id = await vorlageAnlegen(BASIS, akteur);
|
||||
await vorlageArchivieren(id, akteur);
|
||||
expect((await prisma.vorlage.findUniqueOrThrow({ where: { id } })).archiviertAm).not.toBeNull();
|
||||
expect(await vorlagenListe()).toEqual([]);
|
||||
expect(await prisma.protokoll.count({ where: { aktion: "vorlage.archiviert" } })).toBe(1);
|
||||
});
|
||||
|
||||
it("laesst eingebaute Vorlagen nicht archivieren, aber bearbeiten", async () => {
|
||||
const eingebaut = await prisma.vorlage.create({
|
||||
data: { name: "Deployment-Zugang", eingebaut: true, zielbenutzer: "deploy" },
|
||||
});
|
||||
await fehlerVon(vorlageArchivieren(eingebaut.id, akteur));
|
||||
expect((await prisma.vorlage.findUniqueOrThrow({ where: { id: eingebaut.id } })).archiviertAm).toBeNull();
|
||||
|
||||
await vorlageBearbeiten(eingebaut.id, { ...BASIS, name: "Deployment-Zugang" }, akteur);
|
||||
const v = await prisma.vorlage.findUniqueOrThrow({ where: { id: eingebaut.id } });
|
||||
expect(v.eingebaut).toBe(true);
|
||||
expect(v.zielbenutzer).toBe("monitor");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user