const test = require('node:test'); const assert = require('node:assert'); const { buildEntry, filterEntries, createUploadHistory } = require('../src/features/uploadHistory.js'); function memoryFs(initial) { const files = new Map(initial ? [['h.json', initial]] : []); return { files, readFileSync: (f) => { if (!files.has(f)) throw new Error('ENOENT'); return files.get(f); }, writeFileSync: (f, c) => { files.set(f, c); } }; } test('buildEntry: erfolgreicher Upload', () => { const e = buildEntry('Drag & Drop', { owner: 'a', repo: 'b' }, { ok: true, results: [{}, {}, {}] }, 1000, null, 4000); assert.strictEqual(e.status, 'ok'); assert.strictEqual(e.repo, 'a/b'); assert.strictEqual(e.files, 3); assert.strictEqual(e.durationMs, 3000); assert.strictEqual(e.error, ''); }); test('buildEntry: abgebrochen ist kein Fehler und trägt keinen Fehlertext', () => { const e = buildEntry('Ordner', { owner: 'a', repo: 'b' }, { ok: false, cancelled: true, error: 'Upload abgebrochen' }, 0, null, 10); assert.strictEqual(e.status, 'abgebrochen'); assert.strictEqual(e.error, ''); }); test('buildEntry: Fehler und geworfene Ausnahmen', () => { const failed = buildEntry('Dateien', { owner: 'a', repo: 'b' }, { ok: false, error: 'kaputt', failedCount: 2 }, 0, null, 10); assert.strictEqual(failed.status, 'fehler'); assert.strictEqual(failed.error, 'kaputt'); assert.strictEqual(failed.failed, 2); const thrown = buildEntry('Dateien', {}, null, 0, new Error('boom'), 10); assert.strictEqual(thrown.status, 'fehler'); assert.strictEqual(thrown.error, 'boom'); assert.strictEqual(thrown.repo, ''); }); test('buildEntry: Einzeldatei zählt als eine Datei, lange Fehler werden gekürzt', () => { assert.strictEqual(buildEntry('x', {}, { ok: true, singleFile: true }, 0, null, 1).files, 1); assert.strictEqual(buildEntry('x', {}, { ok: false, error: 'e'.repeat(1000) }, 0, null, 1).error.length, 300); }); test('filterEntries: nach Status und Repo-Suche', () => { const list = [ { repo: 'a/alpha', status: 'ok' }, { repo: 'a/beta', status: 'fehler' }, { repo: 'b/Alpha-2', status: 'abgebrochen' } ]; assert.strictEqual(filterEntries(list).length, 3); assert.deepStrictEqual(filterEntries(list, { status: 'fehler' }).map(e => e.repo), ['a/beta']); assert.deepStrictEqual(filterEntries(list, { query: 'ALPHA' }).map(e => e.repo), ['a/alpha', 'b/Alpha-2']); assert.strictEqual(filterEntries(list, { status: 'ok', query: 'beta' }).length, 0); }); test('Verlauf: neueste zuerst, auf max Einträge begrenzt', () => { const history = createUploadHistory({ file: 'h.json', max: 3, fs: memoryFs() }); for (let i = 1; i <= 5; i++) history.record({ n: i }); assert.deepStrictEqual(history.read().map(e => e.n), [5, 4, 3]); }); test('Verlauf: kaputte oder fehlende Datei ergibt eine leere Liste', () => { assert.deepStrictEqual(createUploadHistory({ file: 'h.json', fs: memoryFs() }).read(), []); assert.deepStrictEqual(createUploadHistory({ file: 'h.json', fs: memoryFs('{kaputt') }).read(), []); assert.deepStrictEqual(createUploadHistory({ file: 'h.json', fs: memoryFs('{"a":1}') }).read(), []); }); test('Verlauf: leeren', () => { const history = createUploadHistory({ file: 'h.json', fs: memoryFs() }); history.record({ n: 1 }); history.clear(); assert.deepStrictEqual(history.read(), []); }); test('Verlauf: Schreibfehler werfen nicht', () => { const broken = { readFileSync: () => '[]', writeFileSync: () => { throw new Error('Platte voll'); } }; assert.strictEqual(createUploadHistory({ file: 'h.json', fs: broken }).record({ n: 1 }), false); }); const { toCsv, formatDate } = require('../src/features/uploadHistory.js'); test('formatDate: TT.MM.JJJJ HH:MM:SS, ungültige Eingabe ergibt leeren Text', () => { assert.match(formatDate('2026-10-22T07:11:53.080Z'), /^\d{2}\.\d{2}\.2026 \d{2}:\d{2}:\d{2}$/); assert.strictEqual(formatDate('kein Datum'), ''); }); test('toCsv: BOM, Semikolon, Windows-Zeilenenden, Kopfzeile', () => { const csv = toCsv([{ at: '2026-10-22T07:11:53.080Z', repo: 'a/b', kind: 'Drag & Drop', files: 3, failed: 0, status: 'ok', durationMs: 2500, error: '' }]); assert.strictEqual(csv.charCodeAt(0), 0xFEFF); const lines = csv.slice(1).split('\r\n'); assert.strictEqual(lines[0], 'Datum;Repo;Art;Dateien;Fehlgeschlagen;Ergebnis;Dauer (s);Fehler'); assert.match(lines[1], /;a\/b;Drag & Drop;3;0;Erfolgreich;2\.5;$/); assert.strictEqual(lines[2], ''); }); test('toCsv: Anführungszeichen, Semikolon und Zeilenumbrüche werden maskiert', () => { const csv = toCsv([{ at: '2026-10-22T07:11:53Z', repo: 'a/b', kind: 'x', files: null, failed: 1, status: 'fehler', durationMs: 100, error: 'sagt "nein"; und\nmehr' }]); assert.ok(csv.includes('"sagt ""nein""; und\nmehr"')); assert.match(csv, /;Fehler;0\.1;/); }); test('toCsv: Excel-Formeln werden entschärft', () => { const csv = toCsv([{ at: '2026-10-22T07:11:53Z', repo: '=cmd|calc', kind: '+x', files: 1, failed: 0, status: 'ok', durationMs: 0, error: '@SUMME(A1)' }]); assert.ok(csv.includes(";'=cmd|calc;")); assert.ok(csv.includes(";'+x;")); assert.ok(csv.includes(";'@SUMME(A1)")); }); test('toCsv: leere Liste liefert nur die Kopfzeile', () => { assert.strictEqual(toCsv([]).slice(1).split('\r\n').filter(Boolean).length, 1); });