Files
AfiS-Aktensystem/web/tests/uebersicht.test.ts
T
Harlekin e0b03ea336
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
Gedankenstriche durch Bindestriche ersetzt
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
2026-09-04 08:54:52 +02:00

197 lines
6.8 KiB
TypeScript

// @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<Permission>(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)
})
})