Files
Laufzettel/tests/platzhalter.test.ts
florian.wilde 0060a55387
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(mitarbeiter): Vorgesetzter auch fuer Personen ohne Verzeichnis (#43)
2026-10-04 15:10:34 +00:00

435 lines
15 KiB
TypeScript
Raw Permalink 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 { beforeEach, describe, expect, it } from "vitest";
import { Betriebsmodus, Herkunft } from "@/generated/prisma/enums";
import { ouAusDn, platzhalterWerteDesLaufs } from "@/server/services/platzhalter";
import { platzhalterErsetzen } from "@/lib/platzhalter-werte";
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
/**
* Die Werte hinter den Platzhaltern (#012).
*
* Aus diesen Werten entsteht der PowerShell-Befehl, den ein Techniker auf einem
* Domaenencontroller ausfuehrt. Deshalb steht hier jede Regel des Auftrags
* einzeln, mit den Daten, die im Haus wirklich vorkommen — einschliesslich
* „Jörg Müller-Lüdenscheid“.
*/
const DN = "CN=Maria Huber,OU=Buchhaltung,OU=Benutzer,DC=firma,DC=local";
async function aufbau(modus: Betriebsmodus = Betriebsmodus.INTERN) {
const eigene = await testPrisma.kunde.create({
data: { name: "Muster GmbH", kuerzel: "MUS", istEigeneOrganisation: true },
});
await testPrisma.einstellung.create({ data: { id: "global", modus } });
const abteilung = await testPrisma.abteilung.create({
data: {
kundeId: eigene.id,
name: "Buchhaltung",
zielOu: "OU=Buchhaltung,OU=Benutzer,DC=firma,DC=de",
standardGruppen: ["G-Buchhaltung", "G-Alle"],
},
});
const quelle = await testPrisma.verzeichnisquelle.create({
data: {
kundeId: eigene.id,
name: "AD",
url: "ldaps://dc01.firma.local:636",
basisDn: "DC=firma,DC=local",
filter: "(objectClass=user)",
attributZuordnung: {},
bindDn: "CN=svc,DC=firma,DC=local",
bindPasswortVerschluesselt: "v1.egal",
},
});
const chefin = await testPrisma.mitarbeiter.create({
data: {
kundeId: eigene.id,
quelleId: quelle.id,
externeId: "guid-chefin",
vorname: "Sonja",
nachname: "Brand",
anzeigename: "Sonja Brand",
sam: "sbrand",
herkunft: Herkunft.ABGLEICH,
},
});
const person = await testPrisma.mitarbeiter.create({
data: {
kundeId: eigene.id,
quelleId: quelle.id,
externeId: "guid-maria",
vorname: "Maria",
nachname: "Huber",
anzeigename: "Maria Huber",
mail: "maria.huber@firma.de",
upn: "maria.huber@firma.de",
sam: "mhuber",
abteilung: "Buchhaltung",
titel: "Sachbearbeiterin",
dn: DN,
// Beides, so wie der Abgleich es schreibt: der rohe Wert des
// Verzeichnisses und die gepflegte Zuordnung, aus der gelesen wird (#020).
vorgesetzterExterneId: "guid-chefin",
vorgesetzterId: chefin.id,
herkunft: Herkunft.ABGLEICH,
},
});
const geraet = await testPrisma.geraet.create({
data: {
kundeId: eigene.id,
tagCode: "LZ-7K3F9Q",
hostname: "PC-001",
seriennummer: "SN-4711",
inventarnummer: "INV-99",
registriertAm: new Date(),
},
});
const vorlage = await testPrisma.vorlage.create({ data: { name: "Onboarding" } });
const version = await testPrisma.vorlagenVersion.create({
data: { vorlageId: vorlage.id, version: 1, status: "ENTWURF" },
});
await testPrisma.vorlagenVersion.update({
where: { id: version.id },
data: { status: "VEROEFFENTLICHT", veroeffentlichtAm: new Date() },
});
return { eigene, abteilung, person, chefin, geraet, versionId: version.id };
}
async function laufAnlegen(daten: Parameters<typeof testPrisma.lauf.create>[0]["data"]): Promise<string> {
const lauf = await testPrisma.lauf.create({ data: daten });
return lauf.id;
}
describe("OU aus dem DN", () => {
it("nimmt alles nach dem ersten Komma", () => {
// Akzeptanzkriterium. Das ist der Ort, an dem das Konto liegt – genau den
// braucht ein New-ADUser -Path.
expect(ouAusDn(DN)).toBe("OU=Buchhaltung,OU=Benutzer,DC=firma,DC=local");
});
it("übergeht ein maskiertes Komma im Namen", () => {
// Im DN ist ein Komma innerhalb eines Namens mit Backslash maskiert. Ohne
// diese Regel bliebe von „Huber\, Maria“ nur „ Maria,OU=…“ übrig.
expect(ouAusDn("CN=Huber\\, Maria,OU=Buchhaltung,DC=firma,DC=local")).toBe(
"OU=Buchhaltung,DC=firma,DC=local",
);
});
it("liefert nichts bei einem DN ohne Komma", () => {
expect(ouAusDn("CN=Allein")).toBe("");
expect(ouAusDn("")).toBe("");
});
});
describe.runIf(datenbankVerfuegbar)("Werte eines Laufs", () => {
beforeEach(async () => {
await datenbankLeeren();
});
it("liefert Gerät, Lauf, Mitarbeiter und Abteilung", async () => {
const { eigene, abteilung, person, geraet, versionId } = await aufbau();
const laufId = await laufAnlegen({
vorlagenVersionId: versionId,
titel: "Onboarding Maria Huber",
kundeId: eigene.id,
mitarbeiterId: person.id,
geraetId: geraet.id,
abteilungId: abteilung.id,
ticket: "INC-4711",
});
const werte = await platzhalterWerteDesLaufs({
laufId,
technikerName: "Ali Demir",
jetzt: new Date("2026-09-25T10:00:00Z"),
});
expect(werte).toMatchObject({
hostname: "PC-001",
seriennummer: "SN-4711",
inventarnummer: "INV-99",
ticket: "INC-4711",
datum: "25.09.2026",
techniker: "Ali Demir",
mitarbeiter: "Maria Huber",
benutzer: "Maria Huber",
vorname: "Maria",
nachname: "Huber",
upn: "maria.huber@firma.de",
sam: "mhuber",
mail: "maria.huber@firma.de",
abteilung: "Buchhaltung",
titel: "Sachbearbeiterin",
dn: DN,
gruppen: "G-Buchhaltung, G-Alle",
vorgesetzter: "Sonja Brand",
vorgesetzter_sam: "sbrand",
});
});
it("löst den Vorgesetzten auch für eine von Hand angelegte Person auf", async () => {
// Der Fall aus #020: ohne Verzeichnisquelle gab es vorher kein Feld fuer die
// Zuordnung, und {{vorgesetzter_sam}} blieb im Onboarding-Befehl leer -
// genau dort, wo er gebraucht wird.
const { eigene, versionId } = await aufbau();
const chefin = await testPrisma.mitarbeiter.create({
data: {
kundeId: eigene.id,
vorname: "Petra",
nachname: "Klein",
anzeigename: "Petra Klein",
sam: "pklein",
herkunft: Herkunft.MANUELL,
},
});
const vonHand = await testPrisma.mitarbeiter.create({
data: {
kundeId: eigene.id,
vorname: "Neu",
nachname: "Kollege",
anzeigename: "Neu Kollege",
sam: "nkollege",
herkunft: Herkunft.MANUELL,
vorgesetzterId: chefin.id,
},
});
// Keine Quelle, keine externe Kennung - der alte Leseweg haette hier
// ausgestiegen.
expect(vonHand.quelleId).toBeNull();
expect(vonHand.vorgesetzterExterneId).toBeNull();
const laufId = await laufAnlegen({
vorlagenVersionId: versionId,
titel: "Onboarding Neu Kollege",
kundeId: eigene.id,
mitarbeiterId: vonHand.id,
});
const werte = await platzhalterWerteDesLaufs({
laufId,
technikerName: "Ali Demir",
jetzt: new Date("2026-10-04T10:00:00Z"),
});
expect(werte).toMatchObject({ vorgesetzter: "Petra Klein", vorgesetzter_sam: "pklein" });
});
it("nimmt für die OU den Eltern-Container des dn, nicht die Ziel-OU", async () => {
// Der dn sagt, wo das Konto **wirklich** liegt; die Ziel-OU der Abteilung
// ist die Absicht fuer ein Konto, das es noch nicht gibt.
const { eigene, abteilung, person, versionId } = await aufbau();
const laufId = await laufAnlegen({
vorlagenVersionId: versionId,
titel: "Umzug",
kundeId: eigene.id,
mitarbeiterId: person.id,
abteilungId: abteilung.id,
});
const werte = await platzhalterWerteDesLaufs({ laufId });
expect(werte["ou"]).toBe("OU=Buchhaltung,OU=Benutzer,DC=firma,DC=local");
});
it("nimmt die Ziel-OU der Abteilung, wenn die Person keinen dn hat", async () => {
// Der Onboarding-Fall: das Konto gibt es noch nicht.
const { eigene, abteilung, versionId } = await aufbau();
const laufId = await laufAnlegen({
vorlagenVersionId: versionId,
titel: "Onboarding",
kundeId: eigene.id,
mitarbeiterName: "Jörg Müller-Lüdenscheid",
abteilungId: abteilung.id,
});
const werte = await platzhalterWerteDesLaufs({ laufId });
expect(werte["ou"]).toBe("OU=Buchhaltung,OU=Benutzer,DC=firma,DC=de");
expect(werte["mitarbeiter"]).toBe("Jörg Müller-Lüdenscheid");
expect(werte["benutzer"]).toBe("Jörg Müller-Lüdenscheid");
});
it("lässt im Modus Intern Kunde und Kürzel leer", async () => {
// Akzeptanzkriterium: im Modus Intern gibt es die Kundenebene fuer den
// Benutzer nicht – ihren Namen in einen Befehl zu schreiben waere eine
// Auskunft ueber etwas, das es dort nicht gibt.
const { eigene, versionId } = await aufbau(Betriebsmodus.INTERN);
const laufId = await laufAnlegen({
vorlagenVersionId: versionId,
titel: "Patchday",
kundeId: eigene.id,
});
const werte = await platzhalterWerteDesLaufs({ laufId });
expect(werte["kunde"]).toBeUndefined();
expect(werte["kundenkuerzel"]).toBeUndefined();
// Und in der Ersetzung wird daraus leer, nicht „unbekannt“.
const ergebnis = platzhalterErsetzen("[{{kunde}}]", werte);
expect(ergebnis.text).toBe("[]");
expect(ergebnis.unbekannt).toEqual([]);
});
it("füllt im Modus Dienstleistung Kunde und Kürzel", async () => {
const { eigene, versionId } = await aufbau(Betriebsmodus.DIENSTLEISTUNG);
const laufId = await laufAnlegen({
vorlagenVersionId: versionId,
titel: "Patchday",
kundeId: eigene.id,
});
const werte = await platzhalterWerteDesLaufs({ laufId });
expect(werte["kunde"]).toBe("Muster GmbH");
expect(werte["kundenkuerzel"]).toBe("MUS");
});
it("nimmt die Bezeichnung, wenn ein Gerät keinen Hostname hat", async () => {
const { eigene, versionId } = await aufbau();
const schrank = await testPrisma.geraet.create({
data: {
kundeId: eigene.id,
tagCode: "LZ-8M4G2R",
bezeichnung: "Serverschrank Keller",
registriertAm: new Date(),
},
});
const laufId = await laufAnlegen({
vorlagenVersionId: versionId,
titel: "Wartung",
kundeId: eigene.id,
geraetId: schrank.id,
});
expect((await platzhalterWerteDesLaufs({ laufId }))["hostname"]).toBe("Serverschrank Keller");
});
it("lässt eine Vorlagenvariable den Gerätewert überschreiben", async () => {
/*
* Die Variable ist die ausdrueckliche Angabe **dieses** Laufs, der
* Geraetewert nur die Voreinstellung. Beim Umbenennen eines Rechners steht
* im Befehl der neue Name, nicht der alte aus der Datenbank.
*/
const { eigene, geraet, versionId } = await aufbau();
const laufId = await laufAnlegen({
vorlagenVersionId: versionId,
titel: "Umbenennen",
kundeId: eigene.id,
geraetId: geraet.id,
variablen: { hostname: "PC-NEU-007", os_version: "Windows 11 Pro" },
});
const werte = await platzhalterWerteDesLaufs({ laufId });
expect(werte["hostname"]).toBe("PC-NEU-007");
expect(werte["os_version"]).toBe("Windows 11 Pro");
});
it("lässt eine Variable die gesperrten Platzhalter nicht überschreiben", async () => {
/*
* Der Blocker aus dem Review: die Vorlagenvariablen gewannen gegen **alles**.
* Eine Variable `techniker` liess einen Befehl mit fremdem Namen
* unterschreiben, eine Variable `kunde` holte im Modus INTERN die
* Kundenebene zurueck, die es dort ausdruecklich nicht gibt.
*/
const { eigene, versionId } = await aufbau(Betriebsmodus.INTERN);
const laufId = await laufAnlegen({
vorlagenVersionId: versionId,
titel: "Angriff",
kundeId: eigene.id,
variablen: {
kunde: "Fremde AG",
kundenkuerzel: "FRD",
techniker: "Nicht ich",
datum: "01.01.1999",
generiertes_passwort: "geheim",
},
});
const werte = await platzhalterWerteDesLaufs({
laufId,
technikerName: "Ali Demir",
jetzt: new Date("2026-09-25T10:00:00Z"),
});
expect(werte["kunde"]).toBeUndefined();
expect(werte["kundenkuerzel"]).toBeUndefined();
expect(werte["techniker"]).toBe("Ali Demir");
expect(werte["datum"]).toBe("25.09.2026");
expect(werte["generiertes_passwort"]).toBeUndefined();
// Und das Passwort bleibt das erzeugte, nicht das aus der Variablen.
const ergebnis = platzhalterErsetzen("{{generiertes_passwort}}", werte);
expect(ergebnis.text).not.toBe("geheim");
expect(ergebnis.text).toHaveLength(20);
});
it("nimmt Zahlen und Haken aus den Variablen als Text auf", async () => {
const { eigene, versionId } = await aufbau();
const laufId = await laufAnlegen({
vorlagenVersionId: versionId,
titel: "Prüfung",
kundeId: eigene.id,
variablen: { arbeitsspeicher: 32, verschluesselt: true, kaputt: { tief: 1 } },
});
const werte = await platzhalterWerteDesLaufs({ laufId });
expect(werte["arbeitsspeicher"]).toBe("32");
expect(werte["verschluesselt"]).toBe("true");
// Was kein einfacher Wert ist, bleibt draussen – im Befehl stuende sonst
// „[object Object]“.
expect(werte["kaputt"]).toBeUndefined();
});
it("kommt mit einem Lauf ohne Gerät und ohne Person aus", async () => {
// Ein Lauf ohne beides ist gueltig: Server, Firewall, Patchday. Abzubrechen
// hiesse, dem Techniker die Arbeit zu verweigern.
const { eigene, versionId } = await aufbau();
const laufId = await laufAnlegen({
vorlagenVersionId: versionId,
titel: "Patchday",
kundeId: eigene.id,
});
const werte = await platzhalterWerteDesLaufs({ laufId });
expect(werte["hostname"]).toBeUndefined();
expect(werte["mitarbeiter"]).toBeUndefined();
expect(werte["datum"]).toBeTruthy();
});
it("liefert nichts für einen Lauf, den es nicht gibt", async () => {
expect(await platzhalterWerteDesLaufs({ laufId: "gibtsnicht" })).toEqual({});
});
it("baut einen vollständigen New-ADUser-Befehl", async () => {
/*
* Der Ablauf aus dem Auftrag, Ende zu Ende: Abteilung „Buchhaltung“ gewählt,
* daraus `-Path "OU=Buchhaltung,…"` und die Gruppen `G-Buchhaltung, G-Alle`.
*/
const { eigene, abteilung, versionId } = await aufbau();
const laufId = await laufAnlegen({
vorlagenVersionId: versionId,
titel: "Onboarding",
kundeId: eigene.id,
mitarbeiterName: "Jörg Müller-Lüdenscheid",
abteilungId: abteilung.id,
variablen: { upn: "joerg.mueller-luedenscheid@firma.de", sam: "jmuellerluedensch" },
});
const werte = await platzhalterWerteDesLaufs({ laufId });
const befehl = platzhalterErsetzen(
'New-ADUser -Name "{{mitarbeiter}}" -UserPrincipalName "{{upn}}" ' +
'-SamAccountName "{{sam}}" -Path "{{ou}}"',
werte,
{ art: "befehl", maskieren: (wert) => wert },
);
expect(befehl.text).toBe(
'New-ADUser -Name "Jörg Müller-Lüdenscheid" ' +
'-UserPrincipalName "joerg.mueller-luedenscheid@firma.de" ' +
'-SamAccountName "jmuellerluedensch" ' +
'-Path "OU=Buchhaltung,OU=Benutzer,DC=firma,DC=de"',
);
expect(befehl.unbekannt).toEqual([]);
const gruppen = platzhalterErsetzen("Add-ADGroupMember {{gruppen}}", werte, {
art: "befehl",
maskieren: (wert) => wert,
});
expect(gruppen.text).toBe("Add-ADGroupMember G-Buchhaltung, G-Alle");
});
});