Files
scan-receipts/tests/e2e/upload_whitelist.test.ts
Timo 84b9987c49 Add full application: receipt scanning, auth, billing, and account deletion
Brings the working codebase (Next.js app, auth system, Stripe billing,
Docker/deploy config, tests, docs) into version control on top of the
placeholder initial commit, and adds account self-deletion (Danger Zone
in Settings, password + typed-email confirmation, cascading DB cleanup,
Stripe cancellation) per GDPR right-to-erasure.

Excludes local build caches, node_modules, and internal agent scratch
files; .gitignore hardened to keep those out going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 20:59:04 +02:00

214 lines
8.5 KiB
TypeScript

/**
* Upload-Whitelist Suite
*
* Verifiziert das serverseitige Whitelist-Gate `assertAllowedUploadKind`:
* Nur ausdrücklich erlaubte Dateitypen (Belege: PDF/JPG/PNG/WebP/HEIC, Profilbilder:
* JPG/PNG/WebP) werden akzeptiert — entschieden wird ausschließlich über die
* Magic Bytes des Buffers. Der deklarierte MIME-Type ist vom Client steuerbar
* und darf NIE Zugriff gewähren. Kein beliebiger Dateityp darf die
* Verarbeitungspipeline erreichen.
*/
import { describe, test, expect } from "./runner";
import {
AVATAR_ALLOWED_KINDS,
RECEIPT_ALLOWED_KINDS,
UnsupportedFileTypeError,
assertAllowedUploadKind,
} from "../../src/lib/ingest/acceptedTypes";
function bufferFromHex(hex: string): Buffer {
return Buffer.from(hex.replace(/\s+/g, ""), "hex");
}
/* ----------------------- Echte Magic-Byte-Buffer (Inline) ------------------ */
// JPEG: FF D8 FF
const JPEG_BUFFER = bufferFromHex("FF D8 FF E0 00 10 4A 46 49 46 00 01 01 00 00 01 00 01 00 00");
// PNG: 89 50 4E 47 0D 0A 1A 0A
const PNG_BUFFER = bufferFromHex(
"89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 00 00 00 01 00 00 00 01 08 06 00 00 00 1F 15 C4 89"
);
// WebP: RIFF....WEBP
const WEBP_BUFFER = Buffer.from("RIFF" + "\x10\x00\x00\x00" + "WEBPVP8 ", "latin1");
// PDF: %PDF- im Kopf
const PDF_BUFFER = Buffer.from(
"%PDF-1.7\n%\u00e2\u00e3\u00cf\u00d3\n1 0 obj\n<< /Type /Catalog >>\nendobj\n",
"latin1"
);
// Erkannte, aber für Belege NICHT erlaubte Typen:
const GIF_BUFFER = Buffer.from("GIF89a\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\x00", "latin1");
const TIFF_BUFFER = bufferFromHex("49 49 2A 00 08 00 00 00 00 00 00 00");
const BMP_BUFFER = bufferFromHex("42 4D 36 00 00 00 00 00 00 00 36 00 00 00");
const AVIF_BUFFER = Buffer.from("\x00\x00\x00\x1Cftypavif\x00\x00\x00\x00", "latin1");
const HEIC_BUFFER = Buffer.from("\x00\x00\x00\x18ftypheic\x00\x00\x00\x00", "latin1");
// Keine (erkennbare) Signatur:
const TEXT_BUFFER = Buffer.from("Dies ist nur Textinhalt und keine Datei.", "utf8");
// MZ-Header (Windows-EXE) — von der Erkennung nicht als zulässiger Typ klassifiziert:
const EXE_BUFFER = bufferFromHex("4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00");
const RECEIPT_MESSAGE = "Dateityp nicht erlaubt. Erlaubt sind: PDF, JPG, PNG, WebP, HEIC.";
const AVATAR_MESSAGE = "Dateityp nicht erlaubt. Erlaubt sind: JPG, PNG, WebP.";
function captureError(fn: () => unknown): unknown {
try {
fn();
return null;
} catch (err) {
return err;
}
}
/* -------------------------------------------------------------------------- */
describe("UploadWhitelist — Beleg-Scans (PDF/JPG/PNG/WebP/HEIC)", () => {
test("JPEG-Magic-Bytes werden akzeptiert", () => {
expect(assertAllowedUploadKind(JPEG_BUFFER, "image/jpeg", RECEIPT_ALLOWED_KINDS)).toBe(
"jpeg"
);
});
test("PNG-Magic-Bytes werden akzeptiert", () => {
expect(assertAllowedUploadKind(PNG_BUFFER, "image/png", RECEIPT_ALLOWED_KINDS)).toBe("png");
});
test("WebP-Magic-Bytes werden akzeptiert", () => {
expect(assertAllowedUploadKind(WEBP_BUFFER, "image/webp", RECEIPT_ALLOWED_KINDS)).toBe(
"webp"
);
});
test("PDF-Magic-Bytes werden akzeptiert", () => {
expect(assertAllowedUploadKind(PDF_BUFFER, "application/pdf", RECEIPT_ALLOWED_KINDS)).toBe(
"pdf"
);
});
test("deklarierter MIME-Type ist optional (Magic Bytes allein entscheiden)", () => {
expect(assertAllowedUploadKind(JPEG_BUFFER, undefined, RECEIPT_ALLOWED_KINDS)).toBe("jpeg");
});
test("deklarierter MIME-Type kann eine echte Datei nicht ausbremsen", () => {
// Echte JPEG-Signatur, aber unplausibler/geloggener MIME-Type → Magic Bytes gewinnen.
expect(
assertAllowedUploadKind(JPEG_BUFFER, "application/octet-stream", RECEIPT_ALLOWED_KINDS)
).toBe("jpeg");
expect(
assertAllowedUploadKind(PDF_BUFFER, "text/plain", RECEIPT_ALLOWED_KINDS)
).toBe("pdf");
});
test("deklarierter MIME-Type kann eine echte Datei nicht auf einen anderen erlaubten Typ umbiegen", () => {
// JPEG-Signatur mit deklariertem application/pdf → weiterhin "jpeg", nicht "pdf".
expect(
assertAllowedUploadKind(JPEG_BUFFER, "application/pdf", RECEIPT_ALLOWED_KINDS)
).toBe("jpeg");
});
test("GIF wird abgelehnt (erkannt, aber nicht in der Beleg-Whitelist)", () => {
const err = captureError(() =>
assertAllowedUploadKind(GIF_BUFFER, "image/gif", RECEIPT_ALLOWED_KINDS)
);
expect(err).toBeInstanceOf(UnsupportedFileTypeError);
expect((err as UnsupportedFileTypeError).kind).toBe("gif");
expect((err as UnsupportedFileTypeError).message).toBe(RECEIPT_MESSAGE);
});
test("HEIC-Magic-Bytes werden akzeptiert", () => {
expect(assertAllowedUploadKind(HEIC_BUFFER, "image/heic", RECEIPT_ALLOWED_KINDS)).toBe("heic");
});
test("TIFF/BMP/AVIF werden abgelehnt (erkannt, aber nicht in der Beleg-Whitelist)", () => {
const cases: Array<[Buffer, string, string]> = [
[TIFF_BUFFER, "image/tiff", "tiff"],
[BMP_BUFFER, "image/bmp", "bmp"],
[AVIF_BUFFER, "image/avif", "avif"],
];
for (const [buffer, mime, kind] of cases) {
const err = captureError(() => assertAllowedUploadKind(buffer, mime, RECEIPT_ALLOWED_KINDS));
expect(err).toBeInstanceOf(UnsupportedFileTypeError);
expect((err as UnsupportedFileTypeError).kind).toBe(kind);
expect((err as UnsupportedFileTypeError).message).toBe(RECEIPT_MESSAGE);
}
});
test("Text-Buffer ohne Signatur wird abgelehnt", () => {
const err = captureError(() =>
assertAllowedUploadKind(TEXT_BUFFER, "text/plain", RECEIPT_ALLOWED_KINDS)
);
expect(err).toBeInstanceOf(UnsupportedFileTypeError);
expect((err as UnsupportedFileTypeError).kind).toBe("unknown");
expect((err as UnsupportedFileTypeError).message).toBe(RECEIPT_MESSAGE);
});
test("EXE-Buffer (MZ-Header) wird abgelehnt", () => {
const err = captureError(() =>
assertAllowedUploadKind(EXE_BUFFER, "application/octet-stream", RECEIPT_ALLOWED_KINDS)
);
expect(err).toBeInstanceOf(UnsupportedFileTypeError);
expect((err as UnsupportedFileTypeError).kind).toBe("unknown");
});
test("gespoofte deklarierte MIME-Types gewähren nie Zugriff", () => {
// Text-Inhalt, deklariert als image/jpeg → abgelehnt.
const jpegSpoof = captureError(() =>
assertAllowedUploadKind(TEXT_BUFFER, "image/jpeg", RECEIPT_ALLOWED_KINDS)
);
expect(jpegSpoof).toBeInstanceOf(UnsupportedFileTypeError);
// Text-Inhalt, deklariert als application/pdf → abgelehnt (kein Zugriff über
// den deklarierten Typ — nur die Magic Bytes entscheiden).
const pdfSpoof = captureError(() =>
assertAllowedUploadKind(TEXT_BUFFER, "application/pdf", RECEIPT_ALLOWED_KINDS)
);
expect(pdfSpoof).toBeInstanceOf(UnsupportedFileTypeError);
expect((pdfSpoof as UnsupportedFileTypeError).kind).toBe("unknown");
});
test("leerer Buffer wird abgelehnt", () => {
const err = captureError(() =>
assertAllowedUploadKind(Buffer.alloc(0), "image/jpeg", RECEIPT_ALLOWED_KINDS)
);
expect(err).toBeInstanceOf(UnsupportedFileTypeError);
});
});
describe("UploadWhitelist — Profilbilder (JPG/PNG/WebP, kein PDF)", () => {
test("JPEG/PNG/WebP werden für Profilbilder akzeptiert", () => {
expect(assertAllowedUploadKind(JPEG_BUFFER, "image/jpeg", AVATAR_ALLOWED_KINDS)).toBe(
"jpeg"
);
expect(assertAllowedUploadKind(PNG_BUFFER, "image/png", AVATAR_ALLOWED_KINDS)).toBe("png");
expect(assertAllowedUploadKind(WEBP_BUFFER, "image/webp", AVATAR_ALLOWED_KINDS)).toBe(
"webp"
);
});
test("PDF wird für Profilbilder abgelehnt", () => {
const err = captureError(() =>
assertAllowedUploadKind(PDF_BUFFER, "application/pdf", AVATAR_ALLOWED_KINDS)
);
expect(err).toBeInstanceOf(UnsupportedFileTypeError);
expect((err as UnsupportedFileTypeError).kind).toBe("pdf");
expect((err as UnsupportedFileTypeError).message).toBe(AVATAR_MESSAGE);
});
test("GIF wird für Profilbilder abgelehnt", () => {
const err = captureError(() =>
assertAllowedUploadKind(GIF_BUFFER, "image/gif", AVATAR_ALLOWED_KINDS)
);
expect(err).toBeInstanceOf(UnsupportedFileTypeError);
expect((err as UnsupportedFileTypeError).kind).toBe("gif");
});
test("Text-Inhalt mit deklariertem image/png wird auch für Profilbilder abgelehnt", () => {
const err = captureError(() =>
assertAllowedUploadKind(TEXT_BUFFER, "image/png", AVATAR_ALLOWED_KINDS)
);
expect(err).toBeInstanceOf(UnsupportedFileTypeError);
expect((err as UnsupportedFileTypeError).kind).toBe("unknown");
});
});