Files
Laufzettel/tests/datenmodell.test.ts
T
Florian Wilde 7eb81bd191
Prüfung / pruefung (push) Canceled after 0s
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s
feat(seed): mitgelieferte Vorlagen am Schluessel erkennen
Der Seed erkannte seine sieben Vorlagen am Namen. Wer eine umbenannte, bekam
beim naechsten Lauf eine zweite -- und der Schnellstart stand auf der
Geraeteseite doppelt.

Vorlage.seedSchluessel traegt die Kennung, eindeutig wo gesetzt (partieller
Index, weil eigene Vorlagen keinen tragen). Das Austauschformat kennt das Feld
als seedSchluessel; nicht als schluessel, wie das Issue plante, denn so heisst
dort schon das Feld einer Variablen.

Der Rueckfall auf den Namen bleibt, sonst bekaeme eine Installation von vor
diesem Feld beim ersten Lauf danach genau die Dubletten, die es verhindern
soll. Wird sie so gefunden, traegt der Seed den Schluessel nach -- an der
Entwicklungsdatenbank gefahren: sieben nachgetragen, nichts neu angelegt.

Der Import setzt den Schluessel nicht, auch nicht wenn das Dokument ihn
mitbringt, und der Export schreibt ihn nicht: eine eingespielte Kopie soll
nicht den Platz des Originals einnehmen.

Gegenprobe gefahren: ohne die Suche am Schluessel werden vier Tests rot.

Closes #32
2026-10-04 16:48:30 +02:00

702 lines
26 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 { beforeEach, describe, expect, it } from "vitest";
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
/**
* Haelt die Regeln fest, die in der Datenbank selbst stehen (#006).
*
* Sie stehen dort und nicht nur in der Service-Schicht, damit sie auch dann
* greifen, wenn jemand an ihr vorbei schreibt – per Migration, per psql oder aus
* einem spaeteren Connector. Die Service-Schicht prueft dieselben Regeln noch
* einmal, aber mit verstaendlicheren Meldungen.
*/
async function kundeAnlegen(kuerzel = "TST", eigene = false) {
return testPrisma.kunde.create({
data: { name: `Kunde ${kuerzel}`, kuerzel, istEigeneOrganisation: eigene },
});
}
async function benutzerAnlegen(email = "techniker@firma.example") {
return testPrisma.benutzer.create({
data: { email, name: "Techniker", rolle: "TECHNIKER" },
});
}
/**
* Kleinste Vorlage, die ausreicht, um einen Lauf daran zu haengen.
*
* Erst als Entwurf anlegen, dann veroeffentlichen – an einer veroeffentlichten
* Version laesst sich nichts mehr anhaengen, dafuer sorgen Trigger in der
* Datenbank. Wer die Fixture umdreht, baut genau den Zustand, den die Anwendung
* nie herstellen darf.
*/
async function veroeffentlichteVersionAnlegen() {
const vorlage = await testPrisma.vorlage.create({ data: { name: "Probe" } });
return testPrisma.vorlagenVersion.create({
data: { vorlageId: vorlage.id, version: 1, status: "ENTWURF" },
});
}
/** Schliesst eine Fixture-Version ab, nachdem ihr Aufbau steht. */
async function versionVeroeffentlichen(versionId: string) {
return testPrisma.vorlagenVersion.update({
where: { id: versionId },
// veroeffentlichtAm ist Pflicht, sobald der Status VEROEFFENTLICHT ist –
// eine Bedingung in der Datenbank haelt das fest.
data: { status: "VEROEFFENTLICHT", veroeffentlichtAm: new Date() },
});
}
describe.runIf(datenbankVerfuegbar)("Datenmodell: Regeln in der Datenbank", () => {
beforeEach(async () => {
await datenbankLeeren();
});
describe("Kunde", () => {
it("laesst nur eine eigene Organisation zu", async () => {
await kundeAnlegen("EIGEN", true);
// Gemeint ist der partielle Index, nicht irgendeine Ablehnung.
await expect(kundeAnlegen("ZWEIT", true)).rejects.toThrow(/Kunde_eigene_organisation_einmalig/);
});
it("laesst beliebig viele gewoehnliche Kunden zu", async () => {
await kundeAnlegen("EIGEN", true);
await kundeAnlegen("MUS");
await kundeAnlegen("REI");
expect(await testPrisma.kunde.count()).toBe(3);
});
it("weist ein Kuerzel ausserhalb von 2 bis 6 Zeichen zurueck", async () => {
await expect(kundeAnlegen("X")).rejects.toThrow(/Kunde_kuerzel_form/);
await expect(kundeAnlegen("ZULANG7")).rejects.toThrow(/Kunde_kuerzel_form/);
});
it("weist ein Kuerzel mit Kleinbuchstaben oder Sonderzeichen zurueck", async () => {
await expect(kundeAnlegen("mus")).rejects.toThrow();
await expect(kundeAnlegen("MU-S")).rejects.toThrow();
});
it("haelt das Kuerzel eindeutig", async () => {
await kundeAnlegen("MUS");
await expect(kundeAnlegen("MUS")).rejects.toThrow();
});
});
describe("Einstellung", () => {
it("laesst nur die eine Zeile mit der Kennung global zu", async () => {
await testPrisma.einstellung.create({ data: { id: "global" } });
await expect(testPrisma.einstellung.create({ data: { id: "zweite" } })).rejects.toThrow();
});
});
describe("Geraet", () => {
it("erlaubt einen vorgedruckten Tag ohne Hostname und ohne Bezeichnung", async () => {
const kunde = await kundeAnlegen();
const tag = await testPrisma.geraet.create({
data: { kundeId: kunde.id, tagCode: "LZ-7K3F9Q" },
});
expect(tag.registriertAm).toBeNull();
expect(tag.hostname).toBeNull();
});
it("verlangt beim Registrieren einen Hostname oder eine Bezeichnung", async () => {
const kunde = await kundeAnlegen();
await expect(
testPrisma.geraet.create({
data: { kundeId: kunde.id, tagCode: "LZ-7K3F9Q", registriertAm: new Date() },
}),
).rejects.toThrow(/Geraet_registriert_braucht_namen/);
});
it("wertet reine Leerzeichen nicht als Bezeichnung", async () => {
const kunde = await kundeAnlegen();
await expect(
testPrisma.geraet.create({
data: {
kundeId: kunde.id,
tagCode: "LZ-7K3F9Q",
bezeichnung: " ",
registriertAm: new Date(),
},
}),
).rejects.toThrow();
});
it("nimmt ein registriertes Geraet mit Bezeichnung statt Hostname an", async () => {
const kunde = await kundeAnlegen();
const raum = await testPrisma.geraet.create({
data: {
kundeId: kunde.id,
tagCode: "LZ-M2XH4T",
bezeichnung: "Serverschrank Keller",
typ: "RAUM",
registriertAm: new Date(),
},
});
expect(raum.bezeichnung).toBe("Serverschrank Keller");
});
it("weist einen tagCode in falscher Form zurueck", async () => {
const kunde = await kundeAnlegen();
for (const falsch of ["7K3F9Q", "LZ-7K3F9", "lz-7k3f9q", "LZ-7K3F9O", "LZ-7K3F91"]) {
await expect(
testPrisma.geraet.create({ data: { kundeId: kunde.id, tagCode: falsch } }),
).rejects.toThrow();
}
});
it("haelt den tagCode ueber alle Kunden hinweg eindeutig", async () => {
const einer = await kundeAnlegen("MUS");
const anderer = await kundeAnlegen("REI");
await testPrisma.geraet.create({ data: { kundeId: einer.id, tagCode: "LZ-7K3F9Q" } });
await expect(
testPrisma.geraet.create({ data: { kundeId: anderer.id, tagCode: "LZ-7K3F9Q" } }),
).rejects.toThrow();
});
});
describe("Mitarbeiter", () => {
it("haelt die externe Kennung je Quelle eindeutig", async () => {
const kunde = await kundeAnlegen();
const quelle = await testPrisma.verzeichnisquelle.create({
data: {
kundeId: kunde.id,
name: "Firmen-AD",
url: "ldaps://dc01.firma.local",
bindDn: "CN=Dienst,DC=firma,DC=local",
bindPasswortVerschluesselt: "v1.probe",
basisDn: "DC=firma,DC=local",
filter: "(objectClass=user)",
attributZuordnung: {},
},
});
const stammdaten = {
kundeId: kunde.id,
quelleId: quelle.id,
externeId: "guid-1",
vorname: "Maria",
nachname: "Huber",
anzeigename: "Maria Huber",
herkunft: "ABGLEICH" as const,
};
await testPrisma.mitarbeiter.create({ data: stammdaten });
await expect(testPrisma.mitarbeiter.create({ data: stammdaten })).rejects.toThrow();
});
it("laesst beliebig viele manuell angelegte Personen ohne Quelle zu", async () => {
const kunde = await kundeAnlegen();
// Postgres behandelt NULL als verschieden, deshalb greift der eindeutige
// Index hier nicht – genau so ist es gewollt.
for (const name of ["Ada", "Grace", "Alan"]) {
await testPrisma.mitarbeiter.create({
data: { kundeId: kunde.id, vorname: name, nachname: "Test", anzeigename: name },
});
}
expect(await testPrisma.mitarbeiter.count()).toBe(3);
});
});
describe("Abteilung", () => {
it("haelt den Namen je Kunde eindeutig, aber nicht ueber Kunden hinweg", async () => {
const einer = await kundeAnlegen("MUS");
const anderer = await kundeAnlegen("REI");
await testPrisma.abteilung.create({ data: { kundeId: einer.id, name: "Buchhaltung" } });
await expect(
testPrisma.abteilung.create({ data: { kundeId: einer.id, name: "Buchhaltung" } }),
).rejects.toThrow();
const beimAnderen = await testPrisma.abteilung.create({
data: { kundeId: anderer.id, name: "Buchhaltung" },
});
expect(beimAnderen.name).toBe("Buchhaltung");
});
});
describe("Lauf", () => {
it("laesst einen Lauf ohne Mitarbeiter und ohne Geraet zu", async () => {
const kunde = await kundeAnlegen();
const benutzer = await benutzerAnlegen();
const version = await veroeffentlichteVersionAnlegen();
const lauf = await testPrisma.lauf.create({
data: {
vorlagenVersionId: version.id,
titel: "Patchday 2026-10",
kundeId: kunde.id,
erstelltVonId: benutzer.id,
},
});
expect(lauf.status).toBe("OFFEN");
expect(lauf.gestartetAm).toBeNull();
});
it("weist Mitarbeiter und Freitextname zugleich zurueck", async () => {
const kunde = await kundeAnlegen();
const benutzer = await benutzerAnlegen();
const version = await veroeffentlichteVersionAnlegen();
const person = await testPrisma.mitarbeiter.create({
data: { kundeId: kunde.id, vorname: "Maria", nachname: "Huber", anzeigename: "Maria Huber" },
});
await expect(
testPrisma.lauf.create({
data: {
vorlagenVersionId: version.id,
titel: "Onboarding",
kundeId: kunde.id,
erstelltVonId: benutzer.id,
mitarbeiterId: person.id,
mitarbeiterName: "Maria Huber",
},
}),
).rejects.toThrow();
});
it("verhindert das Loeschen eines Geraets, an dem ein Lauf haengt", async () => {
const kunde = await kundeAnlegen();
const benutzer = await benutzerAnlegen();
const version = await veroeffentlichteVersionAnlegen();
const geraet = await testPrisma.geraet.create({
data: {
kundeId: kunde.id,
tagCode: "LZ-7K3F9Q",
hostname: "PC-001",
typ: "PC",
registriertAm: new Date(),
},
});
await testPrisma.lauf.create({
data: {
vorlagenVersionId: version.id,
titel: "PC Neuinstallation",
kundeId: kunde.id,
erstelltVonId: benutzer.id,
geraetId: geraet.id,
},
});
await expect(testPrisma.geraet.delete({ where: { id: geraet.id } })).rejects.toThrow();
});
it("verhindert das Loeschen einer Version, auf die ein Lauf verweist", async () => {
const kunde = await kundeAnlegen();
const benutzer = await benutzerAnlegen();
const version = await veroeffentlichteVersionAnlegen();
await testPrisma.lauf.create({
data: {
vorlagenVersionId: version.id,
titel: "Probe",
kundeId: kunde.id,
erstelltVonId: benutzer.id,
},
});
await expect(testPrisma.vorlagenVersion.delete({ where: { id: version.id } })).rejects.toThrow();
});
});
describe("Schrittergebnis", () => {
it("laesst je Lauf und Schritt nur ein Ergebnis zu", async () => {
const kunde = await kundeAnlegen();
const benutzer = await benutzerAnlegen();
const version = await veroeffentlichteVersionAnlegen();
const abschnitt = await testPrisma.vorlagenAbschnitt.create({
data: { versionId: version.id, titel: "Vorbereitung" },
});
const schritt = await testPrisma.vorlagenSchritt.create({
data: { abschnittId: abschnitt.id, titel: "Ticket erfasst", pflicht: true },
});
await versionVeroeffentlichen(version.id);
const lauf = await testPrisma.lauf.create({
data: {
vorlagenVersionId: version.id,
titel: "Probe",
kundeId: kunde.id,
erstelltVonId: benutzer.id,
},
});
await testPrisma.schrittergebnis.create({
data: { laufId: lauf.id, vorlagenSchrittId: schritt.id },
});
await expect(
testPrisma.schrittergebnis.create({
data: { laufId: lauf.id, vorlagenSchrittId: schritt.id },
}),
).rejects.toThrow();
});
});
describe("Vorlage", () => {
it("haelt den Seed-Schluessel eindeutig, wo er gesetzt ist", async () => {
await testPrisma.vorlage.create({
data: { name: "PC Neuinstallation", seedSchluessel: "pc-neuinstallation" },
});
await expect(
testPrisma.vorlage.create({
data: { name: "Anders benannt", seedSchluessel: "pc-neuinstallation" },
}),
).rejects.toThrow(/Vorlage_seedSchluessel_einmalig/);
});
it("laesst beliebig viele Vorlagen ohne Seed-Schluessel zu", async () => {
// Der Index ist partiell, sonst waere die zweite eigene Vorlage eine
// Dublette von null (#021).
await testPrisma.vorlage.create({ data: { name: "Eigene A" } });
await testPrisma.vorlage.create({ data: { name: "Eigene B" } });
expect(await testPrisma.vorlage.count({ where: { seedSchluessel: null } })).toBe(2);
});
});
describe("Vorlagen-Version", () => {
it("haelt die Versionsnummer je Vorlage eindeutig", async () => {
const vorlage = await testPrisma.vorlage.create({ data: { name: "PC Neuinstallation" } });
await testPrisma.vorlagenVersion.create({ data: { vorlageId: vorlage.id, version: 1 } });
await expect(
testPrisma.vorlagenVersion.create({ data: { vorlageId: vorlage.id, version: 1 } }),
).rejects.toThrow();
});
it("raeumt Abschnitte, Schritte und Variablen mit der Version ab", async () => {
// Ein Entwurf darf als Ganzes verworfen werden; nur deshalb steht hier
// Cascade und nicht Restrict.
const version = await veroeffentlichteVersionAnlegen();
const abschnitt = await testPrisma.vorlagenAbschnitt.create({
data: { versionId: version.id, titel: "Vorbereitung" },
});
await testPrisma.vorlagenSchritt.create({
data: { abschnittId: abschnitt.id, titel: "Ticket erfasst" },
});
await testPrisma.vorlagenVariable.create({
data: { versionId: version.id, schluessel: "os_version", beschriftung: "Betriebssystem" },
});
await testPrisma.vorlagenVersion.delete({ where: { id: version.id } });
expect(await testPrisma.vorlagenAbschnitt.count()).toBe(0);
expect(await testPrisma.vorlagenSchritt.count()).toBe(0);
expect(await testPrisma.vorlagenVariable.count()).toBe(0);
});
});
});
/**
* Die Zusicherungen, die erst der unabhaengige Review sichtbar gemacht hat:
* Mandantengrenze, Unversehrtheit des Archivs und die Vorleistung fuer Phase 4.
*/
describe.runIf(datenbankVerfuegbar)("Datenmodell: Grenzen und Zusicherungen", () => {
beforeEach(async () => {
await datenbankLeeren();
});
describe("Mandantengrenze", () => {
it("laesst einen Lauf kein Geraet eines fremden Kunden waehlen", async () => {
const einer = await kundeAnlegen("MUS");
const anderer = await kundeAnlegen("REI");
const benutzer = await benutzerAnlegen();
const version = await veroeffentlichteVersionAnlegen();
const fremdesGeraet = await testPrisma.geraet.create({
data: {
kundeId: anderer.id,
tagCode: "LZ-7K3F9Q",
hostname: "REI-PC-001",
registriertAm: new Date(),
},
});
await expect(
testPrisma.lauf.create({
data: {
vorlagenVersionId: version.id,
titel: "Quer ueber Kunden",
kundeId: einer.id,
erstelltVonId: benutzer.id,
geraetId: fremdesGeraet.id,
},
}),
).rejects.toThrow(/Lauf_geraet_gleicher_kunde/);
});
it("laesst einen Lauf keine Person eines fremden Kunden waehlen", async () => {
const einer = await kundeAnlegen("MUS");
const anderer = await kundeAnlegen("REI");
const benutzer = await benutzerAnlegen();
const version = await veroeffentlichteVersionAnlegen();
const fremdePerson = await testPrisma.mitarbeiter.create({
data: { kundeId: anderer.id, vorname: "Maria", nachname: "Huber", anzeigename: "Maria Huber" },
});
await expect(
testPrisma.lauf.create({
data: {
vorlagenVersionId: version.id,
titel: "Quer ueber Kunden",
kundeId: einer.id,
erstelltVonId: benutzer.id,
mitarbeiterId: fremdePerson.id,
},
}),
).rejects.toThrow(/Lauf_mitarbeiter_gleicher_kunde/);
});
it("laesst einem Geraet keinen Besitzer eines fremden Kunden geben", async () => {
const einer = await kundeAnlegen("MUS");
const anderer = await kundeAnlegen("REI");
const fremdePerson = await testPrisma.mitarbeiter.create({
data: { kundeId: anderer.id, vorname: "Maria", nachname: "Huber", anzeigename: "Maria Huber" },
});
await expect(
testPrisma.geraet.create({
data: {
kundeId: einer.id,
tagCode: "LZ-7K3F9Q",
hostname: "MUS-PC-001",
registriertAm: new Date(),
mitarbeiterId: fremdePerson.id,
},
}),
).rejects.toThrow(/Geraet_besitzer_gleicher_kunde/);
});
});
describe("Unversehrtheit des Archivs", () => {
it("verhindert, dass ein Lauf seine Schrittergebnisse mitnimmt", async () => {
const kunde = await kundeAnlegen();
const benutzer = await benutzerAnlegen();
const version = await veroeffentlichteVersionAnlegen();
const abschnitt = await testPrisma.vorlagenAbschnitt.create({
data: { versionId: version.id, titel: "Vorbereitung" },
});
const schritt = await testPrisma.vorlagenSchritt.create({
data: { abschnittId: abschnitt.id, titel: "Ticket erfasst" },
});
await versionVeroeffentlichen(version.id);
const lauf = await testPrisma.lauf.create({
data: {
vorlagenVersionId: version.id,
titel: "Probe",
kundeId: kunde.id,
erstelltVonId: benutzer.id,
},
});
await testPrisma.schrittergebnis.create({
data: { laufId: lauf.id, vorlagenSchrittId: schritt.id },
});
// Ohne diese Sperre verschwaende ein DELETE auf Lauf das halbe Archiv.
await expect(testPrisma.lauf.delete({ where: { id: lauf.id } })).rejects.toThrow();
expect(await testPrisma.schrittergebnis.count()).toBe(1);
});
it("verhindert, dass ein Protokolleintrag seinen Urheber verliert", async () => {
const benutzer = await benutzerAnlegen();
await testPrisma.protokoll.create({
data: {
entitaet: "Benutzer",
entitaetId: benutzer.id,
aktion: "angemeldet",
benutzerId: benutzer.id,
},
});
await expect(testPrisma.benutzer.delete({ where: { id: benutzer.id } })).rejects.toThrow();
});
});
describe("Vorlagen-Versionierung", () => {
it("laesst je Vorlage nur einen Entwurf zu", async () => {
const vorlage = await testPrisma.vorlage.create({ data: { name: "PC Neuinstallation" } });
await testPrisma.vorlagenVersion.create({ data: { vorlageId: vorlage.id, version: 1 } });
await expect(
testPrisma.vorlagenVersion.create({ data: { vorlageId: vorlage.id, version: 2 } }),
).rejects.toThrow(/VorlagenVersion_ein_entwurf/);
});
it("verlangt beim Veroeffentlichen einen Zeitpunkt", async () => {
const vorlage = await testPrisma.vorlage.create({ data: { name: "Probe" } });
await expect(
testPrisma.vorlagenVersion.create({
data: { vorlageId: vorlage.id, version: 1, status: "VEROEFFENTLICHT" },
}),
).rejects.toThrow(/VorlagenVersion_veroeffentlicht_mit_zeitpunkt/);
});
});
describe("Status und Zeitstempel", () => {
it("verlangt einen Startzeitpunkt, sobald der Lauf nicht mehr offen ist", async () => {
const kunde = await kundeAnlegen();
const benutzer = await benutzerAnlegen();
const version = await veroeffentlichteVersionAnlegen();
await expect(
testPrisma.lauf.create({
data: {
vorlagenVersionId: version.id,
titel: "Probe",
kundeId: kunde.id,
erstelltVonId: benutzer.id,
status: "IN_ARBEIT",
},
}),
).rejects.toThrow(/Lauf_begonnen_mit_zeitpunkt/);
});
it("verlangt beim Archivieren Abschluss- und Archivzeitpunkt", async () => {
const kunde = await kundeAnlegen();
const benutzer = await benutzerAnlegen();
const version = await veroeffentlichteVersionAnlegen();
await expect(
testPrisma.lauf.create({
data: {
vorlagenVersionId: version.id,
titel: "Probe",
kundeId: kunde.id,
erstelltVonId: benutzer.id,
status: "ARCHIVIERT",
gestartetAm: new Date(),
},
}),
).rejects.toThrow(/Lauf_abgeschlossen_mit_zeitpunkt/);
});
});
describe("Schrittergebnis", () => {
it("verlangt eine Begruendung bei nicht zutreffend und fehlgeschlagen", async () => {
const kunde = await kundeAnlegen();
const benutzer = await benutzerAnlegen();
const version = await veroeffentlichteVersionAnlegen();
const abschnitt = await testPrisma.vorlagenAbschnitt.create({
data: { versionId: version.id, titel: "Vorbereitung" },
});
const schritt = await testPrisma.vorlagenSchritt.create({
data: { abschnittId: abschnitt.id, titel: "Ticket erfasst" },
});
await versionVeroeffentlichen(version.id);
const lauf = await testPrisma.lauf.create({
data: {
vorlagenVersionId: version.id,
titel: "Probe",
kundeId: kunde.id,
erstelltVonId: benutzer.id,
},
});
for (const zustand of ["NICHT_ZUTREFFEND", "FEHLGESCHLAGEN"] as const) {
await expect(
testPrisma.schrittergebnis.create({
data: { laufId: lauf.id, vorlagenSchrittId: schritt.id, zustand },
}),
).rejects.toThrow(/Schrittergebnis_begruendung_noetig/);
}
const mitNotiz = await testPrisma.schrittergebnis.create({
data: {
laufId: lauf.id,
vorlagenSchrittId: schritt.id,
zustand: "NICHT_ZUTREFFEND",
notiz: "Geraet hat kein TPM.",
},
});
expect(mitNotiz.notiz).toBe("Geraet hat kein TPM.");
});
it("nimmt ein Aktionsergebnis verlustfrei auf", async () => {
// Vorleistung fuer Phase 4: der Wert muss Log, Kennungen und Zeitstempel
// einer ausgefuehrten Aktion tragen koennen, nicht nur einen Haken.
const kunde = await kundeAnlegen();
const benutzer = await benutzerAnlegen();
const version = await veroeffentlichteVersionAnlegen();
const abschnitt = await testPrisma.vorlagenAbschnitt.create({
data: { versionId: version.id, titel: "Konto anlegen" },
});
const schritt = await testPrisma.vorlagenSchritt.create({
data: { abschnittId: abschnitt.id, titel: "New-ADUser", typ: "BEFEHL" },
});
await versionVeroeffentlichen(version.id);
const lauf = await testPrisma.lauf.create({
data: {
vorlagenVersionId: version.id,
titel: "Onboarding",
kundeId: kunde.id,
erstelltVonId: benutzer.id,
},
});
const ergebnis = {
connector: "ldap",
ausgefuehrtAm: "2026-09-25T08:15:00.000Z",
dauerMs: 842,
objekte: [{ typ: "Benutzer", dn: "CN=Maria Huber,OU=Buchhaltung,DC=firma,DC=local" }],
log: ["Verbindung aufgebaut", "Konto angelegt", "Gruppen zugewiesen"],
erfolgreich: true,
};
const gespeichert = await testPrisma.schrittergebnis.create({
data: {
laufId: lauf.id,
vorlagenSchrittId: schritt.id,
zustand: "ERLEDIGT",
wert: ergebnis,
},
});
expect(gespeichert.wert).toEqual(ergebnis);
});
});
describe("Vorleistung fuer Phase 4", () => {
it("laesst einen Lauf ohne erstellenden Menschen zu", async () => {
// Wiederkehrende Pruefungen und der Start per Webhook legen Laeufe an,
// hinter denen kein Benutzer steht.
const kunde = await kundeAnlegen();
const version = await veroeffentlichteVersionAnlegen();
const systemlauf = await testPrisma.lauf.create({
data: { vorlagenVersionId: version.id, titel: "Jaehrliche Pruefung", kundeId: kunde.id },
});
expect(systemlauf.erstelltVonId).toBeNull();
});
});
describe("Vergabebuch fuer tagCodes", () => {
it("haelt einen vergebenen Code fest, auch wenn das Geraet verschwindet", async () => {
const kunde = await kundeAnlegen();
await testPrisma.tagCodeVergabe.create({ data: { tagCode: "LZ-7K3F9Q" } });
const geraet = await testPrisma.geraet.create({
data: { kundeId: kunde.id, tagCode: "LZ-7K3F9Q" },
});
await testPrisma.geraet.delete({ where: { id: geraet.id } });
expect(await testPrisma.geraet.count()).toBe(0);
// Der Code bleibt vergeben – ein alter Aufkleber darf nie auf ein neues
// Objekt zeigen.
expect(await testPrisma.tagCodeVergabe.count()).toBe(1);
await expect(testPrisma.tagCodeVergabe.create({ data: { tagCode: "LZ-7K3F9Q" } })).rejects.toThrow();
});
it("weist einen Code in falscher Form zurueck", async () => {
await expect(testPrisma.tagCodeVergabe.create({ data: { tagCode: "LZ-7K3F9O" } })).rejects.toThrow(
/TagCodeVergabe_form/,
);
});
});
describe("Verzeichnisquelle", () => {
it("weist ein Bind-Passwort ohne Versionspraefix zurueck", async () => {
const kunde = await kundeAnlegen();
await expect(
testPrisma.verzeichnisquelle.create({
data: {
kundeId: kunde.id,
name: "Firmen-AD",
url: "ldaps://dc01.firma.local",
bindDn: "CN=Dienst,DC=firma,DC=local",
// So saehe ein versehentlich im Klartext gespeichertes Passwort aus.
bindPasswortVerschluesselt: "GeheimesPasswort123",
basisDn: "DC=firma,DC=local",
filter: "(objectClass=user)",
attributZuordnung: {},
},
}),
).rejects.toThrow(/Verzeichnisquelle_bindpasswort_verschluesselt/);
});
});
describe("Abgleich-Sperre", () => {
it("laesst dieselbe Sperre nicht zweimal halten", async () => {
const laeuftAbAm = new Date(Date.now() + 60_000);
await testPrisma.abgleichSperre.create({
data: { schluessel: "abgleich:quelle-1", gehaltenVon: "instanz-a", laeuftAbAm },
});
await expect(
testPrisma.abgleichSperre.create({
data: { schluessel: "abgleich:quelle-1", gehaltenVon: "instanz-b", laeuftAbAm },
}),
).rejects.toThrow();
});
});
});