7ff2e6bc4e
Parallel to sync_show. Calls TMDBClient.get_movie_info, combines the TmdbMovieInfo with the on-disk MovieRelease loaded via DotAlfredMovieReleaseRepository.load_by_tmdb_id, and upserts into DotAlfredMovieLibraryIndex. Policy mirrors sync_show with two adaptations specific to movies: * placeholder signature is name == metadata.path (auto-heal writes them equal — the schema requires name to be non-empty so we can't use name == "" as the spec originally suggested), * when the per-movie sidecar is gone but the index entry remains, sync warns and returns the existing entry unchanged (no upsert possible without a release: index.upsert requires folder/imdb_id from the MovieRelease itself). Raises MovieNotFoundInLibrary when neither index nor sidecar carry tmdb_id.
223 lines
7.4 KiB
Python
223 lines
7.4 KiB
Python
"""Tests for ``sync_movie`` — TMDB sync orchestrator for movies."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import timedelta
|
|
|
|
import pytest
|
|
|
|
from alfred.application.exceptions import MovieNotFoundInLibrary
|
|
from alfred.application.movies.sync import sync_movie
|
|
from alfred.domain.shared.value_objects import TmdbId
|
|
from alfred.infrastructure.api.tmdb.dto import TmdbMovieInfo
|
|
from alfred.infrastructure.persistence.dot_alfred.v2.repository import (
|
|
DotAlfredMovieLibraryIndex,
|
|
DotAlfredMovieReleaseRepository,
|
|
)
|
|
|
|
|
|
class _StubTMDBClient:
|
|
"""Minimal stand-in implementing only ``get_movie_info``."""
|
|
|
|
def __init__(self, info: TmdbMovieInfo) -> None:
|
|
self._info = info
|
|
self.calls: list[int] = []
|
|
|
|
def get_movie_info(self, tmdb_id: int) -> TmdbMovieInfo:
|
|
self.calls.append(tmdb_id)
|
|
return self._info
|
|
|
|
|
|
@pytest.fixture
|
|
def tmdb_info_fresh() -> TmdbMovieInfo:
|
|
"""TMDB payload returned by the stub — distinct from any placeholder."""
|
|
return TmdbMovieInfo(
|
|
tmdb_id=27205,
|
|
imdb_id="tt1375666",
|
|
title="Inception",
|
|
release_year=2010,
|
|
)
|
|
|
|
|
|
def _make_index_and_repos(movie_library):
|
|
repo = DotAlfredMovieReleaseRepository(movie_library)
|
|
index = DotAlfredMovieLibraryIndex(movie_library, release_repo=repo)
|
|
return repo, index
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# Happy paths
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TestSyncMovieRefreshesPlaceholder:
|
|
def test_placeholder_entry_refreshes_regardless_of_ttl(
|
|
self, movie_library, inception_release, tmdb_info_fresh, now_utc
|
|
):
|
|
repo, index = _make_index_and_repos(movie_library)
|
|
repo.save(inception_release)
|
|
client = _StubTMDBClient(tmdb_info_fresh)
|
|
|
|
result = sync_movie(
|
|
movie_library,
|
|
tmdb_id=TmdbId(27205),
|
|
index=index,
|
|
release_repo=repo,
|
|
tmdb_client=client,
|
|
ttl_days=365,
|
|
now=lambda: now_utc,
|
|
)
|
|
assert client.calls == [27205]
|
|
assert result.name == "Inception"
|
|
assert result.release_year == 2010
|
|
|
|
|
|
class TestSyncMovieTTLGate:
|
|
def test_fresh_entry_within_ttl_is_noop(
|
|
self, movie_library, inception_release, tmdb_info_fresh, now_utc
|
|
):
|
|
repo, index = _make_index_and_repos(movie_library)
|
|
repo.save(inception_release)
|
|
client = _StubTMDBClient(tmdb_info_fresh)
|
|
sync_movie(
|
|
movie_library,
|
|
tmdb_id=TmdbId(27205),
|
|
index=index,
|
|
release_repo=repo,
|
|
tmdb_client=client,
|
|
ttl_days=14,
|
|
now=lambda: now_utc,
|
|
)
|
|
first = len(client.calls)
|
|
later = now_utc + timedelta(days=3)
|
|
result = sync_movie(
|
|
movie_library,
|
|
tmdb_id=TmdbId(27205),
|
|
index=index,
|
|
release_repo=repo,
|
|
tmdb_client=client,
|
|
ttl_days=14,
|
|
now=lambda: later,
|
|
)
|
|
assert len(client.calls) == first
|
|
assert result.name == "Inception"
|
|
|
|
def test_stale_entry_past_ttl_refreshes(
|
|
self, movie_library, inception_release, tmdb_info_fresh, now_utc
|
|
):
|
|
repo, index = _make_index_and_repos(movie_library)
|
|
repo.save(inception_release)
|
|
client = _StubTMDBClient(tmdb_info_fresh)
|
|
sync_movie(
|
|
movie_library,
|
|
tmdb_id=TmdbId(27205),
|
|
index=index,
|
|
release_repo=repo,
|
|
tmdb_client=client,
|
|
ttl_days=14,
|
|
now=lambda: now_utc,
|
|
)
|
|
first = len(client.calls)
|
|
later = now_utc + timedelta(days=20)
|
|
sync_movie(
|
|
movie_library,
|
|
tmdb_id=TmdbId(27205),
|
|
index=index,
|
|
release_repo=repo,
|
|
tmdb_client=client,
|
|
ttl_days=14,
|
|
now=lambda: later,
|
|
)
|
|
assert len(client.calls) == first + 1
|
|
|
|
|
|
class TestSyncMovieForce:
|
|
def test_force_bypasses_ttl(
|
|
self, movie_library, inception_release, tmdb_info_fresh, now_utc
|
|
):
|
|
repo, index = _make_index_and_repos(movie_library)
|
|
repo.save(inception_release)
|
|
client = _StubTMDBClient(tmdb_info_fresh)
|
|
sync_movie(
|
|
movie_library,
|
|
tmdb_id=TmdbId(27205),
|
|
index=index,
|
|
release_repo=repo,
|
|
tmdb_client=client,
|
|
ttl_days=14,
|
|
now=lambda: now_utc,
|
|
)
|
|
first = len(client.calls)
|
|
sync_movie(
|
|
movie_library,
|
|
tmdb_id=TmdbId(27205),
|
|
index=index,
|
|
release_repo=repo,
|
|
tmdb_client=client,
|
|
ttl_days=14,
|
|
now=lambda: now_utc,
|
|
force=True,
|
|
)
|
|
assert len(client.calls) == first + 1
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# Degenerate paths
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TestSyncMovieMissingSidecar:
|
|
def test_indexed_movie_without_sidecar_logs_and_returns_existing(
|
|
self, movie_library, inception_release, tmdb_info_fresh, now_utc, caplog
|
|
):
|
|
repo, index = _make_index_and_repos(movie_library)
|
|
repo.save(inception_release)
|
|
client = _StubTMDBClient(tmdb_info_fresh)
|
|
sync_movie(
|
|
movie_library,
|
|
tmdb_id=TmdbId(27205),
|
|
index=index,
|
|
release_repo=repo,
|
|
tmdb_client=client,
|
|
ttl_days=14,
|
|
now=lambda: now_utc,
|
|
)
|
|
|
|
# Wipe the per-movie sidecar.
|
|
(movie_library / inception_release.folder / ".alfred").unlink()
|
|
|
|
with caplog.at_level(logging.WARNING):
|
|
result = sync_movie(
|
|
movie_library,
|
|
tmdb_id=TmdbId(27205),
|
|
index=index,
|
|
release_repo=repo,
|
|
tmdb_client=client,
|
|
ttl_days=14,
|
|
now=lambda: now_utc,
|
|
force=True,
|
|
)
|
|
# Existing index entry returned unchanged — sync can't upsert
|
|
# without a release to source folder/imdb_id from.
|
|
assert result.name == "Inception"
|
|
assert any("per-movie sidecar missing" in r.message for r in caplog.records)
|
|
|
|
|
|
class TestSyncMovieNotFound:
|
|
def test_no_index_entry_no_sidecar_raises(
|
|
self, movie_library, tmdb_info_fresh, now_utc
|
|
):
|
|
repo, index = _make_index_and_repos(movie_library)
|
|
client = _StubTMDBClient(tmdb_info_fresh)
|
|
with pytest.raises(MovieNotFoundInLibrary, match="27205"):
|
|
sync_movie(
|
|
movie_library,
|
|
tmdb_id=TmdbId(27205),
|
|
index=index,
|
|
release_repo=repo,
|
|
tmdb_client=client,
|
|
ttl_days=14,
|
|
now=lambda: now_utc,
|
|
)
|