// @vitest-environment node import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { db } from '@/lib/db' import { writeAudit } from '@/lib/audit/write' import { queryAuditLog, AUDIT_PAGE } from '@/lib/audit/query' /** * Das Protokoll ist die Gegenprobe zum ganzen System: hier soll stehen, was * passiert ist, vollständig und unverändert. Geprüft wird deshalb zweierlei - * dass die Filter genau schneiden, und dass eine Zeile nicht verschwinden * kann, wenn ihr Ziel verschwindet. */ const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL const suite = url ? describe : describe.skip const marke = `prot-${Date.now()}` suite('Zugriffsprotokoll', () => { const akteur = `${marke}-akteur` const anderer = `${marke}-anderer` let fallId = '' let fallNummer = '' beforeAll(async () => { const vorlage = await db.documentTemplate.create({ data: { key: `${marke}-vorlage`, version: 1, name: 'Prüfvorlage', fields: [] }, }) for (const id of [akteur, anderer]) { await db.user.create({ data: { id, username: id, displayName: `Name ${id}` } }) } const akte = await db.case.create({ data: { templateId: vorlage.id, templateKey: vorlage.key, createdById: akteur, caseNumber: `Prot-2026-${Date.now().toString(36)}`, title: 'Vorgang', classification: 1, status: 'OPEN', }, }) fallId = akte.id fallNummer = akte.caseNumber await writeAudit(db, { actorId: akteur, action: 'case.create', targetType: 'Case', targetId: fallId, clearanceAtTime: 4, }) await writeAudit(db, { actorId: anderer, action: 'case.read', targetType: 'Case', targetId: fallId, clearanceAtTime: 1, }) await writeAudit(db, { actorId: akteur, action: 'export.pdf', targetType: 'ExportJob', targetId: `${marke}-auftrag`, clearanceAtTime: 4, }) }) afterAll(async () => { await db.$disconnect() }) it('filtert nach Person und schneidet dabei genau', async () => { const { rows } = await queryAuditLog({ actorId: akteur }) const eigene = rows.filter((zeile) => zeile.actorId === akteur) expect(eigene).toHaveLength(rows.length) expect(rows.some((zeile) => zeile.action === 'case.create')).toBe(true) expect(rows.some((zeile) => zeile.actorId === anderer)).toBe(false) }) it('filtert nach Aktion und nach Ziel', async () => { const nurLesen = await queryAuditLog({ action: 'case.read', targetId: fallId }) expect(nurLesen.rows).toHaveLength(1) expect(nurLesen.rows[0]?.actorId).toBe(anderer) const nurExport = await queryAuditLog({ targetType: 'ExportJob', actorId: akteur }) expect(nurExport.rows.every((zeile) => zeile.targetType === 'ExportJob')).toBe(true) }) it('macht aus Kennungen Aktenzeichen und lässt Unauflösbares stehen', async () => { const { rows } = await queryAuditLog({ actorId: akteur }) const zurAkte = rows.find((zeile) => zeile.targetId === fallId) expect(zurAkte?.targetLabel).toBe(fallNummer) expect(zurAkte?.targetHref).toBe(`/faelle/${fallNummer}`) // Ein Auftrag ist kein Datensatz, den das Protokoll auflösen kann. Die // Zeile bleibt trotzdem stehen - ein Protokoll mit Lücken wäre keins. const zumAuftrag = rows.find((zeile) => zeile.targetType === 'ExportJob') expect(zumAuftrag).toBeDefined() expect(zumAuftrag?.targetLabel).toBeNull() }) it('blättert rückwärts ohne Zeile doppelt oder gar nicht zu zeigen', async () => { const erste = await queryAuditLog({}) expect(erste.rows.length).toBeLessThanOrEqual(AUDIT_PAGE) if (erste.more) { const zweite = await queryAuditLog({ before: BigInt(erste.rows[erste.rows.length - 1]!.id) }) const kennungen = new Set(erste.rows.map((zeile) => zeile.id)) expect(zweite.rows.some((zeile) => kennungen.has(zeile.id))).toBe(false) } }) it('lässt sich nicht ändern und nicht entfernen', async () => { const zeile = await db.auditLog.findFirstOrThrow({ where: { actorId: akteur, action: 'case.create' }, }) // Zusicherung 3 der README steht als Regel in der Datenbank, nicht nur // in der Anwendung: das Löschen läuft ins Leere statt zu wirken. await db.auditLog.deleteMany({ where: { id: zeile.id } }) const danach = await db.auditLog.findUnique({ where: { id: zeile.id } }) expect(danach).not.toBeNull() }) })