import { beforeEach, describe, expect, it } from "vitest"; import { Betriebsmodus, LaufStatus, Rolle, SchrittZustand, VersionStatus } from "@/generated/prisma/enums"; import { LaufFehler, laufAbschliessen, laufAnlegen } from "@/server/services/laeufe"; import { schrittergebnisSetzen } from "@/server/services/schrittergebnisse"; import { KOMMENTAR_HOECHSTLAENGE, kommentarHinzufuegen } from "@/server/services/kommentare"; import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db"; /** * Kommentare an Läufen und Schrittergebnissen (#013). * * Ein Kommentar ist der Ort für das, was in kein Feld passt. Er trägt Urheber und * Zeitpunkt selbst, wird nie geändert — und kommt an einem archivierten Lauf * nicht mehr dazu. */ async function grundbestand() { const eigene = await testPrisma.kunde.create({ data: { name: "Eigene Organisation", kuerzel: "EIGEN", istEigeneOrganisation: true }, }); await testPrisma.einstellung.create({ data: { id: "global", modus: Betriebsmodus.INTERN } }); const adminZeile = await testPrisma.benutzer.create({ data: { email: "admin@firma.example", name: "Alex Admin", rolle: Rolle.ADMIN }, }); const technikerZeile = await testPrisma.benutzer.create({ data: { email: "technik@firma.example", name: "Tina Technik", rolle: Rolle.TECHNIKER }, }); return { eigene, admin: { id: adminZeile.id, rolle: Rolle.ADMIN }, techniker: { id: technikerZeile.id, rolle: Rolle.TECHNIKER }, }; } async function vorlageMitVersion() { const vorlage = await testPrisma.vorlage.create({ data: { name: "Offboarding" } }); const version = await testPrisma.vorlagenVersion.create({ data: { vorlageId: vorlage.id, version: 1, status: VersionStatus.ENTWURF }, }); const abschnitt = await testPrisma.vorlagenAbschnitt.create({ data: { versionId: version.id, titel: "Rückgabe", position: 0 }, }); const geraet = await testPrisma.vorlagenSchritt.create({ data: { abschnittId: abschnitt.id, titel: "Notebook zurück", pflicht: true, position: 0 }, }); await testPrisma.vorlagenVersion.update({ where: { id: version.id }, data: { status: VersionStatus.VEROEFFENTLICHT, veroeffentlichtAm: new Date() }, }); return { version, schritte: { geraet } }; } describe.runIf(datenbankVerfuegbar)("Kommentare", () => { beforeEach(async () => { await datenbankLeeren(); delete process.env["OIDC_CUSTOMER_GROUP_PREFIX"]; }); it("schreibt einen Kommentar an den Lauf", async () => { const { admin, techniker } = await grundbestand(); const { version } = await vorlageMitVersion(); const lauf = await laufAnlegen({ vorlagenVersionId: version.id }, admin); const kommentar = await kommentarHinzufuegen( { laufId: lauf.id, rumpf: " Kunde war nicht erreichbar, neuer Termin am Montag. " }, techniker, ); expect(kommentar.laufId).toBe(lauf.id); expect(kommentar.schrittergebnisId).toBeNull(); expect(kommentar.benutzerId).toBe(techniker.id); expect(kommentar.rumpf).toBe("Kunde war nicht erreichbar, neuer Termin am Montag."); }); it("schreibt einen Kommentar an ein Schrittergebnis", async () => { const { admin } = await grundbestand(); const { version, schritte } = await vorlageMitVersion(); const lauf = await laufAnlegen({ vorlagenVersionId: version.id }, admin); const ergebnis = await schrittergebnisSetzen( { laufId: lauf.id, vorlagenSchrittId: schritte.geraet.id, zustand: SchrittZustand.FEHLGESCHLAGEN, notiz: "Netzteil fehlt.", }, admin, ); const kommentar = await kommentarHinzufuegen( { laufId: lauf.id, schrittergebnisId: ergebnis.id, rumpf: "Ersatz ist bestellt." }, admin, ); expect(kommentar.schrittergebnisId).toBe(ergebnis.id); }); it("startet den Lauf nicht", async () => { const { admin } = await grundbestand(); const { version } = await vorlageMitVersion(); const lauf = await laufAnlegen({ vorlagenVersionId: version.id }, admin); await kommentarHinzufuegen({ laufId: lauf.id, rumpf: "Rückfrage an den Kunden gestellt." }, admin); // Eine Rückfrage zu notieren ist keine Abarbeitung. const nachher = await testPrisma.lauf.findUniqueOrThrow({ where: { id: lauf.id } }); expect(nachher.status).toBe(LaufStatus.OFFEN); expect(nachher.gestartetAm).toBeNull(); }); it("nimmt einen Kommentar an einem abgeschlossenen Lauf noch an", async () => { const { admin } = await grundbestand(); const { version, schritte } = await vorlageMitVersion(); const lauf = await laufAnlegen({ vorlagenVersionId: version.id }, admin); await schrittergebnisSetzen( { laufId: lauf.id, vorlagenSchrittId: schritte.geraet.id, zustand: SchrittZustand.ERLEDIGT }, admin, ); await laufAbschliessen(lauf.id, admin); // Das Nachfassen kommt nach dem Abschluss — gesperrt wird erst durch das // Archivieren (siehe laufMussVeraenderbarSein). const kommentar = await kommentarHinzufuegen( { laufId: lauf.id, rumpf: "Nachtrag: Netzteil ist eine Woche später eingetroffen." }, admin, ); expect(kommentar.id).toBeTruthy(); }); it("lehnt einen leeren und einen zu langen Kommentar ab", async () => { const { admin } = await grundbestand(); const { version } = await vorlageMitVersion(); const lauf = await laufAnlegen({ vorlagenVersionId: version.id }, admin); await expect(kommentarHinzufuegen({ laufId: lauf.id, rumpf: " " }, admin)).rejects.toThrow(LaufFehler); await expect( kommentarHinzufuegen({ laufId: lauf.id, rumpf: "x".repeat(KOMMENTAR_HOECHSTLAENGE + 1) }, admin), ).rejects.toThrow(new RegExp(String(KOMMENTAR_HOECHSTLAENGE))); expect(await testPrisma.kommentar.count()).toBe(0); }); it("lehnt ein Schrittergebnis eines anderen Laufs ab", async () => { const { admin } = await grundbestand(); const { version, schritte } = await vorlageMitVersion(); const einer = await laufAnlegen({ vorlagenVersionId: version.id }, admin); const anderer = await laufAnlegen({ vorlagenVersionId: version.id }, admin); const fremdesErgebnis = await testPrisma.schrittergebnis.findFirstOrThrow({ where: { laufId: anderer.id, vorlagenSchrittId: schritte.geraet.id }, }); // Die Kennung kommt von außen: ohne diese Prüfung hinge der Kommentar an // einem Schritt eines fremden Laufs. await expect( kommentarHinzufuegen( { laufId: einer.id, schrittergebnisId: fremdesErgebnis.id, rumpf: "Gehört hier nicht hin." }, admin, ), ).rejects.toThrow(/gehört nicht zu diesem Lauf/); expect(await testPrisma.kommentar.count()).toBe(0); }); it("lässt einen Techniker nicht an den Lauf eines fremden Kunden", async () => { process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "laufzettel-"; const eigene = await testPrisma.kunde.create({ data: { name: "Eigene Organisation", kuerzel: "EIGEN", istEigeneOrganisation: true }, }); await testPrisma.einstellung.create({ data: { id: "global", modus: Betriebsmodus.DIENSTLEISTUNG }, }); const adminZeile = await testPrisma.benutzer.create({ data: { email: "admin@firma.example", name: "Alex Admin", rolle: Rolle.ADMIN }, }); const technikerZeile = await testPrisma.benutzer.create({ data: { email: "technik@firma.example", name: "Tina Technik", rolle: Rolle.TECHNIKER }, }); const kundeA = await testPrisma.kunde.create({ data: { name: "Kunde A", kuerzel: "KA" } }); await testPrisma.benutzerKunde.create({ data: { benutzerId: technikerZeile.id, kundeId: eigene.id }, }); const { version } = await vorlageMitVersion(); const lauf = await laufAnlegen( { vorlagenVersionId: version.id, kundeId: kundeA.id }, { id: adminZeile.id, rolle: Rolle.ADMIN }, ); await expect( kommentarHinzufuegen( { laufId: lauf.id, rumpf: "Darf ich das sehen?" }, { id: technikerZeile.id, rolle: Rolle.TECHNIKER, }, ), ).rejects.toThrow(/Berechtigung/); expect(await testPrisma.kommentar.count()).toBe(0); }); });