feat: troppa roba davvero
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import asyncio
|
||||
|
||||
from models.proxy import ProxyDocument
|
||||
from models.service import ServiceEndpointDocument
|
||||
from services.mitmproxy.health import check_proxy
|
||||
from app.models.health import HealthSnapshot
|
||||
from app.models.proxy import ProxyDocument
|
||||
from app.models.service import ServiceEndpointDocument
|
||||
from app.services.mitmproxy.health import check_proxy
|
||||
|
||||
|
||||
def test_check_proxy_reports_upstream_and_proxy_healthy():
|
||||
@@ -26,11 +27,44 @@ def test_check_proxy_reports_upstream_and_proxy_healthy():
|
||||
await upstream.wait_closed()
|
||||
await proxy.wait_closed()
|
||||
|
||||
assert health["overall"] == "healthy"
|
||||
assert [check["target"] for check in health["checks"]] == [
|
||||
assert isinstance(health, HealthSnapshot)
|
||||
assert health.overall == "healthy"
|
||||
assert [check.target for check in health.checks] == [
|
||||
"upstream",
|
||||
"proxy",
|
||||
]
|
||||
assert all(check.duration_ms is None for check in health.checks)
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_check_proxy_reports_benchmark_latency_when_enabled():
|
||||
async def run():
|
||||
upstream = await asyncio.start_server(http_handler, "127.0.0.1", 0)
|
||||
proxy = await asyncio.start_server(http_handler, "127.0.0.1", 0)
|
||||
try:
|
||||
upstream_port = upstream.sockets[0].getsockname()[1]
|
||||
proxy_port = proxy.sockets[0].getsockname()[1]
|
||||
document = ProxyDocument(
|
||||
proxy_port=proxy_port,
|
||||
name="health",
|
||||
code="",
|
||||
healthcheck={"benchmark_enabled": True},
|
||||
service=ServiceEndpointDocument(port=upstream_port),
|
||||
)
|
||||
|
||||
health = await check_proxy(document)
|
||||
finally:
|
||||
upstream.close()
|
||||
proxy.close()
|
||||
await upstream.wait_closed()
|
||||
await proxy.wait_closed()
|
||||
|
||||
assert isinstance(health, HealthSnapshot)
|
||||
assert all(
|
||||
check.duration_ms is not None and check.duration_ms >= 0
|
||||
for check in health.checks
|
||||
)
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import asyncio
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from app.models.health import HealthSnapshot
|
||||
from app.models.proxy import ProxyDocument
|
||||
from app.models.service import ServiceEndpointDocument
|
||||
from app.services.mitmproxy.health_monitor import HealthMonitor
|
||||
|
||||
|
||||
def make_proxy(**updates):
|
||||
data = {
|
||||
"proxy_port": 8080,
|
||||
"name": "svc",
|
||||
"code": "",
|
||||
"status": "running",
|
||||
"service": ServiceEndpointDocument(port=9000),
|
||||
}
|
||||
data.update(updates)
|
||||
return ProxyDocument(**data)
|
||||
|
||||
|
||||
def make_snapshot(proxy_port: int, *, overall: str = "healthy") -> HealthSnapshot:
|
||||
return HealthSnapshot(
|
||||
proxy_port=proxy_port,
|
||||
timestamp=datetime.now(UTC),
|
||||
enabled=True,
|
||||
overall=overall,
|
||||
checks=[],
|
||||
)
|
||||
|
||||
|
||||
def test_run_cycle_records_and_publishes_running_healthchecks(monkeypatch):
|
||||
monitor = HealthMonitor()
|
||||
proxy = make_proxy()
|
||||
captured: dict[str, object] = {}
|
||||
snapshot = make_snapshot(8080)
|
||||
|
||||
async def fake_list_proxies():
|
||||
return [proxy]
|
||||
|
||||
async def fake_check_proxy(document):
|
||||
captured["checked"] = document.proxy_port
|
||||
return snapshot
|
||||
|
||||
async def fake_record_health_sample(document):
|
||||
captured["recorded"] = document
|
||||
return document
|
||||
|
||||
async def fake_publish(proxy_port, document):
|
||||
captured["published"] = (proxy_port, document)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.mitmproxy.health_monitor.list_proxies",
|
||||
fake_list_proxies,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.services.mitmproxy.health_monitor.check_proxy",
|
||||
fake_check_proxy,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.services.mitmproxy.health_monitor.record_health_sample",
|
||||
fake_record_health_sample,
|
||||
)
|
||||
monkeypatch.setattr(monitor, "publish", fake_publish)
|
||||
|
||||
asyncio.run(monitor.run_cycle())
|
||||
|
||||
assert captured["checked"] == 8080
|
||||
assert captured["recorded"] == snapshot
|
||||
assert captured["published"] == (8080, snapshot)
|
||||
|
||||
|
||||
def test_run_cycle_skips_stopped_or_disabled_proxies(monkeypatch):
|
||||
monitor = HealthMonitor()
|
||||
running_disabled = make_proxy(healthcheck={"enabled": False})
|
||||
stopped = make_proxy(proxy_port=8081, status="stopped")
|
||||
called = False
|
||||
|
||||
async def fake_list_proxies():
|
||||
return [running_disabled, stopped]
|
||||
|
||||
async def fake_check_proxy(_document):
|
||||
nonlocal called
|
||||
called = True
|
||||
return make_snapshot(8080)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.mitmproxy.health_monitor.list_proxies",
|
||||
fake_list_proxies,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.services.mitmproxy.health_monitor.check_proxy",
|
||||
fake_check_proxy,
|
||||
)
|
||||
|
||||
asyncio.run(monitor.run_cycle())
|
||||
|
||||
assert called is False
|
||||
|
||||
|
||||
def test_run_cycle_checks_proxies_in_parallel(monkeypatch):
|
||||
monitor = HealthMonitor()
|
||||
proxies = [
|
||||
make_proxy(proxy_port=8080),
|
||||
make_proxy(proxy_port=8081),
|
||||
make_proxy(proxy_port=8082),
|
||||
]
|
||||
started: list[int] = []
|
||||
release = asyncio.Event()
|
||||
|
||||
async def fake_list_proxies():
|
||||
return proxies
|
||||
|
||||
async def fake_check_proxy(document):
|
||||
started.append(document.proxy_port)
|
||||
if len(started) == len(proxies):
|
||||
release.set()
|
||||
await release.wait()
|
||||
return make_snapshot(document.proxy_port)
|
||||
|
||||
async def fake_record_health_sample(document):
|
||||
return document
|
||||
|
||||
async def fake_publish(_proxy_port, _document):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.mitmproxy.health_monitor.list_proxies",
|
||||
fake_list_proxies,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.services.mitmproxy.health_monitor.check_proxy",
|
||||
fake_check_proxy,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.services.mitmproxy.health_monitor.record_health_sample",
|
||||
fake_record_health_sample,
|
||||
)
|
||||
monkeypatch.setattr(monitor, "publish", fake_publish)
|
||||
|
||||
asyncio.run(asyncio.wait_for(monitor.run_cycle(), timeout=1))
|
||||
|
||||
assert started == [8080, 8081, 8082]
|
||||
|
||||
|
||||
def test_run_cycle_continues_when_one_proxy_check_fails(monkeypatch):
|
||||
monitor = HealthMonitor()
|
||||
proxies = [
|
||||
make_proxy(proxy_port=8080),
|
||||
make_proxy(proxy_port=8081),
|
||||
]
|
||||
captured: list[int] = []
|
||||
|
||||
async def fake_list_proxies():
|
||||
return proxies
|
||||
|
||||
async def fake_check_proxy(document):
|
||||
if document.proxy_port == 8080:
|
||||
raise RuntimeError("boom")
|
||||
return make_snapshot(document.proxy_port)
|
||||
|
||||
async def fake_record_health_sample(document):
|
||||
captured.append(document.proxy_port)
|
||||
return document
|
||||
|
||||
async def fake_publish(proxy_port, _document):
|
||||
captured.append(proxy_port + 10_000)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.mitmproxy.health_monitor.list_proxies",
|
||||
fake_list_proxies,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.services.mitmproxy.health_monitor.check_proxy",
|
||||
fake_check_proxy,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.services.mitmproxy.health_monitor.record_health_sample",
|
||||
fake_record_health_sample,
|
||||
)
|
||||
monkeypatch.setattr(monitor, "publish", fake_publish)
|
||||
|
||||
asyncio.run(monitor.run_cycle())
|
||||
|
||||
assert captured == [8081, 18081]
|
||||
|
||||
|
||||
def test_run_retries_after_cycle_failure(monkeypatch):
|
||||
monitor = HealthMonitor()
|
||||
calls = {"run_cycle": 0}
|
||||
sleep_calls: list[float] = []
|
||||
|
||||
async def fake_run_cycle():
|
||||
calls["run_cycle"] += 1
|
||||
if calls["run_cycle"] == 1:
|
||||
raise RuntimeError("temporary failure")
|
||||
raise asyncio.CancelledError
|
||||
|
||||
async def fake_sleep(seconds):
|
||||
sleep_calls.append(seconds)
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(monitor, "run_cycle", fake_run_cycle)
|
||||
monkeypatch.setattr(
|
||||
"app.services.mitmproxy.health_monitor.asyncio.sleep",
|
||||
fake_sleep,
|
||||
)
|
||||
|
||||
try:
|
||||
asyncio.run(monitor.run())
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
assert calls["run_cycle"] == 2
|
||||
assert len(sleep_calls) == 1
|
||||
assert sleep_calls[0] == pytest.approx(1.0, abs=0.01)
|
||||
@@ -1,9 +1,11 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from models.proxy import ProxyDocument
|
||||
from models.service import ServiceEndpointDocument
|
||||
from services.mitmproxy.manager import MitmdumpManager
|
||||
import pytest
|
||||
|
||||
from app.models.proxy import ProxyDocument
|
||||
from app.models.service import ServiceEndpointDocument
|
||||
from app.services.mitmproxy.manager import MitmdumpError, MitmdumpManager
|
||||
|
||||
|
||||
def make_proxy(**updates):
|
||||
@@ -65,6 +67,20 @@ def test_regular_mode_uses_proxy_port_as_listen_port():
|
||||
assert command[-2:] == ["--listen-port", "8080"]
|
||||
|
||||
|
||||
def test_start_rejects_reserved_backend_port():
|
||||
manager = MitmdumpManager()
|
||||
proxy = make_proxy(proxy_port=3671)
|
||||
|
||||
async def run():
|
||||
await manager.start(proxy)
|
||||
|
||||
with pytest.raises(
|
||||
MitmdumpError,
|
||||
match="Proxy port 3671 is reserved for the backend API",
|
||||
):
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_running_document_without_pid_is_treated_as_stopped():
|
||||
manager = MitmdumpManager()
|
||||
proxy = make_proxy(status="running", pid=None)
|
||||
@@ -167,3 +183,99 @@ def test_reconcile_restores_upstream_when_process_is_gone():
|
||||
"service.upstream_port": None,
|
||||
"last_error": None,
|
||||
}
|
||||
|
||||
|
||||
def test_cleanup_runtime_artifacts_removes_generated_files(tmp_path: Path):
|
||||
manager = MitmdumpManager(work_dir=str(tmp_path / "theplug"))
|
||||
proxy = make_proxy(
|
||||
script_path=str(manager.script_path_for(make_proxy())),
|
||||
config_path=str(manager.config_path_for(make_proxy())),
|
||||
log_path=str(manager.log_path_for(make_proxy())),
|
||||
)
|
||||
|
||||
script_path = Path(proxy.script_path)
|
||||
config_path = Path(proxy.config_path)
|
||||
log_path = Path(proxy.log_path)
|
||||
script_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
script_path.write_text("print('hello')", encoding="utf-8")
|
||||
config_path.write_text("block_global: false\n", encoding="utf-8")
|
||||
log_path.write_text("old log\n", encoding="utf-8")
|
||||
|
||||
manager.cleanup_runtime_artifacts(proxy)
|
||||
|
||||
assert script_path.exists() is False
|
||||
assert log_path.exists() is False
|
||||
assert config_path.exists() is False
|
||||
assert manager.conf_dir_for(proxy).exists() is False
|
||||
|
||||
|
||||
def test_prepare_upstream_retries_until_port_remap_succeeds(monkeypatch):
|
||||
manager = MitmdumpManager()
|
||||
proxy = make_proxy(
|
||||
mode="reverse",
|
||||
service=ServiceEndpointDocument(
|
||||
port=9000,
|
||||
name="echo",
|
||||
folder="/tmp/playground",
|
||||
),
|
||||
)
|
||||
attempted_ports: list[int] = []
|
||||
candidate_ports = iter([54001, 54002, 54003])
|
||||
|
||||
async def fake_change_port(
|
||||
_service_name,
|
||||
_folder,
|
||||
_port_from,
|
||||
port_to,
|
||||
**_kwargs,
|
||||
):
|
||||
attempted_ports.append(port_to)
|
||||
return port_to == 54003
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.mitmproxy.manager.get_random_port",
|
||||
lambda: next(candidate_ports),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.services.mitmproxy.manager.change_port",
|
||||
fake_change_port,
|
||||
)
|
||||
|
||||
upstream_port = asyncio.run(manager.prepare_upstream(proxy))
|
||||
|
||||
assert upstream_port == 54003
|
||||
assert attempted_ports == [54001, 54002, 54003]
|
||||
|
||||
|
||||
def test_prepare_upstream_fails_after_retry_budget(monkeypatch):
|
||||
manager = MitmdumpManager()
|
||||
manager.upstream_port_attempts = 3
|
||||
proxy = make_proxy(
|
||||
mode="reverse",
|
||||
service=ServiceEndpointDocument(
|
||||
port=9000,
|
||||
name="echo",
|
||||
folder="/tmp/playground",
|
||||
),
|
||||
)
|
||||
candidate_ports = iter([54001, 54002, 54003])
|
||||
|
||||
async def fake_change_port(*_args, **_kwargs):
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.mitmproxy.manager.get_random_port",
|
||||
lambda: next(candidate_ports),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"app.services.mitmproxy.manager.change_port",
|
||||
fake_change_port,
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
MitmdumpError,
|
||||
match="Unable to move Docker service port after retries",
|
||||
):
|
||||
asyncio.run(manager.prepare_upstream(proxy))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import pytest
|
||||
from services.mitmproxy.options import (
|
||||
|
||||
from app.services.mitmproxy.options import (
|
||||
MitmOption,
|
||||
parse_options,
|
||||
public_options,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from services.editor import python_lsp
|
||||
from app.services.editor import python_lsp
|
||||
|
||||
|
||||
def test_encode_message():
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import asyncio
|
||||
|
||||
from models.proxy import ProxyDocument
|
||||
from models.service import ServiceEndpointDocument
|
||||
from services.mitmproxy import recovery as recovery_module
|
||||
from app.models.proxy import ProxyDocument
|
||||
from app.models.service import ServiceEndpointDocument
|
||||
from app.services.mitmproxy import recovery as recovery_module
|
||||
|
||||
|
||||
def make_proxy(**updates):
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
from app.services.mitmproxy.templates import BUILTIN_TEMPLATES
|
||||
|
||||
|
||||
def test_builtin_templates_use_four_space_indentation():
|
||||
for template in BUILTIN_TEMPLATES:
|
||||
for line in template.code.splitlines():
|
||||
assert "\t" not in line, f"{template.id} contains a tab: {line!r}"
|
||||
if not line.strip():
|
||||
continue
|
||||
indent = len(line) - len(line.lstrip(" "))
|
||||
assert indent % 4 == 0, (
|
||||
f"{template.id} has non-4-space indentation: {line!r}"
|
||||
)
|
||||
Reference in New Issue
Block a user