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.
176 lines
5.5 KiB
TypeScript
176 lines
5.5 KiB
TypeScript
// @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<string[]> {
|
|
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')
|
|
})
|
|
})
|