feat(verzeichnis): LDAP-Abgleich, Zeitplan, CSV-Import und Log
Quelle anlegen, Verbindung testen, nach Zeitplan oder auf Knopfdruck abgleichen. Gelesen wird seitenweise mit searchPaginated, objectGUID kommt als Puffer und wird gemischt-endian zur GUID umsortiert. Wer im Verzeichnis verschwindet, wird hier inaktiv und nie geloescht. Drei Regeln verhindern, dass eine kaputte Eingabe wie "alle sind gegangen" aussieht: laesst sich kein einziger Eintrag zuordnen, wird niemand deaktiviert; uebersprungene Eintraege werden am dn erkannt und ausgenommen; eine von Hand angelegte Person wird uebernommen statt verdoppelt. Der Zeitplan laeuft ueber node-cron im selben Prozess, gestartet aus src/instrumentation.ts, und nimmt keine eigene Sperre - die liegt in der Datenbank, damit zwei Instanzen sich einig sind. Closes #16 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ab5777a57e
commit
16059e7846
@@ -0,0 +1,430 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { Betriebsmodus, Herkunft } from "@/generated/prisma/enums";
|
||||
import {
|
||||
MITARBEITER_IMPORT_HOECHSTZEILEN,
|
||||
mitarbeiterImportieren,
|
||||
type MitarbeiterImportErgebnis,
|
||||
} from "@/server/services/mitarbeiter";
|
||||
import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db";
|
||||
|
||||
/**
|
||||
* CSV-Import der Personen (#016).
|
||||
*
|
||||
* Festgehalten wird vor allem, was in #011 am Geräteimport schiefging: der
|
||||
* Import meldete „3 von 3 angelegt“, obwohl Zeilen verschluckt worden waren.
|
||||
* Deshalb prüft hier fast jeder Test die **Summe** mit — je Zeile genau ein Fach,
|
||||
* und die Datenbank danach genauso, wie die Zahlen es behaupten.
|
||||
*
|
||||
* Dazu die zweite Regel des Hauses: was aus dem Verzeichnis kommt, wird nicht von
|
||||
* Hand überschrieben.
|
||||
*/
|
||||
|
||||
const KOPF = "givenName,sn,displayName,mail,upn,sam,department,title";
|
||||
|
||||
async function grundbestand(modus: Betriebsmodus = Betriebsmodus.INTERN) {
|
||||
const eigene = await testPrisma.kunde.create({
|
||||
data: { name: "Eigene Organisation", kuerzel: "EIGEN", istEigeneOrganisation: true },
|
||||
});
|
||||
await testPrisma.einstellung.create({ data: { id: "global", modus } });
|
||||
const benutzer = await testPrisma.benutzer.create({
|
||||
data: { email: "admin@firma.example", name: "Administrator", rolle: "ADMIN" },
|
||||
});
|
||||
return { eigene, admin: { id: benutzer.id, rolle: benutzer.rolle } };
|
||||
}
|
||||
|
||||
/** Jede Zeile landet in genau einem Fach – sonst lügt die Erfolgsmeldung. */
|
||||
function summePruefen(ergebnis: MitarbeiterImportErgebnis): void {
|
||||
expect(ergebnis.angelegt + ergebnis.aktualisiert + ergebnis.unveraendert + ergebnis.abgewiesen.length).toBe(
|
||||
ergebnis.gesamt,
|
||||
);
|
||||
}
|
||||
|
||||
describe.runIf(datenbankVerfuegbar)("Personen aus einer CSV-Datei", () => {
|
||||
beforeEach(async () => {
|
||||
await datenbankLeeren();
|
||||
delete process.env["OIDC_CUSTOMER_GROUP_PREFIX"];
|
||||
});
|
||||
|
||||
describe("Zeile für Zeile", () => {
|
||||
it("legt die guten Zeilen an und meldet die kaputte mit ihrer Zeilennummer", async () => {
|
||||
const { admin } = await grundbestand();
|
||||
const text = [
|
||||
KOPF,
|
||||
"Anna,Muster,Anna Muster,anna@firma.example,anna@firma.example,amuster,Vertrieb,Innendienst",
|
||||
'Bert,"Berg,Bert Berg,bert@firma.example,,bberg,Vertrieb,', // Anführungszeichen nicht geschlossen
|
||||
"Clara,Cloud,Clara Cloud,clara@firma.example,,ccloud,Technik,",
|
||||
",,,dora@firma.example,,dfehlt,,", // kein Name in der Zeile
|
||||
"Emil,Eck,Emil Eck,,,eeck,,",
|
||||
].join("\n");
|
||||
|
||||
const ergebnis = await mitarbeiterImportieren(text, admin);
|
||||
|
||||
expect(ergebnis.gesamt).toBe(5);
|
||||
expect(ergebnis.angelegt).toBe(3);
|
||||
summePruefen(ergebnis);
|
||||
// Zeilennummern wie in der Datei: die Kopfzeile ist 1.
|
||||
expect(ergebnis.abgewiesen.map((abweisung) => abweisung.zeile)).toEqual([3, 5]);
|
||||
expect(ergebnis.abgewiesen[0]?.grund).toMatch(/Anführungszeichen/);
|
||||
expect(ergebnis.abgewiesen[1]?.grund).toMatch(/fehlt ein Name/);
|
||||
|
||||
const namen = (await testPrisma.mitarbeiter.findMany({ select: { anzeigename: true } }))
|
||||
.map((zeile) => zeile.anzeigename)
|
||||
.sort();
|
||||
expect(namen).toEqual(["Anna Muster", "Clara Cloud", "Emil Eck"]);
|
||||
});
|
||||
|
||||
it("legt als MANUELL an und protokolliert die Zeilennummer", async () => {
|
||||
// MANUELL, weil niemand aus dieser Datei im Verzeichnis steht — und weil
|
||||
// eine so angelegte Person änderbar bleiben muss.
|
||||
const { admin, eigene } = await grundbestand();
|
||||
await mitarbeiterImportieren([KOPF, "Anna,Muster,,,,amuster,Vertrieb,"].join("\n"), admin);
|
||||
|
||||
const person = await testPrisma.mitarbeiter.findFirstOrThrow();
|
||||
expect(person.herkunft).toBe(Herkunft.MANUELL);
|
||||
expect(person.kundeId).toBe(eigene.id);
|
||||
expect(person.anzeigename).toBe("Anna Muster");
|
||||
expect(person.abteilung).toBe("Vertrieb");
|
||||
|
||||
const eintrag = await testPrisma.protokoll.findFirstOrThrow({
|
||||
where: { aktion: "Mitarbeiter aus CSV angelegt" },
|
||||
});
|
||||
expect(eintrag.nachher).toMatchObject({ anzeigename: "Anna Muster", zeile: 2 });
|
||||
});
|
||||
|
||||
it("versteht deutsches Excel: Semikolon, BOM und deutsche Spaltennamen", async () => {
|
||||
const { admin } = await grundbestand();
|
||||
const text =
|
||||
"Vorname;Nachname;Anzeigename;Mail;UPN;sAMAccountName;Abteilung;Titel\r\n" +
|
||||
'Jörg;Öz;"Öz, Jörg";joerg@firma.example;joerg@firma.example;joez;Technik;"Leiter ""IT"""\r\n';
|
||||
|
||||
const ergebnis = await mitarbeiterImportieren(text, admin);
|
||||
expect(ergebnis.angelegt).toBe(1);
|
||||
summePruefen(ergebnis);
|
||||
await expect(testPrisma.mitarbeiter.findFirstOrThrow()).resolves.toMatchObject({
|
||||
vorname: "Jörg",
|
||||
nachname: "Öz",
|
||||
anzeigename: "Öz, Jörg",
|
||||
titel: 'Leiter "IT"',
|
||||
});
|
||||
});
|
||||
|
||||
it("teilt displayName am letzten Leerzeichen, lässt den Anzeigenamen aber stehen", async () => {
|
||||
const { admin } = await grundbestand();
|
||||
const ergebnis = await mitarbeiterImportieren(
|
||||
["displayName,sam", "Anna van der Berg,aberg"].join("\n"),
|
||||
admin,
|
||||
);
|
||||
|
||||
expect(ergebnis.angelegt).toBe(1);
|
||||
summePruefen(ergebnis);
|
||||
// Die Teilung ist eine Annahme; der Anzeigename ist es nicht — und nur er
|
||||
// steht später in der Oberfläche und im Laufzettel.
|
||||
await expect(testPrisma.mitarbeiter.findFirstOrThrow()).resolves.toMatchObject({
|
||||
vorname: "Anna van der",
|
||||
nachname: "Berg",
|
||||
anzeigename: "Anna van der Berg",
|
||||
});
|
||||
});
|
||||
|
||||
it("gibt die Regel des Dienstes auch zeilenweise weiter", async () => {
|
||||
// Die 20-Zeichen-Grenze des sAMAccountName steht im Dienst und nirgends
|
||||
// sonst. Der Import geht durch denselben Weg, also gilt sie hier auch.
|
||||
const { admin } = await grundbestand();
|
||||
const ergebnis = await mitarbeiterImportieren(
|
||||
[KOPF, "Anna,Muster,,,,viel-zu-langer-anmeldename,,", "Bert,Berg,,,,bberg,,"].join("\n"),
|
||||
admin,
|
||||
);
|
||||
|
||||
expect(ergebnis.angelegt).toBe(1);
|
||||
summePruefen(ergebnis);
|
||||
expect(ergebnis.abgewiesen[0]).toMatchObject({ zeile: 2 });
|
||||
expect(ergebnis.abgewiesen[0]?.grund).toMatch(/höchstens 20/);
|
||||
});
|
||||
|
||||
it("weist eine Datei ohne Namensspalte als Ganzes ab", async () => {
|
||||
const { admin } = await grundbestand();
|
||||
await expect(mitarbeiterImportieren("mail,sam\nanna@firma.example,amuster\n", admin)).rejects.toThrow(
|
||||
/displayName/,
|
||||
);
|
||||
expect(await testPrisma.mitarbeiter.count()).toBe(0);
|
||||
});
|
||||
|
||||
it("bricht bei zu vielen Zeilen ab, bevor eine einzige geschrieben ist", async () => {
|
||||
const { admin } = await grundbestand();
|
||||
const zeilen = Array.from(
|
||||
{ length: MITARBEITER_IMPORT_HOECHSTZEILEN + 1 },
|
||||
(_, index) => `Anna,Muster${index},,,,a${index},,`,
|
||||
);
|
||||
await expect(mitarbeiterImportieren([KOPF, ...zeilen].join("\n"), admin)).rejects.toThrow(
|
||||
new RegExp(`mehr als ${MITARBEITER_IMPORT_HOECHSTZEILEN} Zeilen`),
|
||||
);
|
||||
expect(await testPrisma.mitarbeiter.count()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Wiedererkennen statt verdoppeln", () => {
|
||||
it("erkennt ein Duplikat innerhalb derselben Datei und nennt die erste Zeile", async () => {
|
||||
const { admin } = await grundbestand();
|
||||
const ergebnis = await mitarbeiterImportieren(
|
||||
[
|
||||
KOPF,
|
||||
"Anna,Muster,,anna@firma.example,,amuster,Vertrieb,",
|
||||
"Bert,Berg,,bert@firma.example,,bberg,Vertrieb,",
|
||||
"Anna,Muster-Neu,,anna@firma.example,,AMUSTER,Vertrieb,", // dieselbe Kennung
|
||||
].join("\n"),
|
||||
admin,
|
||||
);
|
||||
|
||||
expect(ergebnis.angelegt).toBe(2);
|
||||
summePruefen(ergebnis);
|
||||
expect(ergebnis.abgewiesen[0]?.zeile).toBe(4);
|
||||
expect(ergebnis.abgewiesen[0]?.grund).toMatch(/schon in Zeile 2/);
|
||||
expect(await testPrisma.mitarbeiter.count()).toBe(2);
|
||||
// Und die erste Zeile gilt, nicht die dritte.
|
||||
await expect(
|
||||
testPrisma.mitarbeiter.findFirstOrThrow({ where: { sam: "amuster" } }),
|
||||
).resolves.toMatchObject({ nachname: "Muster" });
|
||||
});
|
||||
|
||||
it("legt dieselbe Datei beim zweiten Einlesen nicht erneut an", async () => {
|
||||
// Der eigentliche Zweck der Wiedererkennung: eine Onboarding-Liste wandert
|
||||
// mehrfach durch dieses Feld, und niemand zählt vorher nach.
|
||||
const { admin } = await grundbestand();
|
||||
const text = [
|
||||
KOPF,
|
||||
"Anna,Muster,Anna Muster,anna@firma.example,anna@firma.example,amuster,Vertrieb,Innendienst",
|
||||
"Bert,Berg,Bert Berg,bert@firma.example,bert@firma.example,bberg,Technik,",
|
||||
].join("\n");
|
||||
|
||||
const erster = await mitarbeiterImportieren(text, admin);
|
||||
expect(erster.angelegt).toBe(2);
|
||||
|
||||
const zweiter = await mitarbeiterImportieren(text, admin);
|
||||
expect(zweiter).toMatchObject({ gesamt: 2, angelegt: 0, aktualisiert: 0, unveraendert: 2 });
|
||||
summePruefen(zweiter);
|
||||
expect(await testPrisma.mitarbeiter.count()).toBe(2);
|
||||
});
|
||||
|
||||
it("erkennt über den UPN, wenn kein sam in der Datei steht", async () => {
|
||||
const { admin, eigene } = await grundbestand();
|
||||
await testPrisma.mitarbeiter.create({
|
||||
data: {
|
||||
kundeId: eigene.id,
|
||||
vorname: "Anna",
|
||||
nachname: "Muster",
|
||||
anzeigename: "Anna Muster",
|
||||
upn: "anna.muster@firma.example",
|
||||
sam: "amuster",
|
||||
},
|
||||
});
|
||||
|
||||
const ergebnis = await mitarbeiterImportieren(
|
||||
["givenName,sn,upn,department", "Anna,Muster,ANNA.MUSTER@firma.example,Vertrieb"].join("\n"),
|
||||
admin,
|
||||
);
|
||||
|
||||
expect(ergebnis).toMatchObject({ angelegt: 0, aktualisiert: 1 });
|
||||
summePruefen(ergebnis);
|
||||
const person = await testPrisma.mitarbeiter.findFirstOrThrow();
|
||||
expect(person.abteilung).toBe("Vertrieb");
|
||||
// Gross- und Kleinschreibung unterscheidet keine Personen, deshalb bleibt
|
||||
// der vorhandene Wert stehen, statt ihn zu überschreiben.
|
||||
expect(person.upn).toBe("anna.muster@firma.example");
|
||||
});
|
||||
|
||||
it("erkennt ohne sam und ohne UPN über den Anzeigenamen", async () => {
|
||||
// Die Onboarding-Liste mit Name und Abteilung und sonst nichts. Ohne diesen
|
||||
// Rückfall stünde jede Person nach dem zweiten Einlesen doppelt da.
|
||||
const { admin } = await grundbestand();
|
||||
const text = ["displayName,department", "Anna Muster,Vertrieb"].join("\n");
|
||||
|
||||
expect(await mitarbeiterImportieren(text, admin)).toMatchObject({ angelegt: 1 });
|
||||
expect(await mitarbeiterImportieren(text, admin)).toMatchObject({ angelegt: 0, unveraendert: 1 });
|
||||
expect(await testPrisma.mitarbeiter.count()).toBe(1);
|
||||
});
|
||||
|
||||
it("weist eine Zeile ab, die auf zwei Personen passt", async () => {
|
||||
// Hier zu raten hieße, die Stammdaten der falschen Person zu überschreiben.
|
||||
const { admin, eigene } = await grundbestand();
|
||||
for (const [vorname, sam, upn] of [
|
||||
["Anna", "amuster", "a.muster@firma.example"],
|
||||
["Anneliese", "amuster2", "anna@firma.example"],
|
||||
] as const) {
|
||||
await testPrisma.mitarbeiter.create({
|
||||
data: { kundeId: eigene.id, vorname, nachname: "Muster", anzeigename: vorname, sam, upn },
|
||||
});
|
||||
}
|
||||
|
||||
const ergebnis = await mitarbeiterImportieren(
|
||||
[KOPF, "Anna,Muster,,,anna@firma.example,amuster,Vertrieb,"].join("\n"),
|
||||
admin,
|
||||
);
|
||||
|
||||
expect(ergebnis).toMatchObject({ angelegt: 0, aktualisiert: 0, unveraendert: 0 });
|
||||
summePruefen(ergebnis);
|
||||
expect(ergebnis.abgewiesen[0]?.grund).toMatch(/passt auf/);
|
||||
expect(await testPrisma.mitarbeiter.count()).toBe(2);
|
||||
});
|
||||
|
||||
it("aktiviert eine deaktivierte Person nicht wieder", async () => {
|
||||
// Das Wiedereinstellen ist eine Entscheidung und keine Zeile in einer
|
||||
// Datei. Die Oberfläche sagt es genauso — also muss es hier auch gelten.
|
||||
const { admin, eigene } = await grundbestand();
|
||||
await testPrisma.mitarbeiter.create({
|
||||
data: {
|
||||
kundeId: eigene.id,
|
||||
vorname: "Anna",
|
||||
nachname: "Muster",
|
||||
anzeigename: "Anna Muster",
|
||||
sam: "amuster",
|
||||
aktiv: false,
|
||||
},
|
||||
});
|
||||
|
||||
const ergebnis = await mitarbeiterImportieren(
|
||||
[KOPF, "Anna,Muster,,,,amuster,Vertrieb,"].join("\n"),
|
||||
admin,
|
||||
);
|
||||
|
||||
expect(ergebnis).toMatchObject({ angelegt: 0, aktualisiert: 1 });
|
||||
summePruefen(ergebnis);
|
||||
await expect(testPrisma.mitarbeiter.findFirstOrThrow()).resolves.toMatchObject({
|
||||
aktiv: false,
|
||||
abteilung: "Vertrieb",
|
||||
});
|
||||
});
|
||||
|
||||
it("lässt eine leere Zelle nichts löschen", async () => {
|
||||
const { admin, eigene } = await grundbestand();
|
||||
await testPrisma.mitarbeiter.create({
|
||||
data: {
|
||||
kundeId: eigene.id,
|
||||
vorname: "Anna",
|
||||
nachname: "Muster",
|
||||
anzeigename: "Muster, Anna",
|
||||
sam: "amuster",
|
||||
titel: "Innendienst",
|
||||
mail: "anna@firma.example",
|
||||
},
|
||||
});
|
||||
|
||||
const ergebnis = await mitarbeiterImportieren(
|
||||
[KOPF, "Anna,Muster,,,,amuster,Vertrieb,"].join("\n"),
|
||||
admin,
|
||||
);
|
||||
|
||||
expect(ergebnis).toMatchObject({ aktualisiert: 1 });
|
||||
summePruefen(ergebnis);
|
||||
await expect(testPrisma.mitarbeiter.findFirstOrThrow()).resolves.toMatchObject({
|
||||
abteilung: "Vertrieb",
|
||||
titel: "Innendienst",
|
||||
mail: "anna@firma.example",
|
||||
// Kein displayName in der Zeile heißt: der vorhandene bleibt, auch wenn
|
||||
// er anders lautet als „Vorname Nachname“.
|
||||
anzeigename: "Muster, Anna",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Herkunft ABGLEICH", () => {
|
||||
it("überschreibt eine Person aus dem Verzeichnis nicht, sondern meldet die Zeile", async () => {
|
||||
// Der nächste Abgleich drehte die Änderung ohnehin zurück. Stillschweigend
|
||||
// zu überspringen wäre das Schlimmere: der Import meldete dann einen
|
||||
// Erfolg, den es nicht gab.
|
||||
const { admin, eigene } = await grundbestand();
|
||||
await testPrisma.mitarbeiter.create({
|
||||
data: {
|
||||
kundeId: eigene.id,
|
||||
vorname: "Maria",
|
||||
nachname: "Huber",
|
||||
anzeigename: "Maria Huber",
|
||||
sam: "mhuber",
|
||||
abteilung: "Buchhaltung",
|
||||
titel: "Leitung",
|
||||
herkunft: Herkunft.ABGLEICH,
|
||||
},
|
||||
});
|
||||
const protokollVorher = await testPrisma.protokoll.count();
|
||||
|
||||
const ergebnis = await mitarbeiterImportieren(
|
||||
[KOPF, "Maria,Huber-Neu,,neu@firma.example,,mhuber,Vertrieb,Sachbearbeitung"].join("\n"),
|
||||
admin,
|
||||
);
|
||||
|
||||
expect(ergebnis).toMatchObject({ gesamt: 1, angelegt: 0, aktualisiert: 0, unveraendert: 0 });
|
||||
summePruefen(ergebnis);
|
||||
expect(ergebnis.abgewiesen[0]?.zeile).toBe(2);
|
||||
expect(ergebnis.abgewiesen[0]?.grund).toMatch(/kommt aus dem Verzeichnis/);
|
||||
|
||||
await expect(testPrisma.mitarbeiter.findFirstOrThrow()).resolves.toMatchObject({
|
||||
nachname: "Huber",
|
||||
abteilung: "Buchhaltung",
|
||||
titel: "Leitung",
|
||||
mail: null,
|
||||
});
|
||||
expect(await testPrisma.protokoll.count()).toBe(protokollVorher);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Mandantengrenze", () => {
|
||||
it("verwirft im Modus INTERN eine mitgeschickte Kundenkennung", async () => {
|
||||
const { admin, eigene } = await grundbestand();
|
||||
const fremder = await testPrisma.kunde.create({ data: { name: "Fremder", kuerzel: "FREMD" } });
|
||||
|
||||
await mitarbeiterImportieren([KOPF, "Anna,Muster,,,,amuster,,"].join("\n"), admin, fremder.id);
|
||||
|
||||
await expect(testPrisma.mitarbeiter.findFirstOrThrow()).resolves.toMatchObject({
|
||||
kundeId: eigene.id,
|
||||
});
|
||||
});
|
||||
|
||||
it("erkennt nur innerhalb desselben Kunden – und schreibt nicht für einen fremden", async () => {
|
||||
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 } });
|
||||
process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "lz-kunde-";
|
||||
const handelnder = { id: techniker.id, rolle: techniker.rolle };
|
||||
|
||||
// Dieselbe Kennung gibt es beim fremden Kunden schon. Sie darf hier nichts
|
||||
// treffen: sonst schriebe dieser Import in fremde Stammdaten.
|
||||
await testPrisma.mitarbeiter.create({
|
||||
data: {
|
||||
kundeId: fremder.id,
|
||||
vorname: "Anna",
|
||||
nachname: "Fremd",
|
||||
anzeigename: "Anna Fremd",
|
||||
sam: "amuster",
|
||||
},
|
||||
});
|
||||
|
||||
const ergebnis = await mitarbeiterImportieren(
|
||||
[KOPF, "Anna,Muster,,,,amuster,Vertrieb,"].join("\n"),
|
||||
handelnder,
|
||||
meiner.id,
|
||||
);
|
||||
expect(ergebnis).toMatchObject({ angelegt: 1 });
|
||||
summePruefen(ergebnis);
|
||||
await expect(
|
||||
testPrisma.mitarbeiter.findFirstOrThrow({ where: { kundeId: fremder.id } }),
|
||||
).resolves.toMatchObject({ nachname: "Fremd", abteilung: null });
|
||||
|
||||
// Und für einen Kunden, der diesem Techniker nicht gehört, geht gar nichts.
|
||||
await expect(
|
||||
mitarbeiterImportieren([KOPF, "Bert,Berg,,,,bberg,,"].join("\n"), handelnder, fremder.id),
|
||||
).rejects.toThrow(/Berechtigung/);
|
||||
expect(await testPrisma.mitarbeiter.count({ where: { kundeId: fremder.id } })).toBe(1);
|
||||
});
|
||||
|
||||
it("verlangt im Modus DIENSTLEISTUNG eine Kundenwahl", async () => {
|
||||
const { admin } = await grundbestand(Betriebsmodus.DIENSTLEISTUNG);
|
||||
await expect(
|
||||
mitarbeiterImportieren([KOPF, "Anna,Muster,,,,amuster,,"].join("\n"), admin),
|
||||
).rejects.toThrow(/Kunde/);
|
||||
expect(await testPrisma.mitarbeiter.count()).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user