386 lines
14 KiB
TypeScript
386 lines
14 KiB
TypeScript
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<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("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");
|
||
});
|
||
});
|