mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-28 19:06:36 +08:00
190 lines
6.6 KiB
Python
190 lines
6.6 KiB
Python
"""
|
||
网络应用端口组合根契约测试。
|
||
"""
|
||
|
||
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)
|