Files
AfiS-Aktensystem/web/tests/entzug.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

133 lines
4.8 KiB
TypeScript

// @vitest-environment node
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { Client } from 'pg'
import { db } from '@/lib/db'
import { loadSession, createSession } from '@/lib/auth/session'
import { invalidateUser, resolvePermissions } from '@/lib/auth/effective'
/**
* Was passiert, wenn jemand seinen Zugriff verliert.
*
* Vier Wege führen dorthin: Herabstufung, Entzug der AfiS-Rolle, Rauswurf und
* selbst gehen. Der Worker deckt die Discord-Seite ab (siehe
* worker/tests/mitgliedschaft.test.ts) - hier steht die andere Hälfte: dass
* die Web-App den Entzug auch bemerkt.
*
* Der Kern ist, dass die **Zeile in der Datenbank** der Sitzungsnachweis ist
* und nicht das Cookie. Ein Cookie lässt sich nicht zurückziehen, eine Zeile
* schon.
*/
const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL
const suite = url ? describe : describe.skip
const marke = `entzug-${Date.now()}`
suite('Entzug des Zugriffs', () => {
const nutzer = `${marke}-nutzer`
const rolleHoch = `${marke}-rolle-hoch`
const rolleGast = `${marke}-rolle-gast`
beforeAll(async () => {
await db.user.create({ data: { id: nutzer, username: nutzer, displayName: nutzer } })
await db.roleMapping.create({
data: {
discordRoleId: rolleHoch,
label: 'Ermittler',
clearanceLevel: 4,
permissions: ['case.read'],
grantsMembership: true,
},
})
await db.roleMapping.create({
data: {
discordRoleId: rolleGast,
label: 'Gast',
clearanceLevel: 0,
permissions: [],
grantsMembership: false,
},
})
})
afterAll(async () => {
await db.$disconnect()
})
it('nimmt einer Herabstufung sofort die Stufe, sobald der Zwischenspeicher fällt', async () => {
await db.userRole.create({ data: { userId: nutzer, discordRoleId: rolleHoch } })
invalidateUser(nutzer)
expect((await resolvePermissions(nutzer)).clearanceLevel).toBe(4)
// Herabgestuft: die Rolle mit Stufe 4 weg, die Gastrolle bleibt.
await db.userRole.deleteMany({ where: { userId: nutzer, discordRoleId: rolleHoch } })
await db.userRole.create({ data: { userId: nutzer, discordRoleId: rolleGast } })
invalidateUser(nutzer)
const danach = await resolvePermissions(nutzer)
expect(danach.clearanceLevel).toBe(0)
expect(danach.permissions.has('case.read')).toBe(false)
// Und ohne berechtigende Rolle auch keine Mitgliedschaft mehr.
expect(danach.hasMembership).toBe(false)
})
it('meldet jede Rollenänderung über den Kanal, der den Zwischenspeicher leert', async () => {
// Das ist der Grund, warum eine Herabstufung in unter einer Sekunde wirkt
// und nicht erst nach der Verfallszeit von 60 Sekunden. Ohne diesen
// Auslöser bliebe die alte Stufe stehen, obwohl sie in der Datenbank
// längst weg ist.
const horcher = new Client({ connectionString: url })
await horcher.connect()
try {
const gemeldet = new Promise<string>((resolve) => {
horcher.on('notification', (nachricht) => {
if (nachricht.channel === 'member_changed' && nachricht.payload === nutzer) {
resolve(nachricht.payload)
}
})
})
await horcher.query('LISTEN member_changed')
await db.userRole.deleteMany({ where: { userId: nutzer, discordRoleId: rolleGast } })
const abgelaufen = new Promise<never>((_, ab) =>
setTimeout(() => ab(new Error('keine Meldung binnen 5 s')), 5000),
)
expect(await Promise.race([gemeldet, abgelaufen])).toBe(nutzer)
} finally {
await horcher.end()
}
})
it('macht eine gelöschte Sitzung sofort ungültig', async () => {
// Rauswurf und Austritt laufen beide hierüber: der Worker löscht die
// Zeile, und der nächste Request findet nichts mehr.
const sitzung = await createSession(nutzer)
expect(await loadSession(sitzung)).not.toBeNull()
await db.session.deleteMany({ where: { userId: nutzer } })
expect(await loadSession(sitzung)).toBeNull()
})
it('lässt eine abgelaufene Sitzung nicht gelten und räumt sie weg', async () => {
const sitzung = await db.session.create({
data: {
userId: nutzer,
expiresAt: new Date(Date.now() - 1000),
},
select: { id: true },
})
expect(await loadSession(sitzung.id)).toBeNull()
// Und sie ist danach weg, nicht nur abgelehnt.
expect(await db.session.findUnique({ where: { id: sitzung.id } })).toBeNull()
})
it('hält einen stillgelegten Nutzer draussen, auch mit gültiger Sitzung', async () => {
// handleDeparture setzt isActive auf false. getCurrentUser prüft das bei
// jedem Request zusätzlich zur Sitzung.
await db.user.update({ where: { id: nutzer }, data: { isActive: false } })
const konto = await db.user.findUniqueOrThrow({ where: { id: nutzer } })
expect(konto.isActive).toBe(false)
})
})