feat: troppa roba davvero
This commit is contained in:
+385
-21
@@ -1,14 +1,18 @@
|
||||
import asyncio
|
||||
import importlib
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytest
|
||||
from core.security import get_auth_context, require_authenticated_user
|
||||
from models.proxy import ProxyCreateInput, ProxyDocument
|
||||
from models.service import ServiceEndpointDocument
|
||||
from services.mitmproxy.examples import BUILTIN_EXAMPLES
|
||||
from services.mitmproxy.validator import ProxyCodeValidationError
|
||||
from starlette.websockets import WebSocketDisconnect
|
||||
|
||||
proxy_router_module = importlib.import_module("api.proxy.router")
|
||||
from app.core.security import get_auth_context, require_authenticated_user
|
||||
from app.models.health import HealthSnapshot
|
||||
from app.models.proxy import ProxyCreateInput, ProxyDocument
|
||||
from app.models.service import ServiceEndpointDocument
|
||||
from app.services.mitmproxy.templates import BUILTIN_TEMPLATES
|
||||
from app.services.mitmproxy.validator import ProxyCodeValidationError
|
||||
|
||||
proxy_router_module = importlib.import_module("app.api.proxy.router")
|
||||
|
||||
|
||||
def make_proxy_document(
|
||||
@@ -28,6 +32,21 @@ def make_proxy_document(
|
||||
)
|
||||
|
||||
|
||||
def make_health_snapshot(
|
||||
proxy_port: int,
|
||||
*,
|
||||
overall: str = "healthy",
|
||||
timestamp: datetime | None = None,
|
||||
) -> HealthSnapshot:
|
||||
return HealthSnapshot(
|
||||
proxy_port=proxy_port,
|
||||
timestamp=timestamp or datetime.now(UTC),
|
||||
enabled=True,
|
||||
overall=overall,
|
||||
checks=[],
|
||||
)
|
||||
|
||||
|
||||
def test_proxy_requires_auth_without_dependency_overrides(client):
|
||||
client.app.dependency_overrides.pop(require_authenticated_user, None)
|
||||
client.app.dependency_overrides.pop(get_auth_context, None)
|
||||
@@ -80,6 +99,7 @@ def test_list_services_uses_query_scan_root(client, monkeypatch):
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"backend_port": 3671,
|
||||
"scan_root": "/root",
|
||||
"services": [
|
||||
{"name": "echo", "ports": [9081], "bindings": [], "folder": "/root/play"}
|
||||
@@ -105,10 +125,10 @@ def test_get_proxy_returns_404_when_not_found(client, monkeypatch: object):
|
||||
|
||||
|
||||
def test_create_proxy_returns_409_on_port_conflict(client, monkeypatch: object):
|
||||
existing = make_proxy_document(proxy_port=3671)
|
||||
existing = make_proxy_document(proxy_port=4088)
|
||||
|
||||
async def fake_get_proxy_by_port(proxy_port: int) -> ProxyDocument | None:
|
||||
assert proxy_port == 3671
|
||||
assert proxy_port == 4088
|
||||
return existing
|
||||
|
||||
monkeypatch.setattr(
|
||||
@@ -120,7 +140,7 @@ def test_create_proxy_returns_409_on_port_conflict(client, monkeypatch: object):
|
||||
response = client.post(
|
||||
"/proxy",
|
||||
json={
|
||||
"proxy_port": 3671,
|
||||
"proxy_port": 4088,
|
||||
"name": "new",
|
||||
"code": "abc",
|
||||
"service": {"port": 8080},
|
||||
@@ -128,7 +148,44 @@ def test_create_proxy_returns_409_on_port_conflict(client, monkeypatch: object):
|
||||
)
|
||||
|
||||
assert response.status_code == 409
|
||||
assert response.json()["detail"] == "Proxy already exists on port 3671"
|
||||
assert response.json()["detail"] == "Proxy already exists on port 4088"
|
||||
|
||||
|
||||
def test_create_proxy_rejects_backend_api_port(client, monkeypatch: object):
|
||||
create_called = False
|
||||
|
||||
async def fake_get_proxy_by_port(proxy_port: int) -> ProxyDocument | None:
|
||||
assert proxy_port == 3671
|
||||
return None
|
||||
|
||||
async def fake_create_proxy(payload: ProxyCreateInput) -> ProxyDocument:
|
||||
nonlocal create_called
|
||||
create_called = True
|
||||
return make_proxy_document(proxy_port=payload.proxy_port)
|
||||
|
||||
monkeypatch.setattr(
|
||||
proxy_router_module,
|
||||
"get_proxy_by_port",
|
||||
fake_get_proxy_by_port,
|
||||
)
|
||||
monkeypatch.setattr(proxy_router_module, "create_proxy", fake_create_proxy)
|
||||
|
||||
response = client.post(
|
||||
"/proxy",
|
||||
json={
|
||||
"proxy_port": 3671,
|
||||
"name": "backend-clash",
|
||||
"code": "abc",
|
||||
"service": {"port": 8080},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert response.json()["detail"] == (
|
||||
"Proxy port 3671 is reserved for the backend API "
|
||||
"and cannot be used by mitmdump."
|
||||
)
|
||||
assert create_called is False
|
||||
|
||||
|
||||
def test_create_proxy_returns_created_document(client, monkeypatch: object):
|
||||
@@ -470,6 +527,297 @@ def test_proxy_logs_socket_streams_snapshot_and_append(client, monkeypatch, tmp_
|
||||
assert appended == {"type": "append", "log": "line two\n"}
|
||||
|
||||
|
||||
def test_proxy_logs_socket_resets_snapshot_after_log_rotation(
|
||||
client,
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
):
|
||||
log_path = tmp_path / "proxy.log"
|
||||
log_path.write_text("line one\n", encoding="utf-8")
|
||||
proxy = make_proxy_document(proxy_port=4040, log_path=str(log_path))
|
||||
|
||||
async def fake_authenticate(_: object) -> None:
|
||||
return None
|
||||
|
||||
async def fake_get_proxy_by_port(proxy_port: int) -> ProxyDocument | None:
|
||||
assert proxy_port == 4040
|
||||
return proxy
|
||||
|
||||
monkeypatch.setattr(
|
||||
proxy_router_module,
|
||||
"authenticate_proxy_socket",
|
||||
fake_authenticate,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
proxy_router_module,
|
||||
"get_proxy_by_port",
|
||||
fake_get_proxy_by_port,
|
||||
)
|
||||
|
||||
with client.websocket_connect("/proxy/4040/logs/ws?token=test-token") as socket:
|
||||
snapshot = socket.receive_json()
|
||||
assert snapshot == {"type": "snapshot", "log": "line one\n"}
|
||||
|
||||
log_path.write_text("fresh start\n", encoding="utf-8")
|
||||
|
||||
rotated = socket.receive_json()
|
||||
assert rotated == {"type": "snapshot", "log": "fresh start\n"}
|
||||
|
||||
with log_path.open("a", encoding="utf-8") as handle:
|
||||
handle.write("line two\n")
|
||||
handle.flush()
|
||||
|
||||
appended = socket.receive_json()
|
||||
assert appended == {"type": "append", "log": "line two\n"}
|
||||
|
||||
|
||||
def test_remove_proxy_stops_runtime_and_cleans_artifacts(client, monkeypatch: object):
|
||||
proxy = make_proxy_document(
|
||||
proxy_port=4040,
|
||||
log_path="/tmp/theplug/logs/4040.log",
|
||||
).model_copy(
|
||||
update={
|
||||
"script_path": "/tmp/theplug/addons/4040.py",
|
||||
"config_path": "/tmp/theplug/conf/4040/config.yaml",
|
||||
}
|
||||
)
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_get_proxy_by_port(proxy_port: int) -> ProxyDocument | None:
|
||||
assert proxy_port == 4040
|
||||
return proxy
|
||||
|
||||
class FakeManager:
|
||||
async def stop(self, document: ProxyDocument) -> dict[str, object]:
|
||||
captured["stopped"] = document.proxy_port
|
||||
return {
|
||||
"status": "stopped",
|
||||
"pid": None,
|
||||
"service.upstream_port": None,
|
||||
"last_error": None,
|
||||
}
|
||||
|
||||
def cleanup_runtime_artifacts(self, document: ProxyDocument) -> None:
|
||||
captured["cleaned"] = {
|
||||
"proxy_port": document.proxy_port,
|
||||
"script_path": document.script_path,
|
||||
"config_path": document.config_path,
|
||||
"log_path": document.log_path,
|
||||
}
|
||||
|
||||
async def fake_delete_proxy(proxy_port: int) -> bool:
|
||||
captured["deleted"] = proxy_port
|
||||
return True
|
||||
|
||||
async def fake_delete_health_history(proxy_port: int) -> None:
|
||||
captured["history_deleted"] = proxy_port
|
||||
|
||||
monkeypatch.setattr(
|
||||
proxy_router_module,
|
||||
"get_proxy_by_port",
|
||||
fake_get_proxy_by_port,
|
||||
)
|
||||
monkeypatch.setattr(proxy_router_module, "manager", FakeManager())
|
||||
monkeypatch.setattr(
|
||||
proxy_router_module,
|
||||
"delete_health_history",
|
||||
fake_delete_health_history,
|
||||
)
|
||||
monkeypatch.setattr(proxy_router_module, "delete_proxy", fake_delete_proxy)
|
||||
|
||||
response = client.delete("/proxy/4040")
|
||||
|
||||
assert response.status_code == 204
|
||||
assert captured == {
|
||||
"stopped": 4040,
|
||||
"cleaned": {
|
||||
"proxy_port": 4040,
|
||||
"script_path": "/tmp/theplug/addons/4040.py",
|
||||
"config_path": "/tmp/theplug/conf/4040/config.yaml",
|
||||
"log_path": "/tmp/theplug/logs/4040.log",
|
||||
},
|
||||
"deleted": 4040,
|
||||
"history_deleted": 4040,
|
||||
}
|
||||
|
||||
|
||||
def test_proxy_health_runs_immediate_check_and_publishes(client, monkeypatch: object):
|
||||
proxy = make_proxy_document(proxy_port=4040)
|
||||
captured: dict[str, object] = {}
|
||||
snapshot = make_health_snapshot(4040)
|
||||
|
||||
async def fake_get_proxy_by_port(proxy_port: int) -> ProxyDocument | None:
|
||||
assert proxy_port == 4040
|
||||
return proxy
|
||||
|
||||
async def fake_check_proxy(document: ProxyDocument):
|
||||
captured["checked"] = document.proxy_port
|
||||
return snapshot
|
||||
|
||||
async def fake_record_health_sample(document):
|
||||
captured["recorded"] = document
|
||||
return document
|
||||
|
||||
class FakeHealthMonitor:
|
||||
async def publish(self, proxy_port: int, document: HealthSnapshot) -> None:
|
||||
captured["published"] = (proxy_port, document)
|
||||
|
||||
monkeypatch.setattr(
|
||||
proxy_router_module,
|
||||
"get_proxy_by_port",
|
||||
fake_get_proxy_by_port,
|
||||
)
|
||||
monkeypatch.setattr(proxy_router_module, "check_proxy", fake_check_proxy)
|
||||
monkeypatch.setattr(
|
||||
proxy_router_module,
|
||||
"record_health_sample",
|
||||
fake_record_health_sample,
|
||||
)
|
||||
monkeypatch.setattr(proxy_router_module, "health_monitor", FakeHealthMonitor())
|
||||
|
||||
response = client.get("/proxy/4040/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["overall"] == "healthy"
|
||||
assert captured["checked"] == 4040
|
||||
assert captured["recorded"] == snapshot
|
||||
assert captured["published"] == (4040, snapshot)
|
||||
|
||||
|
||||
def test_proxy_health_history_returns_latest_and_history(client, monkeypatch: object):
|
||||
proxy = make_proxy_document(proxy_port=4040)
|
||||
timestamp = datetime.now(UTC)
|
||||
|
||||
async def fake_get_proxy_by_port(proxy_port: int) -> ProxyDocument | None:
|
||||
assert proxy_port == 4040
|
||||
return proxy
|
||||
|
||||
async def fake_get_latest_health_sample(proxy_port: int):
|
||||
assert proxy_port == 4040
|
||||
return make_health_snapshot(4040, timestamp=timestamp)
|
||||
|
||||
async def fake_list_health_history(proxy_port: int, limit: int = 40):
|
||||
assert proxy_port == 4040
|
||||
assert limit == 40
|
||||
return [make_health_snapshot(4040, timestamp=timestamp)]
|
||||
|
||||
monkeypatch.setattr(
|
||||
proxy_router_module,
|
||||
"get_proxy_by_port",
|
||||
fake_get_proxy_by_port,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
proxy_router_module,
|
||||
"get_latest_health_sample",
|
||||
fake_get_latest_health_sample,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
proxy_router_module,
|
||||
"list_health_history",
|
||||
fake_list_health_history,
|
||||
)
|
||||
|
||||
response = client.get("/proxy/4040/health/history")
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["proxy_port"] == 4040
|
||||
assert body["latest"]["overall"] == "healthy"
|
||||
assert len(body["history"]) == 1
|
||||
|
||||
|
||||
def test_proxy_health_history_clamps_requested_limit(client, monkeypatch: object):
|
||||
proxy = make_proxy_document(proxy_port=4040)
|
||||
captured: dict[str, int] = {}
|
||||
|
||||
async def fake_get_proxy_by_port(proxy_port: int) -> ProxyDocument | None:
|
||||
assert proxy_port == 4040
|
||||
return proxy
|
||||
|
||||
async def fake_get_latest_health_sample(_proxy_port: int) -> HealthSnapshot | None:
|
||||
return None
|
||||
|
||||
async def fake_list_health_history(proxy_port: int, limit: int = 40):
|
||||
assert proxy_port == 4040
|
||||
captured["limit"] = limit
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(
|
||||
proxy_router_module,
|
||||
"get_proxy_by_port",
|
||||
fake_get_proxy_by_port,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
proxy_router_module,
|
||||
"get_latest_health_sample",
|
||||
fake_get_latest_health_sample,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
proxy_router_module,
|
||||
"list_health_history",
|
||||
fake_list_health_history,
|
||||
)
|
||||
|
||||
response = client.get("/proxy/4040/health/history", params={"limit": 9999})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert captured["limit"] == 200
|
||||
|
||||
|
||||
def test_proxy_health_socket_streams_latest_and_updates(client, monkeypatch: object):
|
||||
proxy = make_proxy_document(proxy_port=4040)
|
||||
timestamp = datetime.now(UTC)
|
||||
follow_up = make_health_snapshot(4040, overall="unhealthy")
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_authenticate(_: object) -> None:
|
||||
return None
|
||||
|
||||
async def fake_get_proxy_by_port(proxy_port: int) -> ProxyDocument | None:
|
||||
assert proxy_port == 4040
|
||||
return proxy
|
||||
|
||||
async def fake_get_latest_health_sample(proxy_port: int):
|
||||
assert proxy_port == 4040
|
||||
return make_health_snapshot(4040, timestamp=timestamp)
|
||||
|
||||
class FakeHealthMonitor:
|
||||
def subscribe(self, proxy_port: int):
|
||||
assert proxy_port == 4040
|
||||
queue = asyncio.Queue()
|
||||
queue.put_nowait(follow_up)
|
||||
return queue
|
||||
|
||||
def unsubscribe(self, proxy_port: int, _queue) -> None:
|
||||
assert proxy_port == 4040
|
||||
captured["unsubscribed"] = True
|
||||
|
||||
monkeypatch.setattr(
|
||||
proxy_router_module,
|
||||
"authenticate_proxy_socket",
|
||||
fake_authenticate,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
proxy_router_module,
|
||||
"get_proxy_by_port",
|
||||
fake_get_proxy_by_port,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
proxy_router_module,
|
||||
"get_latest_health_sample",
|
||||
fake_get_latest_health_sample,
|
||||
)
|
||||
monkeypatch.setattr(proxy_router_module, "health_monitor", FakeHealthMonitor())
|
||||
|
||||
with client.websocket_connect("/proxy/4040/health/ws?token=test-token") as socket:
|
||||
first = socket.receive_json()
|
||||
assert first["overall"] == "healthy"
|
||||
second = socket.receive_json()
|
||||
assert second["overall"] == "unhealthy"
|
||||
|
||||
assert captured["unsubscribed"] is True
|
||||
|
||||
|
||||
def test_stop_proxy_updates_runtime_state(client, monkeypatch: object):
|
||||
proxy = make_proxy_document(proxy_port=4040).model_copy(
|
||||
update={"status": "running", "pid": 1234}
|
||||
@@ -515,37 +863,46 @@ def test_stop_proxy_updates_runtime_state(client, monkeypatch: object):
|
||||
assert response.json()["status"] == "stopped"
|
||||
|
||||
|
||||
def test_list_mitmproxy_examples_returns_builtins_and_custom(client, monkeypatch):
|
||||
async def fake_list_custom_examples():
|
||||
def test_list_mitmproxy_templates_returns_builtins_and_custom(client, monkeypatch):
|
||||
async def fake_list_custom_templates():
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(
|
||||
proxy_router_module,
|
||||
"list_custom_examples",
|
||||
fake_list_custom_examples,
|
||||
"list_custom_templates",
|
||||
fake_list_custom_templates,
|
||||
)
|
||||
|
||||
response = client.get("/proxy/mitmproxy/examples")
|
||||
response = client.get("/proxy/mitmproxy/templates")
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert len(body) == len(BUILTIN_EXAMPLES)
|
||||
assert len(body) == len(BUILTIN_TEMPLATES)
|
||||
assert body[0]["built_in"] is True
|
||||
assert body[0]["protocol"] in {"http", "tcp"}
|
||||
assert isinstance(body[0]["tags"], list)
|
||||
assert body[0]["action"] in {"block", "rewrite"}
|
||||
|
||||
|
||||
def test_create_mitmproxy_example_validates_and_persists(client, monkeypatch):
|
||||
def test_create_mitmproxy_template_validates_and_persists(client, monkeypatch):
|
||||
captured = {}
|
||||
|
||||
async def fake_validate_proxy_code(code: str) -> None:
|
||||
captured["validated"] = code
|
||||
|
||||
async def fake_create_custom_example(payload):
|
||||
async def fake_create_custom_template(payload):
|
||||
captured["payload"] = payload
|
||||
return {
|
||||
"id": payload.id,
|
||||
"name": payload.name,
|
||||
"description": payload.description,
|
||||
"category": payload.category,
|
||||
"protocol": payload.protocol,
|
||||
"family": payload.family,
|
||||
"tags": payload.tags,
|
||||
"action": payload.action,
|
||||
"summary": payload.summary,
|
||||
"behavior": payload.behavior,
|
||||
"code": payload.code,
|
||||
"source_url": payload.source_url,
|
||||
"built_in": False,
|
||||
@@ -558,21 +915,28 @@ def test_create_mitmproxy_example_validates_and_persists(client, monkeypatch):
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
proxy_router_module,
|
||||
"create_custom_example",
|
||||
fake_create_custom_example,
|
||||
"create_custom_template",
|
||||
fake_create_custom_template,
|
||||
)
|
||||
|
||||
response = client.post(
|
||||
"/proxy/mitmproxy/examples",
|
||||
"/proxy/mitmproxy/templates",
|
||||
json={
|
||||
"id": "custom-header",
|
||||
"name": "Custom header",
|
||||
"description": "Adds a custom response header",
|
||||
"category": "http",
|
||||
"protocol": "http",
|
||||
"family": "custom",
|
||||
"tags": ["observe"],
|
||||
"action": "custom",
|
||||
"summary": "Saved custom template.",
|
||||
"behavior": "Applies the current script as-is.",
|
||||
"code": "def response(flow):\n pass\n",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
assert response.json()["id"] == "custom-header"
|
||||
assert response.json()["protocol"] == "http"
|
||||
assert captured["validated"] == "def response(flow):\n pass\n"
|
||||
|
||||
Reference in New Issue
Block a user