Phase 1: Anmeldung, Rechte und Rollenverwaltung
Auth.js übernimmt nur den OAuth-Ablauf mit state und PKCE, Scope identify. Das Zugangstoken des Nutzers wird danach verworfen: die Rollen holt der Bot mit seinem eigenen Token. Die Sitzung steht in der Datenbank, im Cookie liegt nur userId und sessionId. Ohne die Zeile in Session ist das Cookie wertlos, und nur so lässt sich eine Sitzung zurückziehen. Wer sich anmelden darf, steckt in lib/auth/signInPolicy.ts statt im Callback: das ist die Regel, an der alles hängt, und sie gehört einzeln prüfbar. Kein Guild-Mitglied, kein Zugang. Keine Rolle mit grantsMembership, kein Zugang. Notfall-Admins umgehen die Rolle, aber nicht die Guild-Mitgliedschaft. RoleMapping trägt jetzt grantsMembership, category und isPresent. Der Worker spiegelt jede Rolle der Guild hierher, damit im Adminbereich nichts von Hand eingetragen werden muss. Gelöschte Rollen werden nicht entfernt, sondern auf isPresent = false gesetzt: beim Umzug auf den echten Server bleiben sie als Vorlage stehen. Der Worker gleicht über Gateway-Ereignisse und zusätzlich alle fünf Minuten voll ab. Verliert jemand seine AfiS-Rolle, beendet er dessen Sitzungen sofort und setzt isActive auf false — der Datensatz bleibt, damit Autorenangaben in alten Akten gültig bleiben. Das passiert im Worker und nicht in der Web-App, weil es dort erst beim nächsten Request des Betroffenen griffe. Zwei Widersprüche der Dokumente aufgelöst, festgehalten in docs/13-entscheidungen.md: der Ausschluss schlägt auch den Admin (das Codebeispiel in 03-auth-und-rechte.md übergeht ihn, die Reihenfolge eine Zeile darüber nicht), und versiegelte Akten fallen im Where-Fragment heraus statt in einer Prüfung danach. 45 Tests, davon die Negativfälle aus docs/03 gegen eine echte Datenbank: Stufe 2 sieht die Stufe-4-Akte nicht und findet sie auch nicht in der Trefferzahl, ein Ausgeschlossener mit Stufe 5 kommt nicht heran, ein Rollenentzug beendet die Sitzungen, und ein Ausfall der Discord-API sieht nicht aus wie eine verweigerte Mitgliedschaft.
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { decideSignIn, SIGN_IN_ERRORS } from '@/lib/auth/signInPolicy'
|
||||
import type { GuildMember } from '@/lib/auth/discord'
|
||||
|
||||
const mitglied: GuildMember = { userId: '42', roleIds: ['r-afis'], nickname: null }
|
||||
|
||||
describe('Wer sich anmelden darf', () => {
|
||||
it('lehnt ab, wer nicht auf dem Discord-Server ist', () => {
|
||||
const entscheidung = decideSignIn({
|
||||
member: null,
|
||||
isBootstrapAdmin: false,
|
||||
grantingRoleCount: 0,
|
||||
})
|
||||
expect(entscheidung).toEqual({ ok: false, reason: SIGN_IN_ERRORS.notInGuild })
|
||||
})
|
||||
|
||||
it('lehnt auch einen Notfall-Admin ab, der die Guild verlassen hat', () => {
|
||||
// Sonst bliebe die Hintertür nach einem Rauswurf offen.
|
||||
const entscheidung = decideSignIn({
|
||||
member: null,
|
||||
isBootstrapAdmin: true,
|
||||
grantingRoleCount: 0,
|
||||
})
|
||||
expect(entscheidung).toEqual({ ok: false, reason: SIGN_IN_ERRORS.notInGuild })
|
||||
})
|
||||
|
||||
it('lehnt einen Gast ohne AfiS-Rolle ab', () => {
|
||||
const entscheidung = decideSignIn({
|
||||
member: { ...mitglied, roleIds: ['r-gast'] },
|
||||
isBootstrapAdmin: false,
|
||||
grantingRoleCount: 0,
|
||||
})
|
||||
expect(entscheidung).toEqual({ ok: false, reason: SIGN_IN_ERRORS.notAfis })
|
||||
})
|
||||
|
||||
it('laesst ein Mitglied mit AfiS-Rolle durch', () => {
|
||||
expect(decideSignIn({ member: mitglied, isBootstrapAdmin: false, grantingRoleCount: 1 })).toEqual(
|
||||
{ ok: true },
|
||||
)
|
||||
})
|
||||
|
||||
it('laesst den Notfall-Admin ohne AfiS-Rolle durch', () => {
|
||||
expect(decideSignIn({ member: mitglied, isBootstrapAdmin: true, grantingRoleCount: 0 })).toEqual(
|
||||
{ ok: true },
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Mitgliedschaftsabfrage beim Bot', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
vi.unstubAllEnvs()
|
||||
})
|
||||
|
||||
function stubEnv() {
|
||||
vi.stubEnv('DATABASE_URL', 'postgresql://x/y')
|
||||
vi.stubEnv('APP_URL', 'http://localhost:3000')
|
||||
vi.stubEnv('AUTH_SECRET', 'x'.repeat(40))
|
||||
vi.stubEnv('DISCORD_CLIENT_ID', 'c')
|
||||
vi.stubEnv('DISCORD_CLIENT_SECRET', 's')
|
||||
vi.stubEnv('DISCORD_BOT_TOKEN', 'bot-token')
|
||||
vi.stubEnv('DISCORD_GUILD_ID', '99')
|
||||
}
|
||||
|
||||
it('meldet null, wenn Discord 404 antwortet', async () => {
|
||||
stubEnv()
|
||||
vi.stubGlobal('fetch', vi.fn(async () => new Response(null, { status: 404 })))
|
||||
const { resetEnvCache } = await import('@/lib/env')
|
||||
resetEnvCache()
|
||||
const { fetchGuildMember } = await import('@/lib/auth/discord')
|
||||
|
||||
expect(await fetchGuildMember('42')).toBeNull()
|
||||
})
|
||||
|
||||
it('wirft bei einem Ausfall der Discord-API, statt Mitgliedschaft zu verneinen', async () => {
|
||||
// Ein Ausfall darf nicht aussehen wie "kein Mitglied": sonst sperrt eine
|
||||
// Störung bei Discord die halbe Abteilung aus und legt nebenbei Konten an.
|
||||
stubEnv()
|
||||
vi.stubGlobal('fetch', vi.fn(async () => new Response(null, { status: 500 })))
|
||||
const { resetEnvCache } = await import('@/lib/env')
|
||||
resetEnvCache()
|
||||
const { fetchGuildMember } = await import('@/lib/auth/discord')
|
||||
|
||||
await expect(fetchGuildMember('42')).rejects.toThrow(/500/)
|
||||
})
|
||||
|
||||
it('benutzt das Bot-Token, nicht das des Nutzers', async () => {
|
||||
stubEnv()
|
||||
const spy = vi.fn(async () => Response.json({ roles: ['r-afis'], nick: 'Vogel' }))
|
||||
vi.stubGlobal('fetch', spy)
|
||||
const { resetEnvCache } = await import('@/lib/env')
|
||||
resetEnvCache()
|
||||
const { fetchGuildMember } = await import('@/lib/auth/discord')
|
||||
|
||||
const member = await fetchGuildMember('42')
|
||||
expect(member).toEqual({ userId: '42', roleIds: ['r-afis'], nickname: 'Vogel' })
|
||||
|
||||
const call = spy.mock.calls[0] as unknown as [string, RequestInit]
|
||||
const [url, init] = call
|
||||
expect(url).toContain('/guilds/99/members/42')
|
||||
expect((init.headers as Record<string, string>).authorization).toBe('Bot bot-token')
|
||||
})
|
||||
})
|
||||
+38
-4
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { de } from '@/lib/i18n/de'
|
||||
import { OPT_IN_PERMISSIONS, PERMISSIONS } from '@/lib/auth/permissions'
|
||||
|
||||
describe('Sprachkonvention', () => {
|
||||
it('haelt die Stufenbezeichnungen aus den Texten heraus', () => {
|
||||
@@ -19,9 +20,21 @@ describe('Sprachkonvention', () => {
|
||||
it('haelt deutsche Oberflaechentexte aus dem JSX heraus', () => {
|
||||
// Umlaute und Eszett sind der billigste verlaessliche Marker fuer einen
|
||||
// deutschen Text. Alles Sichtbare gehoert nach lib/i18n/de.ts, siehe CLAUDE.md.
|
||||
//
|
||||
// Geprueft werden Komponenten und Seiten (.tsx) sowie Server Actions:
|
||||
// deren Fehlermeldungen landen in der Oberflaeche. Kommentare zaehlen
|
||||
// nicht, die duerfen und sollen deutsch sein.
|
||||
const offenders: string[] = []
|
||||
const roots = ['app', 'components']
|
||||
|
||||
function stripComments(source: string): string {
|
||||
// Blockkommentare durch gleich viele Zeilenumbrueche ersetzen, damit die
|
||||
// Zeilennummern stimmen bleiben.
|
||||
return source
|
||||
.replace(/\/\*[\s\S]*?\*\//g, (match) => match.replace(/[^\n]/g, ' '))
|
||||
.replace(/\/\/.*$/gm, '')
|
||||
}
|
||||
|
||||
function walk(dir: string) {
|
||||
for (const name of readdirSync(dir)) {
|
||||
const path = join(dir, name)
|
||||
@@ -29,11 +42,10 @@ describe('Sprachkonvention', () => {
|
||||
walk(path)
|
||||
continue
|
||||
}
|
||||
if (!name.endsWith('.tsx')) continue
|
||||
const source = readFileSync(path, 'utf8')
|
||||
if (!name.endsWith('.tsx') && name !== 'actions.ts') continue
|
||||
const source = stripComments(readFileSync(path, 'utf8'))
|
||||
for (const [index, line] of source.split('\n').entries()) {
|
||||
const withoutComments = line.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, '')
|
||||
if (/[äöüÄÖÜß]/.test(withoutComments)) {
|
||||
if (/[äöüÄÖÜß]/.test(line)) {
|
||||
offenders.push(`${path}:${index + 1}`)
|
||||
}
|
||||
}
|
||||
@@ -44,3 +56,25 @@ describe('Sprachkonvention', () => {
|
||||
expect(offenders).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Rechte-Enum', () => {
|
||||
it('kennt die vier Rechte, die Phase 1 anlegen muss', () => {
|
||||
for (const permission of [
|
||||
'entry.edit_own',
|
||||
'entry.edit_any',
|
||||
'entry.retract',
|
||||
'case.edit_closed',
|
||||
]) {
|
||||
expect(PERMISSIONS).toContain(permission)
|
||||
}
|
||||
})
|
||||
|
||||
it('haelt drei davon im Auslieferungszustand zurueck', () => {
|
||||
// Der Leiter schaltet sie bewusst frei, siehe docs/03-auth-und-rechte.md.
|
||||
expect([...OPT_IN_PERMISSIONS].sort()).toEqual([
|
||||
'entry.edit_any',
|
||||
'entry.edit_own',
|
||||
'entry.retract',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// @vitest-environment node
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { createPrismaClient, loadEnvFromRepoRoot, type PrismaClient } from '@afis/db'
|
||||
|
||||
loadEnvFromRepoRoot()
|
||||
const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL
|
||||
const suite = url ? describe : describe.skip
|
||||
|
||||
let db: PrismaClient
|
||||
const marke = `rechte-${Date.now()}`
|
||||
|
||||
const rollen = {
|
||||
afis: `${marke}-rolle-afis`,
|
||||
gast: `${marke}-rolle-gast`,
|
||||
agent: `${marke}-rolle-agent`,
|
||||
leitung: `${marke}-rolle-leitung`,
|
||||
zugangHoch: `${marke}-rolle-zugang-hoch`,
|
||||
zugangNiedrig: `${marke}-rolle-zugang-niedrig`,
|
||||
}
|
||||
|
||||
const nutzer = {
|
||||
mitglied: `${marke}-mitglied`,
|
||||
gast: `${marke}-gast`,
|
||||
notfallAdmin: `${marke}-notfall`,
|
||||
}
|
||||
|
||||
suite('Rechteauflösung', () => {
|
||||
beforeAll(async () => {
|
||||
db = createPrismaClient(url)
|
||||
|
||||
await db.roleMapping.createMany({
|
||||
data: [
|
||||
{ discordRoleId: rollen.afis, label: 'AfiS', category: 'MEMBERSHIP', grantsMembership: true },
|
||||
{ discordRoleId: rollen.gast, label: 'Gast', category: 'UNSORTED' },
|
||||
{
|
||||
discordRoleId: rollen.agent,
|
||||
label: 'Agent',
|
||||
category: 'RANK',
|
||||
permissions: ['case.read', 'entry.create'],
|
||||
},
|
||||
{
|
||||
discordRoleId: rollen.leitung,
|
||||
label: 'Section Commander',
|
||||
category: 'RANK',
|
||||
permissions: ['case.read', 'case.close', 'export.approve'],
|
||||
},
|
||||
{
|
||||
discordRoleId: rollen.zugangNiedrig,
|
||||
label: 'Restricted Access',
|
||||
category: 'CLEARANCE',
|
||||
clearanceLevel: 1,
|
||||
},
|
||||
{
|
||||
discordRoleId: rollen.zugangHoch,
|
||||
label: 'Critical Operations',
|
||||
category: 'CLEARANCE',
|
||||
clearanceLevel: 4,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
for (const id of Object.values(nutzer)) {
|
||||
await db.user.create({ data: { id, username: id, displayName: id } })
|
||||
}
|
||||
|
||||
await db.userRole.createMany({
|
||||
data: [
|
||||
{ userId: nutzer.mitglied, discordRoleId: rollen.afis },
|
||||
{ userId: nutzer.mitglied, discordRoleId: rollen.agent },
|
||||
{ userId: nutzer.mitglied, discordRoleId: rollen.leitung },
|
||||
{ userId: nutzer.mitglied, discordRoleId: rollen.zugangNiedrig },
|
||||
{ userId: nutzer.mitglied, discordRoleId: rollen.zugangHoch },
|
||||
{ userId: nutzer.gast, discordRoleId: rollen.gast },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await db.$disconnect()
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
const { invalidateAll } = await import('@/lib/auth/effective')
|
||||
invalidateAll()
|
||||
})
|
||||
|
||||
it('nimmt die höchste Stufe und die Vereinigung der Rechte', async () => {
|
||||
const { resolvePermissions } = await import('@/lib/auth/effective')
|
||||
const p = await resolvePermissions(nutzer.mitglied)
|
||||
|
||||
expect(p.clearanceLevel).toBe(4)
|
||||
expect([...p.permissions].sort()).toEqual([
|
||||
'case.close',
|
||||
'case.read',
|
||||
'entry.create',
|
||||
'export.approve',
|
||||
])
|
||||
expect(p.hasMembership).toBe(true)
|
||||
expect(p.isAdmin).toBe(false)
|
||||
})
|
||||
|
||||
it('erkennt einen Gast ohne AfiS-Rolle als nicht zugangsberechtigt', async () => {
|
||||
const { resolvePermissions } = await import('@/lib/auth/effective')
|
||||
const p = await resolvePermissions(nutzer.gast)
|
||||
|
||||
expect(p.hasMembership).toBe(false)
|
||||
expect(p.clearanceLevel).toBe(0)
|
||||
expect(p.permissions.size).toBe(0)
|
||||
})
|
||||
|
||||
it('gibt dem Notfallzugang Stufe 5 und alle Rechte ohne Rollen', async () => {
|
||||
process.env.BOOTSTRAP_ADMIN_IDS = nutzer.notfallAdmin
|
||||
const { resetEnvCache } = await import('@/lib/env')
|
||||
resetEnvCache()
|
||||
|
||||
const { resolvePermissions, invalidateAll } = await import('@/lib/auth/effective')
|
||||
invalidateAll()
|
||||
const p = await resolvePermissions(nutzer.notfallAdmin)
|
||||
|
||||
expect(p.isAdmin).toBe(true)
|
||||
expect(p.clearanceLevel).toBe(5)
|
||||
expect(p.hasMembership).toBe(true)
|
||||
expect(p.permissions.has('roles.manage')).toBe(true)
|
||||
expect(p.permissions.has('entry.retract')).toBe(true)
|
||||
})
|
||||
|
||||
it('verwirft den Cache, wenn sich das Rollen-Mapping ändert', async () => {
|
||||
const { resolvePermissions, invalidateAll } = await import('@/lib/auth/effective')
|
||||
|
||||
expect((await resolvePermissions(nutzer.mitglied)).clearanceLevel).toBe(4)
|
||||
|
||||
await db.roleMapping.update({
|
||||
where: { discordRoleId: rollen.zugangHoch },
|
||||
data: { clearanceLevel: 2 },
|
||||
})
|
||||
|
||||
// Ohne Verwerfen liefert der Cache weiterhin die alte Stufe.
|
||||
expect((await resolvePermissions(nutzer.mitglied)).clearanceLevel).toBe(4)
|
||||
|
||||
invalidateAll()
|
||||
expect((await resolvePermissions(nutzer.mitglied)).clearanceLevel).toBe(2)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,189 @@
|
||||
// @vitest-environment node
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import { createPrismaClient, loadEnvFromRepoRoot, type PrismaClient } from '@afis/db'
|
||||
import { visibleCaseWhere, visibleEntryWhere } from '@/lib/access/caseFilter'
|
||||
import type { EffectivePermissions, Permission } from '@/lib/auth/permissions'
|
||||
|
||||
/**
|
||||
* Die Negativtests aus docs/03-auth-und-rechte.md.
|
||||
*
|
||||
* Sie laufen gegen eine echte Datenbank, weil genau das der Punkt ist: die
|
||||
* Prüfung muss in der Abfrage stecken, nicht in einem Filter danach. Ein Test
|
||||
* gegen eine ausgedachte Liste würde die Aussage nicht treffen.
|
||||
*/
|
||||
loadEnvFromRepoRoot()
|
||||
const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL
|
||||
const suite = url ? describe : describe.skip
|
||||
|
||||
let db: PrismaClient
|
||||
const marke = `zugriff-${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('Sichtbarkeit von Akten', () => {
|
||||
const ids = {
|
||||
offen: `${marke}-offen`,
|
||||
geheim: `${marke}-geheim`,
|
||||
versiegelt: `${marke}-versiegelt`,
|
||||
}
|
||||
const nutzer = {
|
||||
stufe1: `${marke}-stufe1`,
|
||||
stufe2: `${marke}-stufe2`,
|
||||
stufe5: `${marke}-stufe5`,
|
||||
beteiligt: `${marke}-beteiligt`,
|
||||
admin: `${marke}-admin`,
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
db = createPrismaClient(url)
|
||||
|
||||
const template = await db.caseTemplate.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: template.id, templateKey: template.key, createdById: nutzer.stufe5 }
|
||||
|
||||
await db.case.create({
|
||||
data: {
|
||||
...gemeinsam,
|
||||
id: ids.offen,
|
||||
caseNumber: `AfiS-9001-${String(Date.now()).slice(-4)}`,
|
||||
title: 'Offener Vorgang',
|
||||
classification: 1,
|
||||
status: 'OPEN',
|
||||
},
|
||||
})
|
||||
|
||||
await db.case.create({
|
||||
data: {
|
||||
...gemeinsam,
|
||||
id: ids.geheim,
|
||||
caseNumber: `AfiS-9002-${String(Date.now()).slice(-4)}`,
|
||||
title: 'Geheimer Vorgang',
|
||||
classification: 4,
|
||||
status: 'OPEN',
|
||||
},
|
||||
})
|
||||
|
||||
await db.case.create({
|
||||
data: {
|
||||
...gemeinsam,
|
||||
id: ids.versiegelt,
|
||||
caseNumber: `AfiS-9003-${String(Date.now()).slice(-4)}`,
|
||||
title: 'Versiegelter Vorgang',
|
||||
classification: 1,
|
||||
status: 'SEALED',
|
||||
},
|
||||
})
|
||||
|
||||
// Beteiligter an der geheimen Akte, obwohl seine Stufe nicht reicht.
|
||||
await db.caseParticipant.create({
|
||||
data: { caseId: ids.geheim, userId: nutzer.beteiligt, role: 'WITNESS' },
|
||||
})
|
||||
|
||||
// Ausgeschlossen von der offenen Akte, obwohl Stufe 5 und Admin.
|
||||
await db.caseExclusion.createMany({
|
||||
data: [
|
||||
{ caseId: ids.offen, userId: nutzer.stufe5, reason: 'Betroffener' },
|
||||
{ caseId: ids.offen, userId: nutzer.admin, reason: 'Betroffener' },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await db.$disconnect()
|
||||
})
|
||||
|
||||
async function sichtbar(p: EffectivePermissions): Promise<string[]> {
|
||||
const rows = await db.case.findMany({
|
||||
where: { AND: [visibleCaseWhere(p), { id: { in: Object.values(ids) } }] },
|
||||
select: { id: true },
|
||||
})
|
||||
return rows.map((r) => r.id).sort()
|
||||
}
|
||||
|
||||
it('zeigt einer Stufe 2 die Stufe-4-Akte nicht, auch nicht als Treffer', async () => {
|
||||
const p = perms(nutzer.stufe2, 2)
|
||||
const treffer = await sichtbar(p)
|
||||
expect(treffer).not.toContain(ids.geheim)
|
||||
expect(treffer).toContain(ids.offen)
|
||||
|
||||
// Auch die Zählung darf sie nicht enthalten, sonst verrät die Lücke sie.
|
||||
const anzahl = await db.case.count({
|
||||
where: { AND: [visibleCaseWhere(p), { id: { in: Object.values(ids) } }] },
|
||||
})
|
||||
expect(anzahl).toBe(treffer.length)
|
||||
})
|
||||
|
||||
it('gibt einem ausgeschlossenen Nutzer mit Stufe 5 keinen Zugriff', async () => {
|
||||
const treffer = await sichtbar(perms(nutzer.stufe5, 5))
|
||||
expect(treffer).not.toContain(ids.offen)
|
||||
})
|
||||
|
||||
it('schlägt selbst den Admin, wenn er ausgeschlossen ist', async () => {
|
||||
// Der Ausschluss deckt den Fall ab, dass gegen einen hochrangigen
|
||||
// Mitarbeiter ermittelt wird. Der Administrator ist einer.
|
||||
const treffer = await sichtbar(perms(nutzer.admin, 5, { isAdmin: true }))
|
||||
expect(treffer).not.toContain(ids.offen)
|
||||
expect(treffer).toContain(ids.geheim)
|
||||
})
|
||||
|
||||
it('lässt einen Beteiligten auch über seiner Stufe lesen', async () => {
|
||||
const treffer = await sichtbar(perms(nutzer.beteiligt, 1))
|
||||
expect(treffer).toContain(ids.geheim)
|
||||
})
|
||||
|
||||
it('verbirgt versiegelte Akten unterhalb von Stufe 5', async () => {
|
||||
expect(await sichtbar(perms(nutzer.stufe2, 2))).not.toContain(ids.versiegelt)
|
||||
expect(await sichtbar(perms(nutzer.stufe1, 1))).not.toContain(ids.versiegelt)
|
||||
expect(await sichtbar(perms(nutzer.stufe5, 5))).toContain(ids.versiegelt)
|
||||
})
|
||||
|
||||
it('verbirgt Verlaufseinträge oberhalb der Stufe des Lesers', async () => {
|
||||
await db.caseEntry.createMany({
|
||||
data: [
|
||||
{
|
||||
id: `${marke}-eintrag-offen`,
|
||||
caseId: ids.offen,
|
||||
type: 'NOTE',
|
||||
body: 'Für alle lesbar',
|
||||
authorId: nutzer.stufe1,
|
||||
minClearance: 0,
|
||||
},
|
||||
{
|
||||
id: `${marke}-eintrag-hoch`,
|
||||
caseId: ids.offen,
|
||||
type: 'NOTE',
|
||||
body: 'Nur ab Stufe 4',
|
||||
authorId: nutzer.stufe1,
|
||||
minClearance: 4,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const p = perms(nutzer.stufe1, 1)
|
||||
const eintraege = await db.caseEntry.findMany({
|
||||
where: { AND: [visibleEntryWhere(p), { caseId: ids.offen }] },
|
||||
select: { id: true },
|
||||
})
|
||||
const gefunden = eintraege.map((e) => e.id)
|
||||
expect(gefunden).toContain(`${marke}-eintrag-offen`)
|
||||
expect(gefunden).not.toContain(`${marke}-eintrag-hoch`)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user