// @vitest-environment node import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { createPrismaClient, loadEnvFromRepoRoot, type PrismaClient } from '@afis/db' import { listLinks } from '@/lib/cases/links' import { listParticipants } from '@/lib/cases/participants' import { visibleCaseWhere } from '@/lib/access/caseFilter' import type { EffectivePermissions, Permission } from '@/lib/auth/permissions' /** * Verknüpfungen zeigen in beide Richtungen. Eine Akte, die der Leser nicht * sehen darf, darf auch von der Gegenseite aus nicht auftauchen - sonst * verrät die verknüpfte Akte ihre Existenz. */ loadEnvFromRepoRoot() const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL const suite = url ? describe : describe.skip let db: PrismaClient const marke = `link-${Date.now()}` function perms(userId: string, clearanceLevel: number): EffectivePermissions { return { userId, clearanceLevel, permissions: new Set(), isAdmin: false, hasMembership: true, } } suite('Verknüpfungen zwischen Akten', () => { const nutzer = { stufe1: `${marke}-stufe1`, stufe4: `${marke}-stufe4` } let offenId = '' let geheimId = '' let zweitOffenId = '' beforeAll(async () => { db = createPrismaClient(url) const template = await db.documentTemplate.create({ data: { key: `${marke}-vorlage`, version: 1, name: 'Prüfvorlage', fields: [] }, }) for (const id of Object.values(nutzer)) { await db.user.create({ data: { id, username: id, displayName: id } }) } const gemeinsam = { templateId: template.id, templateKey: template.key, createdById: nutzer.stufe4, } const offen = await db.case.create({ data: { ...gemeinsam, caseNumber: `${marke}-a`.slice(0, 40), title: 'Offen', classification: 1, status: 'OPEN', }, }) const geheim = await db.case.create({ data: { ...gemeinsam, caseNumber: `${marke}-b`.slice(0, 40), title: 'Geheim', classification: 4, status: 'OPEN', }, }) const zweitOffen = await db.case.create({ data: { ...gemeinsam, caseNumber: `${marke}-c`.slice(0, 40), title: 'Auch offen', classification: 1, status: 'OPEN', }, }) offenId = offen.id geheimId = geheim.id zweitOffenId = zweitOffen.id await db.caseLink.createMany({ data: [ // Hinaus: offen -> geheim { fromId: offen.id, toId: geheim.id, kind: 'RELATED', createdById: nutzer.stufe4 }, // Herein: geheim -> offen, dieselbe Falle aus der anderen Richtung { fromId: geheim.id, toId: offen.id, kind: 'SUCCESSOR', createdById: nutzer.stufe4 }, // Unverfänglich { fromId: offen.id, toId: zweitOffen.id, kind: 'RELATED', createdById: nutzer.stufe4 }, ], }) }) afterAll(async () => { await db.$disconnect() }) it('verbirgt eine zu hoch eingestufte Akte in beiden Richtungen', async () => { const links = await listLinks(perms(nutzer.stufe1, 1), offenId) const ziele = links.map((link) => link.other.id) expect(ziele).toContain(zweitOffenId) expect(ziele).not.toContain(geheimId) // Auch die eingehende Verknüpfung darf nicht als leerer Platzhalter stehen. expect(links).toHaveLength(1) }) it('zeigt der Stufe 4 beide Richtungen mit ihrer jeweiligen Bedeutung', async () => { const links = await listLinks(perms(nutzer.stufe4, 4), offenId) const richtungen = links .filter((link) => link.other.id === geheimId) .map((link) => `${link.direction}:${link.kind}`) .sort() expect(richtungen).toEqual(['in:SUCCESSOR', 'out:RELATED']) }) it('nennt den Namen eines Beteiligten, verweist aber nur auf sichtbare Personalakten', async () => { const verborgen = await db.personnelFile.create({ data: { displayName: `${marke} Verborgene Person`, classification: 4 }, }) await db.caseParticipant.create({ data: { caseId: offenId, personnelFileId: verborgen.id, role: 'SUBJECT' }, }) const beteiligte = await listParticipants(perms(nutzer.stufe1, 1), offenId) const eintrag = beteiligte.find((b) => b.displayName?.includes('Verborgene Person')) // Wer die Ermittlung lesen darf, muss wissen, gegen wen sie läuft. expect(eintrag).toBeDefined() // Was darüber hinaus über die Person aktenkundig ist, bleibt zu. expect(eintrag?.personnelFileId).toBeNull() const fuerStufe4 = await listParticipants(perms(nutzer.stufe4, 4), offenId) expect( fuerStufe4.find((b) => b.displayName?.includes('Verborgene Person'))?.personnelFileId, ).toBe(verborgen.id) }) it('gibt einem Ermittler Zugriff auf die Akte über seiner Stufe', async () => { // Die Rolle entscheidet: nur wer an der Akte arbeitet, öffnet sie. Als // Zeuge stünde er zwar in der Liste, käme aber nicht hinein. await db.caseParticipant.create({ data: { caseId: geheimId, userId: nutzer.stufe1, role: 'INVESTIGATOR' }, }) const p = perms(nutzer.stufe1, 1) const treffer = await db.case.findMany({ where: { AND: [visibleCaseWhere(p), { id: geheimId }] }, select: { id: true }, }) expect(treffer).toHaveLength(1) // Und damit auch auf die Verknüpfung, die vorher verborgen war. const links = await listLinks(p, offenId) expect(links.map((link) => link.other.id)).toContain(geheimId) }) })