feat: 为出站请求统一接入 HTTP 代理
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""统一出站代理策略的单元测试。"""
|
||||
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
|
||||
Reference in New Issue
Block a user