263 lines
7.7 KiB
JavaScript
263 lines
7.7 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);
|
|
});
|
|
|
|
test('uploadViaVM: отмена после первого файла не начинает второй PUT', async () => {
|
|
installFetch(async (url, opts) => ({
|
|
ok: true,
|
|
json: async () => ({ ok: true, session: 'partial-sid', count: 1 }),
|
|
}));
|
|
|
|
const ac = new AbortController();
|
|
const statuses = [];
|
|
const files = [new File(['first'], 'first.txt'), new File(['second'], 'second.txt')];
|
|
const p = uploadViaVM(files, {
|
|
vmUploadUrl: 'https://vm-buffer/upload/',
|
|
signal: ac.signal,
|
|
onFileStatus: (idx, status) => statuses.push({ idx, status }),
|
|
});
|
|
|
|
await tick();
|
|
assert.equal(lastXHR.url.endsWith('_0'), true);
|
|
lastXHR.status = 201;
|
|
lastXHR.onload();
|
|
ac.abort();
|
|
const res = await p;
|
|
|
|
assert.equal(res.ok, false);
|
|
assert.equal(res.aborted, true);
|
|
assert.equal(fetchCalls.length, 1);
|
|
assert.equal(lastXHR.url.endsWith('_0'), true);
|
|
assert.ok(statuses.some(({ idx, status }) => idx === 0 && status.includes('Доставлен')));
|
|
});
|