Die Stammdaten, an denen die Historie haengt. Personen werden deaktiviert, Geraete und Abteilungen archiviert — geloescht wird nichts, woran ein Lauf haengen kann. - Mitarbeiterliste mit Suche ueber Anzeigename, Mail, UPN und sAMAccountName, Filter nach Abteilung, standardmaessig nur aktive; die Suche ist ein eigenes Bauteil, weil #013 und #014 sie wieder brauchen - Personenseite mit Stammdaten, Geraeten, Vorgesetztem; was aus dem Verzeichnis kommt, ist gesperrt — der naechste Abgleich wuerde es ohnehin zurueckdrehen - Abteilungen unter Admin: Ziel-OU, Standardgruppen, Mail-Domain, dazu die Namen, die bei Personen vorkommen, aber nicht gepflegt sind - Geraete mit Suche ueber Hostname, Bezeichnung, Seriennummer und Tag-Code; CSV-Import mit einem Ergebnis je Zeile - Neues Recht mitarbeiterBearbeiten; die Seitenleiste fuehrt Personen und Geraete unter einem Eintrag, im Modus Dienstleistung anders beschriftet Der unabhaengige Review fand zwei Blocker. Der schwerere meldete Erfolg: ein nicht geschlossenes Anfuehrungszeichen verschluckte im CSV-Import alle Folgezeilen bis zum naechsten `"`, und der Import sagte „3 von 3 Zeilen angelegt". Ein Wert reicht jetzt nicht mehr ueber die Zeile hinaus — fuer Hostnamen und Seriennummern ist ein Umbruch praktisch immer ein vergessenes Anfuehrungszeichen, und so endet der Schaden an seiner Zeile. Der zweite: ungepflegte Abteilungen wurden ueber alle Kunden hinweg gruppiert. Dazu sieben weitere Befunde, alle behoben — darunter eine Besitzerauswahl, die Personen fremder Kunden anbot, und ein Techniker, der ein Geraet endgueltig loeschen konnte. 610 Tests gruen, 18 E2E-Ablaeufe gegen den gebauten Stand. Schliesst #011 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
560 lines
24 KiB
TypeScript
560 lines
24 KiB
TypeScript
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);
|
||
});
|
||
});
|
||
});
|