// test/auftraegeAblage.test.js import { test, after } from 'node:test' import assert from 'node:assert/strict' import { createHash } from 'node:crypto' import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs' import os from 'node:os' import path from 'node:path' // Die Ablage liest ihren Ordner aus config.auftraege.uploadDir: AUFTRAEGE_UPLOAD_DIR muss vor dem ersten Import von config.js stehen. // Die Ablage liegt in einem Unterordner der Wurzel, damit „Köder“-Dateien in der Wurzel einen Ausbruch aus der Ablage (../) sichtbar machen. const PRAEFIX = path.join(os.tmpdir(), 'auftraege-ablage-') const wurzel = mkdtempSync(PRAEFIX) const basis = path.join(wurzel, 'ablage') mkdirSync(basis, { mode: 0o700 }) process.env.AUFTRAEGE_UPLOAD_DIR = basis after(() => { if (wurzel.startsWith(PRAEFIX)) rmSync(wurzel, { recursive: true, force: true }) }) const { speicherePdf, lesePdf, entfernePdf } = await import('../server/services/auftraege/ablage.js') const { AuftragError } = await import('../server/services/auftraege/fehler.js') const PDF = Buffer.from('%PDF-1.7\n% Auftragsbestätigung\n%%EOF\n') const sha = (buffer) => createHash('sha256').update(buffer).digest('hex') const nichtGefunden = (err) => err instanceof AuftragError && err.status === 404 && err.message === 'PDF nicht gefunden.' test('speicherePdf legt /.pdf an: Prüfsumme, Rechte 0700 und 0600, Inhalt unverändert', async () => { const ergebnis = await speicherePdf('c1', 'ab1', PDF) assert.deepEqual(ergebnis, { pdfDatei: 'c1/ab1.pdf', pdfSha256: sha(PDF) }) assert.match(ergebnis.pdfSha256, /^[0-9a-f]{64}$/) const datei = path.join(basis, 'c1', 'ab1.pdf') assert.deepEqual(readFileSync(datei), PDF) assert.equal(statSync(datei).mode & 0o777, 0o600) assert.equal(statSync(path.join(basis, 'c1')).mode & 0o777, 0o700) assert.deepEqual(await lesePdf('c1/ab1.pdf'), PDF) }) test('speicherePdf überschreibt nie: die vorhandene Datei bleibt, der zweite Aufruf ist 409', async () => { await speicherePdf('c2', 'ab1', PDF) await assert.rejects( () => speicherePdf('c2', 'ab1', Buffer.from('%PDF-1.7\nanderer Inhalt\n')), (err) => err instanceof AuftragError && err.status === 409, ) assert.deepEqual(readFileSync(path.join(basis, 'c2', 'ab1.pdf')), PDF) assert.equal((await speicherePdf('c3', 'ab1', PDF)).pdfDatei, 'c3/ab1.pdf', 'dieselbe AB-ID bei einem anderen Kunden ist eine andere Datei') }) test('speicherePdf: ungültige Kennungen und Puffer sind Programmfehler und legen nichts an', async () => { const vorher = readdirSync(basis).sort() const kennungen = [['', 'ab1'], ['c1', ''], ['../x', 'ab1'], ['c1', '../ab1'], ['c1/x', 'ab1'], ['c1', 'a.b'], ['c1', 'a'.repeat(65)], [undefined, 'ab1'], ['c1', 5]] for (const [kunde, ab] of kennungen) await assert.rejects(() => speicherePdf(kunde, ab, PDF), TypeError, JSON.stringify([kunde, ab])) for (const buffer of [Buffer.alloc(0), 'text', undefined, null, new Uint8Array(3)]) { await assert.rejects(() => speicherePdf('c9', 'ab9', buffer), TypeError, String(buffer)) } assert.deepEqual(readdirSync(basis).sort(), vorher) }) test('lesePdf: Pfadtricks, fremde Zeichen und falsche Form sind 404; erlaubt sind 64 Zeichen aus A–Z, a–z, 0–9, _ und -', async () => { await speicherePdf('c4', 'ab1', PDF) writeFileSync(path.join(wurzel, 'geheim.pdf'), 'GEHEIM') // neben der Ablage: ein Ausbruch per ../ würde sie lesen const falsch = [ 'c4/../geheim.pdf', '../geheim.pdf', 'c4/ab1.pdf/../../geheim.pdf', '/c4/ab1.pdf', `${basis}/c4/ab1.pdf`, 'c4\\ab1.pdf', 'c4/ab1.PDF', 'c4/ab1.pdf\0', 'c4/ab1.pdf.exe', 'c4/ab 1.pdf', 'c4/ab1', 'ab1.pdf', 'c4/x/ab1.pdf', '', ' ', `${'a'.repeat(65)}/ab1.pdf`, `c4/${'a'.repeat(65)}.pdf`, 'c4/ä.pdf', undefined, null, 42, {}, ] for (const pdfDatei of falsch) await assert.rejects(() => lesePdf(pdfDatei), nichtGefunden, JSON.stringify(pdfDatei)) await assert.rejects(() => lesePdf('c4/gibtsnicht.pdf'), nichtGefunden) const lang = `${'A'.repeat(32)}_${'b'.repeat(31)}` await speicherePdf('K_1-x', lang, PDF) assert.deepEqual(await lesePdf(`K_1-x/${lang}.pdf`), PDF) }) test('entfernePdf entfernt eine gespeicherte Datei und prüft den Pfad wie lesePdf; danach lässt sich dieselbe AB erneut speichern', async () => { await speicherePdf('c5', 'ab1', PDF) await entfernePdf('c5/ab1.pdf') assert.equal(existsSync(path.join(basis, 'c5', 'ab1.pdf')), false) await entfernePdf('c5/ab1.pdf') await assert.rejects(() => lesePdf('c5/ab1.pdf'), nichtGefunden) writeFileSync(path.join(wurzel, 'geheim2.pdf'), 'x') for (const pfad of ['../geheim2.pdf', 'c5/../geheim2.pdf']) await assert.rejects(() => entfernePdf(pfad), nichtGefunden, pfad) assert.ok(existsSync(path.join(wurzel, 'geheim2.pdf'))) assert.equal((await speicherePdf('c5', 'ab1', PDF)).pdfDatei, 'c5/ab1.pdf') })