Files
klarbild/tests/zipnames.test.ts
tillandClaude Opus 5 7d53962543 feat: download all images of a share link as one ZIP
Twenty pictures meant twenty taps. Now a single button above the grid, a plain
GET link so it works without JavaScript, on a page whose recipients are mostly
on a phone.

Written to stream rather than buffer. The existing /api/items/zip loads every
object into memory and appends them all before the consumer attaches; for a
share of full-resolution posters that is gigabytes at once against a 3g limit.
This route reads one file at a time and waits for archiver's `entry` event
before reading the next. Measured: 440 MB of images, 26 s, RSS peaked at 196 MB
against a 120 MB baseline, and the archive verifies.

Guards, all checked: download-locked links return 403 and the button does not
render; a passphrase-protected link needs the proof cookie (401 without);
revoked 410, unknown 404; five packages per link and address per ten minutes;
200 files and 500 MB, past which the archive closes with a HINWEIS.txt saying
what happened and where to get the rest. Counted as one download.

Name collisions inside the archive are numbered, since duplicates only surface
at extraction time where the second silently overwrites the first. Archive
naming pulled into testable helpers with 7 tests (104 total).

Also fixes a bug the screenshot caught: `.bild a` matched the per-image download
button inside figcaption as well as the image link, painting it paper-coloured
with near-white text. It had been effectively invisible since the page was
written; I had looked at it twice without seeing it. Scoped to `.bild > a`.
Contrast measured from real pixels afterwards: 7.73 on both buttons, 5.08 on the
muted text, all above AA.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BNhLunz586g6DeYMa7Qsd1
2026-09-01 04:05:03 +00:00

67 lines
2.7 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { archivName, paketName } from '../src/lib/shares.ts';
/**
* Namen im Sammelpaket. Zwei gleiche Namen in einem ZIP sind beim Packen kein
* Fehler — sie fallen erst beim **Auspacken** auf, wo das zweite das erste
* überschreibt. Deshalb hat das einen Test.
*/
test('Gleiche Dateinamen werden durchnummeriert', () => {
const v = new Map<string, number>();
assert.equal(archivName('bild.png', v), 'bild.png');
assert.equal(archivName('bild.png', v), 'bild_1.png');
assert.equal(archivName('bild.png', v), 'bild_2.png');
assert.equal(archivName('anderes.png', v), 'anderes.png');
});
test('Namen können nicht aus dem Archiv ausbrechen', () => {
const v = new Map<string, number>();
for (const boese of ['../../etc/passwd', 'a/b/c.png', 'a\\b.png', '/absolut.png']) {
const n = archivName(boese, v);
assert.ok(!n.includes('/') && !n.includes('\\'), `Trennzeichen überlebt: ${n}`);
}
});
test('Steuerzeichen fliegen heraus', () => {
const v = new Map<string, number>();
assert.equal(archivName('abc.png', v), 'abc.png');
assert.equal(archivName('zeile\numbruch.png', v), 'zeileumbruch.png');
});
test('Fehlende oder endungslose Namen bekommen etwas Brauchbares', () => {
const v = new Map<string, number>();
assert.equal(archivName(null, v), 'bild.jpg');
assert.equal(archivName('', v), 'bild_1.jpg');
assert.equal(archivName(' ', v), 'bild_2.jpg');
assert.equal(archivName('ohne-endung', v), 'ohne-endung.jpg');
assert.equal(archivName('mit.png', v), 'mit.png');
assert.equal(archivName('mit.PNG', v), 'mit.PNG');
});
test('Echte Klarbild-Namen bleiben unverändert', () => {
const v = new Map<string, number>();
const n = '2026-08-20_143207_lea-badge_the-frame_a3f9.jpg';
assert.equal(archivName(n, v), n);
});
test('Das Paket heißt nach Datum und Titel', () => {
const am = new Date('2026-09-01T10:00:00Z');
assert.equal(paketName('Kita August 2026', am), '2026-09-01_kita-august-2026.zip');
assert.equal(paketName('Müller & Söhne', am), '2026-09-01_mueller-soehne.zip');
assert.equal(paketName(null, am), '2026-09-01_klarbild.zip');
assert.equal(paketName('', am), '2026-09-01_klarbild.zip');
assert.equal(paketName('!!!', am), '2026-09-01_klarbild.zip');
});
test('Der Paketname verträgt sich mit jedem Dateisystem', () => {
const am = new Date('2026-09-01T10:00:00Z');
for (const titel of ['Grüße aus Köln / Teil 2', 'a\\b:c*d?e"f<g>h|i', '../../..', 'x'.repeat(200)]) {
const n = paketName(titel, am);
assert.ok(!/[^a-z0-9_.-]/.test(n), `unerwartetes Zeichen in ${n}`);
assert.ok(n.length < 60, `zu lang: ${n.length}`);
assert.ok(n.endsWith('.zip'));
}
});