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.
197 lines
6.8 KiB
TypeScript
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)
|
|
})
|
|
})
|