// @vitest-environment node import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { createPrismaClient, loadEnvFromRepoRoot, type PrismaClient } from '@afis/db' import { visiblePersonnelWhere } from '@/lib/access/personnelFilter' import { loadBacklinks } from '@/lib/personnel/service' import type { EffectivePermissions, Permission } from '@/lib/auth/permissions' /** * Rückverweise sind der häufigste Ort, an dem ein Aktensystem leckt. * * Eine Personalakte der Stufe 1, auf der „3 Fälle" steht, verrät die Existenz * von Ermittlungen, die der Leser nicht sehen darf — auch dann, wenn kein * Titel danebensteht. Deshalb prüft dieser Test nicht nur die Liste, sondern * ausdrücklich auch die **Zahl**. Siehe docs/05-akten-und-vorlagen.md. */ loadEnvFromRepoRoot() const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL const suite = url ? describe : describe.skip let db: PrismaClient const marke = `personal-${Date.now()}` function perms( userId: string, clearanceLevel: number, options: { isAdmin?: boolean; permissions?: Permission[] } = {}, ): EffectivePermissions { return { userId, clearanceLevel, permissions: new Set(options.permissions ?? []), isAdmin: options.isAdmin ?? false, hasMembership: true, } } suite('Personalakten und Rückverweise', () => { const nutzer = { stufe1: `${marke}-stufe1`, stufe3: `${marke}-stufe3`, betroffen: `${marke}-betroffen`, } const akten = { offen: `${marke}-fall-offen`, geheim: `${marke}-fall-geheim`, } let personalakteId = '' let hoheAkteId = '' 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 person = await db.personnelFile.create({ data: { displayName: `${marke} Betroffener`, userId: nutzer.betroffen, classification: 1, createdById: nutzer.stufe3, }, }) personalakteId = person.id const verborgen = await db.personnelFile.create({ data: { displayName: `${marke} Verborgen`, classification: 4, createdById: nutzer.stufe3 }, }) hoheAkteId = verborgen.id const gemeinsam = { templateId: template.id, templateKey: template.key, createdById: nutzer.stufe3, } await db.case.create({ data: { ...gemeinsam, id: akten.offen, caseNumber: `${marke}-1`.slice(0, 40), title: 'Für alle sichtbar', classification: 1, status: 'OPEN', }, }) await db.case.create({ data: { ...gemeinsam, id: akten.geheim, caseNumber: `${marke}-2`.slice(0, 40), title: 'Nur ab Stufe 3', classification: 3, status: 'OPEN', }, }) // Dieselbe Person, einmal über die Personalakte, einmal über das Konto. await db.caseParticipant.createMany({ data: [ { caseId: akten.offen, personnelFileId: person.id, role: 'WITNESS' }, { caseId: akten.geheim, personnelFileId: person.id, role: 'SUBJECT' }, { caseId: akten.geheim, userId: nutzer.betroffen, role: 'WITNESS' }, ], }) }) afterAll(async () => { await db.$disconnect() }) it('zeigt einer Stufe 1 auf der Personalakte keinen Hinweis auf den Stufe-3-Fall', async () => { const { total, byRole } = await loadBacklinks(perms(nutzer.stufe1, 1), { id: personalakteId, userId: nutzer.betroffen, }) const nummern = byRole.flatMap((gruppe) => gruppe.eintraege.map((e) => e.case.id)) expect(nummern).toContain(akten.offen) expect(nummern).not.toContain(akten.geheim) // Die Zahl ist selbst eine Information: sie darf den verborgenen Fall // nicht mitzählen, auch nicht als anonymen Platzhalter. expect(total).toBe(1) // Und die Rolle taucht gar nicht erst auf, sonst verriete "Betroffener" // die Existenz einer Ermittlung gegen die Person. expect(byRole.map((gruppe) => gruppe.role)).not.toContain('SUBJECT') }) it('zeigt einer Stufe 3 beide Fälle und beide Wege zur Person', async () => { const { total, byRole } = await loadBacklinks(perms(nutzer.stufe3, 3), { id: personalakteId, userId: nutzer.betroffen, }) const rollen = byRole.map((gruppe) => gruppe.role).sort() expect(rollen).toEqual(['SUBJECT', 'WITNESS']) // Drei Einträge: einer über die Personalakte im offenen Fall, zwei im // geheimen — über die Personalakte und über das Discord-Konto. expect(total).toBe(3) }) it('verbirgt eine Personalakte oberhalb der Stufe, auch in der Zählung', async () => { const p = perms(nutzer.stufe1, 1) const wo = { AND: [visiblePersonnelWhere(p), { id: { in: [personalakteId, hoheAkteId] } }], } const treffer = await db.personnelFile.findMany({ where: wo, select: { id: true } }) const anzahl = await db.personnelFile.count({ where: wo }) expect(treffer.map((t) => t.id)).toEqual([personalakteId]) expect(anzahl).toBe(1) }) it('gibt dem Admin die verborgene Personalakte', async () => { const treffer = await db.personnelFile.findMany({ where: { AND: [ visiblePersonnelWhere(perms(nutzer.stufe1, 1, { isAdmin: true })), { id: hoheAkteId }, ], }, select: { id: true }, }) expect(treffer).toHaveLength(1) }) })