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.
251 lines
8.0 KiB
TypeScript
251 lines
8.0 KiB
TypeScript
// @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 {
|
|
addAssignment,
|
|
createPersonnelFile,
|
|
loadBacklinks,
|
|
updatePersonnelFile,
|
|
} 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<Permission>(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('nimmt nur Status-Etiketten an, die es wirklich gibt', async () => {
|
|
const leitung = perms(nutzer.stufe3, 3, { permissions: ['personnel.edit'] })
|
|
const akte = await createPersonnelFile(leitung, {
|
|
displayName: `${marke} Etikettentest`,
|
|
classification: 1,
|
|
// "erfunden" steht in keiner Konfiguration und darf nicht durchrutschen,
|
|
// sonst wäre die Liste im Adminbereich nur eine Empfehlung.
|
|
statusTags: ['tot', 'erfunden', 'verhaftet'],
|
|
})
|
|
|
|
const gespeichert = await db.personnelFile.findUniqueOrThrow({
|
|
where: { id: akte.id },
|
|
select: { statusTags: true },
|
|
})
|
|
expect(gespeichert.statusTags).toEqual(['verhaftet', 'tot'])
|
|
|
|
// Und die Reihenfolge kommt aus der Konfiguration, nicht aus der Eingabe:
|
|
// auf jeder Akte steht dieselbe Abfolge.
|
|
await updatePersonnelFile(leitung, akte.id, { statusTags: ['tot', 'verhaftet'] })
|
|
const danach = await db.personnelFile.findUniqueOrThrow({
|
|
where: { id: akte.id },
|
|
select: { statusTags: true },
|
|
})
|
|
expect(danach.statusTags).toEqual(['verhaftet', 'tot'])
|
|
})
|
|
|
|
it('lässt ein zugeordnetes Mitglied auch über seiner Stufe lesen', async () => {
|
|
const p = perms(nutzer.stufe1, 1)
|
|
|
|
// Vorher: die Akte der Stufe 4 ist für ihn nicht da.
|
|
expect(
|
|
await db.personnelFile.findMany({
|
|
where: { AND: [visiblePersonnelWhere(p), { id: hoheAkteId }] },
|
|
select: { id: true },
|
|
}),
|
|
).toHaveLength(0)
|
|
|
|
await addAssignment(perms(nutzer.stufe3, 3, { isAdmin: true }), {
|
|
personnelFileId: hoheAkteId,
|
|
userId: nutzer.stufe1,
|
|
role: 'INVESTIGATOR',
|
|
})
|
|
|
|
// Danach schon - das ist der ausnahmsweise Zugang, wie bei der Fallakte.
|
|
expect(
|
|
await db.personnelFile.findMany({
|
|
where: { AND: [visiblePersonnelWhere(p), { id: hoheAkteId }] },
|
|
select: { id: true },
|
|
}),
|
|
).toHaveLength(1)
|
|
})
|
|
|
|
it('gibt der betroffenen Person selbst keinen Zugriff auf ihre Akte', async () => {
|
|
const eigene = await db.personnelFile.create({
|
|
data: {
|
|
displayName: `${marke} Eigene`,
|
|
userId: nutzer.betroffen,
|
|
classification: 4,
|
|
},
|
|
})
|
|
|
|
// Eine Beurteilung auf Stufe 4 über einen Mitarbeiter der Stufe 1 wäre
|
|
// sonst genau für den einen Menschen lesbar, vor dem sie schützt.
|
|
const treffer = await db.personnelFile.findMany({
|
|
where: { AND: [visiblePersonnelWhere(perms(nutzer.betroffen, 1)), { id: eigene.id }] },
|
|
select: { id: true },
|
|
})
|
|
expect(treffer).toHaveLength(0)
|
|
})
|
|
|
|
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)
|
|
})
|
|
})
|