// @vitest-environment node import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { readFile } from 'node:fs/promises' import sharp from 'sharp' import { db } from '@/lib/db' import { acceptUpload, loadDeliverable, releaseAttachment } from '@/lib/files/service' import { runCheckChain } from '@/lib/files/process' import { absolutePath } from '@/lib/files/paths' import { AccessError } from '@/lib/errors' import type { EffectivePermissions, Permission } from '@/lib/auth/permissions' /** * Die ganze Kette, an einer echten Datei. * * Auf dem Entwicklungsrechner läuft kein clamav. Genau das macht den Test * wertvoll: er hält fest, dass ein fehlender Scanner eine Datei **nicht** * sauber macht. Wäre das je anders, stünde hier ein roter Test. */ const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL const suite = url ? describe : describe.skip const marke = `kette-${Date.now()}` function perms( userId: string, options: { isAdmin?: boolean; permissions?: Permission[] } = {}, ): EffectivePermissions { return { userId, clearanceLevel: 4, permissions: new Set(options.permissions ?? []), isAdmin: options.isAdmin ?? false, hasMembership: true, } } suite('Prüfkette', () => { const nutzer = perms(`${marke}-nutzer`, { permissions: ['entry.create', 'case.edit'] }) const admin = perms(`${marke}-admin`, { isAdmin: true }) let caseNumber = '' async function bild(): Promise { const buffer = await sharp({ create: { width: 20, height: 20, channels: 3, background: { r: 10, g: 90, b: 200 } }, }) .jpeg() .withMetadata({ exif: { IFD0: { Copyright: 'VERTRAULICHE QUELLE' } } }) .toBuffer() return new Uint8Array(buffer) } beforeAll(async () => { const vorlage = await db.documentTemplate.create({ data: { key: `${marke}-vorlage`, version: 1, name: 'Prüfvorlage', fields: [] }, }) for (const p of [nutzer, admin]) { await db.user.create({ data: { id: p.userId, username: p.userId, displayName: p.userId } }) } const akte = await db.case.create({ data: { caseNumber: `${marke}`.slice(0, 40), title: 'Anhangprüfung', templateId: vorlage.id, templateKey: vorlage.key, createdById: nutzer.userId, classification: 1, status: 'OPEN', }, }) caseNumber = akte.caseNumber }) afterAll(async () => { await db.$disconnect() }) it('nimmt die Datei an und legt sie in der Quarantäne ab', async () => { const { id } = await acceptUpload(nutzer, { owner: { kind: 'case', caseNumber }, fileName: 'urlaubsbild.jpg', declaredType: 'image/jpeg', content: await bild(), }) const anhang = await db.attachment.findUniqueOrThrow({ where: { id } }) expect(anhang.scanStatus).toBe('PENDING') expect(anhang.storageKey).toMatch(/^quarantine\//) // Der Dateiname des Nutzers taucht im Pfad nicht auf. expect(anhang.storageKey).not.toContain('urlaubsbild') await expect(readFile(absolutePath(anhang.storageKey))).resolves.toBeDefined() // Und ausgeliefert wird sie nicht, solange sie dort liegt. expect(await loadDeliverable(nutzer, id)).toBeNull() }) it('macht aus einem fehlenden Scanner keine saubere Datei', async () => { const { id } = await acceptUpload(nutzer, { owner: { kind: 'case', caseNumber }, fileName: 'zweites.jpg', declaredType: 'image/jpeg', content: await bild(), }) await runCheckChain(id) const anhang = await db.attachment.findUniqueOrThrow({ where: { id } }) // Ohne clamd: unentschieden, nicht sauber und nicht blockiert. expect(anhang.scanStatus).toBe('FAILED') expect(anhang.scanMessage).toBeTruthy() expect(await loadDeliverable(admin, id)).toBeNull() }) it('blockiert einen Typ, der nicht auf der Liste steht', async () => { const { id } = await acceptUpload(nutzer, { owner: { kind: 'case', caseNumber }, fileName: 'archiv.zip', declaredType: 'application/zip', content: new Uint8Array([0x50, 0x4b, 0x03, 0x04, 0, 0, 0, 0]), }) await runCheckChain(id) const anhang = await db.attachment.findUniqueOrThrow({ where: { id } }) // Endgültig: das ist kein "wissen wir nicht", das ist ein Nein. expect(anhang.scanStatus).toBe('BLOCKED') expect(anhang.scanMessage).toContain('nicht erlaubt') }) it('blockiert, wenn der gemeldete Typ nicht zum Inhalt passt', async () => { const { id } = await acceptUpload(nutzer, { owner: { kind: 'case', caseNumber }, fileName: 'getarnt.pdf', declaredType: 'application/pdf', content: await bild(), }) await runCheckChain(id) const anhang = await db.attachment.findUniqueOrThrow({ where: { id } }) expect(anhang.scanStatus).toBe('BLOCKED') }) it('gibt eine unvollständige Prüfung nur für Admins frei, und kodiert dabei neu', async () => { const { id } = await acceptUpload(nutzer, { owner: { kind: 'case', caseNumber }, fileName: 'freizugeben.jpg', declaredType: 'image/jpeg', content: await bild(), }) await runCheckChain(id) await expect(releaseAttachment(nutzer, id)).rejects.toThrow(AccessError) await releaseAttachment(admin, id) const anhang = await db.attachment.findUniqueOrThrow({ where: { id } }) expect(anhang.scanStatus).toBe('CLEAN') expect(anhang.storageKey).toMatch(/^store\//) expect(anhang.releasedById).toBe(admin.userId) // Die Freigabe überspringt nur den Scanner. Die Neukodierung lief mit: // die Metadaten des Bildes sind weg. expect(anhang.scanMessage).toContain('Ohne Virenprüfung') const abgelegt = await readFile(absolutePath(anhang.storageKey)) expect(abgelegt.includes(Buffer.from('VERTRAULICHE QUELLE'))).toBe(false) expect(anhang.thumbKey).toBeTruthy() expect(anhang.blurhash).toBeTruthy() expect(anhang.width).toBe(20) // Jetzt, und erst jetzt, ist sie abrufbar. expect(await loadDeliverable(nutzer, id)).not.toBeNull() }) it('lässt eine blockierte Datei auch vom Admin nicht freigeben', async () => { const blockiert = await db.attachment.findFirstOrThrow({ where: { uploadedById: nutzer.userId, scanStatus: 'BLOCKED' }, select: { id: true }, }) await expect(releaseAttachment(admin, blockiert.id)).rejects.toThrow(AccessError) }) })