// @vitest-environment node import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { db } from '@/lib/db' import { loadDashboard } from '@/lib/dashboard/load' import { listWatched, toggleWatch } from '@/lib/cases/watch' import { AccessError } from '@/lib/errors' import type { EffectivePermissions, Permission } from '@/lib/auth/permissions' /** * Die Übersicht ist der Ort, an dem eine unachtsame Abfrage am ehesten eine * Akte verrät: fünf Listen auf einer Seite, jede mit eigenem Filter. Geprüft * wird deshalb jede einzeln gegen eine zu hoch eingestufte Akte. */ const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL const suite = url ? describe : describe.skip const marke = `dash-${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('Übersicht und Beobachtungsliste', () => { const nutzer = { stufe1: `${marke}-stufe1`, stufe4: `${marke}-stufe4` } let offenNummer = '' let geheimId = '' let geheimNummer = '' beforeAll(async () => { const vorlage = 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: vorlage.id, templateKey: vorlage.key, createdById: nutzer.stufe4, } const offen = await db.case.create({ data: { ...gemeinsam, caseNumber: `Dash-2026-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`, title: 'Offener Vorgang', classification: 1, status: 'OPEN', dueAt: new Date(Date.now() + 2 * 86_400_000), }, }) const geheim = await db.case.create({ data: { ...gemeinsam, caseNumber: `Dghm-2026-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`, title: 'Streng vertraulich', classification: 4, status: 'OPEN', // Auch eine Frist: sie darf in keiner Liste auftauchen. dueAt: new Date(Date.now() + 86_400_000), }, }) offenNummer = offen.caseNumber geheimId = geheim.id geheimNummer = geheim.caseNumber // Der Stufe-1-Nutzer ist an beiden Akten beteiligt — an der geheimen aber // nicht über sein Konto, sondern gar nicht. Er darf sie nicht sehen. await db.caseParticipant.create({ data: { caseId: offen.id, userId: nutzer.stufe1, role: 'INVESTIGATOR' }, }) }) afterAll(async () => { await db.$disconnect() }) it('hält eine zu hoch eingestufte Akte aus jeder Liste heraus', async () => { const daten = await loadDashboard(perms(nutzer.stufe1, 1)) const alle = [ ...daten.meine, ...daten.fristen, ...daten.zuletzt, ...daten.beobachtet, ].map((akte) => akte.id) expect(alle).not.toContain(geheimId) // Und die eigene ist da, sonst prüft der Test nichts. expect(daten.meine.map((a) => a.caseNumber)).toContain(offenNummer) expect(daten.fristen.map((a) => a.caseNumber)).toContain(offenNummer) }) it('zeigt der Stufe 4 die Frist der geheimen Akte', async () => { const daten = await loadDashboard(perms(nutzer.stufe4, 4)) expect(daten.fristen.map((a) => a.id)).toContain(geheimId) }) it('lässt nur beobachten, was man sehen darf', async () => { // Beobachten ist kein Weg an der Stufe vorbei. await expect(toggleWatch(perms(nutzer.stufe1, 1), geheimNummer)).rejects.toThrow(AccessError) const an = await toggleWatch(perms(nutzer.stufe1, 1), offenNummer) expect(an.watching).toBe(true) expect((await listWatched(perms(nutzer.stufe1, 1))).map((a) => a.caseNumber)).toContain( offenNummer, ) const aus = await toggleWatch(perms(nutzer.stufe1, 1), offenNummer) expect(aus.watching).toBe(false) }) it('nimmt eine beobachtete Akte aus der Liste, sobald sie zu hoch steigt', async () => { const p = perms(nutzer.stufe4, 4) await toggleWatch(p, geheimNummer) expect((await listWatched(p)).map((a) => a.id)).toContain(geheimId) // Derselbe Beobachtungseintrag, aber jetzt aus Sicht einer niedrigeren // Stufe: die Liste läuft durch dieselbe Sichtbarkeitsprüfung. const abgestiegen = perms(nutzer.stufe4, 1) expect((await listWatched(abgestiegen)).map((a) => a.id)).not.toContain(geheimId) }) it('zählt Ungelesenes je Ort und lässt Eigenes aus', async () => { // Die Zählung lief einmal als eine Abfrage je Ort und ist jetzt eine // einzige über alle. Geprüft wird deshalb, was dabei leicht verrutscht: // die Lesemarke gilt **je Ort**, und eigene Nachrichten zählen nie mit. const p = perms(nutzer.stufe1, 1) const vorlage = await db.documentTemplate.findFirstOrThrow({ where: { key: `${marke}-vorlage` }, }) const zweite = await db.case.create({ data: { templateId: vorlage.id, templateKey: vorlage.key, createdById: nutzer.stufe4, caseNumber: `Ung-2026-${Date.now().toString(36)}`, title: 'Zweiter Vorgang', classification: 1, status: 'OPEN', }, }) await db.caseParticipant.create({ data: { caseId: zweite.id, userId: nutzer.stufe1, role: 'INVESTIGATOR' }, }) const erste = await db.case.findFirstOrThrow({ where: { caseNumber: offenNummer } }) const frueher = new Date(Date.now() - 60_000) await db.chatMessage.createMany({ data: [ // Vor der Lesemarke der ersten Akte: zählt nicht. { id: `${marke}-alt`, caseId: erste.id, authorId: nutzer.stufe4, body: 'alt', createdAt: frueher, }, // Danach: zählt. { id: `${marke}-neu`, caseId: erste.id, authorId: nutzer.stufe4, body: 'neu' }, // Eigene Nachricht: zählt nie. { id: `${marke}-eigen`, caseId: erste.id, authorId: nutzer.stufe1, body: 'eigen' }, // Zweite Akte ohne Lesemarke: alles zählt. { id: `${marke}-z1`, caseId: zweite.id, authorId: nutzer.stufe4, body: 'a' }, { id: `${marke}-z2`, caseId: zweite.id, authorId: nutzer.stufe4, body: 'b' }, ], }) await db.readMarker.create({ data: { userId: nutzer.stufe1, scopeType: 'case', scopeId: erste.id, lastReadAt: new Date(Date.now() - 30_000), }, }) const daten = await loadDashboard(p) const zuErster = daten.ungelesen.find((u) => u.scopeId === erste.id) const zuZweiter = daten.ungelesen.find((u) => u.scopeId === zweite.id) expect(zuErster?.count).toBe(1) expect(zuZweiter?.count).toBe(2) }) })