- копирую модуль upload/ из drhider (слои 1-2) - blueprint: параметр sink (drhider-сессия по умолчанию, сверка — DB+парсинг) - upload_bp: contracts_upload_sink = _store_and_parse - routes: регистрирую create_upload_refs_blueprint(cfg, sink=...) - app.py: корень репо в sys.path (для import upload) - History: план переиспользования + ревью Соннета
200 lines
7.0 KiB
JavaScript
200 lines
7.0 KiB
JavaScript
// Юнит-тесты слоя 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));
|
||
|
||
// Node 18 не имеет globalThis.crypto (в браузере есть) — заглушка для uploadViaVM
|
||
globalThis.crypto = globalThis.crypto || { randomUUID: () => 'test-uuid-1234' };
|
||
|
||
// ── мок XMLHttpRequest ──
|
||
let lastXHR = null;
|
||
class FakeXHR {
|
||
constructor() {
|
||
this.upload = {};
|
||
this.status = 0;
|
||
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; }
|
||
}
|
||
globalThis.XMLHttpRequest = FakeXHR;
|
||
|
||
// ── мок fetch ──
|
||
let fetchCalls = [];
|
||
function installFetch(respond) {
|
||
fetchCalls = [];
|
||
globalThis.fetch = async (url, opts) => {
|
||
fetchCalls.push({ url, opts });
|
||
return respond();
|
||
};
|
||
}
|
||
|
||
// ── 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;
|
||
assert.equal(lastXHR.method, 'PUT');
|
||
assert.equal(lastXHR.url, 'https://vm/tok_0');
|
||
assert.equal(lastXHR.body, f);
|
||
console.log(' putToVm success: ok');
|
||
}
|
||
|
||
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); },
|
||
});
|
||
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');
|
||
}
|
||
|
||
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');
|
||
}
|
||
|
||
// ── прогон ──
|
||
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],
|
||
];
|
||
|
||
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); }
|
||
}
|
||
if (failed) { console.error(failed + ' тестов упало'); process.exit(1); }
|
||
console.log('Все тесты слоя 2 (фронт) прошли');
|