import { Attribute, Change, Client } from "ldapts"; /** * Zugang zum LDAP-Testcontainer und das, was im Fixture steht (#016). * * Die Werte stehen hier und nicht in den Tests, weil zwei Testdateien gegen * denselben Container laufen. Die Erwartungen (GUID-Zeichenketten, * Kontonamen) gehoeren mit dazu: sie beschreiben das Fixture und nicht die * Regeln — die stehen in den Tests. * * Der Container wird mit * `docker compose -f docker-compose.test.yml up -d ldap` gestartet. */ export const LDAP_URL = "ldap://127.0.0.1:1389"; export const LDAP_BASIS_DN = "dc=firma,dc=test"; export const LDAP_BENUTZER_DN = "ou=Benutzer,dc=firma,dc=test"; export const LDAP_BIND_DN = "cn=admin,dc=firma,dc=test"; export const LDAP_BIND_PASSWORT = "ldap-test-geheim"; /** * Die GUID-Zeichenketten der Fixture-Personen. * * Ausgerechnet mit Pythons `uuid.UUID(bytes_le=…)` aus den Bytes in * `daten/50-personen.ldif` — eine unabhaengige Umsetzung derselben * gemischt-endianen Reihenfolge, damit hier nicht die Erwartung aus derselben * Quelle stammt wie das Ergebnis. */ export const GUID = { berger: "1d3e409c-b27a-4bc9-b1e6-f0a2c3d4e5f6", huber: "5f074310-3c35-4a1d-836f-a50f515e7a35", weiss: "04030201-0605-0807-090a-0b0c0d0e0f10", /** „Alte Kraft“: im Verzeichnis vorhanden, aber deaktiviert. */ alteKraft: "ccddeeff-aabb-8899-7766-554433221100", } as const; /** So viele Konten liefert der Standardfilter: drei aktive plus das ohne objectGUID. */ export const FIXTURE_GELESEN = 4; /** Davon zugeordnet, also mit stabiler Kennung. */ export const FIXTURE_PERSONEN = 3; export type ContainerStand = { readonly da: true } | { readonly da: false; readonly grund: string }; /** * Laeuft der Container – und stehen die Fixture-Personen wirklich darin? * * Geprueft wird mit einem echten Bind und einer echten Suche, nicht mit einem * offenen Port: ein gestarteter, aber noch nicht bestueckter Container waere * sonst „da“, und die Tests liefen gegen ein leeres Verzeichnis. Ein stilles * Ueberspringen sieht genauso gruen aus wie ein bestandener Lauf, deshalb sagt * diese Funktion laut, was fehlt – und in CI bricht sie ab. */ export async function ldapContainerStand(): Promise { const client = new Client({ url: LDAP_URL, connectTimeout: 2000, timeout: 5000 }); try { await client.bind(LDAP_BIND_DN, LDAP_BIND_PASSWORT); const { searchEntries } = await client.search(LDAP_BENUTZER_DN, { scope: "sub", filter: "(sAMAccountName=mhuber)", attributes: ["dn"], }); if (searchEntries.length === 0) { return { da: false, grund: "Der Container laeuft, aber das Fixture „mhuber“ fehlt darin." }; } return { da: true }; } catch (fehler) { return { da: false, grund: fehler instanceof Error ? fehler.message : String(fehler) }; } finally { await client.unbind().catch(() => undefined); } } /** * Der Stand samt Hinweis auf der Konsole. In CI ein Abbruch: `pnpm test` ist das * Tor vor jedem Commit, und dort darf der Abgleich nicht ungeprueft bleiben. */ export async function ldapVerfuegbar(): Promise { const stand = await ldapContainerStand(); if (stand.da) return true; const hinweis = `\n LDAP-Testcontainer nicht erreichbar (${stand.grund}).` + "\n Die Verzeichnis- und Abgleichtests gegen das Verzeichnis werden uebersprungen." + "\n Starten mit: docker compose -f docker-compose.test.yml up -d ldap\n"; if (process.env["CI"]) throw new Error(hinweis); // Direkt auf stderr und nicht ueber `console`: Vitest 5 fasst die // Konsolenausgabe einer Testdatei ab und zeigt sie fuer den **Modulrumpf** // nicht an — nachgemessen. Ein `console.warn` hier waere also genau das stille // Ueberspringen, das dieser Hinweis verhindern soll. process.stderr.write(hinweis); return false; } /** Ein Client als Verzeichnisverwalter, fuer Tests, die das Fixture veraendern. */ export async function alsVerwalter(was: (client: Client) => Promise): Promise { const client = new Client({ url: LDAP_URL, connectTimeout: 2000, timeout: 5000 }); try { await client.bind(LDAP_BIND_DN, LDAP_BIND_PASSWORT); return await was(client); } finally { await client.unbind().catch(() => undefined); } } /** * Legt eine zusaetzliche Person an und gibt den Weg zurueck, sie wieder zu * entfernen. * * Das braucht der Test „ein entferntes Konto wird aktiv = false“: er muss ein * Konto wirklich loeschen. Die Fixture-Dateien bleiben dabei unberuehrt, und der * Container ist nach dem Test wieder wie vorher — sonst haengt das Ergebnis des * naechsten Tests an der Reihenfolge. */ export async function personAnlegen(eintrag: { readonly cn: string; readonly sn: string; readonly givenName: string; readonly sam: string; readonly guidBase64: string; readonly userAccountControl?: number; }): Promise { const dn = `cn=${eintrag.cn},ou=Vertrieb,${LDAP_BENUTZER_DN}`; // Als `Attribute[]`, nicht als Objekt: die Objektform von `Client.add` nimmt // nur Zeichenketten, und objectGUID ist ein Puffer. const werte: Attribute[] = [ new Attribute({ type: "objectClass", values: ["inetOrgPerson", "user"] }), new Attribute({ type: "cn", values: [eintrag.cn] }), new Attribute({ type: "sn", values: [eintrag.sn] }), new Attribute({ type: "givenName", values: [eintrag.givenName] }), new Attribute({ type: "displayName", values: [`${eintrag.sn}, ${eintrag.givenName}`] }), new Attribute({ type: "mail", values: [`${eintrag.sam}@firma.test`] }), new Attribute({ type: "userPrincipalName", values: [`${eintrag.sam}@firma.test`] }), new Attribute({ type: "sAMAccountName", values: [eintrag.sam] }), new Attribute({ type: "department", values: ["Vertrieb"] }), new Attribute({ type: "objectCategory", values: ["person"] }), new Attribute({ type: "userAccountControl", values: [String(eintrag.userAccountControl ?? 512)] }), new Attribute({ type: "objectGUID", values: [Buffer.from(eintrag.guidBase64, "base64")] }), ]; await alsVerwalter(async (client) => { await client.add(dn, werte); }); return dn; } export async function personLoeschen(dn: string): Promise { await alsVerwalter(async (client) => { await client.del(dn); }); } /** Ein Attribut aendern, um den zweiten Abgleich etwas zu tun zu geben. */ export async function attributSetzen(dn: string, attribut: string, wert: string): Promise { await alsVerwalter(async (client) => { await client.modify( dn, new Change({ operation: "replace", modification: new Attribute({ type: attribut, values: [wert] }), }), ); }); }