// @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((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((_, 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) }) })