Files
MoviePilot/tests/test_storage_manage.py
T
jxxghp 7d0cf76d9c fix(storage): 修复通用管理契约下存储页报未知错误
- usage 动作返回的 StorageUsage pydantic 模型无法透过
  Response[Dict[str, Any]] 开放映射校验导致 500,统一转为 dict
- support_transtype 返回值恢复 transtype 包装结构,与前端期望一致
- 空结果(无用量/无整理方式)恢复成功空结构语义,
  不再因 bool(结果) 被前端客户端当作业务失败弹窗
- dashboard 用量聚合改为 dict 取值;新增响应校验与空结果回归测试
2026-08-16 07:50:29 +08:00

164 lines
6.6 KiB
Python
Raw 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.
"""
网盘存储通用管理契约(storage_manage)守护测试
验证与通知渠道一致的通用模式:
1. 链层 manage_storage 只透明转发存储标识、动作与参数,不做任何存储特定处理
2. 模块按存储标识路由,动作语义与参数解释封闭在模块内
3. 端点层的 ManageRequest 通用请求结构:target + action + params
"""
from types import SimpleNamespace
from typing import Any, Dict
import pytest
from app import schemas
from app.chain.storage import StorageChain
from app.modules.filemanager import FileManagerModule
class _FakeStorageOper:
"""记录管理动作调用情况的假存储实现"""
schema = SimpleNamespace(value="fakestore")
calls = []
def set_config(self, conf):
_FakeStorageOper.calls.append(("set_config", conf))
def reset_config(self):
_FakeStorageOper.calls.append(("reset_config", None))
def usage(self):
return schemas.StorageUsage(total=100, available=40)
def support_transtype(self):
return {"move": True}
def check_login(self, **kwargs):
_FakeStorageOper.calls.append(("check_login", kwargs))
return {"status": True}, None
@pytest.fixture
def module(monkeypatch):
_FakeStorageOper.calls.clear()
module = FileManagerModule()
monkeypatch.setattr(module, "_support_storages", ["fakestore"])
monkeypatch.setattr(module, "_storage_schemas", [_FakeStorageOper])
return module
def test_manage_request_schema():
"""ManageRequest 仅定义目标标识、动作标识与透传参数,无任何特定领域字段。"""
request = schemas.ManageRequest(target="fakestore", action="usage")
assert request.target == "fakestore"
assert request.action == "usage"
assert request.params == {}
def test_storage_chain_forwards_target_action_and_params(monkeypatch):
"""链层按 storage_manage 契约原样透传,不引入存储特定逻辑。"""
captured = {}
def fake_run_module(self, method, **kwargs):
captured.update(method=method, kwargs=kwargs)
return {"success": True, "data": {"total": 100}}
monkeypatch.setattr(StorageChain, "run_module", fake_run_module)
chain = StorageChain.__new__(StorageChain)
result = chain.manage_storage(storage="fakestore", action="usage", extra="value")
assert captured["method"] == "storage_manage"
assert captured["kwargs"] == {"storage": "fakestore", "action": "usage", "extra": "value"}
assert result["success"] is True
def test_storage_chain_reports_missing_module(monkeypatch):
"""无模块实现 storage_manage 时返回统一失败结构。"""
monkeypatch.setattr(StorageChain, "run_module", lambda self, method, **kwargs: None)
chain = StorageChain.__new__(StorageChain)
result = chain.manage_storage(storage="unknown", action="usage")
assert result["success"] is False
assert result["message"]
def test_storage_manage_rejects_unknown_action(module):
"""动作词汇表之外的请求返回统一错误结构。"""
result = module.storage_manage(storage="fakestore", action="not_an_action")
assert result["success"] is False
assert "不支持" in result["message"]
def test_storage_manage_rejects_unknown_storage(module):
"""未注册的存储标识直接返回错误,不进入动作分发。"""
result = module.storage_manage(storage="unknown_store", action="usage")
assert result["success"] is False
assert "不支持的存储类型" in result["message"]
def test_storage_manage_save_config_passes_conf_through(module):
"""save_config 动作将 params.conf 原样交给存储实现持久化。"""
result = module.storage_manage(
storage="fakestore", action="save_config", conf={"token": "abc"}
)
assert result["success"] is True
assert ("set_config", {"token": "abc"}) in _FakeStorageOper.calls
def test_storage_manage_usage_returns_oper_data(module):
"""usage 动作返回纯 dict 用量数据,空结果同样成功。"""
result = module.storage_manage(storage="fakestore", action="usage")
assert result["success"] is True
assert result["data"] == {"total": 100.0, "available": 40.0}
def test_storage_manage_usage_data_passes_open_mapping_response(module):
"""用量数据必须能透过通用响应 Response[Dict[str, Any]] 的开放映射校验。
回归守护:存储实现返回 pydantic 模型时若未转 dict
端点响应校验会直接 500,前端存储页整体报未知错误。
"""
result = module.storage_manage(storage="fakestore", action="usage")
response = schemas.Response[Dict[str, Any]](
success=result["success"], message=result.get("message"), data=result["data"]
)
assert response.data == {"total": 100.0, "available": 40.0}
def test_storage_manage_usage_defaults_when_oper_returns_none(module, monkeypatch):
"""存储实现查不到用量时返回成功的默认空结构而非业务失败。"""
monkeypatch.setattr(_FakeStorageOper, "usage", lambda self: None)
result = module.storage_manage(storage="fakestore", action="usage")
assert result["success"] is True
assert result["data"] == {"total": 0.0, "available": 0.0}
def test_storage_manage_support_transtype(module):
"""support_transtype 动作返回值包装为 transtype,与旧契约结构一致。"""
result = module.storage_manage(storage="fakestore", action="support_transtype")
assert result["success"] is True
assert result["data"] == {"transtype": {"move": True}}
def test_storage_manage_support_transtype_empty_still_succeeds(module, monkeypatch):
"""存储不支持任何整理方式时返回成功空结构,避免前端把空结果当业务失败。"""
monkeypatch.setattr(_FakeStorageOper, "support_transtype", lambda self: {})
result = module.storage_manage(storage="fakestore", action="support_transtype")
assert result["success"] is True
assert result["data"] == {"transtype": {}}
def test_storage_manage_login_action_forwards_params(module):
"""登录类动作透传表单参数并归一化元组返回值。"""
result = module.storage_manage(storage="fakestore", action="check_login", ck="ck1", t="t1")
assert result["success"] is True
assert result["data"] == {"status": True}
assert ("check_login", {"ck": "ck1", "t": "t1"}) in _FakeStorageOper.calls
def test_storage_manage_reports_unsupported_login_action(module):
"""存储实现未提供对应登录方法时返回明确失败信息。"""
result = module.storage_manage(storage="fakestore", action="generate_qrcode")
assert result["success"] is False
assert "不支持" in result["message"]