mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-31 12:24:39 +08:00
195 lines
8.2 KiB
Python
195 lines
8.2 KiB
Python
"""
|
||
下载保存目录支持目录别名:路径优先、别名兜底、错误文案与链路集成。
|
||
"""
|
||
from pathlib import Path
|
||
from unittest.mock import patch
|
||
|
||
import pytest
|
||
|
||
from app.application.directory import (
|
||
DirectoryHelper,
|
||
specified_download_save_path,
|
||
validate_download_save_path,
|
||
)
|
||
from app.chain.download.subtitle import DownloadSubtitleOwner
|
||
from app.domain.context import MediaInfo
|
||
from app.schemas.system import TransferDirectoryConf
|
||
from app.schemas.types import MediaType
|
||
|
||
|
||
def _download_dirs() -> list[TransferDirectoryConf]:
|
||
"""
|
||
按 priority 排好序的下载目录配置,覆盖本地、远端、重名、路径型别名和空名称("" 与 None)。
|
||
"""
|
||
return [
|
||
TransferDirectoryConf(name="Movies", priority=0, storage="local", download_path="/downloads/en"),
|
||
TransferDirectoryConf(name="可心影视库", priority=1, storage="local", download_path="/downloads/movies"),
|
||
TransferDirectoryConf(name="动漫远程库", priority=2, storage="rclone", download_path="/media/anime"),
|
||
TransferDirectoryConf(name="/downloads/movies", priority=3, storage="local", download_path="/downloads/other"),
|
||
TransferDirectoryConf(name="/legacy/movies", priority=4, storage="local", download_path="/downloads/legacy"),
|
||
TransferDirectoryConf(name="重名目录", priority=5, storage="local", download_path="/downloads/dup-a"),
|
||
TransferDirectoryConf(name="重名目录", priority=6, storage="local", download_path="/downloads/dup-b"),
|
||
TransferDirectoryConf(name="", priority=7, storage="local", download_path="/downloads/unnamed"),
|
||
TransferDirectoryConf(name=None, priority=8, storage="local", download_path="/downloads/noname"),
|
||
]
|
||
|
||
|
||
@pytest.fixture
|
||
def download_dirs():
|
||
"""
|
||
用固定目录配置替换 DirectoryHelper.get_download_dirs。
|
||
"""
|
||
with patch.object(DirectoryHelper, "get_download_dirs", return_value=_download_dirs()):
|
||
yield
|
||
|
||
|
||
@pytest.mark.usefixtures("download_dirs")
|
||
def test_alias_resolves_to_local_root():
|
||
"""
|
||
本地目录别名解析为该目录根路径。
|
||
"""
|
||
assert validate_download_save_path("可心影视库") == "/downloads/movies"
|
||
|
||
|
||
@pytest.mark.usefixtures("download_dirs")
|
||
def test_alias_resolves_to_remote_root_uri():
|
||
"""
|
||
远端目录别名解析为带存储前缀的根 URI,与 /download/paths 的 save_path 一致。
|
||
"""
|
||
assert validate_download_save_path("动漫远程库") == "rclone:/media/anime"
|
||
|
||
|
||
@pytest.mark.usefixtures("download_dirs")
|
||
def test_configured_path_wins_over_same_named_alias():
|
||
"""
|
||
输入既是合法配置路径又是另一目录的别名时,路径语义优先。
|
||
"""
|
||
assert validate_download_save_path("/downloads/movies") == "/downloads/movies"
|
||
|
||
|
||
@pytest.mark.usefixtures("download_dirs")
|
||
def test_path_like_alias_resolves_when_path_is_not_configured():
|
||
"""
|
||
路径形态的别名在路径校验失败后仍按别名解析。
|
||
"""
|
||
assert validate_download_save_path("/legacy/movies") == "/downloads/legacy"
|
||
|
||
|
||
@pytest.mark.usefixtures("download_dirs")
|
||
def test_duplicate_alias_uses_priority_order():
|
||
"""
|
||
重名目录沿用 get_download_dirs 的 priority 顺序取第一个。
|
||
"""
|
||
assert validate_download_save_path("重名目录") == "/downloads/dup-a"
|
||
|
||
|
||
def test_alias_skips_invalid_directory_roots():
|
||
"""
|
||
同名目录中根路径无效的配置被跳过;只剩无效配置时按别名不存在报错。
|
||
"""
|
||
invalid = TransferDirectoryConf(name="X", priority=1, storage="local", download_path="relative/path")
|
||
valid = TransferDirectoryConf(name="X", priority=2, storage="local", download_path="/downloads/x")
|
||
with patch.object(DirectoryHelper, "get_download_dirs", return_value=[invalid, valid]):
|
||
assert validate_download_save_path("X") == "/downloads/x"
|
||
with patch.object(DirectoryHelper, "get_download_dirs", return_value=[invalid]):
|
||
with pytest.raises(ValueError, match="未找到名为「X」的下载目录"):
|
||
validate_download_save_path("X")
|
||
|
||
|
||
@pytest.mark.usefixtures("download_dirs")
|
||
def test_alias_is_trimmed_and_case_sensitive():
|
||
"""
|
||
别名匹配裁掉首尾空白,但不忽略大小写。
|
||
"""
|
||
assert validate_download_save_path(" Movies ") == "/downloads/en"
|
||
with pytest.raises(ValueError, match="未找到名为「movies」的下载目录"):
|
||
validate_download_save_path("movies")
|
||
|
||
|
||
@pytest.mark.usefixtures("download_dirs")
|
||
def test_unknown_alias_reports_missing_directory():
|
||
"""
|
||
不像路径的未知输入给出明确的目录不存在文案。
|
||
"""
|
||
with pytest.raises(ValueError, match="未找到名为「不存在的目录」的下载目录"):
|
||
validate_download_save_path("不存在的目录")
|
||
|
||
|
||
@pytest.mark.usefixtures("download_dirs")
|
||
@pytest.mark.parametrize("value", ["", " ", None])
|
||
def test_blank_value_keeps_empty_path_error(value):
|
||
"""
|
||
空值不参与别名匹配,即使存在 name 为空的目录配置。
|
||
"""
|
||
with pytest.raises(ValueError, match="保存路径不能为空"):
|
||
validate_download_save_path(value)
|
||
|
||
|
||
@pytest.mark.usefixtures("download_dirs")
|
||
@pytest.mark.parametrize(
|
||
("value", "message"),
|
||
[
|
||
("foo/bar", "保存路径必须是绝对路径"),
|
||
("/downloads/../etc", "保存路径不能包含上级目录"),
|
||
("/not/configured", "保存路径不在允许的下载目录范围内"),
|
||
("rclone:/nowhere", "保存路径不在允许的下载目录范围内"),
|
||
("rclone:anime", "保存路径必须是绝对路径"),
|
||
("C:Movies", "保存路径必须是 Windows 绝对路径"),
|
||
],
|
||
)
|
||
def test_path_like_input_keeps_original_error(value, message):
|
||
"""
|
||
带路径特征的输入在别名也不命中时保留原始路径校验文案。
|
||
"""
|
||
with pytest.raises(ValueError, match=message):
|
||
validate_download_save_path(value)
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("storage", "root", "expected_storage"),
|
||
[("local", "/downloads/tv", "local"), ("rclone", "/media/tv", "rclone"), ("local", "D:/Downloads", "local")],
|
||
)
|
||
def test_resolve_media_download_dir_appends_type_folder_for_alias(storage, root, expected_storage):
|
||
"""
|
||
别名命中后链路按该目录的类型子目录规则追加路径。
|
||
"""
|
||
dirs = [
|
||
TransferDirectoryConf(
|
||
name="剧集库", priority=1, storage=storage, download_path=root, download_type_folder=True,
|
||
),
|
||
]
|
||
media = MediaInfo(type=MediaType.TV, title="测试剧集")
|
||
with patch.object(DirectoryHelper, "get_download_dirs", return_value=dirs):
|
||
result = DownloadSubtitleOwner._resolve_media_download_dir(media, save_path="剧集库")
|
||
assert result == (expected_storage, Path(root) / MediaType.TV.value, "")
|
||
|
||
|
||
def test_resolve_media_download_dir_surfaces_unknown_alias_error():
|
||
"""
|
||
未知别名的错误文案沿链路返回给调用方。
|
||
"""
|
||
dirs = [TransferDirectoryConf(name="剧集库", priority=1, storage="local", download_path="/downloads/tv")]
|
||
media = MediaInfo(type=MediaType.TV, title="测试剧集")
|
||
with patch.object(DirectoryHelper, "get_download_dirs", return_value=dirs):
|
||
result = DownloadSubtitleOwner._resolve_media_download_dir(media, save_path="不存在的库")
|
||
assert result == (None, None, "未找到名为「不存在的库」的下载目录")
|
||
|
||
|
||
@pytest.mark.parametrize(("value", "expected"), [(None, None), ("", None), (" ", None), ("剧集库", "剧集库")])
|
||
def test_specified_download_save_path_treats_blank_as_unspecified(value, expected):
|
||
"""
|
||
空白保存目录视为未指定,非空值原样交给后续校验。
|
||
"""
|
||
assert specified_download_save_path(value) == expected
|
||
|
||
|
||
def test_resolve_media_download_dir_uses_default_dir_for_blank_save_path():
|
||
"""
|
||
空字符串保存目录走媒体默认下载目录,而不是报“保存路径不能为空”。
|
||
"""
|
||
default_dir = TransferDirectoryConf(name="剧集库", priority=1, storage="local", download_path="/downloads/tv")
|
||
media = MediaInfo(type=MediaType.TV, title="测试剧集")
|
||
with patch.object(DirectoryHelper, "get_dir", return_value=default_dir):
|
||
result = DownloadSubtitleOwner._resolve_media_download_dir(media, save_path="")
|
||
assert result == ("local", Path("/downloads/tv"), "")
|