feat(mandant): Betriebsmodus, Kundenverwaltung und Einstellungen
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

Laufzettel läuft jetzt in zwei Betriebsmodi, aber nicht in zwei Codepfaden. Jede
kundenbezogene Zeile trägt ohnehin schon eine Kunden-Zuordnung; im Modus INTERN
ist das die eine Organisation, die in der Oberfläche nirgends erscheint. Der
Wechsel ist deshalb eine Datenänderung und keine Migration — dieselbe Zeile
bleibt, nichts hängt um.

src/server/mandant.ts ist die eine Stelle, an der die Mandantenregel steht:
kundeErmitteln verwirft im Modus INTERN eine mitgeschickte kundeId, verlangt im
Modus DIENSTLEISTUNG eine Wahl und prüft, ob der Benutzer den Kunden überhaupt
sehen darf. Die Sichtbarkeit steckte zunächst in einer zweiten Funktion daneben,
an die jeder künftige Aufrufer selbst hätte denken müssen; da #009 diese Signatur
übernimmt, ist sie jetzt eingebaut.

Der unabhängige Review fand drei Blocker, alle behoben.

Die Lesemodelle wandten die Mandantensicht nicht an. kundenListe gab jedem alle
Kunden zurück, und sichtbareKundenIds hatte in der Produktion keinen einzigen
Aufrufer — ein gebundener Techniker hätte Namen, Notizen und Bestandszahlen
fremder Kunden gesehen. Die Funktionen verlangen jetzt den Benutzer und filtern
selbst, wie CLAUDE.md es verlangt.

Mein Kommentar "Alles in einer Transaktion" war falsch: der Protokolleintrag
stand dahinter. Schlug er fehl, war der Modus gewechselt und der Vorgang stand
nirgends. Und Prüfung und Vollzug lagen auseinander, sodass zwei gleichzeitige
Wechsel beide durchliefen und ein zwischendurch angelegter Kunde zu Modus INTERN
bei zwei Kunden führte. Beides reproduziert, beides geschlossen: alles in einer
Transaktion, mit pg_advisory_xact_lock als erster Anweisung.

Dazu sechs kleinere Befunde: ASCII-Faltung in Texten, die Menschen lesen (die
Regel gilt für Bezeichner, nicht für Fließtext — steht jetzt so in CLAUDE.md);
eine Erfolgsmeldung, die nach dem Wechsel unsichtbar blieb; eine Zusage im
Dialog, die der Code nicht durchsetzt; fehlende Fehlerbehandlung beim
Archivieren; ein Handy-Screenshot, der das Navigationsmenü statt des Dialogs
zeigte.

Der Wechsel wurde nicht nur im Service geprüft, sondern per Playwright durch die
Oberfläche gefahren: anmelden, Dialog aufklappen, bestätigen, und danach steht
die Navigation auf Kunden.

336 Tests.

Closes #8

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-25 07:01:46 +02:00
co-authored by Claude Opus 5
parent 40e95dbe5f
commit 8e67591912
43 changed files with 2729 additions and 53 deletions
+504
View File
@@ -0,0 +1,504 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { Betriebsmodus } from "@/generated/prisma/enums";
import {
MandantFehler,
betriebsmodus,
eigeneOrganisation,
kundeErmitteln,
kundeZugriffPruefen,
kundenEbeneSichtbar,
sichtbareKundenIds,
} from "@/server/mandant";
import {
EinstellungsFehler,
einstellungenHolen,
einstellungenSpeichern,
modusWechseln,
modusWechselPruefen,
} from "@/server/services/einstellungen";
import { kundenListe, kundenZurAuswahl } from "@/server/queries/kunden";
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
/**
* Betriebsmodus und Mandantengrenze (#008).
*
* Hier haengen die Akzeptanzkriterien, die den ganzen Zwei-Modi-Entwurf tragen:
* dass im Modus INTERN eine mitgeschickte kundeId verworfen wird, dass der
* Wechsel alle Daten mitnimmt und dass der Rueckweg bei zwei Kunden abgelehnt
* wird.
*
* `betriebsmodus()` benutzt Reacts `cache`. Ausserhalb einer Anfrage merkt das
* nichts – nachgeprueft –, die Tests beeinflussen sich also nicht gegenseitig.
*/
const UMGEBUNG = { ...process.env };
async function grundbestand(modus: Betriebsmodus = Betriebsmodus.INTERN) {
const organisation = await testPrisma.kunde.create({
data: { name: "Eigene Organisation", kuerzel: "EIGEN", istEigeneOrganisation: true },
});
await testPrisma.einstellung.create({ data: { id: "global", modus } });
const admin = await testPrisma.benutzer.create({
data: { email: "admin@firma.example", name: "Administrator", rolle: "ADMIN" },
});
return { organisation, admin };
}
describe.runIf(datenbankVerfuegbar)("Betriebsmodus und Mandantengrenze", () => {
beforeEach(async () => {
await datenbankLeeren();
delete process.env["OIDC_CUSTOMER_GROUP_PREFIX"];
});
afterEach(() => {
process.env = { ...UMGEBUNG };
});
describe("Modus lesen", () => {
it("ist INTERN, solange nichts eingestellt ist", async () => {
// Der sichere Standard: keine Kundenebene, nichts faellt auseinander.
expect(await betriebsmodus()).toBe(Betriebsmodus.INTERN);
});
it("liest den eingestellten Modus aus der Datenbank", async () => {
await grundbestand(Betriebsmodus.DIENSTLEISTUNG);
expect(await betriebsmodus()).toBe(Betriebsmodus.DIENSTLEISTUNG);
});
it("zeigt die Kundenebene nur im Modus Dienstleistung", async () => {
await grundbestand(Betriebsmodus.INTERN);
expect(await kundenEbeneSichtbar()).toBe(false);
await testPrisma.einstellung.update({
where: { id: "global" },
data: { modus: Betriebsmodus.DIENSTLEISTUNG },
});
expect(await kundenEbeneSichtbar()).toBe(true);
});
it("sagt deutlich Bescheid, wenn die eigene Organisation fehlt", async () => {
await expect(eigeneOrganisation()).rejects.toThrow(/pnpm db:seed/);
});
});
describe("kundeErmitteln", () => {
/** Ein Administrator – die Sichtbarkeit schraenkt ihn nie ein. */
const ADMIN = { id: "admin", rolle: "ADMIN" } as const;
it("verwirft im Modus Intern eine mitgeschickte fremde kundeId", async () => {
// Das ist die Regel, die den Zwei-Modi-Entwurf sicher macht: im Modus
// INTERN gibt es keine Kundenwahl, also ist auch nichts zu entscheiden.
const { organisation } = await grundbestand(Betriebsmodus.INTERN);
const fremder = await testPrisma.kunde.create({
data: { name: "Fremdfirma", kuerzel: "FREMD" },
});
expect(await kundeErmitteln(fremder.id, ADMIN)).toBe(organisation.id);
expect(await kundeErmitteln("gibt-es-gar-nicht", ADMIN)).toBe(organisation.id);
expect(await kundeErmitteln(null, ADMIN)).toBe(organisation.id);
expect(await kundeErmitteln(undefined, ADMIN)).toBe(organisation.id);
});
it("verlangt im Modus Dienstleistung eine Wahl", async () => {
await grundbestand(Betriebsmodus.DIENSTLEISTUNG);
await expect(kundeErmitteln(undefined, ADMIN)).rejects.toBeInstanceOf(MandantFehler);
await expect(kundeErmitteln(" ", ADMIN)).rejects.toThrow(/Kunde gewählt/);
});
it("weist im Modus Dienstleistung einen unbekannten Kunden ab", async () => {
await grundbestand(Betriebsmodus.DIENSTLEISTUNG);
await expect(kundeErmitteln("gibt-es-nicht", ADMIN)).rejects.toThrow(/gibt es nicht/);
});
it("weist einen archivierten Kunden ab", async () => {
await grundbestand(Betriebsmodus.DIENSTLEISTUNG);
const archiviert = await testPrisma.kunde.create({
data: { name: "Alte Firma", kuerzel: "ALT", archiviert: true },
});
await expect(kundeErmitteln(archiviert.id, ADMIN)).rejects.toThrow(/archiviert/);
});
it("nimmt im Modus Dienstleistung einen gueltigen Kunden an", async () => {
await grundbestand(Betriebsmodus.DIENSTLEISTUNG);
const mus = await testPrisma.kunde.create({ data: { name: "Musterfirma", kuerzel: "MUS" } });
expect(await kundeErmitteln(mus.id, ADMIN)).toBe(mus.id);
});
it("weist einen Kunden ab, an den der Benutzer nicht gebunden ist", async () => {
// Die Sichtbarkeit steht in kundeErmitteln selbst, damit kein Aufrufer sie
// vergessen kann.
process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "lz-kunde-";
await grundbestand(Betriebsmodus.DIENSTLEISTUNG);
const meiner = await testPrisma.kunde.create({ data: { name: "Meiner", kuerzel: "MEIN" } });
const fremder = await testPrisma.kunde.create({ data: { name: "Fremder", kuerzel: "FREMD" } });
const techniker = await testPrisma.benutzer.create({
data: { email: "t@firma.example", name: "Techniker", rolle: "TECHNIKER" },
});
await testPrisma.benutzerKunde.create({
data: { benutzerId: techniker.id, kundeId: meiner.id },
});
expect(await kundeErmitteln(meiner.id, techniker)).toBe(meiner.id);
await expect(kundeErmitteln(fremder.id, techniker)).rejects.toThrow(/Berechtigung/);
});
});
describe("Sichtbare Kunden", () => {
async function technikerMitBindung() {
const { organisation } = await grundbestand(Betriebsmodus.DIENSTLEISTUNG);
const mus = await testPrisma.kunde.create({ data: { name: "Musterfirma", kuerzel: "MUS" } });
await testPrisma.kunde.create({ data: { name: "Baeckerei", kuerzel: "REI" } });
const techniker = await testPrisma.benutzer.create({
data: { email: "t@firma.example", name: "Techniker", rolle: "TECHNIKER" },
});
await testPrisma.benutzerKunde.create({
data: { benutzerId: techniker.id, kundeId: mus.id },
});
return { organisation, mus, techniker };
}
it("schraenkt ohne gesetztes Praefix niemanden ein", async () => {
// Eine leere Bindung darf nicht als "sieht nichts" gelesen werden.
const { techniker } = await technikerMitBindung();
expect(await sichtbareKundenIds(techniker)).toBeUndefined();
});
it("schraenkt Techniker und Leser auf ihre Kunden ein", async () => {
process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "lz-kunde-";
const { mus, techniker } = await technikerMitBindung();
expect(await sichtbareKundenIds(techniker)).toEqual([mus.id]);
});
it("schraenkt Administrator und Redakteur nie ein", async () => {
process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "lz-kunde-";
await technikerMitBindung();
for (const rolle of ["ADMIN", "REDAKTEUR"] as const) {
expect(await sichtbareKundenIds({ id: "egal", rolle })).toBeUndefined();
}
});
it("schraenkt im Modus Intern nie ein", async () => {
process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "lz-kunde-";
await grundbestand(Betriebsmodus.INTERN);
const leser = await testPrisma.benutzer.create({
data: { email: "l@firma.example", name: "Leser", rolle: "LESER" },
});
expect(await sichtbareKundenIds(leser)).toBeUndefined();
});
it("weist den Zugriff auf einen fremden Kunden ab", async () => {
process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "lz-kunde-";
const { organisation, mus, techniker } = await technikerMitBindung();
await expect(kundeZugriffPruefen(mus.id, techniker)).resolves.toBeUndefined();
await expect(kundeZugriffPruefen(organisation.id, techniker)).rejects.toBeInstanceOf(MandantFehler);
});
});
describe("Wechsel nach Dienstleistung", () => {
it("nimmt alle Daten auf den ersten Kunden mit", async () => {
const { organisation, admin } = await grundbestand(Betriebsmodus.INTERN);
await testPrisma.geraet.create({
data: {
kundeId: organisation.id,
tagCode: "LZ-7K3F9Q",
hostname: "PC-001",
registriertAm: new Date(),
},
});
await testPrisma.mitarbeiter.create({
data: {
kundeId: organisation.id,
vorname: "Maria",
nachname: "Huber",
anzeigename: "Maria Huber",
},
});
await testPrisma.abteilung.create({
data: { kundeId: organisation.id, name: "Buchhaltung" },
});
const vorlage = await testPrisma.vorlage.create({ data: { name: "PC Neuinstallation" } });
const version = await testPrisma.vorlagenVersion.create({
data: {
vorlageId: vorlage.id,
version: 1,
status: "VEROEFFENTLICHT",
veroeffentlichtAm: new Date(),
},
});
await testPrisma.lauf.create({
data: {
vorlagenVersionId: version.id,
titel: "PC-001 neu aufsetzen",
kundeId: organisation.id,
erstelltVonId: admin.id,
},
});
await modusWechseln(
Betriebsmodus.DIENSTLEISTUNG,
{ name: "Musterfirma GmbH", kuerzel: "mus" },
admin.id,
);
expect(await betriebsmodus()).toBe(Betriebsmodus.DIENSTLEISTUNG);
const danach = await testPrisma.kunde.findUniqueOrThrow({ where: { id: organisation.id } });
expect(danach.name).toBe("Musterfirma GmbH");
// Das Kuerzel wird in Grossbuchstaben normalisiert.
expect(danach.kuerzel).toBe("MUS");
// Es bleibt dieselbe Zeile, deshalb haengt nichts um.
expect(await testPrisma.geraet.count({ where: { kundeId: organisation.id } })).toBe(1);
expect(await testPrisma.mitarbeiter.count({ where: { kundeId: organisation.id } })).toBe(1);
expect(await testPrisma.abteilung.count({ where: { kundeId: organisation.id } })).toBe(1);
expect(await testPrisma.lauf.count({ where: { kundeId: organisation.id } })).toBe(1);
expect(await testPrisma.kunde.count()).toBe(1);
});
it("weist ein ungueltiges Kuerzel mit einer verstaendlichen Meldung ab", async () => {
const { admin } = await grundbestand(Betriebsmodus.INTERN);
for (const kuerzel of ["X", "ZULANG7", "MU-S"]) {
await expect(
modusWechseln(Betriebsmodus.DIENSTLEISTUNG, { name: "Firma", kuerzel }, admin.id),
).rejects.toThrow(/2 bis 6/);
}
// Nach einem abgelehnten Wechsel steht der Modus unveraendert.
expect(await betriebsmodus()).toBe(Betriebsmodus.INTERN);
});
it("weist ein bereits vergebenes Kuerzel ab", async () => {
const { admin } = await grundbestand(Betriebsmodus.INTERN);
await testPrisma.kunde.create({ data: { name: "Andere", kuerzel: "MUS" } });
await expect(
modusWechseln(Betriebsmodus.DIENSTLEISTUNG, { name: "Firma", kuerzel: "MUS" }, admin.id),
).rejects.toThrow(/gehört bereits/);
});
it("behaelt Name und Kuerzel, wenn nichts angegeben wird", async () => {
const { organisation, admin } = await grundbestand(Betriebsmodus.INTERN);
await modusWechseln(Betriebsmodus.DIENSTLEISTUNG, {}, admin.id);
const danach = await testPrisma.kunde.findUniqueOrThrow({ where: { id: organisation.id } });
expect(danach.name).toBe(organisation.name);
expect(danach.kuerzel).toBe(organisation.kuerzel);
});
it("haelt den Wechsel im Protokoll fest", async () => {
const { admin } = await grundbestand(Betriebsmodus.INTERN);
await modusWechseln(Betriebsmodus.DIENSTLEISTUNG, { name: "Firma", kuerzel: "FIR" }, admin.id);
const eintrag = await testPrisma.protokoll.findFirstOrThrow({
where: { aktion: "Betriebsmodus gewechselt" },
});
expect(eintrag.benutzerId).toBe(admin.id);
expect(eintrag.vorher).toMatchObject({ modus: "INTERN" });
expect(eintrag.nachher).toMatchObject({ modus: "DIENSTLEISTUNG" });
});
});
describe("Wechsel zurueck nach Intern", () => {
it("wird bei zwei nicht archivierten Kunden abgelehnt", async () => {
const { admin } = await grundbestand(Betriebsmodus.DIENSTLEISTUNG);
await testPrisma.kunde.create({ data: { name: "Zweiter Kunde", kuerzel: "ZWEI" } });
const pruefung = await modusWechselPruefen(Betriebsmodus.INTERN);
expect(pruefung.moeglich).toBe(false);
expect(pruefung.grund).toMatch(/genau einem nicht archivierten Kunden/);
await expect(modusWechseln(Betriebsmodus.INTERN, {}, admin.id)).rejects.toBeInstanceOf(
EinstellungsFehler,
);
expect(await betriebsmodus()).toBe(Betriebsmodus.DIENSTLEISTUNG);
});
it("gelingt, sobald die uebrigen Kunden archiviert sind", async () => {
const { organisation, admin } = await grundbestand(Betriebsmodus.DIENSTLEISTUNG);
const zweiter = await testPrisma.kunde.create({
data: { name: "Zweiter Kunde", kuerzel: "ZWEI" },
});
await testPrisma.kunde.update({ where: { id: zweiter.id }, data: { archiviert: true } });
await modusWechseln(Betriebsmodus.INTERN, {}, admin.id);
expect(await betriebsmodus()).toBe(Betriebsmodus.INTERN);
expect((await eigeneOrganisation()).id).toBe(organisation.id);
// Archivierte Kunden bleiben archiviert und tauchen nicht wieder auf.
const archivierter = await testPrisma.kunde.findUniqueOrThrow({ where: { id: zweiter.id } });
expect(archivierter.archiviert).toBe(true);
});
it("haelt auch den Rueckweg im Protokoll fest", async () => {
const { admin } = await grundbestand(Betriebsmodus.DIENSTLEISTUNG);
await modusWechseln(Betriebsmodus.INTERN, {}, admin.id);
const eintrag = await testPrisma.protokoll.findFirstOrThrow({
where: { aktion: "Betriebsmodus gewechselt" },
});
expect(eintrag.vorher).toMatchObject({ modus: "DIENSTLEISTUNG" });
expect(eintrag.nachher).toMatchObject({ modus: "INTERN" });
});
it("lehnt den Wechsel in den bereits eingestellten Modus ab", async () => {
const { admin } = await grundbestand(Betriebsmodus.INTERN);
const pruefung = await modusWechselPruefen(Betriebsmodus.INTERN);
expect(pruefung.moeglich).toBe(false);
await expect(modusWechseln(Betriebsmodus.INTERN, {}, admin.id)).rejects.toThrow(/bereits/);
});
});
describe("Der Wechsel ist unteilbar", () => {
it("nimmt den Moduswechsel zurueck, wenn das Protokoll nicht geschrieben werden kann", async () => {
// Frueher stand der Protokoll-Eintrag hinter der Transaktion: schlug er
// fehl, war der Modus schon gewechselt und der Vorgang stand nirgends.
await grundbestand(Betriebsmodus.INTERN);
await expect(
modusWechseln(
Betriebsmodus.DIENSTLEISTUNG,
{ name: "Firma", kuerzel: "FIR" },
"diesen-benutzer-gibt-es-nicht",
),
).rejects.toThrow();
expect(await betriebsmodus()).toBe(Betriebsmodus.INTERN);
expect(await testPrisma.protokoll.count()).toBe(0);
// Auch Name und Kuerzel duerfen nicht halb geaendert sein.
const organisation = await testPrisma.kunde.findFirstOrThrow({
where: { istEigeneOrganisation: true },
});
expect(organisation.name).toBe("Eigene Organisation");
expect(organisation.kuerzel).toBe("EIGEN");
});
it("laesst von zwei gleichzeitigen Wechseln nur einen durch", async () => {
// Ohne Schloss liefen beide durch, mit zwei Protokolleintraegen, die beide
// INTERN als Vorzustand behaupteten.
const { admin } = await grundbestand(Betriebsmodus.INTERN);
const ergebnisse = await Promise.allSettled([
modusWechseln(Betriebsmodus.DIENSTLEISTUNG, { name: "Erster", kuerzel: "AAA" }, admin.id),
modusWechseln(Betriebsmodus.DIENSTLEISTUNG, { name: "Zweiter", kuerzel: "BBB" }, admin.id),
]);
expect(ergebnisse.filter((e) => e.status === "fulfilled")).toHaveLength(1);
expect(await testPrisma.protokoll.count({ where: { aktion: "Betriebsmodus gewechselt" } })).toBe(1);
});
});
describe("Lesemodelle wenden die Mandantensicht selbst an", () => {
it("zeigt einem gebundenen Techniker nur seine Kunden", async () => {
// Ein vergessener Filter waere hier kein Schoenheitsfehler, deshalb steht
// die Einschraenkung im Lesemodell und nicht beim Aufrufer.
process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "lz-kunde-";
await grundbestand(Betriebsmodus.DIENSTLEISTUNG);
const meiner = await testPrisma.kunde.create({ data: { name: "Meiner", kuerzel: "MEIN" } });
await testPrisma.kunde.create({ data: { name: "Fremder", kuerzel: "FREMD" } });
const techniker = await testPrisma.benutzer.create({
data: { email: "t@firma.example", name: "Techniker", rolle: "TECHNIKER" },
});
await testPrisma.benutzerKunde.create({
data: { benutzerId: techniker.id, kundeId: meiner.id },
});
const fuerTechniker = await kundenListe(techniker, true);
expect(fuerTechniker.map((z) => z.kuerzel)).toEqual(["MEIN"]);
expect((await kundenZurAuswahl(techniker)).map((z) => z.kuerzel)).toEqual(["MEIN"]);
// Der Administrator sieht weiterhin alles.
const fuerAdmin = await kundenListe({ id: "admin", rolle: "ADMIN" }, true);
expect(fuerAdmin).toHaveLength(3);
});
it("zeigt einem ungebundenen Techniker nichts, statt versehentlich alles", async () => {
// Der Unterschied zwischen "keine Einschraenkung" und "sieht nichts" ist
// der heikelste Teil dieser Regel.
process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "lz-kunde-";
await grundbestand(Betriebsmodus.DIENSTLEISTUNG);
await testPrisma.kunde.create({ data: { name: "Fremder", kuerzel: "FREMD" } });
const techniker = await testPrisma.benutzer.create({
data: { email: "t@firma.example", name: "Techniker", rolle: "TECHNIKER" },
});
expect(await kundenListe(techniker, true)).toEqual([]);
});
it("schraenkt ohne Praefix niemanden ein", async () => {
await grundbestand(Betriebsmodus.DIENSTLEISTUNG);
await testPrisma.kunde.create({ data: { name: "Fremder", kuerzel: "FREMD" } });
const techniker = await testPrisma.benutzer.create({
data: { email: "t@firma.example", name: "Techniker", rolle: "TECHNIKER" },
});
expect(await kundenListe(techniker, true)).toHaveLength(2);
});
});
describe("Wechsel-Auskunft fuer den Bestaetigungsdialog", () => {
it("nennt die Folgen und fragt nach Name und Kuerzel", async () => {
await grundbestand(Betriebsmodus.INTERN);
const pruefung = await modusWechselPruefen(Betriebsmodus.DIENSTLEISTUNG);
expect(pruefung.moeglich).toBe(true);
expect(pruefung.brauchtAngaben).toBe(true);
expect(pruefung.folgen.length).toBeGreaterThan(2);
expect(pruefung.folgen.join(" ")).toMatch(/erste[nr] Kunden/);
});
it("braucht fuer den Rueckweg keine Angaben", async () => {
await grundbestand(Betriebsmodus.DIENSTLEISTUNG);
const pruefung = await modusWechselPruefen(Betriebsmodus.INTERN);
expect(pruefung.moeglich).toBe(true);
expect(pruefung.brauchtAngaben).toBe(false);
});
});
describe("Einstellungen", () => {
it("liefert Modus, Regeln und die eigene Organisation", async () => {
await grundbestand(Betriebsmodus.INTERN);
const einstellungen = await einstellungenHolen();
expect(einstellungen.modus).toBe(Betriebsmodus.INTERN);
expect(einstellungen.eigeneOrganisation.kuerzel).toBe("EIGEN");
expect(einstellungen.namensregeln).toEqual({});
});
it("speichert Namensregeln und Sperr-OU und protokolliert es", async () => {
const { admin } = await grundbestand();
await einstellungenSpeichern(
{
namensregeln: { upn: "{vorname}.{nachname}@firma.de", sam: "{v}{nachname}" },
gesperrteOu: "OU=Gesperrt,DC=firma,DC=local",
},
admin.id,
);
const danach = await einstellungenHolen();
expect(danach.namensregeln.upn).toBe("{vorname}.{nachname}@firma.de");
expect(danach.gesperrteOu).toBe("OU=Gesperrt,DC=firma,DC=local");
expect(await testPrisma.protokoll.count({ where: { aktion: "Einstellungen geändert" } })).toBe(1);
});
it("benennt die eigene Organisation um", async () => {
const { organisation, admin } = await grundbestand();
await einstellungenSpeichern({ eigeneOrganisationName: " Wilde IT " }, admin.id);
const danach = await testPrisma.kunde.findUniqueOrThrow({ where: { id: organisation.id } });
expect(danach.name).toBe("Wilde IT");
});
it("weist einen leeren Namen ab", async () => {
const { admin } = await grundbestand();
await expect(
einstellungenSpeichern({ eigeneOrganisationName: " " }, admin.id),
).rejects.toBeInstanceOf(EinstellungsFehler);
});
it("schreibt nichts, wenn sich nichts aendert", async () => {
const { admin } = await grundbestand();
await einstellungenSpeichern({ namensregeln: {}, gesperrteOu: null }, admin.id);
expect(await testPrisma.protokoll.count()).toBe(0);
});
it("uebersteht einen unbrauchbaren Json-Inhalt in den Namensregeln", async () => {
// Ein von Hand verbogener Wert darf die Einstellungsseite nicht sprengen.
await grundbestand();
await testPrisma.einstellung.update({
where: { id: "global" },
data: { namensregeln: { upn: 42, unsinn: true } },
});
const einstellungen = await einstellungenHolen();
expect(einstellungen.namensregeln).toEqual({});
});
});
});