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, vorgesetzterExterneId: "guid-chefin", 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[0]["data"]): Promise { 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("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"); }); });