feat: troppa roba davvero

This commit is contained in:
gabdevele
2026-05-07 09:21:45 +02:00 Unverified
parent d977a953cc
commit 53dcdafdb1
73 changed files with 4772 additions and 960 deletions
+3 -3
View File
@@ -1,10 +1,10 @@
import importlib
from datetime import UTC, datetime, timedelta
from core.security import AuthContext, get_auth_context
from models.auth import AccessTokenPayload, AuthUserDocument
from app.core.security import AuthContext, get_auth_context
from app.models.auth import AccessTokenPayload, AuthUserDocument
auth_router_module = importlib.import_module("api.auth.router")
auth_router_module = importlib.import_module("app.api.auth.router")
def test_login_success_returns_bearer_token(client, monkeypatch: object):
+13 -6
View File
@@ -1,15 +1,20 @@
import pytest
from services.editor import python_lsp
from starlette.websockets import WebSocketDisconnect
from app.services.editor import python_lsp
def test_python_editor_session(client, monkeypatch):
session = python_lsp.PythonEditorSession(
root_path=python_lsp.WORKSPACE_ROOT,
file_path=python_lsp.WORKSPACE_ROOT / python_lsp.ENTRY_FILE,
)
monkeypatch.setattr("api.editor.router.python_lsp.lsp_binary", lambda: "/bin/lsp")
monkeypatch.setattr("api.editor.router.python_lsp.get_session", lambda: session)
monkeypatch.setattr(
"app.api.editor.router.python_lsp.lsp_binary", lambda: "/bin/lsp"
)
monkeypatch.setattr(
"app.api.editor.router.python_lsp.get_session", lambda: session
)
response = client.get("/proxy/editor/python/session")
@@ -40,11 +45,13 @@ def test_python_editor_socket_accepts_token(client, monkeypatch):
await websocket.close()
monkeypatch.setattr(
"api.editor.router.authenticate_editor_socket",
"app.api.editor.router.authenticate_editor_socket",
fake_authenticate,
)
monkeypatch.setattr("api.editor.router.python_lsp.lsp_binary", lambda: "/bin/lsp")
monkeypatch.setattr("api.editor.router.python_lsp.bridge", fake_bridge)
monkeypatch.setattr(
"app.api.editor.router.python_lsp.lsp_binary", lambda: "/bin/lsp"
)
monkeypatch.setattr("app.api.editor.router.python_lsp.bridge", fake_bridge)
with client.websocket_connect("/proxy/editor/python/ws?token=test-token") as socket:
assert socket.receive_text().endswith("/proxy_addon.py")
+385 -21
View File
@@ -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"
+110 -8
View File
@@ -1,9 +1,14 @@
import asyncio
import os
from collections.abc import Iterator
from datetime import UTC, datetime, timedelta
from pathlib import Path
from uuid import uuid4
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from motor.motor_asyncio import AsyncIOMotorClient
os.environ.setdefault(
"MONGODB_URI",
@@ -19,7 +24,7 @@ os.environ.setdefault("AUTH_PASSWORD", "admin")
@pytest.fixture
def fake_user():
from models.auth import AuthUserDocument
from app.models.auth import AuthUserDocument
return AuthUserDocument(
username="admin",
@@ -29,8 +34,8 @@ def fake_user():
@pytest.fixture
def auth_context(fake_user):
from core.security import AuthContext
from models.auth import AccessTokenPayload
from app.core.security import AuthContext
from app.models.auth import AccessTokenPayload
exp = int((datetime.now(UTC) + timedelta(minutes=720)).timestamp())
payload = AccessTokenPayload(sub=fake_user.username, jti="test-jti", exp=exp)
@@ -39,11 +44,15 @@ def auth_context(fake_user):
@pytest.fixture
def app(fake_user, auth_context) -> FastAPI:
from api.auth.router import router as auth_router
from api.editor.router import router as editor_router
from api.proxy.router import router as proxy_router
from core.security import AuthContext, get_auth_context, require_authenticated_user
from models.auth import AuthUserDocument
from app.api.auth.router import router as auth_router
from app.api.editor.router import router as editor_router
from app.api.proxy.router import router as proxy_router
from app.core.security import (
AuthContext,
get_auth_context,
require_authenticated_user,
)
from app.models.auth import AuthUserDocument
app_instance = FastAPI()
app_instance.include_router(auth_router)
@@ -67,3 +76,96 @@ def app(fake_user, auth_context) -> FastAPI:
def client(app: FastAPI):
with TestClient(app) as test_client:
yield test_client
async def _mongo_ping(uri: str) -> str | None:
client = AsyncIOMotorClient(uri, serverSelectionTimeoutMS=500)
try:
await client.admin.command("ping")
except Exception as error: # pragma: no cover - exercised via skip path
return str(error)
finally:
client.close()
return None
async def _drop_database(uri: str, database_name: str) -> None:
client = AsyncIOMotorClient(uri, serverSelectionTimeoutMS=500)
try:
await client.drop_database(database_name)
finally:
client.close()
@pytest.fixture
def integration_runtime(tmp_path: Path) -> Iterator[dict[str, str]]:
from app.core.config import settings
from app.services.mitmproxy.manager import manager
mongodb_uri = settings.MONGODB_URI
ping_error = asyncio.run(_mongo_ping(mongodb_uri))
if ping_error is not None:
pytest.skip(f"MongoDB is not available for integration tests: {ping_error}")
db_name = f"theplug_test_{uuid4().hex}"
scan_root = tmp_path / "scan-root"
runtime_root = tmp_path / "runtime"
scan_root.mkdir(parents=True, exist_ok=True)
runtime_root.mkdir(parents=True, exist_ok=True)
original_db_name = settings.MONGODB_DB_NAME
original_scan_root = settings.SERVICE_SCAN_ROOT
original_auth_username = settings.AUTH_USERNAME
original_auth_password = settings.AUTH_PASSWORD
original_manager_work_dir = manager.work_dir
settings.MONGODB_DB_NAME = db_name
settings.SERVICE_SCAN_ROOT = str(scan_root)
settings.AUTH_USERNAME = "integration-admin"
settings.AUTH_PASSWORD = "integration-admin"
manager.work_dir = runtime_root
try:
yield {
"db_name": db_name,
"scan_root": str(scan_root),
"runtime_root": str(runtime_root),
}
finally:
manager.work_dir = original_manager_work_dir
manager.processes.clear()
manager.attached_pids.clear()
settings.MONGODB_DB_NAME = original_db_name
settings.SERVICE_SCAN_ROOT = original_scan_root
settings.AUTH_USERNAME = original_auth_username
settings.AUTH_PASSWORD = original_auth_password
asyncio.run(_drop_database(mongodb_uri, db_name))
@pytest.fixture
def real_app(integration_runtime) -> FastAPI:
from app.main import app as real_app_instance
real_app_instance.dependency_overrides.clear()
return real_app_instance
@pytest.fixture
def real_client(real_app: FastAPI):
with TestClient(real_app) as test_client:
yield test_client
@pytest.fixture
def auth_token(real_client: TestClient) -> str:
response = real_client.post(
"/auth/login",
json={"username": "integration-admin", "password": "integration-admin"},
)
assert response.status_code == 200
return response.json()["access_token"]
@pytest.fixture
def auth_headers(auth_token: str) -> dict[str, str]:
return {"Authorization": f"Bearer {auth_token}"}
@@ -0,0 +1,25 @@
import pytest
pytestmark = [pytest.mark.integration, pytest.mark.api]
def test_login_logout_revokes_real_token(real_client):
login_response = real_client.post(
"/auth/login",
json={"username": "integration-admin", "password": "integration-admin"},
)
assert login_response.status_code == 200
token = login_response.json()["access_token"]
headers = {"Authorization": f"Bearer {token}"}
list_response = real_client.get("/proxy", headers=headers)
assert list_response.status_code == 200
assert list_response.json() == []
logout_response = real_client.post("/auth/logout", headers=headers)
assert logout_response.status_code == 204
revoked_response = real_client.get("/proxy", headers=headers)
assert revoked_response.status_code == 401
assert revoked_response.json()["detail"] == "Could not validate credentials"
@@ -0,0 +1,80 @@
import pytest
pytestmark = [pytest.mark.integration, pytest.mark.api]
VALID_PROXY_CODE = """def request(flow):
return None
"""
def test_proxy_crud_round_trip_uses_real_app_and_mongo(real_client, auth_headers):
create_response = real_client.post(
"/proxy",
headers=auth_headers,
json={
"proxy_port": 49081,
"name": "integration-echo",
"code": VALID_PROXY_CODE,
"protocol": "http",
"service": {
"host": "127.0.0.1",
"port": 49082,
},
"healthcheck": {
"enabled": True,
"path": "/healthz",
"benchmark_enabled": True,
},
},
)
assert create_response.status_code == 201
created = create_response.json()
assert created["proxy_port"] == 49081
assert created["name"] == "integration-echo"
assert created["healthcheck"]["benchmark_enabled"] is True
list_response = real_client.get("/proxy", headers=auth_headers)
assert list_response.status_code == 200
assert [item["proxy_port"] for item in list_response.json()] == [49081]
get_response = real_client.get("/proxy/49081", headers=auth_headers)
assert get_response.status_code == 200
assert get_response.json()["service"]["port"] == 49082
update_response = real_client.put(
"/proxy/49081",
headers=auth_headers,
json={
"name": "integration-echo-updated",
"code": VALID_PROXY_CODE.replace("None", '"updated"'),
"healthcheck": {
"enabled": True,
"path": "/ready",
"benchmark_enabled": False,
},
},
)
assert update_response.status_code == 200
updated = update_response.json()
assert updated["name"] == "integration-echo-updated"
assert updated["code"].endswith('return "updated"\n')
assert updated["healthcheck"]["path"] == "/ready"
assert updated["healthcheck"]["benchmark_enabled"] is False
history_response = real_client.get(
"/proxy/49081/health/history",
headers=auth_headers,
)
assert history_response.status_code == 200
history_payload = history_response.json()
assert history_payload["proxy_port"] == 49081
assert history_payload["latest"] is None
assert history_payload["history"] == []
delete_response = real_client.delete("/proxy/49081", headers=auth_headers)
assert delete_response.status_code == 204
missing_response = real_client.get("/proxy/49081", headers=auth_headers)
assert missing_response.status_code == 404
@@ -1,6 +1,8 @@
import asyncio
from pathlib import Path
from integrations.docker.compose import get_all_services_in_dir
from app.integrations.docker.compose import get_all_services_in_dir
from app.integrations.docker.editor import change_port
def test_get_all_services_in_dir_scans_immediate_children(tmp_path: Path):
@@ -48,3 +50,137 @@ services:
services = get_all_services_in_dir(root)
assert services == []
def test_get_all_services_in_dir_returns_empty_when_root_is_inaccessible(
tmp_path: Path,
monkeypatch,
):
root = tmp_path / "workspace"
root.mkdir()
original_iterdir = Path.iterdir
def fake_iterdir(self: Path):
if self == root:
raise PermissionError("denied")
return original_iterdir(self)
monkeypatch.setattr(Path, "iterdir", fake_iterdir)
assert get_all_services_in_dir(root) == []
def test_get_all_services_in_dir_skips_inaccessible_child_folder(
tmp_path: Path,
monkeypatch,
):
root = tmp_path / "workspace"
allowed = root / "allowed"
blocked = root / "blocked"
allowed.mkdir(parents=True)
blocked.mkdir(parents=True)
(allowed / "docker-compose.yml").write_text(
"""
services:
echo:
image: hashicorp/http-echo:1.0
ports:
- "9081:5678"
""".strip(),
encoding="utf-8",
)
original_is_dir = Path.is_dir
def fake_is_dir(self: Path):
if self == blocked:
raise PermissionError("denied")
return original_is_dir(self)
monkeypatch.setattr(Path, "is_dir", fake_is_dir)
services = get_all_services_in_dir(root)
assert services == [
{
"name": "echo",
"ports": [9081],
"bindings": [{"published": 9081, "host": None}],
"folder": allowed,
}
]
def test_change_port_restores_original_compose_when_reload_fails(
tmp_path: Path,
monkeypatch,
):
folder = tmp_path / "workspace"
folder.mkdir()
compose_path = folder / "docker-compose.yml"
original = """
services:
echo:
image: hashicorp/http-echo:1.0
ports:
- "9081:5678"
""".strip()
compose_path.write_text(original, encoding="utf-8")
async def fake_run_docker_command(
_file: Path,
_command: list[str],
) -> dict | None:
return {"status": "error", "reason": "boom"}
monkeypatch.setattr(
"app.integrations.docker.editor.run_docker_command",
fake_run_docker_command,
)
changed = asyncio.run(change_port("echo", folder, 9081, 54001))
assert changed is False
assert compose_path.read_text(encoding="utf-8") == original
assert (folder / "docker-compose.bak.yml").exists() is False
def test_change_port_restores_original_compose_when_reload_raises(
tmp_path: Path,
monkeypatch,
):
folder = tmp_path / "workspace"
folder.mkdir()
compose_path = folder / "docker-compose.yml"
original = """
services:
echo:
image: hashicorp/http-echo:1.0
ports:
- "127.0.0.1:54001:5678"
""".strip()
compose_path.write_text(original, encoding="utf-8")
async def fake_run_docker_command(
_file: Path,
_command: list[str],
) -> dict | None:
raise RuntimeError("docker unavailable")
monkeypatch.setattr(
"app.integrations.docker.editor.run_docker_command",
fake_run_docker_command,
)
changed = asyncio.run(
change_port(
"echo",
folder,
54001,
9081,
host_to=None,
rewrite_host=True,
)
)
assert changed is False
assert compose_path.read_text(encoding="utf-8") == original
@@ -1,4 +1,4 @@
from integrations.docker.ports import _get_service_binding, _replace_port_in_mapping
from app.integrations.docker.ports import _get_service_binding, _replace_port_in_mapping
def test_get_service_binding_includes_loopback_hosts():
+39 -5
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
from services.editor import python_lsp
from app.services.editor import python_lsp
def test_encode_message():
+3 -3
View File
@@ -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):
+13
View File
@@ -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}"
)
+2 -2
View File
@@ -1,10 +1,10 @@
from datetime import UTC, datetime
from core.security import create_access_token, decode_access_token
from app.core.security import create_access_token, decode_access_token
def test_create_access_token_uses_twelve_hour_default_window(monkeypatch):
monkeypatch.setattr("core.security.settings.JWT_EXPIRE_MINUTES", 720)
monkeypatch.setattr("app.core.security.settings.JWT_EXPIRE_MINUTES", 720)
issued_at = datetime.now(UTC)
token = create_access_token(subject="admin")