test: добавить нагрузочные/долгие тесты слоёв 1 и 2 (37 тестов всего)
Deploy drhider / validate (push) Canceled after 0s
Deploy drhider / validate (push) Canceled after 0s
This commit is contained in:
@@ -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)
|
||||
# ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (фронт) прошли');
|
||||
Reference in New Issue
Block a user