mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/jxxghp/MoviePilot.git
synced 2026-09-20 08:03:34 +08:00
240 lines
8.3 KiB
Python
240 lines
8.3 KiB
Python
"""订阅稳定分类引用的写入、兼容与运行时覆盖契约测试。"""
|
|
|
|
from collections.abc import Iterator
|
|
|
|
import pytest
|
|
|
|
from app.application.classification.execution import ClassificationExecutionService
|
|
from app.application.classification.reference import (
|
|
ClassificationCategoryResolver,
|
|
configure_classification_category_resolver,
|
|
normalize_classification_reference_payload,
|
|
reset_classification_category_resolver,
|
|
subscription_classification_override,
|
|
)
|
|
from app.domain.context import MediaInfo
|
|
from app.schemas.category import (
|
|
CategoryConfig,
|
|
ClassificationCategory,
|
|
ClassificationPolicy,
|
|
)
|
|
from app.schemas.types import MediaType
|
|
|
|
|
|
class _MutableClassificationRuntime:
|
|
"""为订阅引用测试提供可切换 revision 的活动策略。"""
|
|
|
|
def __init__(self, policy: ClassificationPolicy) -> None:
|
|
"""保存当前活动策略。"""
|
|
self.policy = policy
|
|
|
|
def active_policy(self) -> ClassificationPolicy:
|
|
"""返回当前活动策略。"""
|
|
return self.policy
|
|
|
|
def legacy_config(self) -> CategoryConfig:
|
|
"""返回空 legacy 配置,确保测试只观察新分类体系。"""
|
|
return CategoryConfig()
|
|
|
|
|
|
def _policy(
|
|
*,
|
|
revision: int = 7,
|
|
automatic_path: list[str] | None = None,
|
|
) -> ClassificationPolicy:
|
|
"""构造同时包含有效、禁用和跨媒体类型分类的测试策略。"""
|
|
return ClassificationPolicy(
|
|
revision=revision,
|
|
categories=[
|
|
ClassificationCategory(
|
|
id="movie.auto",
|
|
media_type="电影",
|
|
name="自动电影",
|
|
path=automatic_path or ["电影", "自动"],
|
|
),
|
|
ClassificationCategory(
|
|
id="movie.favorite",
|
|
media_type="电影",
|
|
name="收藏电影",
|
|
path=["收藏", "当前路径"],
|
|
),
|
|
ClassificationCategory(
|
|
id="movie.disabled",
|
|
media_type="电影",
|
|
name="禁用电影",
|
|
path=["电影", "禁用"],
|
|
enabled=False,
|
|
),
|
|
ClassificationCategory(
|
|
id="tv.favorite",
|
|
media_type="电视剧",
|
|
name="收藏剧集",
|
|
path=["剧集", "收藏"],
|
|
),
|
|
],
|
|
fallbacks={"电影": "movie.auto"},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def classification_runtime() -> Iterator[_MutableClassificationRuntime]:
|
|
"""装配测试策略解析器,并在用例结束后恢复全局状态。"""
|
|
runtime = _MutableClassificationRuntime(_policy())
|
|
previous = configure_classification_category_resolver(
|
|
ClassificationCategoryResolver(runtime)
|
|
)
|
|
try:
|
|
yield runtime
|
|
finally:
|
|
reset_classification_category_resolver(previous)
|
|
|
|
|
|
def _movie() -> MediaInfo:
|
|
"""构造具有完整稳定身份、可执行自动分类的电影。"""
|
|
return MediaInfo(
|
|
media_source="douban",
|
|
media_id="1291561",
|
|
type=MediaType.MOVIE,
|
|
title="测试电影",
|
|
)
|
|
|
|
|
|
def test_subscription_reference_prefers_id_and_refreshes_current_path(
|
|
classification_runtime: _MutableClassificationRuntime,
|
|
) -> None:
|
|
"""稳定 ID 必须覆盖客户端旧路径,并保存当前策略中的路径快照。"""
|
|
normalized = normalize_classification_reference_payload(
|
|
{
|
|
"media_category_id": "movie.favorite",
|
|
"media_category": "收藏/旧路径",
|
|
},
|
|
media_type=MediaType.MOVIE,
|
|
)
|
|
|
|
assert classification_runtime.policy.revision == 7
|
|
assert normalized["media_category_id"] == "movie.favorite"
|
|
assert normalized["media_category"] == "收藏/当前路径"
|
|
|
|
|
|
def test_subscription_reference_accepts_legacy_path_only(
|
|
classification_runtime: _MutableClassificationRuntime,
|
|
) -> None:
|
|
"""旧订阅只有 path 时仍应可写入并形成无稳定 ID 的人工覆盖。"""
|
|
normalized = normalize_classification_reference_payload(
|
|
{"media_category": "旧目录/电影"},
|
|
media_type=MediaType.MOVIE,
|
|
)
|
|
override = subscription_classification_override(
|
|
category_id=normalized["media_category_id"],
|
|
path_snapshot=normalized["media_category"],
|
|
media_type=MediaType.MOVIE,
|
|
)
|
|
|
|
assert classification_runtime.policy.revision == 7
|
|
assert normalized["media_category_id"] is None
|
|
assert normalized["media_category"] == "旧目录/电影"
|
|
assert override is not None
|
|
assert override.category_id is None
|
|
assert override.category_path == ["旧目录", "电影"]
|
|
assert override.source == "subscription"
|
|
|
|
|
|
def test_subscription_reference_explicit_null_clears_id_and_path(
|
|
classification_runtime: _MutableClassificationRuntime,
|
|
) -> None:
|
|
"""PATCH 显式清空稳定 ID 时不得遗留旧路径人工覆盖。"""
|
|
normalized = normalize_classification_reference_payload(
|
|
{
|
|
"media_category_id": None,
|
|
"media_category": "不得残留/旧路径",
|
|
},
|
|
media_type=MediaType.MOVIE,
|
|
)
|
|
|
|
assert classification_runtime.policy.revision == 7
|
|
assert normalized["media_category_id"] is None
|
|
assert normalized["media_category"] is None
|
|
|
|
|
|
def test_subscription_without_override_reclassifies_with_current_policy(
|
|
classification_runtime: _MutableClassificationRuntime,
|
|
) -> None:
|
|
"""未设置人工分类的订阅不得锁定创建时路径,应跟随当前策略自动分类。"""
|
|
service = ClassificationExecutionService(classification_runtime)
|
|
created = service.finalize(_movie())
|
|
classification_runtime.policy = _policy(
|
|
revision=8,
|
|
automatic_path=["电影", "新版自动"],
|
|
)
|
|
override = subscription_classification_override(
|
|
category_id=None,
|
|
path_snapshot=None,
|
|
media_type=MediaType.MOVIE,
|
|
)
|
|
|
|
refreshed = service.finalize(
|
|
created,
|
|
effective_override=override,
|
|
refresh=True,
|
|
)
|
|
|
|
assert override is None
|
|
assert created.library_category == "电影/自动"
|
|
assert refreshed.classification is not None
|
|
assert refreshed.classification.policy_revision == 8
|
|
assert refreshed.classification.effective is not None
|
|
assert refreshed.classification.effective.category_id == "movie.auto"
|
|
assert refreshed.library_category == "电影/新版自动"
|
|
|
|
|
|
def test_subscription_override_survives_auxiliary_refresh(
|
|
classification_runtime: _MutableClassificationRuntime,
|
|
) -> None:
|
|
"""订阅人工覆盖装配后,辅助信息触发的再次分类不得覆盖 effective。"""
|
|
service = ClassificationExecutionService(classification_runtime)
|
|
override = subscription_classification_override(
|
|
category_id="movie.favorite",
|
|
path_snapshot="收藏/旧路径",
|
|
media_type=MediaType.MOVIE,
|
|
)
|
|
overridden = service.finalize(_movie(), effective_override=override)
|
|
overridden.vote_average = 9.5
|
|
|
|
refreshed = service.finalize(overridden, refresh=True)
|
|
|
|
assert override is not None
|
|
assert refreshed.classification is not None
|
|
assert refreshed.classification.recommended is not None
|
|
assert refreshed.classification.recommended.category_id == "movie.auto"
|
|
assert refreshed.classification.effective is not None
|
|
assert refreshed.classification.effective.category_id == "movie.favorite"
|
|
assert refreshed.classification.effective.category_path == ["收藏", "当前路径"]
|
|
assert refreshed.classification.effective.source == "subscription"
|
|
assert refreshed.library_category == "收藏/当前路径"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("category_id", "message"),
|
|
[
|
|
("movie.missing", "不存在"),
|
|
("movie.disabled", "禁用"),
|
|
("tv.favorite", "不一致"),
|
|
],
|
|
)
|
|
def test_new_subscription_rejects_invalid_category_id_even_with_legacy_path(
|
|
classification_runtime: _MutableClassificationRuntime,
|
|
category_id: str,
|
|
message: str,
|
|
) -> None:
|
|
"""新写入的无效稳定 ID 不得借合法旧路径降级为可接受引用。"""
|
|
assert classification_runtime.policy.revision == 7
|
|
|
|
with pytest.raises(ValueError, match=message):
|
|
normalize_classification_reference_payload(
|
|
{
|
|
"media_category_id": category_id,
|
|
"media_category": "兼容/旧路径",
|
|
},
|
|
media_type=MediaType.MOVIE,
|
|
)
|