test: добавить нагрузочные/долгие тесты слоёв 1 и 2 (37 тестов всего)
Deploy drhider / validate (push) Canceled after 0s

This commit is contained in:
“Naeel”
2026-08-25 19:11:29 +03:00
parent 17e6dbd065
commit 52c3eb53f1
3 changed files with 278 additions and 0 deletions
+79
View File
@@ -7,6 +7,7 @@
import os
import sys
import time
# Корень проекта — для импорта пакета upload
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@@ -227,6 +228,84 @@ def test_upload_refs_pull_failed(monkeypatch):
assert rv.status_code == 502 # все ретраи провалились
def test_session_many_files():
"""Нагрузка: 1000 файлов в одной сессии."""
sid = create_session()
for i in range(1000):
assert add_file(sid, "f%d.txt" % i, b"x" * 10) is True
assert file_count(sid) == 1000
assert len(get_files(sid)) == 1000
def test_concurrent_add_files():
"""Конкурентность: 4 потока добавляют файлы в общую сессию."""
import threading
sid = create_session()
def worker(prefix):
for i in range(50):
add_file(sid, "%s_%d.txt" % (prefix, i), b"data")
threads = [threading.Thread(target=worker, args=("t%d" % t,)) for t in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
assert file_count(sid) == 200
def test_session_ttl_cleanup():
"""TTL: короткий TTL → сессия очищается фоновым таймером (долгий тест)."""
from upload.backend.session import configure
configure(ttl_seconds=1)
try:
sid = create_session()
assert add_file(sid, "a.txt", b"x") is True
time.sleep(1.5)
assert get_files(sid) is None # таймер удалил сессию
finally:
configure(ttl_seconds=30 * 60)
def test_add_file_session_limit_boundary():
"""Суммарный лимит: граница ровно на MAX_SESSION_BYTES."""
from upload.backend.session import configure
configure(max_session_bytes=1000)
try:
sid = create_session()
assert add_file(sid, "a", b"x" * 500) is True
assert add_file(sid, "b", b"x" * 500) is True # ровно 1000
assert add_file(sid, "c", b"x") is False # 1001 > 1000
assert file_count(sid) == 2
finally:
configure(max_session_bytes=500 * 1024 * 1024)
def test_safe_name_edge_cases():
"""safe_name: граничные случаи."""
assert safe_name("...") == "..."
assert safe_name("a/../b") == ""
assert safe_name("a//b") == "a/b"
assert safe_name(" ") == " "
assert safe_name("договор/файл.pdf") == "договор/файл.pdf"
assert safe_name("C:\\path\\file") == "C:/path/file"
assert safe_name("/abs/path") == "abs/path"
def test_upload_refs_many_files(monkeypatch):
"""Нагрузка: 100 refs → все pull OK, count=100."""
behavior = {}
files = []
for i in range(100):
url = VM_PREFIX + "tok_%d" % i
behavior[url] = {"payload": b"x" * 10}
files.append({"name": "f%d.txt" % i, "size": 10, "url": url})
c = _make_client(behavior, monkeypatch=monkeypatch)
rv = c.post("/api/upload_refs", json={"session": "", "files": files})
assert rv.status_code == 200
assert rv.get_json()["count"] == 100
# ═══════════════════════════════════════════════════════════════════════════
# main (запуск без pytest)
# ═══════════════════════════════════════════════════════════════════════════
+11
View File
@@ -184,6 +184,16 @@ function testLimits() {
console.log(' лимиты over: ok');
}
function testSuffixes() {
const st = newState();
const c = { maxFileBytes: 10000, maxSessionBytes: 100000 };
addFileWithDedup(st, new File(['aaa'], 'a.txt'), c);
addFileWithDedup(st, new File(['bbbb'], 'a.txt'), c); // a_2.txt
addFileWithDedup(st, new File(['ccccc'], 'a.txt'), c); // a_3.txt
assert.deepEqual(st.files.map(f => f.name), ['a.txt', 'a_2.txt', 'a_3.txt']);
console.log(' суффиксы _2/_3: ok');
}
// ── Прогон ──
const TESTS = [
['decodeZipName', testDecodeZipName],
@@ -193,6 +203,7 @@ const TESTS = [
['не-ZIP бросок', testParseZipNotZip],
['дедуп/суффиксы', testDedup],
['лимиты over', testLimits],
['суффиксы _2/_3', testSuffixes],
];
let failed = 0;
+188
View File
@@ -0,0 +1,188 @@
// Юнит-тесты слоя 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');
}
// ── 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],
['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 (фронт) прошли');