Integrate Obfuscator file picker into Contracts
Deploy contracts-flask / validate (push) Canceled after 0s
Deploy contracts-flask / validate (push) Canceled after 0s
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,199 +1,166 @@
|
||||
// Юнит-тесты слоя 2 (фронт): putToVm + uploadViaVM с моками XMLHttpRequest и fetch.
|
||||
// Запуск: node upload/frontend/test_upload_layer2.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 { putToVm } from './upload/put_to_vm.js';
|
||||
import { uploadViaVM } from './upload/upload_via_vm.js';
|
||||
|
||||
const tick = () => new Promise(r => setTimeout(r, 0));
|
||||
if (typeof globalThis.File === 'undefined') {
|
||||
globalThis.File = class File extends Blob {
|
||||
constructor(parts, name, options = {}) {
|
||||
super(parts, options);
|
||||
this.name = name;
|
||||
this.lastModified = options.lastModified || Date.now();
|
||||
}
|
||||
};
|
||||
}
|
||||
globalThis.crypto = globalThis.crypto || { randomUUID: () => 'test-upload-id' };
|
||||
|
||||
// Node 18 не имеет globalThis.crypto (в браузере есть) — заглушка для uploadViaVM
|
||||
globalThis.crypto = globalThis.crypto || { randomUUID: () => 'test-uuid-1234' };
|
||||
|
||||
// ── мок XMLHttpRequest ──
|
||||
let lastXHR = null;
|
||||
let lastXHR;
|
||||
class FakeXHR {
|
||||
constructor() {
|
||||
this.upload = {};
|
||||
this.status = 0;
|
||||
this.statusText = '';
|
||||
this.timeout = 0;
|
||||
this.onload = null;
|
||||
this.onerror = null;
|
||||
this.ontimeout = null;
|
||||
this.method = null;
|
||||
this.url = null;
|
||||
this.body = null;
|
||||
lastXHR = this;
|
||||
}
|
||||
open(method, url) { this.method = method; this.url = url; }
|
||||
send(body) { this.body = body; }
|
||||
open(method, url) {
|
||||
this.method = method;
|
||||
this.url = url;
|
||||
}
|
||||
send(body) {
|
||||
this.body = body;
|
||||
}
|
||||
complete(status = 200) {
|
||||
this.status = status;
|
||||
this.onload();
|
||||
}
|
||||
}
|
||||
globalThis.XMLHttpRequest = FakeXHR;
|
||||
|
||||
// ── мок fetch ──
|
||||
let fetchCalls = [];
|
||||
function installFetch(respond) {
|
||||
fetchCalls = [];
|
||||
globalThis.fetch = async (url, opts) => {
|
||||
fetchCalls.push({ url, opts });
|
||||
return respond();
|
||||
};
|
||||
let fetchResponder;
|
||||
globalThis.fetch = async (url, options) => {
|
||||
fetchCalls.push({ url, options });
|
||||
return fetchResponder(fetchCalls.length);
|
||||
};
|
||||
|
||||
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||
async function waitForNextXHR(previous) {
|
||||
for (let attempt = 0; attempt < 20 && lastXHR === previous; attempt += 1) await tick();
|
||||
assert.notEqual(lastXHR, previous, 'next PUT request was not started');
|
||||
return lastXHR;
|
||||
}
|
||||
|
||||
// ── putToVm ──
|
||||
async function testPutToVmSuccess() {
|
||||
const f = new File(['abc'], 'a.txt');
|
||||
const p = putToVm(f, 'https://vm/tok_0');
|
||||
lastXHR.status = 200;
|
||||
lastXHR.onload();
|
||||
await p;
|
||||
const file = new File(['payload'], 'a.pdf');
|
||||
const request = putToVm(file, 'https://vm.example/a');
|
||||
assert.equal(lastXHR.method, 'PUT');
|
||||
assert.equal(lastXHR.url, 'https://vm/tok_0');
|
||||
assert.equal(lastXHR.body, f);
|
||||
console.log(' putToVm success: ok');
|
||||
assert.equal(lastXHR.body, file);
|
||||
lastXHR.complete();
|
||||
await request;
|
||||
}
|
||||
|
||||
async function testPutToVmHttpError() {
|
||||
const p = putToVm(new File(['abc'], 'a.txt'), 'https://vm/tok_0');
|
||||
lastXHR.status = 500;
|
||||
lastXHR.onload();
|
||||
await assert.rejects(p, /ВМ: HTTP 500/);
|
||||
console.log(' putToVm HTTP error: ok');
|
||||
}
|
||||
|
||||
async function testPutToVmNetworkError() {
|
||||
const p = putToVm(new File(['abc'], 'a.txt'), 'https://vm/tok_0');
|
||||
lastXHR.onerror();
|
||||
await assert.rejects(p, /Сеть/);
|
||||
console.log(' putToVm network error: ok');
|
||||
}
|
||||
|
||||
async function testPutToVmTimeout() {
|
||||
const p = putToVm(new File(['abc'], 'a.txt'), 'https://vm/tok_0');
|
||||
lastXHR.ontimeout();
|
||||
await assert.rejects(p, /Таймаут/);
|
||||
console.log(' putToVm timeout: ok');
|
||||
}
|
||||
|
||||
async function testPutToVmOnXHR() {
|
||||
let registered = null;
|
||||
const p = putToVm(new File(['abc'], 'a.txt'), 'https://vm/tok_0', { onXHR(x) { registered = x; } });
|
||||
assert.ok(registered, 'onXHR должен получить XHR для отмены');
|
||||
registered.status = 200;
|
||||
registered.onload();
|
||||
await p;
|
||||
console.log(' putToVm onXHR регистрация: ok');
|
||||
}
|
||||
|
||||
// ── uploadViaVM ──
|
||||
async function testUploadViaVMSuccess() {
|
||||
installFetch(() => ({ ok: true, json: async () => ({ ok: true, session: 'sid123', count: 2 }) }));
|
||||
const files = [new File(['a'], 'a.txt'), new File(['bb'], 'b.txt')];
|
||||
const p = uploadViaVM(files, 'https://vm/', { session: '' });
|
||||
await tick();
|
||||
lastXHR.status = 200; lastXHR.onload();
|
||||
await tick();
|
||||
lastXHR.status = 200; lastXHR.onload();
|
||||
const res = await p;
|
||||
assert.equal(res.ok, true);
|
||||
assert.equal(res.session, 'sid123');
|
||||
assert.equal(res.count, 2);
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
const body = JSON.parse(fetchCalls[0].opts.body);
|
||||
assert.equal(body.files.length, 2);
|
||||
assert.equal(body.files[0].name, 'a.txt');
|
||||
assert.equal(body.files[1].name, 'b.txt');
|
||||
assert.ok(body.files[0].url.startsWith('https://vm/'));
|
||||
console.log(' uploadViaVM success: ok');
|
||||
}
|
||||
|
||||
async function testUploadViaVMPutError() {
|
||||
installFetch(() => ({ ok: true, json: async () => ({}) }));
|
||||
const files = [new File(['a'], 'a.txt'), new File(['bb'], 'b.txt')];
|
||||
const p = uploadViaVM(files, 'https://vm/', { session: '' });
|
||||
await tick();
|
||||
lastXHR.onerror(); // первый PUT падает
|
||||
const res = await p;
|
||||
assert.equal(res.ok, false);
|
||||
assert.ok(res.error.includes('Ошибка загрузки на ВМ'));
|
||||
assert.equal(fetchCalls.length, 0); // POST не вызван
|
||||
console.log(' uploadViaVM PUT error: ok');
|
||||
}
|
||||
|
||||
async function testUploadViaVMPostError() {
|
||||
installFetch(() => ({ ok: true, json: async () => ({ ok: false, error: 'Session not found' }) }));
|
||||
const files = [new File(['a'], 'a.txt')];
|
||||
const p = uploadViaVM(files, 'https://vm/', { session: '' });
|
||||
await tick();
|
||||
lastXHR.status = 200; lastXHR.onload();
|
||||
const res = await p;
|
||||
assert.equal(res.ok, false);
|
||||
assert.ok(res.error.includes('Ошибка передачи ссылок'));
|
||||
console.log(' uploadViaVM POST error: ok');
|
||||
}
|
||||
|
||||
async function testUploadViaVMProgress() {
|
||||
installFetch(() => ({ ok: true, json: async () => ({ ok: true, session: 's', count: 1 }) }));
|
||||
const statuses = [];
|
||||
const texts = [];
|
||||
const files = [new File(['abc'], 'a.txt')];
|
||||
const p = uploadViaVM(files, 'https://vm/', {
|
||||
session: '',
|
||||
onStatus(k, html) { statuses.push({ k, html }); },
|
||||
onUploadStatus(t) { texts.push(t); },
|
||||
async function testUploadIncludesPerFilePayloadAndResult() {
|
||||
fetchCalls = [];
|
||||
fetchResponder = async (callNumber) => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
json: async () => ({
|
||||
ok: true,
|
||||
results: [{ ok: true, doc_id: `doc-${callNumber}`, parsed: { status: 'parsed' } }],
|
||||
}),
|
||||
});
|
||||
await tick();
|
||||
lastXHR.status = 200; lastXHR.onload();
|
||||
await p;
|
||||
assert.ok(statuses.some(s => s.html.includes('✓')));
|
||||
assert.ok(texts.some(t => t.includes('Загрузка на ВМ')));
|
||||
assert.ok(texts.some(t => t.includes('Передача ссылок')));
|
||||
console.log(' uploadViaVM progress/статусы: ok');
|
||||
const files = [
|
||||
{ id: 'one', name: 'one.pdf', path: 'one.pdf', file: new File(['1'], 'one.pdf') },
|
||||
{ id: 'two', name: 'two.pdf', path: 'archive.zip/folder/two.pdf', file: new File(['2'], 'two.pdf') },
|
||||
];
|
||||
const completed = [];
|
||||
const previous = lastXHR;
|
||||
const request = uploadViaVM(files, {
|
||||
vmUploadUrl: 'https://vm.example/contracts-upload/',
|
||||
backendUploadUrl: '/api/upload_refs',
|
||||
getExtraPayload: (entry) => ({
|
||||
batch_id: 'batch-1',
|
||||
contract_id: 'contract-1',
|
||||
zip_source: entry.id === 'two' ? 'archive.zip' : null,
|
||||
}),
|
||||
onFileComplete: (event) => completed.push(event),
|
||||
});
|
||||
|
||||
const firstXHR = await waitForNextXHR(previous);
|
||||
firstXHR.complete();
|
||||
const secondXHR = await waitForNextXHR(firstXHR);
|
||||
secondXHR.complete();
|
||||
const result = await request;
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(fetchCalls.length, 2);
|
||||
const firstBody = JSON.parse(fetchCalls[0].options.body);
|
||||
const secondBody = JSON.parse(fetchCalls[1].options.body);
|
||||
assert.equal(firstBody.batch_id, 'batch-1');
|
||||
assert.equal(firstBody.contract_id, 'contract-1');
|
||||
assert.equal(firstBody.files.length, 1);
|
||||
assert.equal(secondBody.zip_source, 'archive.zip');
|
||||
assert.equal(completed.length, 2);
|
||||
assert.equal(completed[1].result.doc_id, 'doc-2');
|
||||
assert.equal(completed[1].source.id, 'two');
|
||||
}
|
||||
|
||||
async function testUploadViaVMSessionPassed() {
|
||||
// session из options пробрасывается в POST-тело
|
||||
installFetch(() => ({ ok: true, json: async () => ({ ok: true, session: 'prev', count: 1 }) }));
|
||||
const files = [new File(['a'], 'a.txt')];
|
||||
const p = uploadViaVM(files, 'https://vm/', { session: 'existingsid' });
|
||||
await tick();
|
||||
lastXHR.status = 200; lastXHR.onload();
|
||||
await p;
|
||||
const body = JSON.parse(fetchCalls[0].opts.body);
|
||||
assert.equal(body.session, 'existingsid');
|
||||
console.log(' uploadViaVM проброс session: ok');
|
||||
async function testUploadReportsPutFailure() {
|
||||
fetchCalls = [];
|
||||
const previous = lastXHR;
|
||||
const request = uploadViaVM([new File(['x'], 'x.pdf')], {
|
||||
vmUploadUrl: 'https://vm.example/',
|
||||
});
|
||||
const xhr = await waitForNextXHR(previous);
|
||||
xhr.onerror();
|
||||
const result = await request;
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(fetchCalls.length, 0);
|
||||
assert.match(result.error, /Ошибка отправки файла/);
|
||||
}
|
||||
|
||||
// ── прогон ──
|
||||
const TESTS = [
|
||||
['putToVm success', testPutToVmSuccess],
|
||||
['putToVm HTTP error', testPutToVmHttpError],
|
||||
['putToVm network error', testPutToVmNetworkError],
|
||||
['putToVm timeout', testPutToVmTimeout],
|
||||
['putToVm onXHR', testPutToVmOnXHR],
|
||||
['uploadViaVM success', testUploadViaVMSuccess],
|
||||
['uploadViaVM PUT error', testUploadViaVMPutError],
|
||||
['uploadViaVM POST error', testUploadViaVMPostError],
|
||||
['uploadViaVM progress', testUploadViaVMProgress],
|
||||
['uploadViaVM проброс session', testUploadViaVMSessionPassed],
|
||||
async function testUploadReportsBackendFailure() {
|
||||
fetchCalls = [];
|
||||
fetchResponder = async () => ({
|
||||
ok: false,
|
||||
status: 502,
|
||||
statusText: 'Bad Gateway',
|
||||
json: async () => ({ ok: false, error: 'failed' }),
|
||||
});
|
||||
const previous = lastXHR;
|
||||
const request = uploadViaVM([new File(['x'], 'x.pdf')], {
|
||||
vmUploadUrl: 'https://vm.example/',
|
||||
backendUploadUrl: '/api/upload_refs',
|
||||
});
|
||||
const xhr = await waitForNextXHR(previous);
|
||||
xhr.complete();
|
||||
const result = await request;
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(result.error, /HTTP 502/);
|
||||
}
|
||||
|
||||
async function testAbortBeforeUpload() {
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
const previous = lastXHR;
|
||||
const result = await uploadViaVM([new File(['x'], 'x.pdf')], {
|
||||
vmUploadUrl: 'https://vm.example/',
|
||||
signal: controller.signal,
|
||||
});
|
||||
assert.equal(result.aborted, true);
|
||||
assert.equal(lastXHR, previous);
|
||||
}
|
||||
|
||||
const tests = [
|
||||
['PUT via WebDAV', testPutToVmSuccess],
|
||||
['per-file extra payload and sink result', testUploadIncludesPerFilePayloadAndResult],
|
||||
['PUT failure', testUploadReportsPutFailure],
|
||||
['backend failure', testUploadReportsBackendFailure],
|
||||
['abort before upload', testAbortBeforeUpload],
|
||||
];
|
||||
|
||||
let failed = 0;
|
||||
for (const [name, fn] of TESTS) {
|
||||
try { await fn(); console.log('PASS ' + name); }
|
||||
catch (e) { failed++; console.error('FAIL ' + name + ': ' + e.message); }
|
||||
for (const [name, test] of tests) {
|
||||
await test();
|
||||
console.log(`PASS ${name}`);
|
||||
}
|
||||
if (failed) { console.error(failed + ' тестов упало'); process.exit(1); }
|
||||
console.log('Все тесты слоя 2 (фронт) прошли');
|
||||
|
||||
Reference in New Issue
Block a user