feat(stammdaten): Mitarbeiter, Abteilungen und Geraete (#25)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

This commit was merged in pull request #25.
This commit is contained in:
2026-09-25 10:52:44 +00:00
parent e3d80e5982
commit f85bc87324
81 changed files with 6784 additions and 59 deletions
+559
View File
@@ -0,0 +1,559 @@
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);
});
});
});