"""配置快照与窄读写端口测试。""" import asyncio from dataclasses import FrozenInstanceError from unittest.mock import MagicMock import pytest from app.application.configuration import ( ApiRuntimeConfig, ChainRuntimeConfig, RuntimeConfiguration, RuntimeSettingsService, SchedulerRuntimeConfig, SystemConfigService, TransferRetryConfig, configure_runtime_configuration, configure_transfer_retry_config, get_api_runtime_config_snapshot, get_transfer_retry_config, ) from app.application.security.userconfig import UserConfigurationService from app.runtime.settings import RuntimeSettingsCompat, configure_runtime_settings_compat class _InlineDatabaseExecutor: """同步执行测试操作,并保留异步应用端口的调用形态。""" async def run(self, operation): """执行并返回操作结果。""" return operation() class _MutableSettings: """记录管理设置服务的读取和更新操作。""" def __init__(self) -> None: """初始化一组可变测试设置。""" self.VALUE = "before" def model_dump(self, *, include=None, exclude=None): """按白名单返回设置字典。""" values = {"VALUE": self.VALUE, "SECRET": "hidden"} if include is not None: values = {key: value for key, value in values.items() if key in include} if exclude is not None: values = {key: value for key, value in values.items() if key not in exclude} return values def update_settings(self, env): """批量更新并返回逐项结果。""" for key, value in env.items(): setattr(self, key, value) return {key: (True, "") for key in env} def update_setting(self, key, value): """更新单个设置。""" setattr(self, key, value) return True, "" def test_runtime_settings_service_hides_mutable_settings_implementation() -> None: """管理 API 通过窄服务读取和修改设置,不依赖全局 Settings 类型。""" settings = _MutableSettings() service = RuntimeSettingsService(settings) assert service.contains("VALUE") assert service.get("VALUE") == "before" assert service.snapshot(include={"VALUE"}) == {"VALUE": "before"} assert service.update("VALUE", "after") == (True, "") assert service.update_many({"VALUE": "final"}) == {"VALUE": (True, "")} assert service.get("VALUE") == "final" def test_runtime_settings_compat_delegates_to_concrete_service_backend() -> None: """兼容 Settings 代理委托到真实设置对象时不会在 model_dump 中递归。""" service = RuntimeSettingsService(_MutableSettings()) configure_runtime_settings_compat(service) assert RuntimeSettingsCompat().model_dump(include={"VALUE"}) == { "VALUE": "before" } def test_system_config_service_supports_separate_reader_and_writer() -> None: """应用服务可以分别注入只读与写入适配器。""" reader = MagicMock() reader.get.return_value = "old" writer = MagicMock() writer.set.return_value = True writer.delete.return_value = True service = SystemConfigService( reader=reader, writer=writer, async_executor=_InlineDatabaseExecutor(), ) assert service.get("key") == "old" assert service.set("key", "new") is True assert asyncio.run(service.async_set("key", "new")) is True service.delete("key") assert asyncio.run(service.async_delete("key")) is True reader.get.assert_called_once_with("key") assert writer.set.call_args_list == [ (("key", "new"), {}), (("key", "new"), {}), ] assert writer.delete.call_args_list == [ (("key",), {}), (("key",), {}), ] def test_user_configuration_service_supports_sync_and_async_writes() -> None: """用户配置服务的同步与异步入口执行同一仓储方法。""" repository = MagicMock() repository.set.return_value = True service = UserConfigurationService( repository, async_executor=_InlineDatabaseExecutor(), ) assert service.set("alice", "theme", "dark") is True assert asyncio.run(service.async_set("alice", "theme", "light")) is True assert repository.set.call_args_list == [ ((), {"username": "alice", "key": "theme", "value": "dark"}), ((), {"username": "alice", "key": "theme", "value": "light"}), ] def test_transfer_retry_provider_returns_frozen_snapshot_per_call() -> None: """配置工厂在每次用例入口创建新快照,旧快照不受 reload 后状态影响。""" state = {"value": 2} configure_transfer_retry_config( lambda: TransferRetryConfig(max_failed_retries=state["value"]) ) before_reload = get_transfer_retry_config() state["value"] = 4 after_reload = get_transfer_retry_config() assert before_reload.max_failed_retries == 2 assert after_reload.max_failed_retries == 4 def test_api_runtime_provider_returns_frozen_snapshot_per_request() -> None: """API 每次请求读取新配置,但已取得的快照保持不变。""" state = {"enabled": False} configure_runtime_configuration( RuntimeConfiguration( api=lambda: ApiRuntimeConfig( advanced_mode=False, access_token_expire_minutes=60, btrfs_fsid_dedup=False, ai_agent_enable=state["enabled"], ), scheduler=lambda: SchedulerRuntimeConfig( False, "Asia/Shanghai", 1, False, "", None, None, False, 24, "rss", 30, False, None, None, False, None, False, None, ), chain=lambda: ChainRuntimeConfig(media_extensions=(".mkv",)), ) ) before_reload = get_api_runtime_config_snapshot() state["enabled"] = True after_reload = get_api_runtime_config_snapshot() assert before_reload.ai_agent_enable is False assert after_reload.ai_agent_enable is True def test_chain_runtime_config_is_an_instance_scoped_frozen_snapshot() -> None: """Chain 配置应随实例固定,避免同一业务调用中途读取到 reload 后的新值。""" snapshot = ChainRuntimeConfig( media_extensions=(".mkv",), superuser="root", media_recognize_share=True, resource_url="https://example.test/#/resource", ) assert snapshot.superuser == "root" assert snapshot.media_recognize_share is True assert snapshot.resource_url == "https://example.test/#/resource" with pytest.raises(FrozenInstanceError): snapshot.superuser = "changed" # type: ignore[misc]