mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-06-01 07:26:50 +00:00
181 lines
5.1 KiB
Python
181 lines
5.1 KiB
Python
from types import SimpleNamespace
|
||
from unittest.mock import patch
|
||
|
||
from app.modules.filemanager.storages.u115 import U115Pan
|
||
from app.schemas import FileItem
|
||
|
||
|
||
def _target_dir() -> FileItem:
|
||
"""
|
||
构造 115 上传目标目录。
|
||
"""
|
||
return FileItem(
|
||
storage="u115",
|
||
path="/library/Test Show (2026)/Season 1",
|
||
type="dir",
|
||
name="Season 1",
|
||
fileid="100",
|
||
)
|
||
|
||
|
||
def _fake_sha1(*_args, **_kwargs) -> str:
|
||
"""
|
||
返回固定 SHA1,避免单测重复计算文件哈希。
|
||
"""
|
||
return "sha1"
|
||
|
||
|
||
def _fake_request_api(_method, endpoint, *_args, **_kwargs):
|
||
"""
|
||
模拟 115 初始化、凭证和断点续传接口。
|
||
"""
|
||
if endpoint == "/open/upload/init":
|
||
return {
|
||
"state": True,
|
||
"data": {
|
||
"bucket": "bucket",
|
||
"object": "object",
|
||
"callback": {"callback": "callback", "callback_var": "var"},
|
||
"pick_code": "pickcode",
|
||
"status": 1,
|
||
},
|
||
}
|
||
if endpoint == "/open/upload/get_token":
|
||
return {
|
||
"endpoint": "endpoint",
|
||
"AccessKeyId": "access_key_id",
|
||
"AccessKeySecret": "access_key_secret",
|
||
"SecurityToken": "security_token",
|
||
}
|
||
if endpoint == "/open/upload/resume":
|
||
return None
|
||
return None
|
||
|
||
|
||
class _FakeBucket:
|
||
"""
|
||
模拟 OSS 分片上传客户端。
|
||
"""
|
||
|
||
complete_payload = {"state": True}
|
||
|
||
def __init__(self, *_args, **_kwargs):
|
||
"""
|
||
初始化伪造 Bucket。
|
||
"""
|
||
pass
|
||
|
||
def init_multipart_upload(self, *_args, **_kwargs):
|
||
"""
|
||
返回固定 upload_id。
|
||
"""
|
||
return SimpleNamespace(upload_id="upload_id")
|
||
|
||
def upload_part(self, *_args, **_kwargs):
|
||
"""
|
||
返回固定分片 etag。
|
||
"""
|
||
return SimpleNamespace(etag="etag")
|
||
|
||
def complete_multipart_upload(self, *_args, **_kwargs):
|
||
"""
|
||
返回可配置的 115 回调结果。
|
||
"""
|
||
response = SimpleNamespace(json=lambda: self.complete_payload)
|
||
return SimpleNamespace(status=200, resp=SimpleNamespace(response=response))
|
||
|
||
|
||
def _build_storage() -> U115Pan:
|
||
"""
|
||
构造跳过初始化流程的 115 存储实例。
|
||
"""
|
||
storage = object.__new__(U115Pan)
|
||
storage._calc_sha1 = _fake_sha1
|
||
storage.get_item = lambda _path: None
|
||
storage._request_api = _fake_request_api
|
||
return storage
|
||
|
||
|
||
def _upload_with_fakes(storage: U115Pan, target_dir: FileItem, local_file):
|
||
"""
|
||
使用伪造 OSS 和进度回调执行上传。
|
||
"""
|
||
|
||
with patch(
|
||
"app.modules.filemanager.storages.u115.oss2.StsAuth",
|
||
return_value=object(),
|
||
), patch(
|
||
"app.modules.filemanager.storages.u115.oss2.Bucket",
|
||
_FakeBucket,
|
||
), patch(
|
||
"app.modules.filemanager.storages.u115.transfer_process",
|
||
return_value=lambda _progress: None,
|
||
):
|
||
return storage.upload(target_dir, local_file)
|
||
|
||
|
||
def test_upload_returns_target_fileitem_when_uploaded_metadata_is_delayed(tmp_path):
|
||
"""
|
||
115 上传完成后目录索引暂不可见时,应返回可落库的目标文件项。
|
||
"""
|
||
local_file = tmp_path / "Test.Show.S01E01.mkv"
|
||
local_file.write_bytes(b"movie")
|
||
|
||
storage = _build_storage()
|
||
uploaded_item = _upload_with_fakes(storage, _target_dir(), local_file)
|
||
|
||
assert uploaded_item is not None
|
||
assert uploaded_item.storage == "u115"
|
||
assert uploaded_item.path == "/library/Test Show (2026)/Season 1/Test.Show.S01E01.mkv"
|
||
assert uploaded_item.type == "file"
|
||
assert uploaded_item.size == local_file.stat().st_size
|
||
|
||
|
||
def test_upload_returns_none_when_complete_callback_reports_failure(tmp_path):
|
||
"""
|
||
115 完成回调失败时,不应把文件视为上传成功。
|
||
"""
|
||
local_file = tmp_path / "Test.Show.S01E02.mkv"
|
||
local_file.write_bytes(b"movie")
|
||
|
||
storage = _build_storage()
|
||
with patch.object(
|
||
_FakeBucket,
|
||
"complete_payload",
|
||
{"state": False, "message": "callback failed"},
|
||
):
|
||
uploaded_item = _upload_with_fakes(storage, _target_dir(), local_file)
|
||
|
||
assert uploaded_item is None
|
||
|
||
|
||
def test_upload_uses_dynamic_part_size(tmp_path):
|
||
"""
|
||
115 上传应根据文件大小动态控制 OSS 分片大小。
|
||
"""
|
||
local_file = tmp_path / "Test.Show.S01E03.mkv"
|
||
local_file.write_bytes(b"movie")
|
||
|
||
storage = _build_storage()
|
||
with patch(
|
||
"app.modules.filemanager.storages.u115.determine_part_size",
|
||
return_value=local_file.stat().st_size,
|
||
) as determine_part_size_mock:
|
||
uploaded_item = _upload_with_fakes(storage, _target_dir(), local_file)
|
||
|
||
assert uploaded_item is not None
|
||
determine_part_size_mock.assert_called_once_with(
|
||
local_file.stat().st_size, preferred_size=10 * 1024 * 1024
|
||
)
|
||
|
||
|
||
def test_upload_part_size_grows_for_large_files():
|
||
"""
|
||
大文件上传应自动放大分片大小,避免产生过多分片。
|
||
"""
|
||
file_size = int(5.6 * 1024 * 1024 * 1024)
|
||
|
||
part_size = U115Pan._U115Pan__get_upload_part_size(file_size)
|
||
|
||
assert part_size == 64 * 1024 * 1024
|