release: v2.1.16

Add queue search and filters, safe upload diagnostics, session report exports, account check visibility, and explicit interrupted-upload recovery.\n\nSanitize diagnostic response snippets in persisted results and rotation logs, protect CSV exports against formula injection, and add regression coverage for diagnostics and reports.
This commit is contained in:
Sucukdeluxe
2026-08-12 12:04:47 +02:00
parent c843b4e623
commit a299fc204b
16 changed files with 361 additions and 17 deletions
+31
View File
@@ -0,0 +1,31 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const { buildSessionReport, buildSessionReportCsv } = require('../lib/session-report');
test('erstellt einen Hosterbericht mit Erfolgsquote, Dauer, Bytes und Fehlern', () => {
const report = buildSessionReport({
id: 'batch-1',
timestamp: '2026-08-12T12:00:00.000Z',
files: [{
name: 'video.mp4',
size: 1024,
results: [
{ hoster: 'byse.sx', status: 'done', durationSec: 4, attempt: 1 },
{ hoster: 'doodstream.com', status: 'error', durationSec: 3, attempt: 2, error: 'HTTP 503' }
]
}]
});
assert.equal(report.totals.total, 2);
assert.equal(report.totals.succeeded, 1);
assert.equal(report.hosters['byse.sx'].bytes, 1024);
assert.equal(report.hosters['doodstream.com'].attempts, 2);
assert.deepEqual(report.hosters['doodstream.com'].errors, { 'HTTP 503': 1 });
assert.match(buildSessionReportCsv(report), /doodstream\.com/);
});
test('entschärft Formeln und Geheimnisse im CSV-Bericht', () => {
const report = buildSessionReport({ files: [{ size: 1, results: [{ hoster: 'byse.sx', status: 'error', error: '=CMD() token=secret' }] }] });
assert.match(buildSessionReportCsv(report), /1× =CMD\(\) token=\[redacted\]/);
});
+27
View File
@@ -0,0 +1,27 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const { normalizeFailureDetails } = require('../lib/upload-diagnostics');
test('normalisiert Fehlerdiagnosen ohne URLs oder lange Antworten zu speichern', () => {
const result = normalizeFailureDetails({
http: 503,
contentType: 'text/html; charset=utf-8',
payloadSnippet: 'https://private.example/upload?key=secret '.repeat(80),
uploadUrl: 'https://private.example/upload?key=secret'
});
assert.equal(result.httpStatus, 503);
assert.equal(result.contentType, 'text/html; charset=utf-8');
assert.match(result.responseSnippet, /^private\.example\/upload/);
assert.ok(result.responseSnippet.length <= 320);
});
test('ignoriert leere und ungültige Diagnosefelder', () => {
assert.equal(normalizeFailureDetails({ http: 'x', contentType: '', payloadSnippet: '' }), null);
});
test('bereinigt Zugangsdaten und mehrere URLs aus Hosterantworten', () => {
const result = normalizeFailureDetails({ payloadSnippet: 'token=secret https://one.example/a?key=one password: hunter2 https://two.example/b' });
assert.equal(result.responseSnippet, 'token=[redacted] one.example/a password: [redacted] two.example/b');
});
+20
View File
@@ -199,6 +199,26 @@ describe('UploadManager', () => {
assert.equal(summary.succeeded, 0);
});
it('stores only sanitized hoster diagnostics in rotation logs', async () => {
const error = new Error('Upload rejected');
error.fileRejected = true;
error.diagnostic = {
http: 403,
contentType: 'application/json',
payloadSnippet: 'token=very-secret https://cdn.example.test/upload?apiKey=also-secret'
};
mockUploadFile.mock.mockImplementation(async () => { throw error; });
const mgr = new UploadManager({ 'doodstream.com': { retries: 0, parallelCount: 1, maxSpeedKbs: 0, restartBelowKbs: 0, timeIntervalSec: 0, maxSizeMb: 0 } });
const logs = [];
mgr.on('rot-log', (entry) => logs.push(entry));
await mgr.startBatch([{ file: '/test/diagnostic.mp4', hoster: 'doodstream.com', apiKey: 'key1' }]);
const failure = logs.find(entry => entry.event === 'upload-failure');
assert.equal(failure.payloadSnippet, 'token=[redacted] cdn.example.test/upload');
});
it('cancel aborts running uploads', async () => {
mockUploadFile.mock.mockImplementation(async (hoster, filePath, apiKey, onProgress, signal) => {
// Simulate a slow upload