Files
AstrBot/tests/unit/test_network_utils.py
LIghtJUNction 273b6777cf test: fix unit tests after master→dev merge
- Fix test_long_term_memory.py: Image(file=), Provider spec, config keys
- Fix test_star_manager.py: Metric.upload AsyncMock, os.path.exists patch
- Fix test_kb_helper.py: chunk assertion signature, session.begin mock
- Fix test_file_extract.py: match actual code behavior (AsyncClient created before read)
- Fix test_pipeline_process_stage/respond_stage.py: reload stage modules to avoid stale imports from bootstrap tests
- Fix test_provider_register.py: handle pre-existing registry entries
- Fix test_pipeline_scheduler.py: remove blanket xfail, apply per-test xfail
- Mark 23 lifecycle tests xfail due to state machine changes
- Fix scheduler.py: missing break after generator stage stops propagation
- Frontend build verified
2026-05-08 12:17:58 +08:00

237 lines
8.7 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"),
):
instance = exc("msg") if isinstance(exc, type) else exc
assert is_connection_error(instance) is False
def test_httpx_decoding_error_is_treated_as_connection_error(self):
assert is_connection_error(httpx.DecodingError("msg")) is True
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()