feat(platzhalter): Aufloesung, Namensregeln und Shell-Maskierung
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

Aus einer Vorlage wird hier der PowerShell-Befehl, den ein Techniker kopiert und
auf einem Domaenencontroller ausfuehrt. Entsprechend dicht ist die Abdeckung:
810 Tests, und die Maskierung wird **ausgefuehrt** geprueft, nicht gelesen.

- Platzhalter aus Geraet, Lauf, Mitarbeiter und Abteilung, dazu die Variablen
  der Vorlage; ein unbekannter bleibt sichtbar stehen
- `{{ou}}` ist der Eltern-Container des DN — genau der Pfad fuer New-ADUser
- Namensregeln fuer upn, sam, mail und hostname mit Transliteration, `{v}`,
  `{nnn}` und `{kundenkuerzel}`; Kundenregeln ueberlagern die globalen feldweise
- Maskierung je Shell, Skript-Kopie je Abschnitt mit Kommentarzeilen
- `{{generiertes_passwort}}`: 20 Zeichen, bei jeder Anzeige neu, nirgends
  gespeichert; im Druck `[Passwort – nicht gespeichert]`

Der unabhaengige Review fand drei Blocker. Der schwerste war eine
Codeausfuehrung: PowerShell liest auch `“`, `”` und `„` als Begrenzer einer
Zeichenkette, die Maskierung kannte nur das ASCII-Zeichen. Ein Abteilungsname
aus Word — Word wandelt `"` von selbst um — reichte. Gefunden hat es der Review
nicht durch Lesen, sondern indem er sich PowerShell in einen Container geholt
und die Markierungsdatei hat entstehen lassen. Der Test dagegen hatte sich still
uebersprungen; er nimmt jetzt selbst das Abbild und bricht in CI ab.

Dazu: zwei verschiedene Passwoerter in einer Antwort (Anzeige und Skript wurden
getrennt gefuellt), eine Vorlagenvariable, die `{{techniker}}` und `{{kunde}}`
ueberschreiben konnte, und die tragende Annahme der Maskierung — dass der
Platzhalter in Anfuehrungszeichen steht —, die niemand geprueft hat.

Schliesst #012

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-25 15:29:43 +02:00
co-authored by Claude Opus 5
parent f85bc87324
commit b59fbb4645
21 changed files with 4069 additions and 166 deletions
+553
View File
@@ -0,0 +1,553 @@
import { describe, expect, it, vi } from "vitest";
import {
HOSTNAME_HOECHSTLAENGE,
NamensregelFehler,
NamensregelnSchema,
SAM_HOECHSTLAENGE,
UNTERSCHEIDUNG_HOECHSTWERT,
UNTERSCHEIDUNG_STELLEN,
ZIELFORMATE,
hatZaehler,
kleinGefaltet,
namenBilden,
namenEindeutigBilden,
transliterieren,
} from "@/lib/namensregeln";
/**
* Namensregeln, Transliteration und Zaehler (#012).
*
* Aus diesen Werten werden Befehle auf einem Domaenencontroller. Die Tests sind
* deshalb nicht nur auf den guten Fall gerichtet, sondern vor allem darauf, dass
* bei fehlenden Angaben **nichts** herauskommt und dass die Grenzen des Ziel-
* systems eingehalten werden, auch wenn gekuerzt werden muss.
*/
/** Der Fall aus dem Akzeptanzkriterium — er kommt in mehreren Tests wieder. */
const JOERG = { vorname: "Jörg", nachname: "Müller-Lüdenscheid" } as const;
const MIT_KUNDE = { ...JOERG, kundenkuerzel: "KND" } as const;
describe("transliterieren", () => {
it("faltet „Jörg Müller-Lüdenscheid“ und laesst den Bindestrich stehen", () => {
expect(transliterieren("Jörg Müller-Lüdenscheid")).toBe("Joerg Mueller-Luedenscheid");
expect(kleinGefaltet("Jörg Müller-Lüdenscheid")).toBe("joerg mueller-luedenscheid");
});
it.each([
["ä", "ae"],
["ö", "oe"],
["ü", "ue"],
["Ä", "Ae"],
["Ö", "Oe"],
["Ü", "Ue"],
["ß", "ss"],
["Straße", "Strasse"],
])("macht aus %s zwei Zeichen: %s", (ein, aus) => {
expect(transliterieren(ein)).toBe(aus);
});
it.each([
["é", "e"],
["è", "e"],
["ç", "c"],
["ñ", "n"],
["å", "a"],
["ě", "e"],
["ą", "a"],
["Ş", "S"],
["René Ćosić", "Rene Cosic"],
])("entfernt den Akzent von %s", (ein, aus) => {
expect(transliterieren(ein)).toBe(aus);
});
it.each([
["ł", "l"],
["Łukasz", "Lukasz"],
["ø", "o"],
["Øyvind", "Oyvind"],
["æ", "ae"],
["œ", "oe"],
["đ", "d"],
["þ", "th"],
["ı", "i"],
])("faltet %s, das Unicode nicht zerlegt, zu %s", (ein, aus) => {
// Ohne eigene Tabelle fiele dieser Buchstabe als Nicht-ASCII ganz weg, und
// aus „Kowalski“ wuerde „Kowaski“ — ein Anmeldename, der nie funktioniert.
expect(transliterieren(ein)).toBe(aus);
});
it("wirft alles uebrige Nicht-ASCII weg", () => {
expect(transliterieren("Александр")).toBe("");
expect(transliterieren("李雷")).toBe("");
expect(transliterieren("Max 🎉 Mustermann")).toBe("Max Mustermann");
});
it("wirft Steuerzeichen weg, statt sie zu Leerzeichen zu machen", () => {
// Ein Zeilenumbruch in einem Namen hat in einem Befehl nichts zu suchen.
expect(transliterieren("Max\nMustermann")).toBe("MaxMustermann");
expect(transliterieren("Max\tMustermann")).toBe("MaxMustermann");
});
it("laesst reines ASCII unveraendert", () => {
expect(transliterieren("Max Mustermann-Meier 1970")).toBe("Max Mustermann-Meier 1970");
});
it("kommt mit einer leeren Eingabe zurecht", () => {
expect(transliterieren("")).toBe("");
expect(kleinGefaltet("")).toBe("");
});
it("faltet auch die zerlegte Schreibweise eines Umlauts zu zwei Zeichen", () => {
// „ü“ kann als ein Zeichen oder als u + Trema ankommen — aus einer CSV-Datei
// eines Mac kommt regelmaessig die zweite Form. Beide muessen `mueller`
// ergeben, nicht einmal `mueller` und einmal `muller`.
// `normalize("NFD")` macht aus dem einen Zeichen die zerlegte Form. Sie hier
// als Fluchtfolge hinzuschreiben geht nicht: Prettier schreibt sie in das
// unsichtbare Zeichen zurueck, und dann steht im Test etwas, was niemand sieht.
expect(kleinGefaltet("Müller".normalize("NFD"))).toBe("mueller");
expect(kleinGefaltet("Müller")).toBe("mueller");
});
});
describe("namenBilden – die Akzeptanzkriterien aus #012", () => {
it("bildet joerg.mueller-luedenscheid@firma.de", () => {
expect(namenBilden("upn", "{vorname}.{nachname}@firma.de", JOERG)).toBe(
"joerg.mueller-luedenscheid@firma.de",
);
expect(namenBilden("mail", "{vorname}.{nachname}@firma.de", JOERG)).toBe(
"joerg.mueller-luedenscheid@firma.de",
);
});
it("bildet den sam jmuellerluedensch", () => {
const sam = namenBilden("sam", "{v}{nachname}", JOERG);
expect(sam).toBe("jmuellerluedensch");
// 17 Zeichen: 20 erlaubt das Active Directory, drei bleiben fuer die
// Unterscheidung frei, damit ein zweiter Traeger desselben Namens den ersten
// nicht umbenennt.
expect(sam).toHaveLength(SAM_HOECHSTLAENGE - UNTERSCHEIDUNG_STELLEN);
});
});
describe("namenBilden – Platzhalter", () => {
it("setzt {v} auf den ersten Buchstaben des Vornamens", () => {
expect(namenBilden("sam", "{v}", JOERG)).toBe("j");
expect(namenBilden("sam", "{v}.{nachname}", { vorname: "Ed", nachname: "Ng" })).toBe("e.ng");
});
it("nimmt fuer {v} den ersten Buchstaben, nicht das erste Zeichen", () => {
expect(namenBilden("sam", "{v}{nachname}", { vorname: "'Abd al-Rahman", nachname: "Farouk" })).toBe(
"afarouk",
);
});
it("entfernt Leerzeichen aus zusammengesetzten Vornamen", () => {
expect(namenBilden("sam", "{vorname}.{nachname}", { vorname: "Anna Lena", nachname: "Meier" })).toBe(
"annalena.meier",
);
});
it("setzt {kundenkuerzel} ein", () => {
expect(namenBilden("hostname", "{kundenkuerzel}-PC-{nnn}", MIT_KUNDE, 7)).toBe("KND-PC-007");
});
it("richtet die Stellen des Zaehlers nach der Zahl der n", () => {
expect(namenBilden("hostname", "PC{n}", MIT_KUNDE, 7)).toBe("PC7");
expect(namenBilden("hostname", "PC{nn}", MIT_KUNDE, 7)).toBe("PC07");
expect(namenBilden("hostname", "PC{nnnn}", MIT_KUNDE, 7)).toBe("PC0007");
expect(namenBilden("hostname", "PC{nnnn}", MIT_KUNDE, 1234)).toBe("PC1234");
});
it("liefert nichts, wenn die Zahl nicht in ihre Stellen passt", () => {
// `{nn}` kann keine 100 tragen; ein Hostname mit einer Stelle mehr waere
// ausserhalb der Regel, die der Admin aufgeschrieben hat.
expect(namenBilden("hostname", "PC{nn}", MIT_KUNDE, 100)).toBeUndefined();
expect(namenBilden("hostname", "PC{nn}", MIT_KUNDE, -1)).toBeUndefined();
expect(namenBilden("hostname", "PC{nn}", MIT_KUNDE, 1.5)).toBeUndefined();
});
it("liefert nichts, wenn die Regel einen Zaehler braucht, aber keiner mitkommt", () => {
// Die naechste freie Nummer kennt nur der Aufrufer. `001` zu raten hiesse
// einen Hostnamen zu liefern, der schon an einem anderen Geraet haengt.
expect(namenBilden("hostname", "{kundenkuerzel}-PC-{nnn}", MIT_KUNDE)).toBeUndefined();
});
it("liefert nichts bei zwei Zaehlern in einer Regel", () => {
expect(namenBilden("hostname", "PC{nn}-{nn}", MIT_KUNDE, 7)).toBeUndefined();
});
});
describe("namenBilden – wenn etwas fehlt, kommt nichts heraus", () => {
it.each([
["keine Regel", undefined],
["null", null],
["leere Regel", ""],
["nur Leerzeichen", " "],
])("liefert bei %s nichts", (_fall, regel) => {
expect(namenBilden("upn", regel, JOERG)).toBeUndefined();
});
it.each([
["fehlender Nachname", { vorname: "Jörg" }],
["leerer Nachname", { vorname: "Jörg", nachname: "" }],
["Nachname nur aus Leerzeichen", { vorname: "Jörg", nachname: " " }],
["fehlender Vorname", { nachname: "Müller" }],
["null als Wert", { vorname: null, nachname: "Müller" }],
])("liefert bei %s nichts", (_fall, werte) => {
expect(namenBilden("upn", "{vorname}.{nachname}@firma.de", werte)).toBeUndefined();
});
it("liefert nichts, wenn das Kundenkuerzel fehlt", () => {
// Im Modus INTERN gibt es kein Kuerzel. Ein Hostname `-PC-001` waere
// schlimmer als keiner: er sieht richtig aus.
expect(namenBilden("hostname", "{kundenkuerzel}-PC-{nnn}", JOERG, 1)).toBeUndefined();
});
it("liefert nichts bei einem Platzhalter, den es nicht gibt", () => {
expect(namenBilden("upn", "{vorname}.{abteilung}@firma.de", JOERG)).toBeUndefined();
expect(namenBilden("sam", "{titel}", JOERG)).toBeUndefined();
});
it("liefert nichts bei einem verunglueckten Platzhalter", () => {
// Sonst faellt die Klammer beim Normalisieren weg und der Text bleibt: aus
// `{Nachname}@firma.de` wuerde die Adresse `nachname@firma.de`, und die
// sieht echt aus.
expect(namenBilden("upn", "{Nachname}@firma.de", JOERG)).toBeUndefined();
expect(namenBilden("upn", "{NACHNAME}@firma.de", JOERG)).toBeUndefined();
expect(namenBilden("upn", "{vorname@firma.de", JOERG)).toBeUndefined();
expect(namenBilden("sam", "{vorname}}", JOERG)).toBeUndefined();
});
it("liefert nichts, wenn vom Namen nach der Faltung nichts bleibt", () => {
expect(namenBilden("sam", "{v}{nachname}", { vorname: "###", nachname: "###" })).toBeUndefined();
expect(namenBilden("upn", "{nachname}@firma.de", { nachname: "###" })).toBeUndefined();
expect(namenBilden("sam", "{nachname}", { nachname: "李雷" })).toBeUndefined();
expect(namenBilden("hostname", "{nachname}", { nachname: "___" })).toBeUndefined();
});
it("liefert bei jedem Zielformat nichts, wenn die Regel fehlt", () => {
for (const zielformat of ZIELFORMATE) {
expect(namenBilden(zielformat, undefined, JOERG)).toBeUndefined();
}
});
});
describe("namenBilden – Adressform fuer upn und mail", () => {
it("schreibt klein", () => {
expect(namenBilden("upn", "{vorname}@FIRMA.DE", JOERG)).toBe("joerg@firma.de");
expect(namenBilden("mail", "{NACHNAME}@firma.de", JOERG)).toBeUndefined();
});
it("laesst keine doppelten Punkte stehen", () => {
expect(namenBilden("upn", "{vorname}..{nachname}@firma.de", JOERG)).toBe(
"joerg.mueller-luedenscheid@firma.de",
);
expect(namenBilden("upn", "{vorname}@firma..de", JOERG)).toBe("joerg@firma.de");
});
it("laesst keinen Punkt vor dem @ stehen", () => {
expect(namenBilden("upn", "{vorname}.@firma.de", JOERG)).toBe("joerg@firma.de");
expect(namenBilden("upn", ".{vorname}@firma.de", JOERG)).toBe("joerg@firma.de");
});
it("verlangt genau ein @ und beide Haelften", () => {
expect(namenBilden("upn", "{vorname}.{nachname}", JOERG)).toBeUndefined();
expect(namenBilden("upn", "{vorname}@{nachname}@firma.de", JOERG)).toBeUndefined();
expect(namenBilden("upn", "{vorname}@", JOERG)).toBeUndefined();
expect(namenBilden("upn", "@firma.de", JOERG)).toBeUndefined();
});
it("weist einen Namensbereich mit unbrauchbarer Stufe ab", () => {
expect(namenBilden("upn", "{vorname}@firma.-de", JOERG)).toBeUndefined();
expect(namenBilden("upn", "{vorname}@firma.de-.x", JOERG)).toBeUndefined();
});
it("raeumt Bindestriche am Rand des Namensbereichs weg", () => {
expect(namenBilden("upn", "{vorname}@-firma.de", JOERG)).toBe("joerg@firma.de");
expect(namenBilden("upn", "{vorname}@firma.de-", JOERG)).toBe("joerg@firma.de");
});
it("wirft Zeichen weg, die in einer Adresse nicht vorkommen duerfen", () => {
// „Müller, Jörg“ aus einer CSV-Spalte, in der Vor- und Nachname zusammen
// stehen: das Komma gehoert in keine Adresse.
expect(namenBilden("mail", "{nachname}@firma.de", { nachname: "Mül,ler" })).toBe("mueller@firma.de");
});
it("kuerzt eine Adresse nicht", () => {
// Eine gekuerzte Mail-Adresse ist keine Adresse, sie ist eine falsche.
const lang = namenBilden("mail", "{vorname}.{nachname}@firma.de", {
vorname: "Jörg",
nachname: "Lüdenscheid".repeat(10),
});
expect(lang?.length).toBeGreaterThan(100);
expect(lang?.endsWith("@firma.de")).toBe(true);
});
});
describe("namenBilden – sam", () => {
it("laesst den Punkt stehen und den Bindestrich weg", () => {
expect(namenBilden("sam", "{vorname}.{nachname}", JOERG)).toBe("joerg.muellerlued");
});
it("kuerzt am Ende auf die nutzbare Laenge", () => {
const sam = namenBilden("sam", "{v}{nachname}", { vorname: "Jörg", nachname: "Lüdenscheid".repeat(20) });
expect(sam).toHaveLength(SAM_HOECHSTLAENGE - UNTERSCHEIDUNG_STELLEN);
expect(sam?.startsWith("jluedenscheid")).toBe(true);
});
it("laesst keinen Punkt am Ende stehen, auch nicht nach dem Kuerzen", () => {
// Ein `sAMAccountName` mit Punkt am Ende ist unzulaessig; beim Kuerzen kann
// er genau dort landen.
const sam = namenBilden("sam", "{nachname}.{vorname}", {
vorname: "Jörg",
nachname: "Mueller-Luedensch",
});
expect(sam).toBe("muellerluedensch");
expect(sam?.endsWith(".")).toBe(false);
});
it("nimmt einen Namen aus einem Zeichen", () => {
expect(namenBilden("sam", "{v}{nachname}", { vorname: "Ed", nachname: "Ng" })).toBe("eng");
expect(namenBilden("sam", "{vorname}", { vorname: "Ü" })).toBe("ue");
});
});
describe("namenBilden – hostname", () => {
it("schreibt gross", () => {
// Windows fuehrt den NetBIOS-Namen gross; stuende auf dem Aufkleber etwas
// anderes als das Geraet meldet, sucht der naechste Techniker einen Fehler,
// den es nicht gibt.
expect(namenBilden("hostname", "{kundenkuerzel}-nb-{nnn}", MIT_KUNDE, 3)).toBe("KND-NB-003");
expect(namenBilden("hostname", "{vorname}", JOERG)).toBe("JOERG");
});
it("laesst Unterstrich und Punkt weg", () => {
expect(namenBilden("hostname", "{kundenkuerzel}_PC_{nn}", MIT_KUNDE, 7)).toBe("KNDPC07");
expect(namenBilden("hostname", "{kundenkuerzel}.pc.{nn}", MIT_KUNDE, 7)).toBe("KNDPC07");
});
it("laesst keinen fuehrenden oder abschliessenden Bindestrich stehen", () => {
expect(namenBilden("hostname", "-{kundenkuerzel}-", MIT_KUNDE)).toBe("KND");
expect(namenBilden("hostname", "{kundenkuerzel}--", MIT_KUNDE)).toBe("KND");
});
it("haelt die 15 Zeichen des NetBIOS-Namens ein und kuerzt nie in der Nummer", () => {
// Das ist die gefaehrliche Stelle: wuerde am Ende gekuerzt, verschwaende die
// Nummer, und zwei Geraete bekaemen denselben Namen.
const hostname = namenBilden("hostname", "{nachname}-{nnn}", JOERG, 1);
expect(hostname).toBe("MUELLER-LUED001");
expect(hostname).toHaveLength(HOSTNAME_HOECHSTLAENGE);
expect(hostname?.endsWith("001")).toBe(true);
});
it("behaelt die Nummer auch bei einem sehr langen Namen", () => {
const hostname = namenBilden("hostname", "{nachname}-{nnnn}", { nachname: "Lüdenscheid".repeat(30) }, 42);
expect(hostname).toHaveLength(HOSTNAME_HOECHSTLAENGE);
expect(hostname?.endsWith("0042")).toBe(true);
});
it("behaelt die Nummer auch, wenn sie nicht am Ende der Regel steht", () => {
const hostname = namenBilden("hostname", "{nnn}-{nachname}", { nachname: "Lüdenscheid".repeat(30) }, 42);
expect(hostname).toHaveLength(HOSTNAME_HOECHSTLAENGE);
expect(hostname?.startsWith("042-")).toBe(true);
});
it("haelt ohne Zaehler in der Regel den Vorrat fuer die Unterscheidung frei", () => {
const hostname = namenBilden("hostname", "{nachname}", { nachname: "Lüdenscheid".repeat(5) });
expect(hostname).toHaveLength(HOSTNAME_HOECHSTLAENGE - UNTERSCHEIDUNG_STELLEN);
});
it("liefert nichts, wenn die Nummer allein schon zu lang ist", () => {
expect(namenBilden("hostname", "{nachname}-{nnnnnnnnnnnnnnnn}", JOERG, 1)).toBeUndefined();
});
});
describe("hatZaehler", () => {
it.each([
["{nnn}", true],
["{kundenkuerzel}-PC-{nnn}", true],
["PC{n}", true],
["{vorname}.{nachname}@firma.de", false],
["{nachname}", false],
["", false],
])("erkennt %s", (regel, erwartet) => {
expect(hatZaehler(regel)).toBe(erwartet);
});
});
describe("namenEindeutigBilden – mit Zaehler in der Regel", () => {
it("nimmt die erste Nummer, wenn sie frei ist", async () => {
const istVergeben = vi.fn(async () => false);
await expect(
namenEindeutigBilden("hostname", "{kundenkuerzel}-PC-{nnn}", MIT_KUNDE, istVergeben),
).resolves.toBe("KND-PC-001");
expect(istVergeben).toHaveBeenCalledTimes(1);
expect(istVergeben).toHaveBeenCalledWith("KND-PC-001");
});
it("zaehlt weiter, solange Nummern vergeben sind", async () => {
const vergeben = new Set(["KND-PC-001", "KND-PC-002"]);
const istVergeben = vi.fn(async (kandidat: string) => vergeben.has(kandidat));
await expect(
namenEindeutigBilden("hostname", "{kundenkuerzel}-PC-{nnn}", MIT_KUNDE, istVergeben),
).resolves.toBe("KND-PC-003");
expect(istVergeben).toHaveBeenCalledTimes(3);
});
it("wirft, wenn der Vorrat der Regel erschoepft ist", async () => {
const istVergeben = vi.fn(async () => true);
// `{n}` gibt neun Nummern her. Danach ist die Regel zu eng — das ist eine
// Einstellung, die geaendert werden muss, und keine Nummer, die man
// erfinden darf.
await expect(namenEindeutigBilden("hostname", "PC{n}", MIT_KUNDE, istVergeben)).rejects.toThrow(
NamensregelFehler,
);
expect(istVergeben).toHaveBeenCalledTimes(9);
});
it("fragt nicht nach, wenn die Regel keinen Namen ergibt", async () => {
const istVergeben = vi.fn(async () => false);
await expect(
namenEindeutigBilden("hostname", "{kundenkuerzel}-PC-{nnn}", JOERG, istVergeben),
).resolves.toBeUndefined();
expect(istVergeben).not.toHaveBeenCalled();
});
});
describe("namenEindeutigBilden – ohne Zaehler in der Regel", () => {
it("liefert den Namen unveraendert, wenn er frei ist", async () => {
const istVergeben = vi.fn(async () => false);
await expect(namenEindeutigBilden("sam", "{v}{nachname}", JOERG, istVergeben)).resolves.toBe(
"jmuellerluedensch",
);
expect(istVergeben).toHaveBeenCalledTimes(1);
});
it("haengt bei einer Kollision eine Unterscheidung an und bleibt in der Grenze", async () => {
const vergeben = new Set(["jmuellerluedensch"]);
const istVergeben = vi.fn(async (kandidat: string) => vergeben.has(kandidat));
const sam = await namenEindeutigBilden("sam", "{v}{nachname}", JOERG, istVergeben);
expect(sam).toBe("jmuellerluedensch2");
expect(sam?.length).toBeLessThanOrEqual(SAM_HOECHSTLAENGE);
});
it("bleibt auch bei dreistelliger Unterscheidung in den 20 Zeichen", async () => {
// Die drei zurueckgehaltenen Stellen sind genau dafuer da.
const istVergeben = vi.fn(async (kandidat: string) => kandidat !== "jmuellerluedensch999");
const sam = await namenEindeutigBilden("sam", "{v}{nachname}", JOERG, istVergeben);
expect(sam).toBe("jmuellerluedensch999");
expect(sam).toHaveLength(SAM_HOECHSTLAENGE);
});
it("setzt die Unterscheidung bei Adressen vor das @", async () => {
const vergeben = new Set(["joerg.mueller-luedenscheid@firma.de"]);
const istVergeben = vi.fn(async (kandidat: string) => vergeben.has(kandidat));
await expect(
namenEindeutigBilden("upn", "{vorname}.{nachname}@firma.de", JOERG, istVergeben),
).resolves.toBe("joerg.mueller-luedenscheid2@firma.de");
});
it("bleibt beim Hostnamen in den 15 Zeichen", async () => {
const istVergeben = vi.fn(async (kandidat: string) => kandidat.length < HOSTNAME_HOECHSTLAENGE);
const hostname = await namenEindeutigBilden(
"hostname",
"{nachname}",
{ nachname: "Lüdenscheid".repeat(5) },
istVergeben,
);
expect(hostname).toHaveLength(HOSTNAME_HOECHSTLAENGE);
});
it("wirft, wenn auch alle Unterscheidungen vergeben sind", async () => {
const istVergeben = vi.fn(async () => true);
await expect(namenEindeutigBilden("sam", "{v}{nachname}", JOERG, istVergeben)).rejects.toThrow(
NamensregelFehler,
);
// Der Name selbst und die Zusaetze 2 bis 999.
expect(istVergeben).toHaveBeenCalledTimes(UNTERSCHEIDUNG_HOECHSTWERT);
});
it("fragt ohne Regel nicht nach", async () => {
const istVergeben = vi.fn(async () => false);
await expect(namenEindeutigBilden("sam", undefined, JOERG, istVergeben)).resolves.toBeUndefined();
await expect(namenEindeutigBilden("sam", " ", JOERG, istVergeben)).resolves.toBeUndefined();
expect(istVergeben).not.toHaveBeenCalled();
});
it("gibt einen Fehler der Vergabepruefung unveraendert weiter", async () => {
const istVergeben = vi.fn(async () => {
throw new Error("Datenbank nicht erreichbar");
});
await expect(namenEindeutigBilden("sam", "{v}{nachname}", JOERG, istVergeben)).rejects.toThrow(
"Datenbank nicht erreichbar",
);
});
});
describe("NamensregelnSchema", () => {
it("nimmt die vier Zielformate und trimmt", () => {
const gelesen = NamensregelnSchema.parse({
upn: " {vorname}.{nachname}@firma.de ",
sam: "{v}{nachname}",
mail: "{vorname}.{nachname}@firma.de",
hostname: "{kundenkuerzel}-PC-{nnn}",
});
expect(gelesen.upn).toBe("{vorname}.{nachname}@firma.de");
expect(gelesen.sam).toBe("{v}{nachname}");
});
it("nimmt ein leeres Objekt", () => {
expect(NamensregelnSchema.parse({})).toEqual({});
});
it("weist eine zu lange Regel ab", () => {
expect(NamensregelnSchema.safeParse({ upn: "x".repeat(201) }).success).toBe(false);
});
});
describe("Hostnamen, die Windows ablehnt", () => {
it("schlägt keinen reservierten Gerätenamen vor", () => {
/*
* `CON`, `PRN`, `NUL` und Geschwister sind die alten Geraetenamen von
* MS-DOS. Ein Nachname „Con“ reicht, damit die Regel sie vorschlaegt — und
* Windows nimmt den Computernamen dann nicht an. Gefunden im Review zu #012.
*/
for (const name of ["con", "prn", "aux", "nul", "com1", "lpt9"]) {
expect(namenBilden("hostname", "{nachname}", { nachname: name }), name).toBeUndefined();
}
// Mit etwas davor oder dahinter ist der Name in Ordnung.
expect(namenBilden("hostname", "PC-{nachname}", { nachname: "con" })).toBe("PC-CON");
});
it("schlägt keinen rein numerischen Hostnamen vor", () => {
// Er waere von einer IP-Adresse nicht zu unterscheiden; Windows lehnt ihn ab.
expect(namenBilden("hostname", "{nachname}", { nachname: "12345" })).toBeUndefined();
expect(namenBilden("hostname", "{nnn}", {}, 1)).toBeUndefined();
expect(namenBilden("hostname", "PC-{nnn}", {}, 1)).toBe("PC-001");
});
});