// @vitest-environment node import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { db } from '@/lib/db' import { enqueueCaseNotification } from '@/lib/notify/enqueue' import type { Prisma } from '@afis/db' /** * Discord kennt unsere Klassifizierung nicht. * * Deshalb entscheidet **diese Anwendung**, wer eine Meldung bekommt und was * darin stehen darf; der Worker liefert nur aus. Diese Tests prüfen genau die * beiden Zusicherungen, auf denen das beruht. */ const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL const suite = url ? describe : describe.skip const marke = `notify-${Date.now()}` suite('Benachrichtigungen', () => { const nutzer = { autor: `${marke}-autor`, stufe1: `${marke}-stufe1`, stufe4: `${marke}-stufe4`, aus: `${marke}-aus`, ausgeschlossen: `${marke}-ausgeschlossen`, } let geheimId = '' let geheimNummer = '' async function empfaengerVon(kind: string): Promise { const zeilen = await db.notificationOutbox.findMany({ where: { caseId: geheimId, kind }, select: { recipientId: true }, }) return zeilen .map((z) => z.recipientId) .filter((id): id is string => id !== null) .sort() } beforeAll(async () => { const vorlage = await db.documentTemplate.create({ data: { key: `${marke}-vorlage`, version: 1, name: 'Prüfvorlage', fields: [] }, }) // Eine Rolle mit Stufe 4, damit die Rechteauflösung etwas zu tun hat. const rolle = await db.roleMapping.create({ data: { discordRoleId: `${marke}-rolle`, label: 'Prüfstufe 4', clearanceLevel: 4, permissions: [], grantsMembership: true, category: 'CLEARANCE', }, }) for (const [name, id] of Object.entries(nutzer)) { await db.user.create({ data: { id, username: id, displayName: id, notifyMode: name === 'aus' ? 'OFF' : 'ALL', ...(name === 'stufe4' || name === 'ausgeschlossen' ? { roles: { create: { discordRoleId: rolle.discordRoleId } } } : {}), }, }) } const akte = await db.case.create({ data: { caseNumber: `Meld-2026-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`, title: 'Streng vertraulicher Vorgang', templateId: vorlage.id, templateKey: vorlage.key, createdById: nutzer.autor, classification: 4, status: 'OPEN', }, }) geheimId = akte.id geheimNummer = akte.caseNumber await db.caseExclusion.create({ data: { caseId: akte.id, userId: nutzer.ausgeschlossen, reason: 'Betroffener' }, }) }) afterAll(async () => { await db.$disconnect() }) it('schreibt nur an die, die die Akte auch sehen dürfen', async () => { await db.$transaction(async (tx) => { await enqueueCaseNotification(tx as Prisma.TransactionClient, { kind: 'entry.new', caseId: geheimId, caseNumber: geheimNummer, classification: 4, title: 'Streng vertraulicher Vorgang', actorId: nutzer.autor, actorName: 'Autor', }) }) const ziele = await empfaengerVon('entry.new') // Stufe 4 bekommt die Meldung. expect(ziele).toContain(nutzer.stufe4) // Stufe 1 nicht — die Akte gibt es für ihn nicht. expect(ziele).not.toContain(nutzer.stufe1) // Wer abgeschaltet hat, bekommt nichts. expect(ziele).not.toContain(nutzer.aus) // Der Ausschluss schlägt auch die Stufe. expect(ziele).not.toContain(nutzer.ausgeschlossen) // Und der Auslöser bekommt keine Meldung über sich selbst. expect(ziele).not.toContain(nutzer.autor) }) it('lässt den Titel ab Stufe 3 weg', async () => { const zeile = await db.notificationOutbox.findFirstOrThrow({ where: { caseId: geheimId, kind: 'entry.new' }, select: { meta: true }, }) const meta = zeile.meta as { caseNumber?: string; title?: string | null } // Das Aktenzeichen darf hinaus, der Titel nicht: Discord kennt unsere // Klassifizierung nicht, der Link führt zur App, die selbst prüft. expect(meta.caseNumber).toBe(geheimNummer) expect(meta.title).toBeNull() }) it('legt eine Zeile für den allgemeinen Kanal an, ohne Empfänger', async () => { const allgemein = await db.notificationOutbox.findMany({ where: { caseId: geheimId, kind: 'entry.new', recipientId: null }, }) expect(allgemein).toHaveLength(1) }) it('nimmt bei niedriger Stufe den Titel mit', async () => { const offen = await db.case.create({ data: { caseNumber: `Offn-2026-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`, title: 'Harmloser Vorgang', templateId: (await db.documentTemplate.findFirstOrThrow({ where: { key: `${marke}-vorlage` }, })).id, templateKey: `${marke}-vorlage`, createdById: nutzer.autor, classification: 1, status: 'OPEN', }, }) await db.$transaction(async (tx) => { await enqueueCaseNotification(tx as Prisma.TransactionClient, { kind: 'entry.new', caseId: offen.id, caseNumber: offen.caseNumber, classification: 1, title: 'Harmloser Vorgang', actorId: nutzer.autor, actorName: 'Autor', }) }) const zeile = await db.notificationOutbox.findFirstOrThrow({ where: { caseId: offen.id }, select: { meta: true }, }) expect((zeile.meta as { title?: string | null }).title).toBe('Harmloser Vorgang') }) })