Files
Laufzettel/tests/fixtures/ldap/container.ts
T
florian.wilde ae290345b7
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(verzeichnis): LDAP-Abgleich, Zeitplan, CSV-Import und Log (#30)
2026-09-26 00:21:56 +00:00

162 lines
6.5 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 { 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<ContainerStand> {
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<boolean> {
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<T>(was: (client: Client) => Promise<T>): Promise<T> {
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<string> {
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<void> {
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<void> {
await alsVerwalter(async (client) => {
await client.modify(
dn,
new Change({
operation: "replace",
modification: new Attribute({ type: attribut, values: [wert] }),
}),
);
});
}