import { beforeEach, describe, expect, it } from "vitest"; import { Betriebsmodus, GeraeteTyp, Rolle } from "@/generated/prisma/enums"; import { istTagCode } from "@/lib/tagcode"; import { GERAETE_IMPORT_HOECHSTZEILEN, GeraeteFehler, geraeteImportieren, geraetAnlegen, geraetArchivieren, geraetBearbeiten, geraetLoeschen, } from "@/server/services/geraete"; import { besitzerKandidaten, geraetHolen, geraeteListe } from "@/server/queries/geraete"; import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db"; /** * Geräte, Tag-Codes und der CSV-Import (#011). * * Festgehalten werden die Zusagen aus dem Auftrag: ein registriertes Gerät hat * einen Namen, ein `tagCode` ändert sich nie und kommt nie zurück, an einem * Gerät mit Läufen wird nichts gelöscht, und eine kaputte CSV-Zeile bricht den * Import nicht ab. Dazu die Mandantensicht des Lesemodells — im Review von #009 * war genau dort ein Suchfilter, der die Kundenbedingung überschrieben hat. */ 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: Rolle.ADMIN }, }); return { eigene, admin: { id: benutzer.id, rolle: Rolle.ADMIN } }; } /** Ein Lauf an einem Gerät – mehr braucht es dafür nicht als eine veröffentlichte Version. */ async function laufAnlegen(kundeId: string, geraetId: string) { 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(), }, }); return testPrisma.lauf.create({ data: { vorlagenVersionId: version.id, titel: "Neuinstallation", kundeId, geraetId }, }); } describe.runIf(datenbankVerfuegbar)("Geräte", () => { beforeEach(async () => { await datenbankLeeren(); delete process.env["OIDC_CUSTOMER_GROUP_PREFIX"]; }); describe("Anlegen", () => { it("erzeugt einen Tag-Code und schreibt ihn ins Vergabebuch", async () => { const { admin, eigene } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-VERTRIEB-07", typ: GeraeteTyp.PC }, admin); expect(istTagCode(geraet.tagCode)).toBe(true); expect(geraet.kundeId).toBe(eigene.id); expect(geraet.registriertAm).not.toBeNull(); // Beides in einer Transaktion: ein Code ohne Eintrag wäre nach dem Löschen // des Geräts wieder frei. await expect( testPrisma.tagCodeVergabe.findUnique({ where: { tagCode: geraet.tagCode } }), ).resolves.not.toBeNull(); }); it("lehnt ein registriertes Gerät ohne Hostname und ohne Bezeichnung ab", async () => { const { admin } = await grundbestand(); await expect(geraetAnlegen({ seriennummer: "SN-1" }, admin)).rejects.toThrow(GeraeteFehler); await expect(geraetAnlegen({ hostname: " ", bezeichnung: "" }, admin)).rejects.toThrow( /Hostname oder eine Bezeichnung/, ); // Und es ist auch kein Tag-Code verbraucht worden. expect(await testPrisma.tagCodeVergabe.count()).toBe(0); expect(await testPrisma.geraet.count()).toBe(0); }); it("erlaubt einen vorgedruckten Tag ohne jeden Namen", async () => { const { admin } = await grundbestand(); const tag = await geraetAnlegen({ registrieren: false }, admin); expect(tag.registriertAm).toBeNull(); expect(tag.hostname).toBeNull(); expect(tag.bezeichnung).toBeNull(); }); it("nimmt einen Besitzer nur, wenn er zum selben Kunden gehört", async () => { const { admin, eigene } = await grundbestand(); const fremder = await testPrisma.kunde.create({ data: { name: "Fremder", kuerzel: "FREMD" } }); const eigenePerson = await testPrisma.mitarbeiter.create({ data: { kundeId: eigene.id, vorname: "Ada", nachname: "Lovelace", anzeigename: "Ada Lovelace" }, }); const fremdePerson = await testPrisma.mitarbeiter.create({ data: { kundeId: fremder.id, vorname: "Max", nachname: "Fremd", anzeigename: "Max Fremd" }, }); const geraet = await geraetAnlegen( { hostname: "NB-ADA", typ: GeraeteTyp.NOTEBOOK, mitarbeiterId: eigenePerson.id }, admin, ); expect(geraet.mitarbeiterId).toBe(eigenePerson.id); await expect( geraetAnlegen({ hostname: "NB-MAX", mitarbeiterId: fremdePerson.id }, admin), ).rejects.toThrow(/anderen Kunden/); }); it("protokolliert das Anlegen", async () => { const { admin } = await grundbestand(); const geraet = await geraetAnlegen({ bezeichnung: "Serverschrank Keller" }, admin); const eintraege = await testPrisma.protokoll.findMany({ where: { entitaet: "Geraet", entitaetId: geraet.id }, }); expect(eintraege).toHaveLength(1); expect(eintraege[0]?.aktion).toBe("Gerät angelegt"); expect(eintraege[0]?.nachher).toMatchObject({ tagCode: geraet.tagCode }); }); }); describe("Bearbeiten", () => { it("weist jeden Änderungsversuch am Tag-Code ab", async () => { const { admin } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-1" }, admin); await expect( geraetBearbeiten(geraet.id, { tagCode: "LZ-ZZZZZZ", hostname: "PC-2" }, admin), ).rejects.toThrow(/bleibt, wie er ist/); // Nichts ist passiert – auch nicht der Hostname, der im selben Aufruf stand. const frisch = await testPrisma.geraet.findUniqueOrThrow({ where: { id: geraet.id } }); expect(frisch.tagCode).toBe(geraet.tagCode); expect(frisch.hostname).toBe("PC-1"); }); it("lässt den unveränderten Tag-Code mitkommen, ohne sich zu beschweren", async () => { const { admin } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-1" }, admin); const nachher = await geraetBearbeiten(geraet.id, { tagCode: geraet.tagCode, hostname: "PC-2" }, admin); expect(nachher.hostname).toBe("PC-2"); }); it("lässt den Hostname nicht leeren, wenn dann kein Name übrig bleibt", async () => { const { admin } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-1" }, admin); await expect(geraetBearbeiten(geraet.id, { hostname: "" }, admin)).rejects.toThrow( /Hostname oder eine Bezeichnung/, ); // Mit einer Bezeichnung als Ersatz geht es. const nachher = await geraetBearbeiten( geraet.id, { hostname: "", bezeichnung: "Kiste im Regal" }, admin, ); expect(nachher.hostname).toBeNull(); expect(nachher.bezeichnung).toBe("Kiste im Regal"); }); it("protokolliert nur, was sich geändert hat", async () => { const { admin } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-1", seriennummer: "SN-1" }, admin); await geraetBearbeiten(geraet.id, { hostname: "PC-2", seriennummer: "SN-1" }, admin); const eintrag = await testPrisma.protokoll.findFirstOrThrow({ where: { entitaet: "Geraet", entitaetId: geraet.id, aktion: { contains: "geändert" } }, }); expect(eintrag.aktion).toBe("Hostname geändert"); expect(eintrag.vorher).toMatchObject({ Hostname: "PC-1" }); expect(eintrag.nachher).toMatchObject({ Hostname: "PC-2" }); expect(eintrag.nachher).not.toHaveProperty("Seriennummer"); }); it("schreibt gar nichts, wenn sich nichts geändert hat", async () => { const { admin } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-1" }, admin); await geraetBearbeiten(geraet.id, { hostname: "PC-1" }, admin); expect(await testPrisma.protokoll.count({ where: { aktion: { contains: "geändert" } } })).toBe(0); }); it("registriert einen vorgedruckten Tag, sobald er einen Namen bekommt", async () => { const { admin } = await grundbestand(); const tag = await geraetAnlegen({ registrieren: false }, admin); await expect(geraetBearbeiten(tag.id, { registrieren: true }, admin)).rejects.toThrow( /Hostname oder eine Bezeichnung/, ); const nachher = await geraetBearbeiten(tag.id, { registrieren: true, hostname: "PC-NEU" }, admin); expect(nachher.registriertAm).not.toBeNull(); expect(nachher.tagCode).toBe(tag.tagCode); }); }); describe("Archivieren und Löschen", () => { it("lässt ein Gerät mit Läufen nicht löschen, nur archivieren", async () => { const { admin, eigene } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-1" }, admin); await laufAnlegen(eigene.id, geraet.id); await expect(geraetLoeschen(geraet.id, admin)).rejects.toThrow(/nur archivieren/); expect(await testPrisma.geraet.count()).toBe(1); await geraetArchivieren(geraet.id, true, admin); const frisch = await testPrisma.geraet.findUniqueOrThrow({ where: { id: geraet.id } }); expect(frisch.archiviert).toBe(true); // Und zurück. await geraetArchivieren(geraet.id, false, admin); await expect(testPrisma.geraet.findUniqueOrThrow({ where: { id: geraet.id } })).resolves.toMatchObject({ archiviert: false, }); }); it("gibt einen Tag-Code auch nach dem Löschen nicht zurück", async () => { const { admin } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-WEG" }, admin); await geraetLoeschen(geraet.id, admin); expect(await testPrisma.geraet.count()).toBe(0); // Das Vergabebuch behält ihn: ein übrig gebliebener Aufkleber darf nie auf // ein zweites Objekt zeigen. await expect( testPrisma.tagCodeVergabe.findUnique({ where: { tagCode: geraet.tagCode } }), ).resolves.not.toBeNull(); }); it("protokolliert das Löschen, und der Eintrag überlebt das Gerät", async () => { const { admin } = await grundbestand(); const geraet = await geraetAnlegen({ hostname: "PC-WEG" }, admin); await geraetLoeschen(geraet.id, admin); const eintrag = await testPrisma.protokoll.findFirstOrThrow({ where: { entitaetId: geraet.id, aktion: "Gerät gelöscht" }, }); expect(eintrag.vorher).toMatchObject({ tagCode: geraet.tagCode, hostname: "PC-WEG" }); }); }); describe("CSV-Import", () => { it("verliert keine Zeile an einem kaputten Anführungszeichen", async () => { /* * Der Blocker aus dem Review: ein Anführungszeichen, das in einer Zelle * beginnt und nicht schliesst, verschluckte alle Folgezeilen bis zum * naechsten `"`. Der Import meldete dafuer **Erfolg** — drei von drei * Zeilen angelegt —, und in einer Bezeichnung stand der Text der * verschluckten Zeilen. Jetzt endet der Schaden an seiner Zeile. */ const { admin } = await grundbestand(); const text = [ "hostname,label,serialNumber", "PC-1,,SN-1", 'PC-2,"Regal,SN-2', "PC-3,,SN-3", 'PC-4,"Kiste",SN-4', "PC-5,,SN-5", ].join("\n"); const ergebnis = await geraeteImportieren(text, admin); expect(ergebnis.angelegt).toBe(4); expect(ergebnis.abgewiesen).toHaveLength(1); expect(ergebnis.abgewiesen[0]?.zeile).toBe(3); expect(ergebnis.abgewiesen[0]?.grund).toMatch(/Anführungszeichen/); const namen = (await testPrisma.geraet.findMany({ select: { hostname: true } })) .map((zeile) => zeile.hostname) .sort(); expect(namen).toEqual(["PC-1", "PC-3", "PC-4", "PC-5"]); }); it("weist eine doppelte Zeile ab, statt den Bestand zu verdoppeln", async () => { // Dieselbe Datei zweimal eingelesen verdoppelte sonst alles – und weil ein // tagCode nie zurueckkommt, waeren die Aufkleber dafuer schon gedruckt. const { admin } = await grundbestand(); const text = ["hostname", "PC-DOPPELT", "PC-DOPPELT", "PC-EINMAL"].join("\n"); const erster = await geraeteImportieren(text, admin); expect(erster.angelegt).toBe(2); expect(erster.abgewiesen[0]?.grund).toMatch(/schon weiter oben/); const zweiter = await geraeteImportieren(text, admin); expect(zweiter.angelegt).toBe(0); expect(zweiter.abgewiesen).toHaveLength(3); expect(zweiter.abgewiesen[0]?.grund).toMatch(/gibt es schon/); expect(await testPrisma.geraet.count()).toBe(2); }); const KOPF = "hostname,label,serialNumber,inventoryNumber,type,model"; it("legt die guten Zeilen an und meldet die kaputten einzeln", async () => { const { admin } = await grundbestand(); const text = [ KOPF, "PC-BUERO-01,,SN-1,INV-1,PC,OptiPlex", ",Serverschrank Keller,,,SONSTIGES,", "PC-KAPUTT,,SN-3,,Tablet,", // unbekannter Typ ",,SN-4,,PC,", // weder Hostname noch Bezeichnung "NB-05,,SN-5,,notebook,Latitude", // Schreibweise unerheblich ].join("\r\n"); const ergebnis = await geraeteImportieren(text, admin); expect(ergebnis.gesamt).toBe(5); expect(ergebnis.angelegt).toBe(3); // Zeilennummern wie in der Datei: Kopfzeile ist 1. expect(ergebnis.abgewiesen.map((a) => a.zeile)).toEqual([4, 5]); expect(ergebnis.abgewiesen[0]?.grund).toMatch(/kein bekannter Gerätetyp/); expect(ergebnis.abgewiesen[1]?.grund).toMatch(/Weder ein Hostname noch eine Bezeichnung/); const geraete = await testPrisma.geraet.findMany({ orderBy: { tagCode: "asc" } }); expect(geraete.map((g) => g.hostname ?? g.bezeichnung).sort()).toEqual([ "NB-05", "PC-BUERO-01", "Serverschrank Keller", ]); expect(geraete.find((g) => g.hostname === "NB-05")?.typ).toBe(GeraeteTyp.NOTEBOOK); }); it("erzeugt je Zeile einen eigenen Tag-Code und trägt jeden ein", async () => { const { admin } = await grundbestand(); const text = [KOPF, "PC-1,,,,PC,", "PC-2,,,,PC,", "PC-3,,,,PC,"].join("\n"); await geraeteImportieren(text, admin); const codes = (await testPrisma.geraet.findMany({ select: { tagCode: true } })).map((g) => g.tagCode); expect(codes).toHaveLength(3); expect(new Set(codes).size).toBe(3); expect(codes.every(istTagCode)).toBe(true); expect(await testPrisma.tagCodeVergabe.count()).toBe(3); }); it("versteht deutsches Excel: Semikolon, BOM und deutsche Spaltennamen", async () => { const { admin } = await grundbestand(); const text = "Hostname;Bezeichnung;Seriennummer;Inventarnummer;Typ;Modell\r\n" + 'PC-EXCEL;;"SN;mit;Semikolon";INV-9;Drucker;"Kyocera ""P3260"""\r\n'; const ergebnis = await geraeteImportieren(text, admin); expect(ergebnis.angelegt).toBe(1); const geraet = await testPrisma.geraet.findFirstOrThrow(); expect(geraet.seriennummer).toBe("SN;mit;Semikolon"); expect(geraet.modell).toBe('Kyocera "P3260"'); expect(geraet.typ).toBe(GeraeteTyp.DRUCKER); }); it("setzt ohne Typspalte den Standard, ohne etwas zu erfinden", async () => { const { admin } = await grundbestand(); const ergebnis = await geraeteImportieren("hostname\nPC-OHNE-TYP\n", admin); expect(ergebnis.angelegt).toBe(1); await expect(testPrisma.geraet.findFirstOrThrow()).resolves.toMatchObject({ typ: GeraeteTyp.SONSTIGES, }); }); it("weist eine Datei ohne Namensspalte als Ganzes ab", async () => { const { admin } = await grundbestand(); await expect(geraeteImportieren("serialNumber,type\nSN-1,PC\n", admin)).rejects.toThrow(/hostname/); expect(await testPrisma.geraet.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: GERAETE_IMPORT_HOECHSTZEILEN + 1 }, (_, i) => `PC-${i},,,,PC,`); await expect(geraeteImportieren([KOPF, ...zeilen].join("\n"), admin)).rejects.toThrow( new RegExp(`mehr als ${GERAETE_IMPORT_HOECHSTZEILEN} Zeilen`), ); expect(await testPrisma.geraet.count()).toBe(0); }); it("protokolliert jede eingelesene Zeile mit ihrer Nummer", async () => { const { admin } = await grundbestand(); await geraeteImportieren([KOPF, "PC-1,,,,PC,"].join("\n"), admin); const eintrag = await testPrisma.protokoll.findFirstOrThrow({ where: { aktion: "Gerät aus CSV angelegt" }, }); expect(eintrag.nachher).toMatchObject({ hostname: "PC-1", zeile: 2 }); }); }); describe("Lesemodell und Filter", () => { it("findet über Hostname, Bezeichnung, Seriennummer und Tag-Code", async () => { const { admin } = await grundbestand(); const pc = await geraetAnlegen( { hostname: "PC-BUERO-01", seriennummer: "SN-4711", typ: GeraeteTyp.PC }, admin, ); await geraetAnlegen({ bezeichnung: "Serverschrank Keller", typ: GeraeteTyp.SERVER }, admin); const namen = async (suche: string) => (await geraeteListe(admin, { suche })).map((zeile) => zeile.anzeige); expect(await namen("buero")).toEqual(["PC-BUERO-01"]); expect(await namen("keller")).toEqual(["Serverschrank Keller"]); expect(await namen("4711")).toEqual(["PC-BUERO-01"]); // Die sechs Zeichen ohne das Präfix genügen – abgetippt vom Aufkleber. expect(await namen(pc.tagCode.slice(3))).toEqual(["PC-BUERO-01"]); }); it("filtert nach Typ und zeigt Archivierte nur auf Wunsch", async () => { const { admin } = await grundbestand(); await geraetAnlegen({ hostname: "PC-1", typ: GeraeteTyp.PC }, admin); const drucker = await geraetAnlegen({ bezeichnung: "Drucker Flur", typ: GeraeteTyp.DRUCKER }, admin); await geraetArchivieren(drucker.id, true, admin); expect((await geraeteListe(admin)).map((z) => z.anzeige)).toEqual(["PC-1"]); expect((await geraeteListe(admin, { auchArchivierte: true })).map((z) => z.anzeige)).toEqual([ "PC-1", "Drucker Flur", ]); expect( (await geraeteListe(admin, { typ: GeraeteTyp.DRUCKER, auchArchivierte: true })).map((z) => z.anzeige), ).toEqual(["Drucker Flur"]); expect(await geraeteListe(admin, { typ: GeraeteTyp.FIREWALL })).toEqual([]); }); it("zeigt Besitzer und Anzahl der Läufe", async () => { const { admin, eigene } = await grundbestand(); const person = await testPrisma.mitarbeiter.create({ data: { kundeId: eigene.id, vorname: "Ada", nachname: "Lovelace", anzeigename: "Ada Lovelace" }, }); const geraet = await geraetAnlegen({ hostname: "NB-ADA", mitarbeiterId: person.id }, admin); await laufAnlegen(eigene.id, geraet.id); const zeile = await geraetHolen(geraet.id, admin); expect(zeile?.besitzerName).toBe("Ada Lovelace"); expect(zeile?.besitzerAktiv).toBe(true); expect(zeile?.anzahlLaeufe).toBe(1); }); }); /** * Die Mandantensicht. Der wichtigste Test dieser Datei: ein Suchbegriff darf * die Kundenbedingung nicht aushebeln. Vor #009 tat er das, weil zwei `OR` im * selben Objektliteral standen und das zweite gewann. */ describe("Mandantensicht", () => { async function zweiKunden() { const { eigene } = await grundbestand(Betriebsmodus.DIENSTLEISTUNG); process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "lz-kunde-"; 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: Rolle.TECHNIKER }, }); await testPrisma.benutzerKunde.create({ data: { benutzerId: techniker.id, kundeId: meiner.id }, }); await testPrisma.geraet.create({ data: { kundeId: meiner.id, tagCode: "LZ-AAAAAA", hostname: "GEHEIM-MEIN", registriertAm: new Date(), }, }); await testPrisma.geraet.create({ data: { kundeId: fremder.id, tagCode: "LZ-BBBBBB", hostname: "GEHEIM-FREMD", registriertAm: new Date(), }, }); return { eigene, meiner, fremder, techniker: { id: techniker.id, rolle: Rolle.TECHNIKER } }; } it("zeigt einem gebundenen Techniker nur die Geräte seiner Kunden", async () => { const { techniker } = await zweiKunden(); expect((await geraeteListe(techniker)).map((z) => z.anzeige)).toEqual(["GEHEIM-MEIN"]); }); it("lässt sich die Kundengrenze nicht durch einen Suchbegriff aushebeln", async () => { const { techniker } = await zweiKunden(); // Genau der Fall aus dem Review: die Suche bringt ein eigenes `OR` mit. expect(await geraeteListe(techniker, { suche: "GEHEIM-FREMD" })).toEqual([]); expect(await geraeteListe(techniker, { suche: "GEHEIM" })).toHaveLength(1); expect(await geraeteListe(techniker, { suche: "LZ-BBBBBB" })).toEqual([]); // Und auch nicht mit Typ- und Archivfilter zusammen. expect( await geraeteListe(techniker, { suche: "GEHEIM", auchArchivierte: true, typ: GeraeteTyp.SONSTIGES }), ).toHaveLength(1); }); it("gibt ein fremdes Gerät auch beim Einzelabruf nicht heraus", async () => { const { techniker, fremder } = await zweiKunden(); const fremdesGeraet = await testPrisma.geraet.findFirstOrThrow({ where: { kundeId: fremder.id }, }); expect(await geraetHolen(fremdesGeraet.id, techniker)).toBeUndefined(); }); it("schlägt als Besitzer nur aktive Personen sichtbarer Kunden vor", async () => { const { techniker, meiner, fremder } = await zweiKunden(); await testPrisma.mitarbeiter.createMany({ data: [ { kundeId: meiner.id, vorname: "A", nachname: "Aktiv", anzeigename: "Anna Aktiv" }, { kundeId: meiner.id, vorname: "D", nachname: "Weg", anzeigename: "Dieter Weg", aktiv: false, }, { kundeId: fremder.id, vorname: "F", nachname: "Fremd", anzeigename: "Frida Fremd" }, ], }); const auswahl = await besitzerKandidaten(techniker); expect(auswahl.kandidaten.map((k) => k.anzeigename)).toEqual(["Anna Aktiv"]); expect(auswahl.gekuerzt).toBe(false); }); it("nennt bei jedem Kandidaten den Kunden, damit die Auswahl ihn filtern kann", async () => { /* * Der Befund aus dem Review: die Seite holt die Kandidaten **einmal** und * gab sie in jede Karte. Ein Administrator sieht alle Kunden — die * Auswahlliste bot damit Personen an, die der Dienst danach abweist, weil * ein Geraet nur einer Person desselben Kunden gehoeren kann. Ohne * `kundeId` am Kandidaten laesst sich das in der Oberflaeche nicht * verhindern. */ const { meiner, fremder } = await zweiKunden(); const admin = await testPrisma.benutzer.create({ data: { email: "a2@firma.example", name: "Admin", rolle: Rolle.ADMIN }, }); await testPrisma.mitarbeiter.createMany({ data: [ { kundeId: meiner.id, vorname: "A", nachname: "A", anzeigename: "Anna A" }, { kundeId: fremder.id, vorname: "B", nachname: "B", anzeigename: "Bert B" }, ], }); const auswahl = await besitzerKandidaten({ id: admin.id, rolle: admin.rolle }); expect(auswahl.kandidaten.map((k) => k.anzeigename)).toEqual(["Anna A", "Bert B"]); expect(auswahl.kandidaten.map((k) => k.kundeId).sort()).toEqual([meiner.id, fremder.id].sort()); // Was die Oberflaeche daraus macht: nur die des eigenen Kunden. expect(auswahl.kandidaten.filter((k) => k.kundeId === meiner.id).map((k) => k.anzeigename)).toEqual([ "Anna A", ]); }); it("sagt, wenn die Besitzerauswahl gekürzt wurde", async () => { const { techniker, meiner } = await zweiKunden(); await testPrisma.mitarbeiter.createMany({ data: Array.from({ length: 3 }, (_, i) => ({ kundeId: meiner.id, vorname: "P", nachname: `Nummer ${i}`, anzeigename: `Person ${i}`, })), }); const auswahl = await besitzerKandidaten(techniker, 2); expect(auswahl.kandidaten).toHaveLength(2); expect(auswahl.gekuerzt).toBe(true); }); }); });