Files
Laufzettel/tests/e2e/verzeichnis.spec.ts
T
florian.wilde fbbb634e56
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(doku): Anleitung, PDF und Ende-zu-Ende-Abnahme (#36)
2026-09-26 09:04:15 +00:00

142 lines
6.2 KiB
TypeScript
Raw 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 { expect, test, type Locator, type Page } from "@playwright/test";
import "dotenv/config";
import { verschluesseln } from "@/lib/geheimnisse";
import { AD_ZUORDNUNG } from "@/lib/verzeichnis";
import { prisma } from "@/lib/db";
import { STANDARD_FILTER } from "@/server/ldap/zuordnung";
import { anmelden, betriebsmodus } from "./helfer";
/**
* Admin → Verzeichnis durch die Oberflaeche (#016).
*
* Ohne LDAP-Container: geprueft wird, was auch ohne Verzeichnis gilt — dass die
* Attributzuordnung bedienbar ist, dass eine krumme Eingabe abgewiesen wird und
* dass eine gute wirklich in der Datenbank landet. Was der Abgleich **tut**,
* steht in `tests/abgleich.test.ts` gegen einen echten Server.
*
* Voraussetzungen prueft `tests/e2e/global-setup.ts`. Die Quelle, an der hier
* gearbeitet wird, legt der Ablauf selbst an und loescht sie hinterher: vorher
* nahm er die erste vorhandene, schaltete sich mit `test.skip` ab, wenn es keine
* gab, und schrieb die Attributzuordnung einer fremden — im Zweifel produktiven
* — Quelle um (#018). Angefasst wird nur der Kasten mit dem eigenen Namen.
*
* Die Seite gibt es nur im Modus INTERN (#008); im Modus DIENSTLEISTUNG leitet
* sie um, und dann schlaegt dieser Ablauf fehl. Das ist die richtige Antwort:
* die Ablaeufe pruefen den Modus aus der `.env`, und der ist INTERN.
*/
/*
* Das Verzeichnis gibt es **nur** im Modus INTERN (#008): im Modus
* DIENSTLEISTUNG leitet die Seite auf `/admin` um, und es gibt nichts zu
* pruefen. Uebersprungen mit Grund — ein Ablauf, der dort rot wuerde, sagte
* etwas ueber den Modus und nichts ueber das Verzeichnis.
*/
test.skip(betriebsmodus() === "DIENSTLEISTUNG", "Das Verzeichnis gibt es nur im Modus INTERN (#008).");
const STEMPEL = Date.now();
const QUELLEN_NAME = `ZZ E2E Verzeichnis ${STEMPEL}`;
/** Der Kasten dieser Quelle – alles andere auf der Seite bleibt unberührt. */
function kasten(seite: Page): Locator {
return seite.locator("li").filter({ has: seite.getByRole("heading", { name: QUELLEN_NAME }) });
}
test.beforeAll(async () => {
const kunde = await prisma.kunde.findFirstOrThrow({
where: { istEigeneOrganisation: true },
select: { id: true },
});
await prisma.verzeichnisquelle.create({
data: {
kundeId: kunde.id,
name: QUELLEN_NAME,
url: "ldaps://dc-e2e.firma.local:636",
bindDn: "CN=e2e,OU=Dienstkonten,DC=firma,DC=local",
// Der CHECK in der Datenbank verlangt das Formatpraefix; verschluesselt wird
// deshalb wirklich und nicht mit einer Zeichenkette getan.
bindPasswortVerschluesselt: verschluesseln(`e2e-${STEMPEL}`),
basisDn: "OU=Benutzer,DC=firma,DC=local",
filter: STANDARD_FILTER,
attributZuordnung: { ...AD_ZUORDNUNG },
abgleichIntervallMinuten: 60,
// Kein Zeitplan: eine aktive Quelle auf einen Rechner, den es nicht gibt,
// liesse den Abgleich im Hintergrund in Fehler laufen.
aktiv: false,
},
});
});
test.describe("Verzeichnis", () => {
test("macht die Attributzuordnung bedienbar und prüft sie", async ({ page }) => {
test.slow();
await anmelden(page);
await page.goto("/admin/verzeichnis");
const meine = kasten(page);
await expect(meine).toHaveCount(1);
await meine.locator("summary", { hasText: "Einstellungen bearbeiten" }).click();
await meine.locator("summary", { hasText: "Attributzuordnung anpassen" }).click();
const kennung = meine.locator('input[name="attr.externeId"]');
await expect(kennung).toBeVisible();
await expect(kennung).toHaveValue("objectGUID");
// Eine Klammer darf nicht in eine LDAP-Anfrage wandern.
const titel = meine.locator('input[name="attr.titel"]');
await titel.fill("(objectClass=*)");
await meine.getByRole("button", { name: "Speichern" }).click();
await expect(meine.locator('[role="alert"]').first()).toContainText("kein Attributname");
// Und eine gute Eingabe steht danach wirklich in der Datenbank.
await titel.fill("personalTitle");
await meine.getByRole("button", { name: "Speichern" }).click();
await expect(meine.locator('[role="status"]').last()).toContainText("gespeichert");
const nachher = await prisma.verzeichnisquelle.findFirstOrThrow({ where: { name: QUELLEN_NAME } });
expect((nachher.attributZuordnung as Record<string, string>)["titel"]).toBe("personalTitle");
// Neu geladen steht sie auch im Formular – sonst waere sie nur gespeichert,
// aber nicht bearbeitbar.
await page.reload();
const nochmal = kasten(page);
await nochmal.locator("summary", { hasText: "Einstellungen bearbeiten" }).click();
await nochmal.locator("summary", { hasText: "Attributzuordnung anpassen" }).click();
await expect(nochmal.locator('input[name="attr.titel"]')).toHaveValue("personalTitle");
});
test("zeigt das Abgleich-Log und trägt am Handy ohne horizontales Scrollen", async ({ page }) => {
await anmelden(page);
await page.goto("/admin/verzeichnis");
const meine = kasten(page);
/*
* Ohne Bedingung: die eigene Quelle steht da, also steht auch ihr Log da.
* Vorher hing der Klick an einem `if (count > 0)` — fehlte der Aufklapper,
* bestand der Test, ohne etwas geprueft zu haben.
*/
const log = meine.locator("summary", { hasText: "Abgleich-Log" });
await log.click();
// Noch nie abgeglichen – und genau das sagt die Seite, statt leer zu bleiben.
await expect(meine.getByText(/Noch kein Lauf/)).toBeVisible();
const masse = await page.evaluate(() => ({
scroll: document.documentElement.scrollWidth,
sichtbar: document.documentElement.clientWidth,
}));
expect(masse.scroll).toBeLessThanOrEqual(masse.sichtbar);
});
});
test.afterAll(async () => {
// An dieser Quelle haengt keine Person: sie war nie aktiv und hat nie
// abgeglichen. Haengte eine, verweigerte der Fremdschluessel das Loeschen.
const quellen = await prisma.verzeichnisquelle.findMany({
where: { name: { startsWith: "ZZ E2E Verzeichnis" } },
select: { id: true },
});
const kennungen = quellen.map((quelle) => quelle.id);
if (kennungen.length > 0) {
await prisma.protokoll.deleteMany({ where: { entitaetId: { in: kennungen } } });
await prisma.verzeichnisquelle.deleteMany({ where: { id: { in: kennungen } } });
}
await prisma.$disconnect();
});