214 lines
7.5 KiB
JavaScript
214 lines
7.5 KiB
JavaScript
// Юнит-тесты фронта модуля upload: zip (кириллица, вложенный zip, не-doc) + дедуп/лимиты.
|
|
// Запуск: node upload/frontend/test_upload_frontend.mjs
|
|
|
|
import assert from 'node:assert/strict';
|
|
|
|
// ── Полифилл File (Node 18 не имеет global File) ──
|
|
if (typeof globalThis.File === 'undefined') {
|
|
globalThis.File = class File extends Blob {
|
|
constructor(parts, name, opts) {
|
|
super(parts, opts);
|
|
this.name = name;
|
|
this.lastModified = (opts && opts.lastModified) || Date.now();
|
|
}
|
|
};
|
|
}
|
|
|
|
import { listZipFiles } from './zip/list_zip_files.js';
|
|
import { parseZip } from './zip/parse_zip.js';
|
|
import { decodeZipName } from './zip/decode_zip_name.js';
|
|
import { addFileWithDedup } from './table/add_file_with_dedup.js';
|
|
|
|
const ALLOWED = ['.pdf', '.doc', '.docx', '.txt', '.md'];
|
|
|
|
// ── CRC32 (для сборки тестовых ZIP) ──
|
|
const CRC_TABLE = (() => {
|
|
const t = new Uint32Array(256);
|
|
for (let n = 0; n < 256; n++) {
|
|
let c = n;
|
|
for (let k = 0; k < 8; k++) c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
|
|
t[n] = c >>> 0;
|
|
}
|
|
return t;
|
|
})();
|
|
|
|
function crc32(bytes) {
|
|
let c = 0xffffffff;
|
|
for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
|
|
return (c ^ 0xffffffff) >>> 0;
|
|
}
|
|
|
|
// ── Сборка минимального ZIP (method 0, UTF-8-флаг) ──
|
|
function makeZip(entries) {
|
|
const enc = new TextEncoder();
|
|
const chunks = [];
|
|
const central = [];
|
|
let offset = 0;
|
|
const utf8Flag = 0x0800;
|
|
for (const e of entries) {
|
|
const nameB = enc.encode(e.name);
|
|
const dataB = typeof e.data === 'string' ? enc.encode(e.data) : e.data;
|
|
const crc = crc32(dataB);
|
|
|
|
const lh = new DataView(new ArrayBuffer(30));
|
|
lh.setUint32(0, 0x04034b50, true);
|
|
lh.setUint16(4, 20, true);
|
|
lh.setUint16(6, utf8Flag, true);
|
|
lh.setUint16(8, 0, true);
|
|
lh.setUint16(10, 0, true);
|
|
lh.setUint16(12, 0x21, true);
|
|
lh.setUint32(14, crc, true);
|
|
lh.setUint32(18, dataB.length, true);
|
|
lh.setUint32(22, dataB.length, true);
|
|
lh.setUint16(26, nameB.length, true);
|
|
lh.setUint16(28, 0, true);
|
|
chunks.push(Buffer.from(lh.buffer), Buffer.from(nameB), Buffer.from(dataB));
|
|
|
|
const cd = new DataView(new ArrayBuffer(46));
|
|
cd.setUint32(0, 0x02014b50, true);
|
|
cd.setUint16(4, 20, true);
|
|
cd.setUint16(6, 20, true);
|
|
cd.setUint16(8, utf8Flag, true);
|
|
cd.setUint16(10, 0, true);
|
|
cd.setUint16(12, 0, true);
|
|
cd.setUint16(14, 0x21, true);
|
|
cd.setUint32(16, crc, true);
|
|
cd.setUint32(20, dataB.length, true);
|
|
cd.setUint32(24, dataB.length, true);
|
|
cd.setUint16(28, nameB.length, true);
|
|
cd.setUint16(30, 0, true);
|
|
cd.setUint16(32, 0, true);
|
|
cd.setUint16(34, 0, true);
|
|
cd.setUint16(36, 0, true);
|
|
cd.setUint32(38, 0, true);
|
|
cd.setUint32(42, offset, true);
|
|
central.push(Buffer.from(cd.buffer), Buffer.from(nameB));
|
|
offset += 30 + nameB.length + dataB.length;
|
|
}
|
|
|
|
const cdSize = central.reduce((s, x) => s + x.byteLength, 0);
|
|
const cdStart = offset;
|
|
const eocd = new DataView(new ArrayBuffer(22));
|
|
eocd.setUint32(0, 0x06054b50, true);
|
|
eocd.setUint16(4, 0, true);
|
|
eocd.setUint16(6, 0, true);
|
|
eocd.setUint16(8, entries.length, true);
|
|
eocd.setUint16(10, entries.length, true);
|
|
eocd.setUint32(12, cdSize, true);
|
|
eocd.setUint32(16, cdStart, true);
|
|
eocd.setUint16(20, 0, true);
|
|
chunks.push(...central, Buffer.from(eocd.buffer));
|
|
return Buffer.concat(chunks);
|
|
}
|
|
|
|
// ── Тесты decodeZipName ──
|
|
function testDecodeZipName() {
|
|
const utf8 = new TextEncoder().encode('договор.pdf');
|
|
assert.equal(decodeZipName(utf8, true), 'договор.pdf');
|
|
// Без UTF-8-флага, но байты — валидный UTF-8 с кириллицей → эвристика берёт как есть
|
|
assert.equal(decodeZipName(utf8, false), 'договор.pdf');
|
|
console.log(' decodeZipName: ok');
|
|
}
|
|
|
|
// ── Тесты listZipFiles ──
|
|
async function testZipCyrillic() {
|
|
const zip = makeZip([{ name: 'договор.pdf', data: '%PDF-1.4 fake' }]);
|
|
const files = await listZipFiles(new File([zip], 'a.zip'), ALLOWED);
|
|
assert.equal(files.length, 1);
|
|
assert.equal(files[0].name, 'договор.pdf');
|
|
console.log(' zip кириллица: ok');
|
|
}
|
|
|
|
async function testZipNested() {
|
|
const inner = makeZip([{ name: 'inner.txt', data: 'hi' }]);
|
|
const outer = makeZip([
|
|
{ name: 'inner.zip', data: inner },
|
|
{ name: 'skip.txt', data: 'x' },
|
|
]);
|
|
const files = await listZipFiles(new File([outer], 'outer.zip'), ALLOWED);
|
|
const names = files.map(f => f.name).sort();
|
|
assert.deepEqual(names, ['inner.txt', 'skip.txt']);
|
|
console.log(' вложенный zip: ok');
|
|
}
|
|
|
|
async function testZipNonDoc() {
|
|
const zip = makeZip([
|
|
{ name: 'doc.pdf', data: 'pdf' },
|
|
{ name: 'img.png', data: 'png' },
|
|
{ name: 'readme.txt', data: 'readme' },
|
|
]);
|
|
const files = await listZipFiles(new File([zip], 'a.zip'), ALLOWED);
|
|
const names = files.map(f => f.name).sort();
|
|
assert.deepEqual(names, ['doc.pdf', 'readme.txt']);
|
|
console.log(' не-doc фильтр: ok');
|
|
}
|
|
|
|
async function testParseZipNotZip() {
|
|
await assert.rejects(() => parseZip(new Uint8Array([1, 2, 3])), /Не ZIP/);
|
|
console.log(' не-ZIP бросок: ok');
|
|
}
|
|
|
|
// ── Тесты addFileWithDedup ──
|
|
function newState() {
|
|
return { files: [], fileMeta: new Map(), overNames: new Set(), busy: false, proc: null };
|
|
}
|
|
|
|
function testDedup() {
|
|
const st = newState();
|
|
const c = { maxFileBytes: 100, maxSessionBytes: 300 };
|
|
// одинаковое имя+размер → дедуп (false), список не растёт
|
|
assert.equal(addFileWithDedup(st, new File(['aaa'], 'a.txt'), c), true);
|
|
assert.equal(st.files.length, 1);
|
|
assert.equal(addFileWithDedup(st, new File(['aaa'], 'a.txt'), c), false);
|
|
assert.equal(st.files.length, 1);
|
|
// имя то же, размер другой → суффикс _2
|
|
assert.equal(addFileWithDedup(st, new File(['bbbb'], 'a.txt'), c), true);
|
|
assert.equal(st.files.length, 2);
|
|
assert.equal(st.files[1].name, 'a_2.txt');
|
|
console.log(' дедуп/суффиксы: ok');
|
|
}
|
|
|
|
function testLimits() {
|
|
// лимит на один файл
|
|
const st = newState();
|
|
const c = { maxFileBytes: 100, maxSessionBytes: 300 };
|
|
addFileWithDedup(st, new File([new Uint8Array(150)], 'big.bin'), c);
|
|
assert.ok(st.overNames.has('big.bin'));
|
|
assert.equal(st.files.length, 1);
|
|
// суммарный лимит сессии
|
|
const st2 = newState();
|
|
const c2 = { maxFileBytes: 1000, maxSessionBytes: 200 };
|
|
addFileWithDedup(st2, new File([new Uint8Array(150)], 'f1.bin'), c2);
|
|
addFileWithDedup(st2, new File([new Uint8Array(60)], 'f2.bin'), c2); // 150+60=210 > 200 → over
|
|
assert.ok(st2.overNames.has('f2.bin'));
|
|
console.log(' лимиты over: ok');
|
|
}
|
|
|
|
// ── Прогон ──
|
|
const TESTS = [
|
|
['decodeZipName', testDecodeZipName],
|
|
['zip кириллица', testZipCyrillic],
|
|
['вложенный zip', testZipNested],
|
|
['не-doc фильтр', testZipNonDoc],
|
|
['не-ZIP бросок', testParseZipNotZip],
|
|
['дедуп/суффиксы', testDedup],
|
|
['лимиты over', testLimits],
|
|
];
|
|
|
|
let failed = 0;
|
|
for (const [name, fn] of TESTS) {
|
|
try {
|
|
await fn();
|
|
console.log('PASS ' + name);
|
|
} catch (err) {
|
|
failed++;
|
|
console.error('FAIL ' + name + ': ' + err.message);
|
|
}
|
|
}
|
|
|
|
if (failed) {
|
|
console.error(failed + ' тестов упало');
|
|
process.exit(1);
|
|
}
|
|
console.log('Все фронт-тесты прошли');
|