142 lines
6.2 KiB
TypeScript
142 lines
6.2 KiB
TypeScript
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();
|
||
});
|