import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { Betriebsmodus, VersionStatus } from "@/generated/prisma/enums"; import { RECHTE } from "@/server/auth/rollen"; import { protokollDesLaufs, protokollSeite, protokollUrheber } from "@/server/queries/protokoll"; import { datenbankLeeren, datenbankVerfuegbar, testPrisma } from "./helpers/db"; /** * Das Protokoll ansehen (#023). * * Drei Fragen tragen das Ganze: sieht jemand nur seine Kunden, kommt ein * Techniker ueberhaupt heran, und steht irgendwo ein erzeugtes Passwort. */ /** * Ein auf Kunden beschraenkter Benutzer entsteht nur unter drei Bedingungen — * Modus DIENSTLEISTUNG, Rolle unter REDAKTEUR, und ein gesetztes * `OIDC_CUSTOMER_GROUP_PREFIX`. Ohne die dritte liefert `sichtbareKundenIds` * `undefined`, und jeder Filtertest waere ein Test ohne Gegenstand. */ const VORHER = process.env["OIDC_CUSTOMER_GROUP_PREFIX"]; async function aufbau() { const eigene = await testPrisma.kunde.create({ data: { name: "Eigene Organisation", kuerzel: "EIGEN", istEigeneOrganisation: true }, }); const fremd = await testPrisma.kunde.create({ data: { name: "Fremd GmbH", kuerzel: "FREMD" } }); await testPrisma.einstellung.create({ data: { id: "global", modus: Betriebsmodus.DIENSTLEISTUNG }, }); const admin = await testPrisma.benutzer.create({ data: { email: "admin@firma.example", name: "Admin", rolle: "ADMIN" }, }); const techniker = await testPrisma.benutzer.create({ data: { email: "tech@firma.example", name: "Techniker", rolle: "TECHNIKER" }, }); // Nur an die eigene Organisation gebunden – „Fremd GmbH“ ist für ihn unsichtbar. await testPrisma.benutzerKunde.create({ data: { benutzerId: techniker.id, kundeId: eigene.id }, }); return { eigene, fremd, admin: { id: admin.id, rolle: admin.rolle }, techniker: { id: techniker.id, rolle: techniker.rolle }, }; } describe("Recht auf das Protokoll", () => { it("liegt bei verwaltung, also nur bei ADMIN", () => { // Das Protokoll sagt, wer was getan hat, und ist damit selbst eine Auskunft // über Menschen. TECHNIKER und LESER kommen nicht auf die Seite. expect(RECHTE.verwaltung).toEqual(["ADMIN"]); expect(RECHTE.verwaltung).not.toContain("TECHNIKER"); expect(RECHTE.verwaltung).not.toContain("LESER"); }); }); describe.runIf(datenbankVerfuegbar)("Protokoll ansehen", () => { beforeEach(async () => { await datenbankLeeren(); process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = "kunde-"; }); afterEach(() => { if (VORHER === undefined) delete process.env["OIDC_CUSTOMER_GROUP_PREFIX"]; else process.env["OIDC_CUSTOMER_GROUP_PREFIX"] = VORHER; }); it("zeigt einem beschränkten Benutzer keinen Eintrag eines fremden Kunden", async () => { const { eigene, fremd, admin, techniker } = await aufbau(); await testPrisma.protokoll.createMany({ data: [ { kundeId: eigene.id, entitaet: "Geraet", entitaetId: "g-eigen", aktion: "Gerät angelegt (eigen)", benutzerId: admin.id, }, { kundeId: fremd.id, entitaet: "Geraet", entitaetId: "g-fremd", aktion: "Gerät angelegt (fremd)", benutzerId: admin.id, }, ], }); const fuerTechniker = await protokollSeite({}, techniker); expect(fuerTechniker.zeilen.map((zeile) => zeile.aktion)).toEqual(["Gerät angelegt (eigen)"]); expect(fuerTechniker.gesamt).toBe(1); // Der ADMIN sieht beide – er ist auf keinen Kunden beschränkt. const fuerAdmin = await protokollSeite({}, admin); expect(fuerAdmin.gesamt).toBe(2); }); it("zeigt globale Einträge auch einem beschränkten Benutzer", async () => { const { admin, techniker } = await aufbau(); await testPrisma.protokoll.create({ data: { entitaet: "Einstellung", entitaetId: "global", aktion: "Betriebsmodus gewechselt", benutzerId: admin.id, }, }); const ergebnis = await protokollSeite({}, techniker); expect(ergebnis.zeilen.map((zeile) => zeile.aktion)).toEqual(["Betriebsmodus gewechselt"]); }); it("verbirgt einen nicht zuordenbaren Eintrag vor einem beschränkten Benutzer", async () => { // Ein Eintrag zu einem inzwischen gelöschten Gerät: ohne `kundeId` und ohne // Lauf lässt er sich keinem Kunden zuordnen. `vorher`/`nachher` können einen // Hostnamen enthalten, deshalb wird im Zweifel zugemacht (#023). const { admin, techniker } = await aufbau(); await testPrisma.protokoll.create({ data: { entitaet: "Geraet", entitaetId: "laengst-geloescht", aktion: "Gerät gelöscht", benutzerId: admin.id, vorher: { hostname: "FREMD-PC-007" }, }, }); expect((await protokollSeite({}, techniker)).gesamt).toBe(0); expect((await protokollSeite({}, admin)).gesamt).toBe(1); }); it("ordnet einen laufbezogenen Eintrag über den Lauf zu, auch ohne eigene kundeId", async () => { const { fremd, admin, techniker } = await aufbau(); const vorlage = await testPrisma.vorlage.create({ data: { name: "Irgendeine" } }); const version = await testPrisma.vorlagenVersion.create({ data: { vorlageId: vorlage.id, version: 1, status: VersionStatus.ENTWURF }, }); await testPrisma.vorlagenVersion.update({ where: { id: version.id }, data: { status: VersionStatus.VEROEFFENTLICHT, veroeffentlichtAm: new Date() }, }); const lauf = await testPrisma.lauf.create({ data: { vorlagenVersionId: version.id, titel: "Lauf bei Fremd", kundeId: fremd.id }, }); await testPrisma.protokoll.create({ data: { laufId: lauf.id, entitaet: "Lauf", aktion: "Lauf angelegt", entitaetId: lauf.id, benutzerId: admin.id, }, }); expect((await protokollSeite({}, techniker)).gesamt).toBe(0); const fuerAdmin = await protokollSeite({}, admin); expect(fuerAdmin.zeilen[0]?.laufTitel).toBe("Lauf bei Fremd"); expect(fuerAdmin.zeilen[0]?.kundeName).toBe("Fremd GmbH"); }); it("nennt einen Eintrag ohne Urheber als Systemlauf, nicht als Lücke", async () => { const { eigene, admin } = await aufbau(); await testPrisma.protokoll.create({ data: { kundeId: eigene.id, entitaet: "Verzeichnisquelle", entitaetId: "q-1", aktion: "Verzeichnis abgeglichen", benutzerId: null, }, }); const ergebnis = await protokollSeite({}, admin); expect(ergebnis.zeilen[0]?.urheber).toBeNull(); }); it("filtert nach Urheber, Zeitraum und Volltext", async () => { const { eigene, admin, techniker } = await aufbau(); await testPrisma.protokoll.createMany({ data: [ { kundeId: eigene.id, entitaet: "Benutzer", entitaetId: "b-1", aktion: "Rolle geändert", benutzerId: admin.id, erstelltAm: new Date("2026-09-01T10:00:00Z"), }, { kundeId: eigene.id, entitaet: "Geraet", entitaetId: "g-1", aktion: "Gerät archiviert", benutzerId: techniker.id, erstelltAm: new Date("2026-10-04T10:00:00Z"), }, ], }); expect((await protokollSeite({ suche: "rolle" }, admin)).gesamt).toBe(1); expect((await protokollSeite({ urheberId: techniker.id }, admin)).gesamt).toBe(1); expect((await protokollSeite({ vonDatum: "2026-10-01" }, admin)).gesamt).toBe(1); // Einschliesslich des bis-Tages – sonst fehlt der Eintrag von heute. expect((await protokollSeite({ bisDatum: "2026-10-04" }, admin)).gesamt).toBe(2); expect((await protokollSeite({ vonDatum: "2026-09-01", bisDatum: "2026-09-01" }, admin)).gesamt).toBe(1); }); it("blättert und zählt richtig", async () => { const { eigene, admin } = await aufbau(); await testPrisma.protokoll.createMany({ data: Array.from({ length: 55 }, (_, nummer) => ({ kundeId: eigene.id, entitaet: "Geraet", entitaetId: `g-${nummer}`, aktion: `Eintrag ${String(nummer).padStart(2, "0")}`, benutzerId: admin.id, erstelltAm: new Date(Date.UTC(2026, 9, 1, 0, nummer)), })), }); const erste = await protokollSeite({}, admin); expect(erste.gesamt).toBe(55); expect(erste.seiten).toBe(2); expect(erste.zeilen).toHaveLength(50); // Neueste zuerst. expect(erste.zeilen[0]?.aktion).toBe("Eintrag 54"); const zweite = await protokollSeite({ seite: 2 }, admin); expect(zweite.zeilen).toHaveLength(5); expect(zweite.zeilen.at(-1)?.aktion).toBe("Eintrag 00"); }); it("nennt als Urheber nur die, die auch im sichtbaren Teil vorkommen", async () => { const { eigene, fremd, admin, techniker } = await aufbau(); await testPrisma.protokoll.createMany({ data: [ { kundeId: eigene.id, entitaet: "Geraet", entitaetId: "g-1", aktion: "eigen", benutzerId: techniker.id, }, { kundeId: fremd.id, entitaet: "Geraet", entitaetId: "g-2", aktion: "fremd", benutzerId: admin.id, }, ], }); expect((await protokollUrheber(techniker)).map((eintrag) => eintrag.name)).toEqual(["Techniker"]); expect((await protokollUrheber(admin)).map((eintrag) => eintrag.name).sort()).toEqual([ "Admin", "Techniker", ]); }); it("gibt am Lauf nur dessen Einträge, neueste zuerst", async () => { const { eigene, admin } = await aufbau(); const vorlage = await testPrisma.vorlage.create({ data: { name: "Irgendeine" } }); const version = await testPrisma.vorlagenVersion.create({ data: { vorlageId: vorlage.id, version: 1, status: VersionStatus.ENTWURF }, }); const lauf = await testPrisma.lauf.create({ data: { vorlagenVersionId: version.id, titel: "Der Lauf", kundeId: eigene.id }, }); const anderer = await testPrisma.lauf.create({ data: { vorlagenVersionId: version.id, titel: "Ein anderer", kundeId: eigene.id }, }); await testPrisma.protokoll.createMany({ data: [ { laufId: lauf.id, entitaet: "Lauf", entitaetId: lauf.id, aktion: "zuerst", benutzerId: admin.id, erstelltAm: new Date("2026-10-01T10:00:00Z"), }, { laufId: lauf.id, entitaet: "Lauf", entitaetId: lauf.id, aktion: "danach", benutzerId: admin.id, erstelltAm: new Date("2026-10-02T10:00:00Z"), }, { laufId: anderer.id, entitaet: "Lauf", entitaetId: anderer.id, aktion: "fremder Lauf", benutzerId: admin.id, }, ], }); const eintraege = await protokollDesLaufs(lauf.id); expect(eintraege.map((eintrag) => eintrag.aktion)).toEqual(["danach", "zuerst"]); }); });