Files
archived-MoviePilot/tests/test_alist_storage.py
2026-05-20 22:28:38 +08:00

267 lines
9.4 KiB
Python

import importlib.util
import sys
import types
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
def _load_alist_module():
module_name = "_test_alist_module"
app_module = types.ModuleType("app")
schemas_module = types.ModuleType("app.schemas")
cache_module = types.ModuleType("app.core.cache")
config_module = types.ModuleType("app.core.config")
log_module = types.ModuleType("app.log")
storages_module = types.ModuleType("app.modules.filemanager.storages")
exception_module = types.ModuleType("app.schemas.exception")
types_module = types.ModuleType("app.schemas.types")
http_module = types.ModuleType("app.utils.http")
singleton_module = types.ModuleType("app.utils.singleton")
url_module = types.ModuleType("app.utils.url")
class _FileItem:
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
class _StorageSchemaValue:
def __init__(self, value):
self.value = value
class _Logger:
def debug(self, *_args, **_kwargs):
pass
def warn(self, *_args, **_kwargs):
pass
def warning(self, *_args, **_kwargs):
pass
def error(self, *_args, **_kwargs):
pass
def critical(self, *_args, **_kwargs):
pass
def info(self, *_args, **_kwargs):
pass
class _StorageBase:
def __init__(self):
pass
def get_conf(self):
return {}
class _OperationInterrupted(Exception):
pass
class _RequestUtils:
def __init__(self, *args, **kwargs):
pass
class _UrlUtils:
@staticmethod
def standardize_base_url(url):
return url.rstrip("/") if url else ""
@staticmethod
def adapt_request_url(base, path):
return f"{base() if callable(base) else base}{path}"
@staticmethod
def quote(path):
return path
def _cached(*_args, **_kwargs):
def decorator(func):
func.cache_clear = lambda: None
return func
return decorator
schemas_module.FileItem = _FileItem
schemas_module.StorageUsage = object
cache_module.cached = _cached
config_module.settings = types.SimpleNamespace(
OPENLIST_SNAPSHOT_CHECK_FOLDER_MODTIME=True,
TEMP_PATH=Path("/tmp"),
)
config_module.global_vars = types.SimpleNamespace(
is_transfer_stopped=lambda *_args, **_kwargs: False
)
log_module.logger = _Logger()
storages_module.StorageBase = _StorageBase
storages_module.transfer_process = lambda *_args, **_kwargs: (lambda *_a, **_k: None)
exception_module.OperationInterrupted = _OperationInterrupted
types_module.StorageSchema = types.SimpleNamespace(Alist=_StorageSchemaValue("alist"))
http_module.RequestUtils = _RequestUtils
singleton_module.WeakSingleton = type
url_module.UrlUtils = _UrlUtils
app_module.schemas = schemas_module
stub_modules = {
"app": app_module,
"app.schemas": schemas_module,
"app.core.cache": cache_module,
"app.core.config": config_module,
"app.log": log_module,
"app.modules.filemanager.storages": storages_module,
"app.schemas.exception": exception_module,
"app.schemas.types": types_module,
"app.utils.http": http_module,
"app.utils.singleton": singleton_module,
"app.utils.url": url_module,
}
for stub_module in stub_modules.values():
stub_module._alist_test_stub = True
alist_path = Path(__file__).resolve().parents[1] / "app" / "modules" / "filemanager" / "storages" / "alist.py"
spec = importlib.util.spec_from_file_location(module_name, alist_path)
module = importlib.util.module_from_spec(spec)
assert spec and spec.loader
with patch.dict(sys.modules, stub_modules):
spec.loader.exec_module(module)
return module
alist_module = _load_alist_module()
Alist = alist_module.Alist
FileItem = alist_module.schemas.FileItem
class _FakeResponse:
def __init__(self, payload: dict, status_code: int = 200):
self._payload = payload
self.status_code = status_code
def json(self):
return self._payload
class AlistStorageTest(unittest.TestCase):
def setUp(self):
self.storage = Alist()
@staticmethod
def _dir_item(path: str = "/"):
return FileItem(storage="alist", type="dir", path=path)
@staticmethod
def _page_payload(start: int, count: int, total: int) -> dict:
return {
"code": 200,
"message": "success",
"data": {
"content": [
{
"name": f"dir-{index}",
"size": 0,
"is_dir": True,
"modified": "2024-05-17T13:47:55.4174917+08:00",
"thumb": "",
}
for index in range(start, start + count)
],
"total": total,
},
}
def test_list_fetches_all_pages_when_per_page_is_default(self):
responses = [
_FakeResponse(self._page_payload(0, 200, 205)),
_FakeResponse(self._page_payload(200, 5, 205)),
]
request_utils = MagicMock()
request_utils.post_res.side_effect = responses
with patch.object(Alist, "get_conf", return_value={"url": "http://openlist.test", "token": "token"}):
with patch.object(alist_module, "RequestUtils", return_value=request_utils):
items = self.storage.list(self._dir_item())
self.assertEqual(205, len(items))
self.assertEqual("/dir-0/", items[0].path)
self.assertEqual("/dir-204/", items[-1].path)
self.assertEqual(2, request_utils.post_res.call_count)
self.assertEqual(1, request_utils.post_res.call_args_list[0].kwargs["json"]["page"])
self.assertEqual(2, request_utils.post_res.call_args_list[1].kwargs["json"]["page"])
def test_list_respects_explicit_per_page_without_auto_paging(self):
request_utils = MagicMock()
request_utils.post_res.return_value = _FakeResponse(self._page_payload(0, 50, 205))
with patch.object(Alist, "get_conf", return_value={"url": "http://openlist.test", "token": "token"}):
with patch.object(alist_module, "RequestUtils", return_value=request_utils):
items = self.storage.list(self._dir_item(), per_page=50)
self.assertEqual(50, len(items))
self.assertEqual(1, request_utils.post_res.call_count)
def test_create_folder_returns_target_when_openlist_metadata_is_delayed(self):
"""
OpenList 创建目录成功但元数据延迟可见时,应返回可用的目标目录项。
"""
request_utils = MagicMock()
request_utils.post_res.return_value = _FakeResponse(
{"code": 200, "message": "success", "data": None}
)
with patch.object(Alist, "get_conf", return_value={"url": "http://openlist.test", "token": "token"}):
with patch.object(self.storage, "_Alist__get_header_with_token", return_value={}):
with patch.object(alist_module, "RequestUtils", return_value=request_utils):
with patch.object(self.storage, "_delay_get_item", return_value=None):
folder = self.storage.create_folder(
self._dir_item("/library/Test Show (2026)"),
"Season 1",
)
self.assertIsNotNone(folder)
self.assertEqual("/library/Test Show (2026)/Season 1/", folder.path)
self.assertEqual("alist", folder.storage)
self.assertEqual("dir", folder.type)
def test_move_item_returns_target_when_openlist_metadata_is_delayed(self):
"""
OpenList 操作成功但目标元数据延迟可见时,应返回可用的目标文件项。
"""
source = FileItem(
storage="alist",
type="file",
path="/downloads/Test.Show.S01E01.mkv",
name="Test.Show.S01E01.mkv",
basename="Test.Show.S01E01",
extension="mkv",
size=1024,
modify_time=1715939275.0,
)
request_utils = MagicMock()
request_utils.post_res.return_value = _FakeResponse(
{"code": 200, "message": "success", "data": None}
)
with patch.object(Alist, "get_conf", return_value={"url": "http://openlist.test", "token": "token"}):
with patch.object(self.storage, "_Alist__get_header_with_token", return_value={}):
with patch.object(alist_module, "RequestUtils", return_value=request_utils):
with patch.object(self.storage, "_delay_get_item", return_value=None):
target = self.storage.move_item(
source,
Path("/library/Test Show (2026)/Season 1"),
"Test.Show.S01E01.mkv",
)
self.assertIsNotNone(target)
self.assertEqual(
"/library/Test Show (2026)/Season 1/Test.Show.S01E01.mkv",
target.path,
)
self.assertEqual("alist", target.storage)
self.assertEqual("file", target.type)
self.assertEqual(1024, target.size)
if __name__ == "__main__":
unittest.main()