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.
133 lines
4.8 KiB
TypeScript
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)
|
|
})
|
|
})
|