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.
319 lines
12 KiB
TypeScript
319 lines
12 KiB
TypeScript
// @vitest-environment node
|
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
import { db } from '@/lib/db'
|
|
import {
|
|
assignToDepartment,
|
|
caseTypesOfDepartment,
|
|
casesPerDepartment,
|
|
listDepartmentCases,
|
|
listDepartments,
|
|
listMembers,
|
|
} from '@/lib/departments/service'
|
|
import { createEntry, editEntry, listEntries, retractEntry } from '@/lib/departments/entries'
|
|
import { AccessError } from '@/lib/errors'
|
|
import type { EffectivePermissions, Permission } from '@/lib/auth/permissions'
|
|
|
|
/**
|
|
* Die Abteilung ist die Ebene über der Personalakte, und sie fasst zusammen,
|
|
* was einzeln bereits geschützt ist. Genau darin liegt die Gefahr: eine
|
|
* Zusammenfassung, die nicht filtert, verrät mehr als jede Einzelansicht.
|
|
* Geprüft wird deshalb jede Liste und jede Zahl gegen einen zu niedrig
|
|
* eingestuften Leser.
|
|
*/
|
|
const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL
|
|
const suite = url ? describe : describe.skip
|
|
|
|
const marke = `abt-${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('Abteilungen', () => {
|
|
const nutzer = { stufe1: `${marke}-s1`, stufe4: `${marke}-s4` }
|
|
let abteilungId = ''
|
|
let abteilungKey = ''
|
|
let geheimeAbteilungKey = ''
|
|
let personOffenId = ''
|
|
let personGeheimId = ''
|
|
let offenId = ''
|
|
let geheimId = ''
|
|
|
|
beforeAll(async () => {
|
|
for (const id of Object.values(nutzer)) {
|
|
await db.user.create({ data: { id, username: id, displayName: id } })
|
|
}
|
|
const vorlage = await db.documentTemplate.create({
|
|
data: { key: `${marke}-vorlage`, version: 1, name: 'Korruption', fields: [] },
|
|
})
|
|
const vorlage2 = await db.documentTemplate.create({
|
|
data: { key: `${marke}-bussgeld`, version: 1, name: 'Bussgeld', fields: [] },
|
|
})
|
|
|
|
const abteilung = await db.department.create({
|
|
data: { key: `${marke}-SHD`.slice(0, 21).toUpperCase(), name: 'Sicherheitsdienst' },
|
|
})
|
|
abteilungId = abteilung.id
|
|
abteilungKey = abteilung.key
|
|
|
|
const geheim = await db.department.create({
|
|
data: { key: `${marke}-GEH`.slice(0, 21).toUpperCase(), name: 'Verdeckt', minClearance: 4 },
|
|
})
|
|
geheimeAbteilungKey = geheim.key
|
|
|
|
// Zwei Mitglieder: eines für jeden sichtbar, eines auf Stufe 4.
|
|
const offenePerson = await db.personnelFile.create({
|
|
data: {
|
|
displayName: `${marke} Offene Person`,
|
|
classification: 1,
|
|
departmentId: abteilung.id,
|
|
},
|
|
})
|
|
const geheimePerson = await db.personnelFile.create({
|
|
data: {
|
|
displayName: `${marke} Geheime Person`,
|
|
classification: 4,
|
|
departmentId: abteilung.id,
|
|
},
|
|
})
|
|
personOffenId = offenePerson.id
|
|
personGeheimId = geheimePerson.id
|
|
|
|
const gemeinsam = { createdById: nutzer.stufe4 }
|
|
const offen = await db.case.create({
|
|
data: {
|
|
...gemeinsam,
|
|
templateId: vorlage.id,
|
|
templateKey: vorlage.key,
|
|
caseNumber: `Abt-2026-${Date.now().toString(36)}o`,
|
|
title: 'Offener Vorgang',
|
|
classification: 1,
|
|
status: 'OPEN',
|
|
},
|
|
})
|
|
const geheimerFall = await db.case.create({
|
|
data: {
|
|
...gemeinsam,
|
|
templateId: vorlage2.id,
|
|
templateKey: vorlage2.key,
|
|
caseNumber: `Abt-2026-${Date.now().toString(36)}g`,
|
|
title: 'Streng vertraulich',
|
|
classification: 4,
|
|
status: 'OPEN',
|
|
},
|
|
})
|
|
offenId = offen.id
|
|
geheimId = geheimerFall.id
|
|
|
|
// Beide Fälle hängen über eine Person an der Abteilung.
|
|
await db.caseParticipant.create({
|
|
data: { caseId: offen.id, personnelFileId: offenePerson.id, role: 'SUBJECT' },
|
|
})
|
|
await db.caseParticipant.create({
|
|
data: { caseId: geheimerFall.id, personnelFileId: offenePerson.id, role: 'SUSPECT' },
|
|
})
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await db.$disconnect()
|
|
})
|
|
|
|
it('verbirgt eine Abteilung über der eigenen Stufe', async () => {
|
|
const niedrig = await listDepartments(perms(nutzer.stufe1, 1))
|
|
expect(niedrig.map((a) => a.key)).not.toContain(geheimeAbteilungKey)
|
|
|
|
const hoch = await listDepartments(perms(nutzer.stufe4, 4))
|
|
expect(hoch.map((a) => a.key)).toContain(geheimeAbteilungKey)
|
|
})
|
|
|
|
it('zählt als Mitglieder nur, was der Fragende sehen darf', async () => {
|
|
const mitRecht = (stufe: number) =>
|
|
listDepartments(perms(nutzer.stufe1, stufe, { permissions: ['personnel.read'] }))
|
|
|
|
const niedrig = (await mitRecht(1)).find((a) => a.key === abteilungKey)
|
|
const hoch = (await mitRecht(4)).find((a) => a.key === abteilungKey)
|
|
|
|
// Sonst verriete die Zahl, wie viele Personalakten über seiner Stufe liegen.
|
|
expect(niedrig?.memberCount).toBe(1)
|
|
expect(hoch?.memberCount).toBe(2)
|
|
|
|
// Ohne das Recht auf Personalakten steht dort keine Zahl, auch keine Null:
|
|
// eine Null wäre die Auskunft „hier ist niemand".
|
|
const ohneRecht = await listDepartments(perms(nutzer.stufe4, 4))
|
|
expect(ohneRecht.find((a) => a.key === abteilungKey)?.memberCount).toBeNull()
|
|
})
|
|
|
|
it('zeigt Mitglieder nur mit dem Recht auf Personalakten', async () => {
|
|
const ohneRecht = await listMembers(perms(nutzer.stufe4, 4), abteilungId)
|
|
expect(ohneRecht).toHaveLength(0)
|
|
|
|
const mitRecht = await listMembers(
|
|
perms(nutzer.stufe4, 4, { permissions: ['personnel.read'] }),
|
|
abteilungId,
|
|
)
|
|
expect(mitRecht.map((m) => m.id)).toEqual(
|
|
expect.arrayContaining([personOffenId, personGeheimId]),
|
|
)
|
|
})
|
|
|
|
it('hält eine zu hoch eingestufte Akte aus Liste und Diagramm heraus', async () => {
|
|
const niedrig = perms(nutzer.stufe1, 1)
|
|
|
|
const liste = await listDepartmentCases(niedrig, abteilungId)
|
|
expect(liste.map((a) => a.id)).toContain(offenId)
|
|
expect(liste.map((a) => a.id)).not.toContain(geheimId)
|
|
|
|
// Und die Zahl im Diagramm stimmt mit der Liste überein - ein Zähler, der
|
|
// mehr zählt als die Liste zeigt, wäre selbst eine Auskunft.
|
|
const verteilung = await caseTypesOfDepartment(niedrig, abteilungId)
|
|
const summe = verteilung.reduce((s, stueck) => s + stueck.value, 0)
|
|
expect(summe).toBe(liste.length)
|
|
expect(verteilung.map((s) => s.label)).not.toContain(`${marke}-bussgeld`)
|
|
|
|
// Stufe 4 sieht beide.
|
|
const hoch = await caseTypesOfDepartment(perms(nutzer.stufe4, 4), abteilungId)
|
|
expect(hoch.reduce((s, stueck) => s + stueck.value, 0)).toBe(2)
|
|
})
|
|
|
|
it('zählt auch über Abteilungen hinweg nur Sichtbares', async () => {
|
|
const niedrig = await casesPerDepartment(perms(nutzer.stufe1, 1), 'Ohne Freigabe')
|
|
const eintrag = niedrig.find((s) => s.label === 'Sicherheitsdienst')
|
|
expect(eintrag?.value).toBe(1)
|
|
|
|
const hoch = await casesPerDepartment(perms(nutzer.stufe4, 4), 'Ohne Freigabe')
|
|
expect(hoch.find((s) => s.label === 'Sicherheitsdienst')?.value).toBe(2)
|
|
})
|
|
|
|
it('führt eine Abteilung ohne Freigabe geschwärzt, statt sie wegzulassen', async () => {
|
|
// Der Fall, den es zu prüfen gilt: ein Fall, den der Leser sehen darf,
|
|
// gehört zu einer Abteilung, die er nicht sehen darf.
|
|
const geheim = await db.department.findFirstOrThrow({ where: { key: geheimeAbteilungKey } })
|
|
const person = await db.personnelFile.create({
|
|
data: {
|
|
displayName: `${marke}-verdeckt`,
|
|
classification: 1,
|
|
departmentId: geheim.id,
|
|
},
|
|
})
|
|
await db.caseParticipant.create({
|
|
data: { caseId: offenId, personnelFileId: person.id, role: 'WITNESS' },
|
|
})
|
|
|
|
const niedrig = await casesPerDepartment(perms(nutzer.stufe1, 1), 'Ohne Freigabe')
|
|
// Der Name der Abteilung steht nicht da …
|
|
expect(niedrig.map((s) => s.label)).not.toContain('Verdeckt')
|
|
// … aber der Anteil fehlt auch nicht, sonst summierte sich das Diagramm
|
|
// nicht auf die Zahl, die daneben steht.
|
|
expect(niedrig.find((s) => s.label === 'Ohne Freigabe')?.value).toBe(1)
|
|
|
|
// Mit Freigabe steht der Name da.
|
|
const hoch = await casesPerDepartment(perms(nutzer.stufe4, 4), 'Ohne Freigabe')
|
|
expect(hoch.map((s) => s.label)).toContain('Verdeckt')
|
|
expect(hoch.map((s) => s.label)).not.toContain('Ohne Freigabe')
|
|
})
|
|
|
|
it('lässt nur zuordnen, wer die Personalakte auch ändern darf', async () => {
|
|
await expect(
|
|
assignToDepartment(perms(nutzer.stufe1, 1, { permissions: ['personnel.read'] }), {
|
|
personnelFileId: personOffenId,
|
|
departmentId: abteilungId,
|
|
}),
|
|
).rejects.toThrow(AccessError)
|
|
|
|
await assignToDepartment(
|
|
perms(nutzer.stufe4, 4, { permissions: ['personnel.read', 'personnel.edit'] }),
|
|
{ personnelFileId: personOffenId, departmentId: null },
|
|
)
|
|
const danach = await db.personnelFile.findUniqueOrThrow({ where: { id: personOffenId } })
|
|
expect(danach.departmentId).toBeNull()
|
|
|
|
// Wieder zurück, damit die übrigen Prüfungen ihre Grundlage behalten.
|
|
await assignToDepartment(
|
|
perms(nutzer.stufe4, 4, { permissions: ['personnel.read', 'personnel.edit'] }),
|
|
{ personnelFileId: personOffenId, departmentId: abteilungId },
|
|
)
|
|
})
|
|
|
|
describe('Vermerke', () => {
|
|
const schreiber = () =>
|
|
perms(nutzer.stufe4, 4, {
|
|
permissions: ['entry.create', 'entry.edit_own', 'entry.retract'],
|
|
})
|
|
|
|
it('verbirgt einen Vermerk über der Stufe des Lesers', async () => {
|
|
const id = `${marke}-v1`
|
|
await createEntry(schreiber(), {
|
|
id,
|
|
departmentKey: abteilungKey,
|
|
body: 'Nur für die Leitung',
|
|
minClearance: 4,
|
|
})
|
|
|
|
const niedrig = await listEntries(perms(nutzer.stufe1, 1), abteilungId)
|
|
expect(niedrig.map((v) => v.id)).not.toContain(id)
|
|
|
|
const hoch = await listEntries(perms(nutzer.stufe4, 4), abteilungId)
|
|
expect(hoch.map((v) => v.id)).toContain(id)
|
|
})
|
|
|
|
it('lässt niemanden über seiner eigenen Stufe schreiben', async () => {
|
|
await expect(
|
|
createEntry(perms(nutzer.stufe1, 1, { permissions: ['entry.create'] }), {
|
|
id: `${marke}-v2`,
|
|
departmentKey: abteilungKey,
|
|
body: 'Zu hoch gegriffen',
|
|
minClearance: 4,
|
|
}),
|
|
).rejects.toThrow(AccessError)
|
|
})
|
|
|
|
it('bewahrt bei einer Änderung den vorherigen Stand', async () => {
|
|
const id = `${marke}-v3`
|
|
await createEntry(schreiber(), {
|
|
id,
|
|
departmentKey: abteilungKey,
|
|
body: 'Erster Stand',
|
|
minClearance: 0,
|
|
})
|
|
await editEntry(schreiber(), { id, body: 'Zweiter Stand' })
|
|
|
|
const fassungen = await db.departmentEntryVersion.findMany({ where: { entryId: id } })
|
|
expect(fassungen).toHaveLength(1)
|
|
expect(fassungen[0]?.body).toBe('Erster Stand')
|
|
|
|
const jetzt = await db.departmentEntry.findUniqueOrThrow({ where: { id } })
|
|
expect(jetzt.body).toBe('Zweiter Stand')
|
|
expect(jetzt.version).toBe(2)
|
|
})
|
|
|
|
it('lässt beim Zurückziehen einen Grabstein stehen', async () => {
|
|
const id = `${marke}-v4`
|
|
await createEntry(schreiber(), {
|
|
id,
|
|
departmentKey: abteilungKey,
|
|
body: 'Wird zurückgezogen',
|
|
minClearance: 0,
|
|
})
|
|
await retractEntry(schreiber(), { id, reason: 'Verwechslung mit einem anderen Vorgang' })
|
|
|
|
const danach = await db.departmentEntry.findUniqueOrThrow({ where: { id } })
|
|
expect(danach.retractedAt).not.toBeNull()
|
|
expect(danach.retractionReason).toContain('Verwechslung')
|
|
|
|
// Und er ist weiterhin da: es gibt kein Löschen.
|
|
await db.departmentEntry.deleteMany({ where: { id } })
|
|
expect(await db.departmentEntry.findUnique({ where: { id } })).not.toBeNull()
|
|
})
|
|
})
|
|
})
|