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

289 lines
11 KiB
Python

"""
验证 composition owner 对称撤销其发布的 lifespan provider。
"""
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
from fastapi import HTTPException
from app.adapters.external import server as server_provider
from app.adapters.web.security import access as access_provider
from app.application import agenttask as task_provider
from app.application import image as image_provider
from app.application import network as network_provider
from app.application import outbox as outbox_provider
from app.application.chain import context as chain_provider
from app.application.messaging import chat as chat_provider
from app.application.messaging import ingress as ingress_provider
from app.application.security import auth as auth_provider
from app.application.security import passkey as passkey_provider
from app.application.security import user as user_provider
from app.startup.composition import agent as agent_composition
from app.startup.composition import chain as chain_composition
from app.startup.composition import network as network_composition
from app.startup.composition import outbox as outbox_composition
from app.startup.composition import security as security_composition
from app.startup.composition import server as server_composition
def test_agent_owner_reset_is_symmetric_and_idempotent(monkeypatch) -> None:
"""
Agent owner 撤销数据上下文、任务执行与两类会话服务。
"""
monkeypatch.setattr(chat_provider, "_configured_agent_chat_service", None)
monkeypatch.setattr(chat_provider, "_configured_agent_chat_persistence", None)
monkeypatch.setattr(task_provider, "_service", None)
data_state: dict[str, object] = {}
composition = SimpleNamespace(
data=SimpleNamespace(chat=object(), chat_persistence=object()),
execution=object(),
)
agent_composition.publish_agent_services(
composition,
data_context_registrar=lambda data: data_state.update(current=data),
)
assert chat_provider.get_configured_agent_chat_service() is composition.data.chat
assert (
chat_provider.get_configured_agent_chat_persistence()
is composition.data.chat_persistence
)
assert task_provider.get_agent_task_execution_service() is composition.execution
assert data_state["current"] is composition.data
def reset_data() -> None:
"""
撤销测试中的 Agent 数据上下文。
"""
data_state.pop("current", None)
agent_composition.reset_agent_services(data_context_resetter=reset_data)
agent_composition.reset_agent_services(data_context_resetter=reset_data)
assert data_state == {}
for getter in (
chat_provider.get_configured_agent_chat_service,
chat_provider.get_configured_agent_chat_persistence,
task_provider.get_agent_task_execution_service,
):
with pytest.raises(RuntimeError):
getter()
def test_security_owner_reset_is_symmetric_and_idempotent(monkeypatch) -> None:
"""
Security owner 撤销身份查询、认证、PassKey 与 Web 访问 provider。
"""
monkeypatch.setattr(user_provider, "_configured_user_id_lookup", None)
monkeypatch.setattr(user_provider, "_configured_user_name_lookup", None)
monkeypatch.setattr(user_provider, "_configured_user_channel_lookup", None)
monkeypatch.setattr(auth_provider, "_configured_auth_service", None)
monkeypatch.setattr(passkey_provider.PasskeyChallengeStore, "_cache", None)
monkeypatch.setattr(passkey_provider, "_configured_passkey_service", None)
monkeypatch.setattr(access_provider, "_superuser_token_payload_provider", None)
monkeypatch.setattr(access_provider, "_token_identity_validator", None)
user_provider.configure_user_lookups(
by_id=lambda _user_id: object(),
by_name=lambda _username: object(),
by_channel=lambda **_bindings: "user",
)
# 仅验证组合根重置时的对象身份,使用无业务行为的占位依赖。
auth_provider.configure_auth_service(object()) # type: ignore[arg-type]
# 仅验证组合根重置时的对象身份,使用无业务行为的占位依赖。
passkey_provider.configure_passkey_challenge_cache(object()) # type: ignore[arg-type]
# 仅验证组合根重置时的对象身份,使用无业务行为的占位依赖。
passkey_provider.configure_passkey_service(object()) # type: ignore[arg-type]
access_provider.set_superuser_token_payload_provider(lambda: object())
access_provider.set_token_identity_validator(lambda _payload: None)
security_composition.reset_security_access()
security_composition.reset_security_access()
security_composition.reset_security_services()
security_composition.reset_security_services()
for getter in (
user_provider.get_configured_user_id_lookup,
user_provider.get_configured_user_name_lookup,
user_provider.get_configured_user_channel_lookup,
auth_provider.get_configured_auth_service,
passkey_provider.get_configured_passkey_service,
passkey_provider.PasskeyChallengeStore._get_cache,
):
with pytest.raises(RuntimeError):
getter()
with pytest.raises(HTTPException) as error:
access_provider._create_superuser_token_payload()
assert error.value.status_code == 503
assert access_provider._token_identity_validator is None
def test_network_owner_reset_is_symmetric_and_idempotent(monkeypatch) -> None:
"""
Network owner 撤销探测、图片和消息回环端口。
"""
monkeypatch.setattr(network_provider, "_configured_network_test_service", None)
monkeypatch.setattr(image_provider, "_image_transport", None)
monkeypatch.setattr(image_provider, "_internal_address", None)
monkeypatch.setattr(ingress_provider, "_message_ingress_port", None)
# 仅验证组合根重置时的对象身份,使用无业务行为的占位依赖。
network_provider.configure_network_test_service(object()) # type: ignore[arg-type]
image_provider.configure_image_ports(
# 仅验证组合根重置时的对象身份,使用无业务行为的占位依赖。
transport=object(), # type: ignore[arg-type]
# 仅验证组合根重置时的对象身份,使用无业务行为的占位依赖。
internal_address=object(), # type: ignore[arg-type]
)
# 仅验证组合根重置时的对象身份,使用无业务行为的占位依赖。
ingress_provider.configure_message_ingress_port(object()) # type: ignore[arg-type]
network_composition.reset_application_network_ports()
network_composition.reset_application_network_ports()
for getter in (
network_provider.get_configured_network_test_service,
image_provider._image_ports_snapshot,
ingress_provider._message_ingress_snapshot,
):
with pytest.raises(RuntimeError):
getter()
def test_outbox_owner_reset_is_symmetric_and_idempotent(monkeypatch) -> None:
"""
Outbox owner 撤销 dispatcher 工厂并恢复显式未配置错误。
"""
monkeypatch.setattr(outbox_provider, "_configured_dispatcher", None)
outbox_provider.configure_outbox_dispatcher(Mock())
outbox_composition.reset_outbox_services()
outbox_composition.reset_outbox_services()
with pytest.raises(RuntimeError, match="Outbox dispatcher 尚未配置"):
outbox_provider.dispatch_pending_outbox()
def test_server_owner_reset_is_symmetric_and_idempotent(monkeypatch) -> None:
"""
Server owner 同时撤销上报与分享服务。
"""
monkeypatch.setattr(server_provider, "_server_report_service", None)
monkeypatch.setattr(server_provider, "_server_sharing_service", None)
server_provider.configure_server_application_services(
report_service=object(),
sharing_service=object(),
)
server_composition.reset_server_services()
server_composition.reset_server_services()
with pytest.raises(RuntimeError):
server_provider.MoviePilotServerHelper._report_service()
with pytest.raises(RuntimeError):
server_provider.MoviePilotServerHelper._sharing_service()
def test_chain_owner_reset_is_symmetric_and_idempotent(monkeypatch) -> None:
"""
Chain owner 撤销无参上下文,并把壁纸来源恢复为空结果。
"""
monkeypatch.setattr(
chain_provider,
"_context_provider",
chain_provider._unconfigured_chain_runtime_context,
)
chain_provider.configure_chain_runtime_context_provider(lambda: object())
image_provider.configure_wallpaper_providers(
tmdb_wallpaper=lambda: "tmdb",
tmdb_wallpapers=lambda _count: ["tmdb"],
mediaserver_wallpaper=lambda: "media",
mediaserver_wallpapers=lambda _count: ["media"],
)
chain_composition.reset_chain_services()
chain_composition.reset_chain_services()
with pytest.raises(RuntimeError, match="Chain 运行上下文尚未由启动组合根配置"):
chain_provider.get_chain_runtime_context()
wallpaper = image_provider.WallpaperHelper()
assert wallpaper.get_tmdb_wallpaper() is None
assert wallpaper.get_tmdb_wallpapers() == []
assert wallpaper.get_mediaserver_wallpaper() is None
assert wallpaper.get_mediaserver_wallpapers() == []
def test_composition_owner_resets_run_in_reverse_publication_order(monkeypatch) -> None:
"""
多 provider owner 必须按发布逆序撤销,避免依赖先于消费者消失。
"""
calls: list[str] = []
for module, names in (
(
agent_composition,
(
"reset_agent_task_execution",
"reset_agent_chat_persistence",
"reset_agent_chat_service",
),
),
(
security_composition,
(
"reset_passkey_service",
"reset_passkey_challenge_cache",
"reset_auth_service",
"reset_user_lookups",
),
),
(
network_composition,
(
"reset_message_ingress_port",
"reset_image_ports",
"reset_network_test_service",
),
),
(
chain_composition,
(
"configure_chain_runtime_context_provider",
"reset_wallpaper_providers",
),
),
):
for name in names:
monkeypatch.setattr(
module,
name,
lambda *args, _name=name, **kwargs: calls.append(_name),
)
agent_composition.reset_agent_services(
data_context_resetter=lambda: calls.append("reset_agent_data_context"),
)
security_composition.reset_security_services()
network_composition.reset_application_network_ports()
chain_composition.reset_chain_services()
assert calls == [
"reset_agent_data_context",
"reset_agent_task_execution",
"reset_agent_chat_persistence",
"reset_agent_chat_service",
"reset_passkey_service",
"reset_passkey_challenge_cache",
"reset_auth_service",
"reset_user_lookups",
"reset_message_ingress_port",
"reset_image_ports",
"reset_network_test_service",
"configure_chain_runtime_context_provider",
"reset_wallpaper_providers",
]