- web/features/downloads.py: фоновая операция типа «download» — urllib по 1 МиБ,
таймаут 30 с, User-Agent Virtuality/<версия>, запись в .<имя>.part и переименование
в конце; в сообщении скорость и остаток («245 МБ из 3,1 ГБ · 12 МБ/с · осталось 4 мин»);
отмена между кусками (POST /api/operations/{id}/cancel и /downloads/{id}/cancel);
проверка схемы, имени файла, занятости имени и свободного места (Content-Length + 512 МиБ);
для каталога — проверка контрольной суммы (SHA256SUMS/SHA512SUMS/одиночный хеш),
при несовпадении файл удаляется; после ISO — virsh pool-refresh virtuality-iso,
после .img.xz — распаковка и запуск конвертации в qcow2 как при загрузке с компьютера.
- /iso и /disk-images: карточка «Скачать по ссылке», на /disk-images — «Каталог готовых
систем» для процессора сервера с пометками «Рекомендуем», «Загружен», «Скачивается»;
идущие скачивания вверху страниц с живым прогрессом (опрос раз в 2 с) и «Отменить».
- Тесты: парсер контрольных сумм, проверка ссылок и имён, загрузка с локального
http-сервера, редирект, отмена, несовпадение суммы, нехватка места, .img.xz, маршруты.
- В ветку скопированы web/core.py, web/catalog.py, presenters.format_bytes и tests/fixtures
из общей рабочей копии (без изменений), conftest подменяет run_cmd и каталоги и в core.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019LEUXMcdZo7b87mjkUsPgg
146 строки
5.8 KiB
Python
146 строки
5.8 KiB
Python
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
WEB_DIR = ROOT / "web"
|
|
sys.path.insert(0, str(WEB_DIR))
|
|
|
|
os.environ.setdefault("VIRTUALITY_AUTH_USER", "tester")
|
|
os.environ.setdefault("VIRTUALITY_SESSION_SECRET", "test-secret-" + "0" * 32)
|
|
|
|
DOMINFO = """Id: 1
|
|
Name: web01
|
|
UUID: 6c1b3c1e-0000-0000-0000-000000000001
|
|
OS Type: hvm
|
|
State: running
|
|
CPU(s): 2
|
|
Max memory: 2097152 KiB
|
|
Used memory: 2097152 KiB
|
|
Persistent: yes
|
|
Autostart: enable
|
|
"""
|
|
|
|
DUMPXML = """<domain type='kvm'>
|
|
<name>web01</name>
|
|
<memory unit='KiB'>2097152</memory>
|
|
<vcpu placement='static'>2</vcpu>
|
|
<os><type arch='x86_64' machine='pc-q35'>hvm</type><boot dev='hd'/></os>
|
|
<devices>
|
|
<disk type='file' device='disk'><target dev='vda' bus='virtio'/></disk>
|
|
</devices>
|
|
</domain>
|
|
"""
|
|
|
|
|
|
def fake_result(stdout: str = "", ok: bool = True, stderr: str = "") -> dict:
|
|
return {"ok": ok, "code": 0 if ok else 1, "stdout": stdout, "stderr": stderr, "cmd": ""}
|
|
|
|
|
|
def fake_run_cmd(cmd, timeout=12, **_kwargs):
|
|
joined = " ".join(cmd)
|
|
if cmd[:2] == ["virsh", "list"]:
|
|
return fake_result(" Id Name State\n-----------------------\n 1 web01 running\n - db01 shut off")
|
|
if cmd[:2] == ["virsh", "dominfo"]:
|
|
return fake_result(DOMINFO) if cmd[-1] in ("web01", "db01") else fake_result(ok=False, stderr="failed to get domain")
|
|
if cmd[:2] == ["virsh", "dumpxml"]:
|
|
return fake_result(DUMPXML)
|
|
if cmd[:2] == ["virsh", "domstate"]:
|
|
return fake_result("running")
|
|
if cmd[:2] == ["virsh", "domifaddr"]:
|
|
return fake_result(" Name MAC address Protocol Address\n vnet0 52:54:00:aa:bb:cc ipv4 192.168.100.51/24")
|
|
if cmd[:2] == ["virsh", "domiflist"]:
|
|
return fake_result(" Interface Type Source Model MAC\n vnet0 network virtuality-nat virtio 52:54:00:aa:bb:cc")
|
|
if cmd[:2] == ["virsh", "vncdisplay"]:
|
|
return fake_result(":0")
|
|
if cmd[:2] == ["virsh", "pool-list"]:
|
|
return fake_result(" Name State Autostart\n---\n virtuality-images active yes")
|
|
if cmd[:2] == ["virsh", "pool-refresh"]:
|
|
return fake_result(f"Pool {cmd[-1]} refreshed")
|
|
if cmd[:2] == ["virsh", "net-list"]:
|
|
return fake_result(" Name State Autostart Persistent\n---\n virtuality-nat active yes yes")
|
|
if cmd[:2] == ["systemctl", "is-active"]:
|
|
return fake_result("active")
|
|
if cmd[:1] == ["hostname"]:
|
|
return fake_result("192.168.1.10" if "-I" in cmd else "virtuality-test")
|
|
if cmd[:3] == ["ip", "route", "show"]:
|
|
return fake_result("default via 192.168.1.1 dev eth0")
|
|
if cmd[:1] in (["uptime"], ["uname"]):
|
|
return fake_result("up 1 hour" if cmd[0] == "uptime" else "6.8.0")
|
|
if "nft" in joined or "iptables" in joined or "ufw" in joined or "sysctl" in joined:
|
|
return fake_result()
|
|
return fake_result()
|
|
|
|
|
|
def feature_modules():
|
|
"""Загруженные модули web/features/* — им тоже подменяем run_cmd."""
|
|
return [module for name, module in sys.modules.items() if name.startswith("features.") and hasattr(module, "run_cmd")]
|
|
|
|
|
|
@pytest.fixture()
|
|
def data_dirs(tmp_path, monkeypatch):
|
|
import app
|
|
import core
|
|
import host_profile
|
|
import network_core
|
|
import update_core
|
|
|
|
dirs = {
|
|
"iso": tmp_path / "iso",
|
|
"images": tmp_path / "images",
|
|
"disk_images": tmp_path / "disk-images",
|
|
"operations": tmp_path / "operations",
|
|
"backups": tmp_path / "backups",
|
|
"network": tmp_path / "network",
|
|
"config": tmp_path / "config",
|
|
"nft": tmp_path / "nft",
|
|
"update": tmp_path / "update",
|
|
}
|
|
for path in dirs.values():
|
|
path.mkdir()
|
|
for module in (app, core):
|
|
monkeypatch.setattr(module, "ISO_DIR", dirs["iso"])
|
|
monkeypatch.setattr(module, "IMAGES_DIR", dirs["images"])
|
|
monkeypatch.setattr(module, "DISK_IMAGES_DIR", dirs["disk_images"])
|
|
monkeypatch.setattr(module, "OPERATIONS_DIR", dirs["operations"])
|
|
monkeypatch.setattr(core, "BACKUPS_DIR", dirs["backups"])
|
|
monkeypatch.setattr(core, "CONFIG_DIR", dirs["config"])
|
|
monkeypatch.setattr(network_core, "CONFIG_DIR", dirs["config"])
|
|
monkeypatch.setattr(network_core, "NETWORK_DIR", dirs["network"])
|
|
monkeypatch.setattr(network_core, "NFT_DIR", dirs["nft"])
|
|
monkeypatch.setattr(network_core, "PORT_FORWARDS_FILE", dirs["network"] / "port_forwards.json")
|
|
monkeypatch.setattr(network_core, "NAT_XML_FILE", dirs["network"] / "virtuality-nat.xml")
|
|
monkeypatch.setattr(network_core, "NFT_FILE", dirs["nft"] / "virtuality.nft")
|
|
monkeypatch.setattr(network_core, "enable_ip_forward", lambda: None)
|
|
monkeypatch.setattr(network_core, "disable_rp_filter", lambda: None)
|
|
monkeypatch.setattr(host_profile, "PROFILE_FILE", dirs["config"] / "host_profile.json")
|
|
monkeypatch.setattr(update_core, "STATE_DIR", dirs["update"])
|
|
monkeypatch.setattr(update_core, "STATE_FILE", dirs["update"] / "state.json")
|
|
monkeypatch.setattr(update_core, "LOG_FILE", dirs["update"] / "update.log")
|
|
monkeypatch.setattr(update_core, "SOURCE_DIR", ROOT)
|
|
for module in (app, core, network_core, host_profile, *feature_modules()):
|
|
monkeypatch.setattr(module, "run_cmd", fake_run_cmd)
|
|
return dirs
|
|
|
|
|
|
@pytest.fixture()
|
|
def client(data_dirs):
|
|
from fastapi.testclient import TestClient
|
|
|
|
import app
|
|
|
|
app.login_throttle = app.auth.LoginThrottle()
|
|
return TestClient(app.app, base_url="http://panel.test")
|
|
|
|
|
|
@pytest.fixture()
|
|
def logged_in(client, monkeypatch):
|
|
import app
|
|
|
|
monkeypatch.setattr(app.auth, "verify_password", lambda user, password: password == "correct")
|
|
response = client.post("/login", data={"username": "tester", "password": "correct"}, follow_redirects=False)
|
|
assert response.status_code == 303
|
|
return client
|