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

206 lines
7.9 KiB
Python

"""
手动下载后,订阅检查应把部分入库事实同步到卡片进度。
"""
from contextlib import contextmanager
from dataclasses import replace
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
import app.chain.subscribe.refresh as subscribe_refresh
from app.application.subscription.contract import SubscriptionSnapshot, build_subscribe_meta
from app.application.subscription.mutation import SyncSubscriptionMutationService
from app.chain.download import DownloadChain
from app.chain.subscribe.facade import SubscribeChain
from app.db.adapters.outbox import SqlAlchemyOutboxDispatchStore, SqlAlchemyOutboxStager
from app.db.adapters.subscription import SessionSubscriptionRepository
from app.db.models.outbox import OutboxMessage
from app.db.models.subscribe import Subscribe
from app.db.session import SessionFactory
from app.db.uow import SqlAlchemyUnitOfWork
from app.domain.context import MediaInfo
from app.schemas.mediaserver import ExistMediaInfo
from app.schemas.types import MediaSource, MediaType
@pytest.fixture
def progress_scope(monkeypatch):
"""
保留缺集计算和进度派生,仅隔离媒体库与持久化边界。
"""
subscribe = SubscriptionSnapshot(
id=6966, name="Doctor X", type=MediaType.TV.value,
media_source=MediaSource.TMDB, media_id="6966", season=1,
total_episode=12, lack_episode=11, note=[1], state="R",
last_update="2026-10-01 12:00:00",
)
media = MediaInfo(
title="Doctor X", type=MediaType.TV,
media_source=MediaSource.TMDB, media_id="6966",
seasons={1: list(range(1, 13))},
)
download = object.__new__(DownloadChain)
download._media_exists_with_server_cache = Mock(
return_value=ExistMediaInfo(seasons={1: [1, 2]})
)
monkeypatch.setattr(subscribe_refresh, "DownloadChain", lambda: download)
chain = object.__new__(SubscribeChain)
chain.finish_subscribe_or_not = Mock()
chain._SubscribeChain__apply_episodes_refresh = Mock(return_value=12)
state = SimpleNamespace(subscribe=subscribe, media=media, chain=chain, download=download)
def apply_update(current, fields, **_kwargs):
"""
保存派生快照,供重复检查读取同一持久化状态。
"""
state.subscribe = replace(current, **fields)
return state.subscribe
chain._SubscribeChain__apply_subscribe_update = Mock(side_effect=apply_update)
return state
def _check(scope):
"""
使用当前订阅快照执行真实的现存媒体检查。
"""
return scope.chain.check_and_handle_existing_media(
subscribe=scope.subscribe, meta=build_subscribe_meta(scope.subscribe),
mediainfo=scope.media, mediakey="tmdb:6966",
)
def test_partial_library_updates_stale_card_progress(progress_scope):
"""
前两集已入库且 note 只含第一集时,卡片应从 1/12 校正为 2/12。
"""
for _ in range(2):
completed, missing = _check(progress_scope)
assert not completed
assert missing["tmdb:6966"][1].episodes == list(range(3, 13))
assert progress_scope.subscribe.lack_episode == 10
assert progress_scope.subscribe.note == [1]
assert progress_scope.subscribe.last_update == "2026-10-01 12:00:00"
progress_scope.chain.finish_subscribe_or_not.assert_not_called()
def test_partial_library_respects_start_and_downloaded_facts(progress_scope):
"""
进度复用订阅范围和已下载事实,不重复计数或包含其他季。
"""
progress_scope.subscribe = replace(progress_scope.subscribe, start_episode=3, note=[3, 5])
progress_scope.download._media_exists_with_server_cache.return_value = ExistMediaInfo(
seasons={1: [1, 2, 3, 4, 4], 2: list(range(1, 13))}
)
completed, missing = _check(progress_scope)
assert not completed
assert missing["tmdb:6966"][1].episodes == list(range(6, 13))
assert progress_scope.subscribe.lack_episode == 7
progress_scope.chain.finish_subscribe_or_not.assert_not_called()
def test_failed_missing_query_preserves_progress(progress_scope):
"""
无法取得缺集事实时,不能把未知状态当成全部完成。
"""
progress_scope.chain.resolve_subscribe_missing = Mock(return_value=(False, {}))
completed, missing = _check(progress_scope)
assert not completed
assert missing == {}
assert progress_scope.subscribe.lack_episode == 11
progress_scope.chain._SubscribeChain__apply_subscribe_update.assert_not_called()
progress_scope.chain.finish_subscribe_or_not.assert_not_called()
def test_best_version_check_preserves_quality_progress(progress_scope):
"""
洗版订阅继续按下载质量事实处理,不用部分入库覆盖其进度。
"""
progress_scope.subscribe = replace(
progress_scope.subscribe, best_version=1, episode_priority={"1": 80},
)
completed, missing = _check(progress_scope)
assert not completed
assert missing
assert progress_scope.subscribe.lack_episode == 11
assert progress_scope.subscribe.episode_priority == {"1": 80}
progress_scope.chain._SubscribeChain__apply_subscribe_update.assert_not_called()
progress_scope.chain.finish_subscribe_or_not.assert_not_called()
def test_complete_library_keeps_completion_path(progress_scope):
"""
整季入库仍交给原有完成流程,避免在检查入口重复结算。
"""
progress_scope.download._media_exists_with_server_cache.return_value = ExistMediaInfo(
seasons={1: list(range(1, 13))}
)
completed, missing = _check(progress_scope)
assert completed
assert missing == {}
progress_scope.chain.finish_subscribe_or_not.assert_called_once()
assert progress_scope.chain.finish_subscribe_or_not.call_args.kwargs["force"]
progress_scope.chain._SubscribeChain__apply_subscribe_update.assert_not_called()
def test_partial_progress_persists_without_overwriting_extra_fields(progress_scope, db):
"""
真实订阅写入只更新缺失集数,保留数据库中较新的附加字段。
"""
db.watermark(Subscribe, OutboxMessage)
row = db.add(Subscribe(
name="Doctor X", type=MediaType.TV.value,
media_source=MediaSource.TMDB.value, media_id="6966", season=1,
total_episode=12, lack_episode=11, note=[1, 99],
episode_priority={"1": 80, "99": 90}, current_priority=80,
sites=[1, 2], filter_groups=["WEB-DL"], state="R", best_version=0,
last_update="2026-10-01 12:00:00", manual_total_episode=1,
))
progress_scope.subscribe = replace(progress_scope.subscribe, id=row.id, manual_total_episode=1)
published = []
@contextmanager
def mutation_scope():
"""
用真实短事务和 outbox 保存进度并捕获修改事件。
"""
with SessionFactory() as session:
yield SyncSubscriptionMutationService(
repository=SessionSubscriptionRepository(session),
unit_of_work=SqlAlchemyUnitOfWork(session),
outbox=SqlAlchemyOutboxStager(session),
dispatch_store=SqlAlchemyOutboxDispatchStore(SessionFactory),
publish_modified=published.append,
)
progress_scope.chain.sync_subscription_mutation_scope = mutation_scope
progress_scope.chain._SubscribeChain__apply_subscribe_update = (
SubscribeChain._SubscribeChain__apply_subscribe_update.__get__(progress_scope.chain)
)
completed, _missing = _check(progress_scope)
assert not completed
db.session.expire_all()
stored = db.session.get(Subscribe, row.id)
assert stored.lack_episode == 10
assert stored.note == [1, 99]
assert stored.episode_priority == {"1": 80, "99": 90}
assert stored.current_priority == 80
assert stored.sites == [1, 2]
assert stored.filter_groups == ["WEB-DL"]
assert stored.last_update == "2026-10-01 12:00:00"
assert len(published) == 1
assert published[0]["subscribe_info"]["lack_episode"] == 10
assert published[0]["subscribe_info"]["note"] == [1, 99]