import { beforeEach, describe, expect, it } from "vitest"; import { kundeHolen, kundenListe, kundenZurAuswahl } from "@/server/queries/kunden"; import { KundenFehler, kuerzelPruefen, kundeAnlegen, kundeArchivieren, kundeBearbeiten, type KundenAenderung, type NeuerKunde, } from "@/server/services/kunden"; import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db"; /** Die Lesemodelle verlangen einen Benutzer; ein Administrator sieht alles. */ const ADMIN = { id: "admin", rolle: "ADMIN" } as const; /** * Kundenverwaltung gegen die echte Datenbank (#008). * * Zwei Regeln haengen unmittelbar am Betriebsmodus und stehen deshalb hier fest: * die eigene Organisation entsteht nicht ueber `kundeAnlegen`, und sie laesst * sich nicht archivieren – sonst waere der Modus `INTERN` ohne Kunden. */ /** Urheber fuer die Protokolleintraege; ohne echten Benutzer greift der Fremdschluessel. */ async function adminAnlegen() { return testPrisma.benutzer.create({ data: { email: "admin@firma.example", name: "Administrator", rolle: "ADMIN" }, }); } async function eigeneOrganisationAnlegen() { return testPrisma.kunde.create({ data: { name: "Eigene Firma", kuerzel: "EIGEN", istEigeneOrganisation: true }, }); } /** Kleinste Vorlage, die ausreicht, um einen Lauf daran zu haengen. */ async function veroeffentlichteVersionAnlegen() { const vorlage = await testPrisma.vorlage.create({ data: { name: "Probe" } }); return testPrisma.vorlagenVersion.create({ data: { vorlageId: vorlage.id, version: 1, status: "VEROEFFENTLICHT", veroeffentlichtAm: new Date(), }, }); } describe.runIf(datenbankVerfuegbar)("Kundenverwaltung", () => { beforeEach(async () => { await datenbankLeeren(); }); describe("Kuerzel", () => { it("trimmt und schreibt gross", () => { expect(kuerzelPruefen("mus")).toBe("MUS"); expect(kuerzelPruefen(" REI ")).toBe("REI"); expect(kuerzelPruefen("mus ")).toBe("MUS"); }); it("weist alles ab, was nicht 2 bis 6 Zeichen A-Z und 0-9 ist", () => { // "MUeS" waere in Ordnung, ein Umlaut nicht: das Kuerzel steht auf // Etiketten und in Hostnamen. for (const falsch of ["X", "ZULANG7", "MU-S", "MU_S", "MÜS", "", " "]) { expect(() => kuerzelPruefen(falsch)).toThrow(KundenFehler); } }); it("sagt in der Meldung, was erwartet wird", () => { expect(() => kuerzelPruefen("X")).toThrow(/2 bis 6 Zeichen/); }); it("nimmt das getrimmte Kuerzel beim Anlegen", async () => { const admin = await adminAnlegen(); const kunde = await kundeAnlegen({ name: " Musterfirma ", kuerzel: " mus " }, admin.id); expect(kunde.kuerzel).toBe("MUS"); expect(kunde.name).toBe("Musterfirma"); }); it("weist ein doppeltes Kuerzel ab und benennt den vorhandenen Kunden", async () => { const admin = await adminAnlegen(); await kundeAnlegen({ name: "Musterfirma", kuerzel: "MUS" }, admin.id); await expect(kundeAnlegen({ name: "Muster GmbH", kuerzel: "mus" }, admin.id)).rejects.toThrow( /Musterfirma/, ); expect(await testPrisma.kunde.count()).toBe(1); }); it("weist ein doppeltes Kuerzel auch gegen einen archivierten Kunden ab", async () => { // Sonst waere das Kuerzel zweimal im Umlauf, sobald jemand den alten // Kunden zurueckholt – und der eindeutige Index kennt kein Archiv. const admin = await adminAnlegen(); const alt = await kundeAnlegen({ name: "Alte Firma", kuerzel: "ALT" }, admin.id); await kundeArchivieren(alt.id, true, admin.id); await expect(kundeAnlegen({ name: "Neue Firma", kuerzel: "ALT" }, admin.id)).rejects.toBeInstanceOf( KundenFehler, ); }); it("weist ein doppeltes Kuerzel auch beim Bearbeiten ab", async () => { const admin = await adminAnlegen(); await kundeAnlegen({ name: "Musterfirma", kuerzel: "MUS" }, admin.id); const rei = await kundeAnlegen({ name: "Baeckerei Reiter", kuerzel: "REI" }, admin.id); await expect(kundeBearbeiten(rei.id, { kuerzel: "MUS" }, admin.id)).rejects.toBeInstanceOf( KundenFehler, ); const unveraendert = await testPrisma.kunde.findUniqueOrThrow({ where: { id: rei.id } }); expect(unveraendert.kuerzel).toBe("REI"); }); }); describe("Name", () => { it("weist einen leeren Namen ab", async () => { const admin = await adminAnlegen(); for (const name of ["", " "]) { await expect(kundeAnlegen({ name, kuerzel: "MUS" }, admin.id)).rejects.toBeInstanceOf(KundenFehler); } expect(await testPrisma.kunde.count()).toBe(0); }); it("weist einen leeren Namen auch beim Bearbeiten ab", async () => { const admin = await adminAnlegen(); const kunde = await kundeAnlegen({ name: "Musterfirma", kuerzel: "MUS" }, admin.id); await expect(kundeBearbeiten(kunde.id, { name: " " }, admin.id)).rejects.toBeInstanceOf(KundenFehler); }); }); describe("Eigene Organisation", () => { it("entsteht nie ueber kundeAnlegen, egal was in der Eingabe steht", async () => { // Sie entsteht nur im Seed und beim Moduswechsel. Die Eingabe wird hier // absichtlich ueber die Typgrenze hinaus befuellt, wie es ein Aufrufer aus // einem Formular koennte. const admin = await adminAnlegen(); const eingabe = { name: "Untergeschoben", kuerzel: "UGS", istEigeneOrganisation: true } as NeuerKunde; const kunde = await kundeAnlegen(eingabe, admin.id); expect(kunde.istEigeneOrganisation).toBe(false); expect(await testPrisma.kunde.count({ where: { istEigeneOrganisation: true } })).toBe(0); }); it("laesst sich nicht archivieren", async () => { const admin = await adminAnlegen(); const eigene = await eigeneOrganisationAnlegen(); await expect(kundeArchivieren(eigene.id, true, admin.id)).rejects.toThrow(/einzige Kunde/); const unveraendert = await testPrisma.kunde.findUniqueOrThrow({ where: { id: eigene.id } }); expect(unveraendert.archiviert).toBe(false); expect(await testPrisma.protokoll.count({ where: { entitaetId: eigene.id } })).toBe(0); }); it("laesst sich umbenennen, ohne ihre Rolle zu verlieren", async () => { const admin = await adminAnlegen(); const eigene = await eigeneOrganisationAnlegen(); const geaendert = await kundeBearbeiten(eigene.id, { name: "Neuer Firmenname" }, admin.id); expect(geaendert.name).toBe("Neuer Firmenname"); expect(geaendert.istEigeneOrganisation).toBe(true); }); }); describe("Bearbeiten", () => { it("aendert istEigeneOrganisation nicht", async () => { const admin = await adminAnlegen(); const kunde = await kundeAnlegen({ name: "Musterfirma", kuerzel: "MUS" }, admin.id); const aenderung = { name: "Musterfirma AG", istEigeneOrganisation: true } as KundenAenderung; const geaendert = await kundeBearbeiten(kunde.id, aenderung, admin.id); expect(geaendert.name).toBe("Musterfirma AG"); expect(geaendert.istEigeneOrganisation).toBe(false); }); it("protokolliert das Umbenennen mit Vorher und Nachher", async () => { const admin = await adminAnlegen(); const kunde = await kundeAnlegen({ name: "Musterfirma", kuerzel: "MUS" }, admin.id); await kundeBearbeiten(kunde.id, { name: "Musterfirma AG" }, admin.id); const eintraege = await testPrisma.protokoll.findMany({ where: { entitaet: "Kunde", entitaetId: kunde.id, aktion: { contains: "geändert" } }, }); expect(eintraege).toHaveLength(1); expect(eintraege[0]?.vorher).toEqual({ name: "Musterfirma" }); expect(eintraege[0]?.nachher).toEqual({ name: "Musterfirma AG" }); expect(eintraege[0]?.benutzerId).toBe(admin.id); }); it("protokolliert eine Kuerzelaenderung mit Vorher und Nachher", async () => { const admin = await adminAnlegen(); const kunde = await kundeAnlegen({ name: "Musterfirma", kuerzel: "MUS" }, admin.id); await kundeBearbeiten(kunde.id, { kuerzel: "mu2" }, admin.id); const eintraege = await testPrisma.protokoll.findMany({ where: { entitaetId: kunde.id, aktion: { contains: "Kürzel" } }, }); expect(eintraege).toHaveLength(1); expect(eintraege[0]?.vorher).toEqual({ kuerzel: "MUS" }); expect(eintraege[0]?.nachher).toEqual({ kuerzel: "MU2" }); }); it("schreibt nichts, wenn sich nichts aendert", async () => { const admin = await adminAnlegen(); const kunde = await kundeAnlegen( { name: "Musterfirma", kuerzel: "MUS", notizen: "Zwei Standorte" }, admin.id, ); await kundeBearbeiten( kunde.id, // Auch die Schreibweise des Kuerzels macht keinen Unterschied, weil es // vor dem Vergleich normalisiert wird. { name: " Musterfirma ", kuerzel: "mus", notizen: "Zwei Standorte" }, admin.id, ); expect( await testPrisma.protokoll.count({ where: { entitaetId: kunde.id, aktion: { contains: "geändert" } }, }), ).toBe(0); }); it("loescht Notizen mit null", async () => { const admin = await adminAnlegen(); const kunde = await kundeAnlegen({ name: "Musterfirma", kuerzel: "MUS", notizen: "Alt" }, admin.id); const geaendert = await kundeBearbeiten(kunde.id, { notizen: null }, admin.id); expect(geaendert.notizen).toBeNull(); }); it("weist eine unbekannte Kennung mit einer eigenen Meldung ab", async () => { const admin = await adminAnlegen(); await expect(kundeBearbeiten("gibtsnicht", { name: "Egal" }, admin.id)).rejects.toBeInstanceOf( KundenFehler, ); }); }); describe("Archivieren", () => { it("archiviert und gibt wieder frei, je mit Protokolleintrag", async () => { const admin = await adminAnlegen(); const kunde = await kundeAnlegen({ name: "Musterfirma", kuerzel: "MUS" }, admin.id); await kundeArchivieren(kunde.id, true, admin.id); expect((await testPrisma.kunde.findUniqueOrThrow({ where: { id: kunde.id } })).archiviert).toBe(true); await kundeArchivieren(kunde.id, false, admin.id); expect((await testPrisma.kunde.findUniqueOrThrow({ where: { id: kunde.id } })).archiviert).toBe(false); const eintraege = await testPrisma.protokoll.findMany({ where: { entitaet: "Kunde", entitaetId: kunde.id, aktion: { contains: "Kunde " } }, orderBy: { erstelltAm: "asc" }, }); // Angelegt, archiviert, wieder freigegeben. expect(eintraege.map((eintrag) => eintrag.aktion)).toEqual([ "Kunde angelegt", "Kunde archiviert", "Kunde wieder freigegeben", ]); expect(eintraege[1]?.vorher).toEqual({ archiviert: false }); expect(eintraege[1]?.nachher).toEqual({ archiviert: true }); expect(eintraege[2]?.nachher).toEqual({ archiviert: false }); }); it("schreibt nichts, wenn der Zustand schon stimmt", async () => { const admin = await adminAnlegen(); const kunde = await kundeAnlegen({ name: "Musterfirma", kuerzel: "MUS" }, admin.id); await kundeArchivieren(kunde.id, false, admin.id); expect(await testPrisma.protokoll.count({ where: { entitaetId: kunde.id } })).toBe(1); }); }); describe("Lesemodelle", () => { /** Ein Kunde mit zwei Geraeten, einem Mitarbeiter und einem Lauf. */ async function kundeMitBestandAnlegen(adminId: string) { const kunde = await kundeAnlegen({ name: "Musterfirma", kuerzel: "MUS" }, adminId); await testPrisma.geraet.create({ data: { kundeId: kunde.id, tagCode: "LZ-7K3F9Q" } }); await testPrisma.geraet.create({ data: { kundeId: kunde.id, tagCode: "LZ-M2XH4T" } }); await testPrisma.mitarbeiter.create({ data: { kundeId: kunde.id, vorname: "Maria", nachname: "Huber", anzeigename: "Maria Huber" }, }); const version = await veroeffentlichteVersionAnlegen(); await testPrisma.lauf.create({ data: { vorlagenVersionId: version.id, titel: "PC Neuinstallation", kundeId: kunde.id, erstelltVonId: adminId, }, }); return kunde; } it("zaehlt Geraete, Mitarbeiter und Laeufe je Kunde", async () => { const admin = await adminAnlegen(); const mus = await kundeMitBestandAnlegen(admin.id); await kundeAnlegen({ name: "Baeckerei Reiter", kuerzel: "REI" }, admin.id); const liste = await kundenListe(ADMIN); const musZeile = liste.find((zeile) => zeile.id === mus.id); expect(musZeile?.anzahlGeraete).toBe(2); expect(musZeile?.anzahlMitarbeiter).toBe(1); expect(musZeile?.anzahlLaeufe).toBe(1); const reiZeile = liste.find((zeile) => zeile.kuerzel === "REI"); expect(reiZeile?.anzahlGeraete).toBe(0); expect(reiZeile?.anzahlLaeufe).toBe(0); }); it("sortiert nicht archivierte zuerst, dann nach Namen", async () => { const admin = await adminAnlegen(); const zeta = await kundeAnlegen({ name: "Zeta Werke", kuerzel: "ZET" }, admin.id); const alpha = await kundeAnlegen({ name: "Alpha AG", kuerzel: "ALP" }, admin.id); const beta = await kundeAnlegen({ name: "Beta GmbH", kuerzel: "BET" }, admin.id); await kundeArchivieren(alpha.id, true, admin.id); const liste = await kundenListe(ADMIN, true); expect(liste.map((zeile) => zeile.id)).toEqual([beta.id, zeta.id, alpha.id]); }); it("blendet archivierte aus, solange nicht danach gefragt wird", async () => { const admin = await adminAnlegen(); const mus = await kundeAnlegen({ name: "Musterfirma", kuerzel: "MUS" }, admin.id); const alt = await kundeAnlegen({ name: "Alte Firma", kuerzel: "ALT" }, admin.id); await kundeArchivieren(alt.id, true, admin.id); expect((await kundenListe(ADMIN, false)).map((zeile) => zeile.id)).toEqual([mus.id]); expect((await kundenListe(ADMIN)).map((zeile) => zeile.id)).toEqual([mus.id]); expect((await kundenListe(ADMIN, true)).map((zeile) => zeile.id)).toContain(alt.id); }); it("liefert einen einzelnen Kunden mit Zaehlungen, sonst undefined", async () => { const admin = await adminAnlegen(); const mus = await kundeMitBestandAnlegen(admin.id); const zeile = await kundeHolen(mus.id, ADMIN); expect(zeile?.kuerzel).toBe("MUS"); expect(zeile?.anzahlGeraete).toBe(2); expect(zeile?.istEigeneOrganisation).toBe(false); expect(await kundeHolen("gibtsnicht", ADMIN)).toBeUndefined(); }); it("nennt im Auswahlfeld keine archivierten Kunden", async () => { const admin = await adminAnlegen(); await kundeAnlegen({ name: "Musterfirma", kuerzel: "MUS" }, admin.id); const alt = await kundeAnlegen({ name: "Alte Firma", kuerzel: "ALT" }, admin.id); await kundeArchivieren(alt.id, true, admin.id); const auswahl = await kundenZurAuswahl(ADMIN); expect(auswahl.map((eintrag) => eintrag.kuerzel)).toEqual(["MUS"]); }); }); });