Files
MoviePilot/tests/test_network_composition.py
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

190 lines
6.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
网络应用端口组合根契约测试。
"""
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
import httpx2
import pytest
from app.startup.composition import network as network_composition
@pytest.mark.asyncio
async def test_network_test_transport_enforces_safe_http_options(monkeypatch) -> None:
"""
网络探测适配器固定证书校验、超时和手动重定向策略。
"""
captured = {}
response = SimpleNamespace(status_code=200, headers={}, text="ok")
class _RequestUtils:
"""
捕获网络探测适配器使用的 HTTP 参数。
"""
def __init__(self, **kwargs) -> None:
"""
记录通用请求工具的构造参数。
"""
captured["options"] = kwargs
async def get_res(self, url, allow_redirects=True):
"""
记录请求地址和重定向开关并返回固定响应。
"""
captured["url"] = url
captured["allow_redirects"] = allow_redirects
return response
monkeypatch.setattr(network_composition, "AsyncRequestUtils", _RequestUtils)
result = await network_composition._NetworkTestTransportAdapter().get(
"https://example.com/health",
proxy="http://proxy.example:7890",
headers={"Authorization": "Bearer test"},
user_agent="MoviePilot-Test",
)
assert result is response
assert captured == {
"url": "https://example.com/health",
"allow_redirects": False,
"options": {
"proxies": "http://proxy.example:7890",
"headers": {"Authorization": "Bearer test"},
"timeout": 10,
"ua": "MoviePilot-Test",
"verify": True,
"follow_redirects": False,
},
}
def test_image_transport_forwards_sync_request_options(monkeypatch) -> None:
"""
同步图片适配器完整透传请求参数,并单独传递目标 URL。
"""
response = SimpleNamespace(status_code=200, content=b"image", headers={}, json=lambda: {"ok": True})
request = MagicMock()
request.get_res.return_value = response
request_factory = MagicMock(return_value=request)
monkeypatch.setattr(network_composition, "RequestUtils", request_factory)
options = {
"timeout": 15,
"proxies": "http://proxy.example:7890",
"headers": {"Accept": "image/*"},
}
result = network_composition._ImageTransportAdapter().get(
"https://images.example/poster.jpg",
options=options,
)
assert result.status_code == 200 and result.content == b"image"
assert result.json() == {"ok": True}
request_factory.assert_called_once_with(**options)
request.get_res.assert_called_once_with(
url="https://images.example/poster.jpg",
allow_redirects=True,
)
@pytest.mark.asyncio
async def test_image_transport_forwards_async_request_options(monkeypatch) -> None:
"""
异步图片适配器完整透传请求参数,并单独传递目标 URL。
"""
response = SimpleNamespace(status_code=200, content=b"image", headers={}, json=lambda: {"ok": True})
request = MagicMock()
request.get_res = AsyncMock(return_value=response)
request_factory = MagicMock(return_value=request)
monkeypatch.setattr(network_composition, "AsyncRequestUtils", request_factory)
options = {
"timeout": 20,
"proxies": None,
"verify": True,
}
result = await network_composition._ImageTransportAdapter().async_get(
"https://images.example/backdrop.webp",
options=options,
)
assert result.status_code == 200 and result.content == b"image"
assert result.json() == {"ok": True}
request_factory.assert_called_once_with(**options)
request.get_res.assert_awaited_once_with(
url="https://images.example/backdrop.webp",
allow_redirects=True,
)
@pytest.mark.parametrize("response_kind", ["requests", "httpx"])
def test_image_response_preserves_repeated_cookie_headers(response_kind) -> None:
"""
Set-Cookie 中的 Expires 逗号不能当分隔符,多条头必须保持独立。
"""
cookies = ["expired=old; Expires=Thu, 01 Jan 1970 00:00:00 GMT", "session=new; Path=/"]
if response_kind == "httpx":
response = httpx2.Response(200, headers=[("Set-Cookie", value) for value in cookies], json={"ok": True})
else:
response = SimpleNamespace(
status_code=200, content=b"image", headers={"Set-Cookie": ", ".join(cookies)},
raw=SimpleNamespace(headers=SimpleNamespace(getlist=lambda _name: cookies)), json=lambda: {"ok": True},
)
result = network_composition._ImageTransportAdapter._adapt_response(response)
assert result.set_cookie_headers == tuple(cookies)
assert result.json() == {"ok": True}
def test_internal_address_adapter_delegates_to_ip_utility(monkeypatch) -> None:
"""
内部地址判断必须原样委托通用 IP 工具。
"""
probe = MagicMock(return_value=True)
monkeypatch.setattr(network_composition.IpUtils, "is_internal", probe)
assert network_composition._InternalAddressAdapter().is_internal("http://192.168.1.10/image.jpg") is True
probe.assert_called_once_with("http://192.168.1.10/image.jpg")
def test_network_port_composition_does_not_read_settings_early(monkeypatch) -> None:
"""
端口装配只登记延迟 reader,不得在配置发布前读取 RuntimeSettings。
"""
get_runtime_settings = MagicMock(side_effect=AssertionError("装配阶段不得读取 RuntimeSettings"))
configured = {}
monkeypatch.setattr(
network_composition,
"get_runtime_settings",
get_runtime_settings,
)
monkeypatch.setattr(
network_composition,
"configure_network_test_service",
lambda service: configured.update(network=service),
)
monkeypatch.setattr(
network_composition,
"configure_image_ports",
lambda **ports: configured.update(image=ports),
)
monkeypatch.setattr(
network_composition,
"configure_message_ingress_port",
lambda port: configured.update(message=port),
)
network_composition.configure_application_network_ports()
get_runtime_settings.assert_not_called()
assert set(configured) == {"network", "image", "message"}
runtime_settings = MagicMock()
runtime_settings.get.return_value = "configured"
get_runtime_settings.side_effect = None
get_runtime_settings.return_value = runtime_settings
assert configured["network"]._settings("PROXY", None) == "configured"
runtime_settings.get.assert_called_once_with("PROXY", None)