import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' /** * Was der S3-Treiber tatsächlich an den Objektspeicher schickt. * * Anlass: Der Treiber setzte bei jedem PutObject fest * `ServerSideEncryption: 'AES256'`. Das mitgelieferte MinIO läuft ohne KMS * und lehnte das mit `NotImplemented` ab — es ließ sich kein einziges * Dokument ablegen, und wer der INSTALLATION.md folgte, landete * zwangsläufig dort. * * Gemerkt hat das kein Test, weil in der Entwicklung der Dateitreiber * läuft und der S3-Pfad nie ausgeführt wurde. Diese Datei schließt genau * diese Lücke: Sie fängt den Befehl ab, bevor er das Netz sieht, und * schaut hinein. */ process.env.S3_ENDPOINT = 'http://minio:9000' process.env.S3_BUCKET = 'betreuungs-cockpit' process.env.S3_ACCESS_KEY_ID = 'probe-zugang' process.env.S3_SECRET_ACCESS_KEY = 'probe-geheimnis' /** Der letzte Befehl, den der Treiber abgeschickt hat. */ let letzterBefehl: Record | null = null vi.mock('@aws-sdk/client-s3', () => { class PutObjectCommand { input: Record constructor(input: Record) { this.input = input } } class GetObjectCommand extends PutObjectCommand {} class DeleteObjectCommand extends PutObjectCommand {} class HeadObjectCommand extends PutObjectCommand {} class ListObjectsV2Command extends PutObjectCommand {} class S3Client { async send(befehl: { input: Record }) { letzterBefehl = befehl.input return {} } } return { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand, HeadObjectCommand, ListObjectsV2Command, } }) const SCHLUESSEL = 'betreute/cmt4ey0jm0003o001wq4rc3m9/dokumente/ba7a8c1a-d4c5-41c7-8f5f-ebfb4805b0e5.pdf' const INHALT = Buffer.from('%PDF-1.4 ein Bescheid', 'utf8') /** * Der Treiber baut seinen Klienten einmal und merkt ihn sich. Damit die * Umgebungsvariable je Testfall neu gelesen wird, muss das Modul frisch * geladen werden. */ async function treiberFrisch() { vi.resetModules() return import('@/lib/speicher/s3') } describe('S3-Treiber: serverseitige Verschlüsselung', () => { beforeEach(() => { letzterBefehl = null delete process.env.S3_SSE }) afterEach(() => { delete process.env.S3_SSE }) it('schickt ohne S3_SSE keinen Verschlüsselungskopf mit', async () => { const s3 = await treiberFrisch() await s3.schreiben(SCHLUESSEL, INHALT, 'application/pdf') expect(letzterBefehl).not.toBeNull() expect(letzterBefehl).not.toHaveProperty('ServerSideEncryption') }) it('schickt ihn mit, wenn S3_SSE gesetzt ist', async () => { process.env.S3_SSE = 'AES256' const s3 = await treiberFrisch() await s3.schreiben(SCHLUESSEL, INHALT, 'application/pdf') expect(letzterBefehl?.ServerSideEncryption).toBe('AES256') }) it('übernimmt auch einen anderen Wert, den ein Anbieter verlangt', async () => { process.env.S3_SSE = 'aws:kms' const s3 = await treiberFrisch() await s3.schreiben(SCHLUESSEL, INHALT, 'application/pdf') expect(letzterBefehl?.ServerSideEncryption).toBe('aws:kms') }) it('behandelt eine leere Angabe wie "aus"', async () => { // Sonst schickte eine leergelassene Zeile in der .env einen leeren // Kopf, und der Anbieter antwortete mit einem Fehler, den niemand // versteht. process.env.S3_SSE = ' ' const s3 = await treiberFrisch() await s3.schreiben(SCHLUESSEL, INHALT, 'application/pdf') expect(letzterBefehl).not.toHaveProperty('ServerSideEncryption') }) }) describe('S3-Treiber: was sonst noch mitgeht', () => { beforeEach(() => { letzterBefehl = null delete process.env.S3_SSE }) it('legt den Inhalt verschlüsselt ab, nicht im Klartext', async () => { const s3 = await treiberFrisch() await s3.schreiben(SCHLUESSEL, INHALT, 'application/pdf') const koerper = letzterBefehl?.Body as Buffer expect(koerper.subarray(0, 4).toString('ascii')).toBe('BCD1') expect(koerper.includes(Buffer.from('ein Bescheid', 'utf8'))).toBe(false) }) it('meldet den Inhaltstyp nicht als PDF - dort liegt Geheimtext', async () => { const s3 = await treiberFrisch() await s3.schreiben(SCHLUESSEL, INHALT, 'application/pdf') expect(letzterBefehl?.ContentType).toBe('application/octet-stream') expect(letzterBefehl?.ContentDisposition).toBe('attachment') }) })