Files
upload-platform/tests/test_upload_layer2.test.mjs
T

234 lines
6.8 KiB
JavaScript

// Юнит-тесты слоя 2 (фронт): putToVm + uploadViaVM с пофайловым транзитом.
// Запуск: node --test tests/test_upload_layer2.test.mjs
import test from 'node:test';
import assert from 'node:assert/strict';
// Полифилл File/Blob при необходимости
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/frontend/upload/put_to_vm.js';
import { uploadViaVM } from '../upload/frontend/upload/upload_via_vm.js';
const tick = () => new Promise(r => setTimeout(r, 0));
// Мок 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.onabort = null;
this.method = null;
this.url = null;
this.body = null;
this.aborted = false;
lastXHR = this;
}
open(method, url) { this.method = method; this.url = url; }
send(body) { this.body = body; }
abort() {
this.aborted = true;
if (this.onabort) this.onabort();
}
}
globalThis.XMLHttpRequest = FakeXHR;
// Мок fetch
let fetchCalls = [];
function installFetch(handler) {
fetchCalls = [];
globalThis.fetch = async (url, opts) => {
fetchCalls.push({ url, opts });
return handler(url, opts);
};
}
test('putToVm: успешная отправка', async () => {
const f = new File(['test-content'], 'doc.pdf');
const p = putToVm(f, 'https://vm-buffer/token_0');
lastXHR.status = 200;
lastXHR.onload();
await p;
assert.equal(lastXHR.method, 'PUT');
assert.equal(lastXHR.url, 'https://vm-buffer/token_0');
assert.equal(lastXHR.body, f);
});
test('putToVm: ошибка HTTP статуса', async () => {
const p = putToVm(new File(['abc'], 'doc.pdf'), 'https://vm-buffer/token_0');
lastXHR.status = 502;
lastXHR.statusText = 'Bad Gateway';
lastXHR.onload();
await assert.rejects(p, /HTTP 502/);
});
test('putToVm: сетевая ошибка', async () => {
const p = putToVm(new File(['abc'], 'doc.pdf'), 'https://vm-buffer/token_0');
lastXHR.onerror();
await assert.rejects(p, /Сетевая ошибка/);
});
test('putToVm: таймаут', async () => {
const p = putToVm(new File(['abc'], 'doc.pdf'), 'https://vm-buffer/token_0');
lastXHR.ontimeout();
await assert.rejects(p, /Таймаут/);
});
test('putToVm: прерывание через AbortSignal', async () => {
const ac = new AbortController();
const p = putToVm(new File(['abc'], 'doc.pdf'), 'https://vm-buffer/token_0', { signal: ac.signal });
ac.abort();
await assert.rejects(p, (err) => err.name === 'AbortError');
assert.equal(lastXHR.aborted, true);
});
test('uploadViaVM: пофайловый транзит (N файлов -> N PUT + N upload_refs)', async () => {
let callCount = 0;
installFetch(async (url, opts) => {
callCount++;
const body = JSON.parse(opts.body);
return {
ok: true,
json: async () => ({
ok: true,
session: body.session || 'created-sid-1',
count: callCount,
}),
};
});
const files = [
new File(['hello'], 'first.txt'),
new File(['world-data'], 'second.pdf'),
];
const statuses = [];
const completed = [];
const p = uploadViaVM(files, {
vmUploadUrl: 'https://vm-buffer/upload/',
backendUploadUrl: '/api/upload_refs',
onFileStatus: (idx, status) => statuses.push({ idx, status }),
onFileComplete: (info) => completed.push(info),
});
// Файл 1: завершаем PUT
await tick();
assert.equal(lastXHR.method, 'PUT');
assert.ok(lastXHR.url.startsWith('https://vm-buffer/upload/'));
lastXHR.status = 201;
lastXHR.onload();
// Файл 2: завершаем PUT
await tick();
assert.equal(lastXHR.method, 'PUT');
assert.ok(lastXHR.url.startsWith('https://vm-buffer/upload/'));
lastXHR.status = 201;
lastXHR.onload();
const res = await p;
assert.equal(res.ok, true);
assert.equal(res.session, 'created-sid-1');
assert.equal(res.count, 2);
// Проверяем, что было РОВНО 2 вызова fetch (/api/upload_refs), по одному на каждый файл
assert.equal(fetchCalls.length, 2);
const call1Body = JSON.parse(fetchCalls[0].opts.body);
assert.equal(call1Body.files.length, 1);
assert.equal(call1Body.files[0].name, 'first.txt');
const call2Body = JSON.parse(fetchCalls[1].opts.body);
assert.equal(call2Body.files.length, 1);
assert.equal(call2Body.files[0].name, 'second.pdf');
// Сессия, созданная на 1 шаге, проброшена во 2 шаг
assert.equal(call2Body.session, 'created-sid-1');
assert.equal(completed.length, 2);
assert.equal(completed[0].name, 'first.txt');
assert.equal(completed[1].name, 'second.pdf');
});
test('uploadViaVM: поддержка формата FilePicker.getFiles() ({name, size, file, path})', async () => {
installFetch(async (url, opts) => {
return {
ok: true,
json: async () => ({ ok: true, session: 'sid-p', count: 1 }),
};
});
const pickerFiles = [
{
name: 'archive_doc.txt',
size: 42,
file: new File(['x'.repeat(42)], 'archive_doc.txt'),
path: 'folder/archive_doc.txt',
},
];
const p = uploadViaVM(pickerFiles, {
vmUploadUrl: 'https://vm-buffer/upload/',
});
await tick();
lastXHR.status = 200;
lastXHR.onload();
const res = await p;
assert.equal(res.ok, true);
assert.equal(res.count, 1);
const body = JSON.parse(fetchCalls[0].opts.body);
assert.equal(body.files[0].name, 'archive_doc.txt');
assert.equal(body.files[0].size, 42);
});
test('uploadViaVM: обработка ошибки PUT', async () => {
installFetch(async () => ({ ok: true, json: async () => ({ ok: true }) }));
const files = [new File(['bad'], 'err.txt')];
const p = uploadViaVM(files, {
vmUploadUrl: 'https://vm-buffer/upload/',
});
await tick();
lastXHR.onerror();
const res = await p;
assert.equal(res.ok, false);
assert.ok(res.error.includes('Ошибка отправки файла'));
assert.equal(fetchCalls.length, 0); // POST не вызывался
});
test('uploadViaVM: прерывание через signal', async () => {
installFetch(async () => ({ ok: true, json: async () => ({ ok: true }) }));
const ac = new AbortController();
const files = [new File(['1'], 'f1.txt'), new File(['2'], 'f2.txt')];
const p = uploadViaVM(files, {
vmUploadUrl: 'https://vm-buffer/upload/',
signal: ac.signal,
});
await tick();
ac.abort();
const res = await p;
assert.equal(res.ok, false);
assert.equal(res.aborted, true);
});