Files
AstrBot/tests/unit/test_network_utils.py
LIghtJUNction 169a856a3a test: mass-coverage push — ~1000+ new unit tests
Coverage improvements:
- agent/tool/core: 150+ tests (FunctionTool, ToolSet, ToolSchema, run_context, messages, registry)
- provider: 152 tests (entities, Provider ABC, register, manager)
- star module: 58 tests (Context, PluginManager, StarHandler)
- platform: 71 tests (Platform ABC, manager, MessageSession)
- pipeline: 130 tests (process/respond stages, scheduler, bootstrap, conversation_mgr)
- kb+skills: 96 tests (mgr, helpers, chunkers, skill_manager)
- config+builtins: 92 tests (astrbot_config, config_mgr, default, LTM, star main)
- cron+db+event_bus: 89 tests (cron events/manager, PO models, BaseDatabase, EventBus)
- dashboard routes: expanded smoke tests for all route modules
2026-04-29 08:40:26 +08:00

234 lines
8.5 KiB
Python

"""Unit tests for astrbot.core.utils.network_utils.
Expands upon the existing 2 monkeypatch-based tests with comprehensive
coverage of is_connection_error, log_connection_failure, and
create_proxy_client edge cases.
"""
import os
import ssl
from unittest.mock import MagicMock, patch
import httpx
import pytest
from astrbot.core.utils import network_utils as network_utils_module
from astrbot.core.utils.network_utils import (
create_proxy_client,
is_connection_error,
log_connection_failure,
)
# ---------------------------------------------------------------------------
# Existing tests (preserved verbatim)
# ---------------------------------------------------------------------------
def test_create_proxy_client_reuses_shared_ssl_context(
monkeypatch: pytest.MonkeyPatch,
):
captured_calls: list[dict] = []
headers = {"X-Test-Header": "value"}
class _FakeAsyncClient:
def __init__(self, **kwargs):
captured_calls.append(kwargs)
monkeypatch.setattr(network_utils_module.httpx, "AsyncClient", _FakeAsyncClient)
network_utils_module.create_proxy_client("OpenAI")
network_utils_module.create_proxy_client("OpenAI", proxy="http://127.0.0.1:7890")
network_utils_module.create_proxy_client("OpenAI", headers=headers)
network_utils_module.create_proxy_client("OpenAI", proxy="")
assert len(captured_calls) == 4
assert "proxy" not in captured_calls[0]
assert captured_calls[1]["proxy"] == "http://127.0.0.1:7890"
assert captured_calls[2]["headers"] is headers
assert "proxy" not in captured_calls[3]
assert isinstance(captured_calls[0]["verify"], ssl.SSLContext)
assert captured_calls[0]["verify"] is captured_calls[1]["verify"]
assert captured_calls[1]["verify"] is captured_calls[2]["verify"]
assert captured_calls[2]["verify"] is captured_calls[3]["verify"]
def test_create_proxy_client_allows_verify_override(
monkeypatch: pytest.MonkeyPatch,
):
captured_calls: list[dict] = []
custom_verify = ssl.create_default_context()
class _FakeAsyncClient:
def __init__(self, **kwargs):
captured_calls.append(kwargs)
monkeypatch.setattr(network_utils_module.httpx, "AsyncClient", _FakeAsyncClient)
network_utils_module.create_proxy_client("OpenAI", verify=custom_verify)
assert len(captured_calls) == 1
assert captured_calls[0]["verify"] is custom_verify
# ---------------------------------------------------------------------------
# is_connection_error
# ---------------------------------------------------------------------------
class TestIsConnectionError:
def test_httpx_connect_error(self):
assert is_connection_error(httpx.ConnectError("refused")) is True
def test_httpx_timeout_errors(self):
for exc_cls in (
httpx.ConnectTimeout,
httpx.ReadTimeout,
httpx.WriteTimeout,
httpx.PoolTimeout,
):
assert is_connection_error(exc_cls("timeout")) is True
def test_httpx_network_errors(self):
for exc_cls in (httpx.NetworkError, httpx.ProxyError, httpx.RequestError):
assert is_connection_error(exc_cls("err")) is True
def test_builtin_network_exceptions(self):
for exc in (ConnectionError("conn"), TimeoutError("timeout"), OSError("os")):
assert is_connection_error(exc) is True
def test_non_network_exceptions_return_false(self):
for exc in (
ValueError("v"),
TypeError("t"),
RuntimeError("r"),
BaseException(),
):
assert is_connection_error(exc) is False
def test_httpx_non_network_errors_return_false(self):
request = httpx.Request("GET", "http://example.com")
response = httpx.Response(200, request=request)
for exc in (
httpx.HTTPStatusError("err", request=request, response=response),
httpx.InvalidURL("invalid"),
httpx.CookieConflict("cookies"),
):
assert is_connection_error(exc) is False
def test_cause_chain_unwraps_to_network_error(self):
inner = httpx.ConnectError("inner")
outer = ValueError("wrapper")
outer.__cause__ = inner
assert is_connection_error(outer) is True
def test_cause_chain_no_network_error(self):
inner = ValueError("inner")
outer = RuntimeError("outer")
outer.__cause__ = inner
assert is_connection_error(outer) is False
def test_self_cause_does_not_infinite_loop(self):
exc = ValueError("self")
exc.__cause__ = exc
assert is_connection_error(exc) is False
# ---------------------------------------------------------------------------
# log_connection_failure
# ---------------------------------------------------------------------------
class TestLogConnectionFailure:
@patch("astrbot.core.utils.network_utils.logger.error")
def test_with_explicit_proxy(self, mock_log_error: MagicMock):
log_connection_failure("GPT", Exception("fail"), proxy="http://proxy:8080")
mock_log_error.assert_called_once()
msg = mock_log_error.call_args[0][0]
assert "GPT" in msg
assert "http://proxy:8080" in msg
assert "fail" in msg
@patch("astrbot.core.utils.network_utils.logger.error")
def test_without_proxy_falls_to_simple_message(self, mock_log_error: MagicMock):
log_connection_failure("GPT", Exception("fail"))
mock_log_error.assert_called_once()
msg = mock_log_error.call_args[0][0]
assert "网络连接失败" in msg
assert "GPT" in msg
@patch("astrbot.core.utils.network_utils.logger.error")
@patch.dict(os.environ, {"http_proxy": "http://env-proxy:3128"}, clear=False)
def test_falls_back_to_env_http_proxy(
self, mock_log_error: MagicMock
):
log_connection_failure("GPT", Exception("fail"))
mock_log_error.assert_called_once()
msg = mock_log_error.call_args[0][0]
assert "http://env-proxy:3128" in msg
@patch("astrbot.core.utils.network_utils.logger.error")
@patch.dict(os.environ, {"https_proxy": "https://secure-proxy:8443"}, clear=False)
def test_falls_back_to_env_https_proxy(
self, mock_log_error: MagicMock
):
log_connection_failure("GPT", Exception("fail"))
mock_log_error.assert_called_once()
msg = mock_log_error.call_args[0][0]
assert "https://secure-proxy:8443" in msg
@patch("astrbot.core.utils.network_utils.logger.error")
def test_empty_proxy_string_uses_no_proxy_message(
self, mock_log_error: MagicMock
):
"""Empty-string proxy should not trigger the proxy-specific log line."""
with patch.dict(os.environ, {"http_proxy": "", "https_proxy": ""}):
log_connection_failure("GPT", Exception("fail"), proxy="")
mock_log_error.assert_called_once()
msg = mock_log_error.call_args[0][0]
assert "代理" not in msg
@patch("astrbot.core.utils.network_utils.logger.error")
def test_error_type_name_in_message(self, mock_log_error: MagicMock):
log_connection_failure(
"GPT", httpx.ConnectTimeout("upstream timed out")
)
mock_log_error.assert_called_once()
msg = mock_log_error.call_args[0][0]
assert "ConnectTimeout" in msg
# ---------------------------------------------------------------------------
# create_proxy_client
# ---------------------------------------------------------------------------
class TestCreateProxyClientIntegration:
"""Integration-style smoke tests against the real httpx.AsyncClient."""
def test_returns_async_client_without_proxy(self):
client = create_proxy_client("Test")
assert isinstance(client, httpx.AsyncClient)
client.aclose()
def test_returns_async_client_with_proxy(self):
client = create_proxy_client("Test", proxy="http://127.0.0.1:7890")
assert isinstance(client, httpx.AsyncClient)
client.aclose()
def test_with_custom_headers(self):
headers = {"Authorization": "Bearer token"}
client = create_proxy_client("Test", headers=headers)
assert isinstance(client, httpx.AsyncClient)
client.aclose()
def test_with_explicit_ssl_context(self):
ctx = ssl.create_default_context()
client = create_proxy_client("Test", verify=ctx)
assert isinstance(client, httpx.AsyncClient)
client.aclose()
def test_with_verify_disabled(self):
client = create_proxy_client("Test", verify=False)
assert isinstance(client, httpx.AsyncClient)
client.aclose()