Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
127 lines
4.4 KiB
TypeScript
127 lines
4.4 KiB
TypeScript
// @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()
|
|
})
|
|
})
|