// Юнит-тесты слоя 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 (фронт) прошли');