Files
MoviePilot/tests/test_download_source_classification.py
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

279 lines
9.0 KiB
Python
Raw Permalink 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.
"""
已有下载任务的资源目录分类测试。
"""
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
import app.api.endpoints.download as download_endpoint
import app.application.download.classification as classification_module
from app.application.directory import DirectoryHelper
from app.application.download.classification import (
DownloadSourceClassificationPlan,
DownloadSourceClassificationService,
resolve_download_source_classification,
)
from app.application.history import DownloadHistorySnapshot
from app.domain.context import MusicInfo
from app.schemas.download import DownloadSourceClassificationRequest
from app.schemas.system import TransferDirectoryConf
from app.schemas.transfer import DownloaderTorrent
from app.schemas.types import MediaType
HASH = "a" * 40
def _history(**overrides: object) -> DownloadHistorySnapshot:
"""
构造含稳定音乐分类快照的下载历史。
"""
values = {
"id": 1,
"path": "/volume1/UT/Musics/Example",
"type": MediaType.MUSIC.value,
"title": "Example",
"media_category": "Album",
}
values.update(overrides)
return DownloadHistorySnapshot(**values)
def test_resolve_source_classification_uses_task_root_and_history_category(monkeypatch):
"""
不论任务内容名称如何,目标都应是配置根目录下的历史分类。
"""
directory = TransferDirectoryConf(
storage="local",
download_path="/volume1/UT/Musics",
download_category_folder=True,
)
helper = MagicMock()
helper.get_download_dir_by_task_path.return_value = directory
helper.has_fixed_category.return_value = False
helper.resolve_media_category.return_value = SimpleNamespace(
usable=True,
path=("Album",),
)
monkeypatch.setattr(
classification_module,
"validate_download_save_path",
lambda value: value,
)
plan = resolve_download_source_classification(
DownloaderTorrent(hash=HASH, save_path="/volume1/UT/Musics"),
_history(),
directory_helper=helper,
)
assert plan == DownloadSourceClassificationPlan(
current_save_path="/volume1/UT/Musics",
target_save_path="/volume1/UT/Musics/Album",
category="Album",
changed=True,
)
def test_task_path_matching_prefers_the_deepest_configured_download_root(monkeypatch):
"""
嵌套资源目录同时命中时必须使用最具体的配置根目录。
"""
generic = TransferDirectoryConf(
storage="local",
download_path="/downloads",
download_category_folder=True,
)
music = TransferDirectoryConf(
storage="local",
download_path="/downloads/music",
download_category_folder=True,
)
helper = DirectoryHelper(classification_resolver=MagicMock())
monkeypatch.setattr(helper, "get_download_dirs", lambda: [generic, music])
matched = helper.get_download_dir_by_task_path(
MusicInfo(title="Example", category="Album"),
"/downloads/music/Album",
)
assert matched is music
def test_source_classification_preview_has_no_downloader_side_effect():
"""
预览只返回当前与目标目录,不调用下载器。
"""
torrent = DownloaderTorrent(hash=HASH, downloader="qb-main", save_path="/downloads")
update_torrent = MagicMock()
service = DownloadSourceClassificationService(
list_torrents=lambda **_kwargs: [torrent],
get_history_by_hash=lambda _hash: _history(),
update_torrent=update_torrent,
resolve_plan=lambda _torrent, _history, _category: DownloadSourceClassificationPlan(
current_save_path="/downloads",
target_save_path="/downloads/Album",
category="Album",
changed=True,
),
)
result = service.plan(hash_value=HASH)
assert result["target_save_path"] == "/downloads/Album"
assert result["executed"] is False
update_torrent.assert_not_called()
def test_source_classification_execute_delegates_location_move_to_downloader():
"""
确认执行时只向下载器提交校验后的目标保存目录。
"""
torrent = DownloaderTorrent(hash=HASH, downloader="qb-main", save_path="/downloads")
update_torrent = MagicMock(return_value={"save_path": True})
service = DownloadSourceClassificationService(
list_torrents=lambda **_kwargs: [torrent],
get_history_by_hash=lambda _hash: _history(),
update_torrent=update_torrent,
resolve_plan=lambda _torrent, _history, _category: DownloadSourceClassificationPlan(
current_save_path="/downloads",
target_save_path="/downloads/Album",
category="Album",
changed=True,
),
)
result = service.plan(hash_value=HASH, execute=True)
assert result["executed"] is True
update_torrent.assert_called_once_with(
hash_string=HASH,
downloader="qb-main",
save_path="/downloads/Album",
)
def test_source_classification_rejects_missing_history_category(monkeypatch):
"""
无分类历史时不应猜测目录或移动做种数据。
"""
directory = TransferDirectoryConf(
storage="local",
download_path="/downloads",
download_category_folder=True,
)
helper = MagicMock()
helper.get_download_dir_by_task_path.return_value = directory
helper.has_fixed_category.return_value = False
helper.resolve_media_category.return_value = SimpleNamespace(
usable=False,
path=(),
)
monkeypatch.setattr(
classification_module,
"validate_download_save_path",
lambda value: value,
)
with pytest.raises(ValueError, match="没有可用的媒体分类"):
resolve_download_source_classification(
DownloaderTorrent(hash=HASH, save_path="/downloads"),
_history(media_category=None),
directory_helper=helper,
)
def test_source_classification_accepts_an_enabled_manual_category(monkeypatch):
"""
旧下载历史无分类时,允许用户精确选择当前策略中的已启用分类。
"""
directory = TransferDirectoryConf(
storage="local",
download_path="/downloads",
download_category_folder=True,
)
helper = MagicMock()
helper.get_download_dir_by_task_path.return_value = directory
helper.has_fixed_category.return_value = False
helper.classification_category_paths.return_value = (("Album",),)
helper.resolve_media_category.return_value = SimpleNamespace(
usable=True,
path=("Album",),
)
monkeypatch.setattr(
classification_module,
"validate_download_save_path",
lambda value: value,
)
plan = resolve_download_source_classification(
DownloaderTorrent(hash=HASH, save_path="/downloads"),
_history(media_category=None),
media_category="Album",
directory_helper=helper,
)
assert plan.target_save_path == "/downloads/Album"
assert plan.category == "Album"
def test_source_classification_rejects_an_unknown_manual_category():
"""
手动分类不能成为绕过活动策略校验的任意子目录。
"""
helper = MagicMock()
helper.classification_category_paths.return_value = (("Album",),)
with pytest.raises(ValueError, match="不存在、已停用或与媒体类型不匹配"):
resolve_download_source_classification(
DownloaderTorrent(hash=HASH, save_path="/downloads"),
_history(media_category=None),
media_category="Unknown",
directory_helper=helper,
)
helper.get_download_dir_by_task_path.assert_not_called()
@pytest.mark.asyncio
async def test_classify_source_endpoint_preserves_preview_mode(monkeypatch):
"""
REST 预览应保留 execute=false,不把查询变成移动。
"""
chain = SimpleNamespace(
list_torrents=MagicMock(),
download_history_repository=SimpleNamespace(get_by_hash=MagicMock()),
update_torrent=MagicMock(),
)
media_chain = object()
organize = MagicMock(
return_value={
"hash": HASH,
"downloader": "qb-main",
"current_save_path": "/downloads",
"target_save_path": "/downloads/Album",
"category": "Album",
"changed": True,
"executed": False,
}
)
payload = DownloadSourceClassificationRequest(
downloader="qb-main",
execute=False,
)
monkeypatch.setattr(download_endpoint, "DownloadChain", lambda: chain)
monkeypatch.setattr(download_endpoint, "MediaChain", lambda: media_chain)
monkeypatch.setattr(download_endpoint, "organize_existing_source", organize)
response = await download_endpoint.classify_source(
HASH,
payload,
SimpleNamespace(),
)
assert response.success is True
assert response.data["target_save_path"] == "/downloads/Album"
assert response.data["executed"] is False
organize.assert_called_once_with(HASH, payload, chain, media_chain)