"""统一出站代理策略的单元测试。""" from __future__ import annotations import asyncio import ast from pathlib import Path from app.shared.config import Settings from app.shared.proxy import httpx_client_options, playwright_launch_proxy from app.trading.worker import client as worker_client from app.trading.worker.client import GatewayClient PROJECT_ROOT = Path(__file__).resolve().parent.parent def _settings(**overrides) -> Settings: return Settings(_env_file=None, **overrides) def test_disabled_proxy_does_not_inherit_host_environment() -> None: settings = _settings() assert httpx_client_options(settings) == {"trust_env": False} assert playwright_launch_proxy(settings) is None def test_authenticated_proxy_encodes_httpx_credentials() -> None: settings = _settings( proxy_server="http://proxy.example:8080", proxy_username="user@example.com", proxy_password="pa:ss /#", proxy_bypass="localhost,.internal.example", ) assert httpx_client_options(settings) == { "trust_env": False, "proxy": "http://user%40example.com:pa%3Ass%20%2F%23@proxy.example:8080", } assert playwright_launch_proxy(settings) == { "server": "http://proxy.example:8080", "username": "user@example.com", "password": "pa:ss /#", "bypass": "localhost,.internal.example", } def test_internal_hosts_bypass_httpx_proxy() -> None: settings = _settings( proxy_server="http://proxy.example:8080", proxy_bypass="localhost,.internal.example,*.svc", ) assert settings.proxy_bypasses("http://localhost:31109") assert settings.proxy_bypasses("https://api.internal.example/path") assert settings.proxy_bypasses("https://orders.svc") assert not settings.proxy_bypasses("https://www.rakuten.co.jp/") assert httpx_client_options(settings, target_url="http://localhost:31109") == { "trust_env": False } assert httpx_client_options(settings, target_url="https://www.rakuten.co.jp/") == { "trust_env": False, "proxy": "http://proxy.example:8080", } def test_gateway_client_uses_its_base_url_for_proxy_policy(monkeypatch) -> None: settings = _settings(proxy_server="http://proxy.example:8080") captured: dict[str, object] = {} def fake_httpx_options(actual_settings: Settings, *, target_url: str | None = None) -> dict[str, object]: captured["settings"] = actual_settings captured["target_url"] = target_url return {"trust_env": False} monkeypatch.setattr(worker_client, "httpx_client_options", fake_httpx_options) client = GatewayClient("https://gateway.example", "token", settings=settings) try: assert captured == {"settings": settings, "target_url": "https://gateway.example"} finally: asyncio.run(client.aclose()) def test_every_project_http_client_and_browser_launch_has_proxy_policy() -> None: """新增出站入口时,强制它显式接入统一代理策略。""" missing_httpx_policy: list[Path] = [] missing_browser_proxy: list[Path] = [] for root_name in ("app", "scripts"): for path in (PROJECT_ROOT / root_name).rglob("*.py"): tree = ast.parse(path.read_text(encoding="utf-8")) for node in ast.walk(tree): if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute): continue if ( node.func.attr == "AsyncClient" and isinstance(node.func.value, ast.Name) and node.func.value.id == "httpx" ): uses_policy = any( keyword.arg is None and isinstance(keyword.value, ast.Call) and isinstance(keyword.value.func, ast.Name) and keyword.value.func.id == "httpx_client_options" for keyword in node.keywords ) if not uses_policy: missing_httpx_policy.append(path) if node.func.attr in {"launch", "launch_persistent_context"} and not any( keyword.arg == "proxy" for keyword in node.keywords ): missing_browser_proxy.append(path) assert not missing_httpx_policy assert not missing_browser_proxy