pediatric-ai-scribe-v3/test/learning-upload-limits.test.js
Daniel bf4f895f2c
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 47s
Forgejo Docker Build / Root app tests (push) Successful in 49s
Forgejo Android APK / Build signed APK (push) Successful in 1m55s
Forgejo Docker Build / Build Docker image (push) Successful in 9s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
fix: article uploads are 10 MB, type-checked both ways, and sniffed
The ceiling was 100 MB per file with ten files allowed at once, and every file
is held whole in memory to be parsed — so the old limit let a single request ask
for a gigabyte of heap. A source article that size is not a thing anyone
uploads here. Now 10 MB, defined once and used by both the multer limit and the
post-upload check.

The filter accepted `allowed mime OR allowed extension`, so naming a file .pdf
was enough on its own, whatever it declared — and the extension is chosen by
whoever uploads. Both are required now.

Neither of those sees any bytes: multer filters on the headers, before the file
has arrived. verifySources() runs once the buffer exists and refuses a file
whose contents are not what its type claims, using the same helper as documents,
S3 uploads and assistant attachments. It runs before extraction, because an
extractor handed a malformed file is where the damage would happen.

The CMS screen said 100 MB and listed four of the ten accepted formats; it now
says what the server actually does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-12 19:25:41 +02:00

42 lines
2.1 KiB
JavaScript

// Source articles for AI generation are held whole in memory to be parsed, so
// the ceiling matters, and the declared type is chosen by whoever uploads.
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const route = fs.readFileSync(path.join(__dirname, '..', 'src/routes/learningAI.js'), 'utf8');
test('the per-file ceiling is 10 MB, in one place', () => {
assert.match(route, /var MAX_SOURCE_BYTES = 10 \* 1024 \* 1024;/);
assert.match(route, /fileSize: MAX_SOURCE_BYTES/);
assert.doesNotMatch(route, /100 \* 1024 \* 1024/, 'the old 100 MB ceiling is gone');
});
test('the type and the extension must both be allowed, not either', () => {
// It was `mime OR extension`, so naming a file .pdf was enough on its own.
assert.match(route, /ALLOWED_SOURCE_TYPES\.includes\(file\.mimetype\) && ALLOWED_SOURCE_EXTENSIONS\.test\(file\.originalname\)/);
assert.doesNotMatch(route, /allowed\.includes\(file\.mimetype\) \|\| file\.originalname\.match/);
});
test('the bytes are sniffed once the buffer exists, before anything parses them', () => {
// multer filters on headers alone, before any byte has arrived.
assert.match(route, /function verifySources\(files\)/);
assert.match(route, /fileType\.matches\(file\.mimetype, file\.buffer\)/);
assert.match(route, /is not the file type it claims to be/);
// And it runs ahead of extraction, not after.
const call = route.indexOf('verifySources(req.files)');
const extract = route.indexOf('await extractText(');
assert.ok(call > -1 && call < extract, 'verification must precede extraction');
});
test('the size is re-checked on the buffer, not trusted from the header', () => {
assert.match(route, /file\.size > MAX_SOURCE_BYTES/);
assert.match(route, /is larger than 10 MB/);
});
test('what the screen promises matches what the server accepts', () => {
const cms = fs.readFileSync(path.join(__dirname, '..', 'public/components/cms.html'), 'utf8');
assert.match(cms, /max 10 MB each, up to 10 files/);
assert.doesNotMatch(cms, /100 MB/);
});