"""Tests for the filesystem-backed ``.alfred`` repository.""" from __future__ import annotations import pytest import yaml # Phase 3 (refactor/dot-alfred-v2): v1 repository is intentionally # left in tree as a frozen reference until Phase 4 deletes both v1 # and this test module in one swing. pytest.skip( "v1 dot_alfred repository — replaced in Phase 4", allow_module_level=True, ) from alfred.domain.shared.media import AudioTrack, SubtitleTrack from alfred.domain.shared.value_objects import FilePath, ImdbId from alfred.domain.tv_shows.builders import TVShowBuilder from alfred.domain.tv_shows.entities import Episode from alfred.infrastructure.persistence.dot_alfred import ( DotAlfredTVShowRepository, ShowFolderUnknown, ) from alfred.infrastructure.persistence.dot_alfred.repository import ( SIDECAR_FILENAME, ) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_breaking_bad_episodic(): """Breaking Bad with one EPISODIC season carrying two episodes.""" return ( TVShowBuilder( imdb_id="tt0903747", title="Breaking Bad", tmdb_id=1396, ) .add_episode( Episode( season_number=5, episode_number=1, title="Live Free or Die", file_path=FilePath("Breaking.Bad.S05E01.mkv"), audio_tracks=( AudioTrack( index=0, codec=None, channels=None, channel_layout=None, language="eng", ), ), subtitle_tracks=( SubtitleTrack( index=0, codec=None, language="eng", is_default=False, is_forced=False, ), ), ) ) .add_episode( Episode( season_number=5, episode_number=2, title="Madrigal", file_path=FilePath("Breaking.Bad.S05E02.mkv"), audio_tracks=( AudioTrack( index=0, codec=None, channels=None, channel_layout=None, language="eng", ), ), ) ) .build() ) def _make_foundation_pack(): """Foundation as a PACK season (no episodes in the aggregate).""" return TVShowBuilder( imdb_id="tt0804484", title="Foundation", tmdb_id=84958, ).build() # --------------------------------------------------------------------------- # save + find_by_imdb_id round-trip # --------------------------------------------------------------------------- class TestSaveAndRead: def test_save_then_find_by_imdb_id(self, tmp_path): # Folder must exist before save (the repo never invents one). (tmp_path / "Breaking.Bad").mkdir() repo = DotAlfredTVShowRepository(tmp_path) show = _make_breaking_bad_episodic() repo.save(show) recovered = repo.find_by_imdb_id(ImdbId("tt0903747")) assert recovered is not None assert recovered.imdb_id == show.imdb_id assert recovered.tmdb_id == show.tmdb_id assert recovered.seasons_count == 1 assert recovered.episode_count == 2 def test_find_by_imdb_id_uses_folder_name_as_title(self, tmp_path): (tmp_path / "Breaking.Bad").mkdir() repo = DotAlfredTVShowRepository(tmp_path) repo.save(_make_breaking_bad_episodic()) recovered = repo.find_by_imdb_id(ImdbId("tt0903747")) assert recovered is not None assert recovered.title == "Breaking.Bad" def test_find_returns_none_for_unknown(self, tmp_path): repo = DotAlfredTVShowRepository(tmp_path) assert repo.find_by_imdb_id(ImdbId("tt9999999")) is None def test_find_returns_none_for_cold_folder(self, tmp_path): # Folder exists but has no .alfred — cold scan returns None. (tmp_path / "Breaking.Bad").mkdir() repo = DotAlfredTVShowRepository(tmp_path) assert repo.find_by_imdb_id(ImdbId("tt0903747")) is None def test_pack_season_round_trip(self, tmp_path): (tmp_path / "Foundation").mkdir() repo = DotAlfredTVShowRepository(tmp_path) repo.save(_make_foundation_pack()) recovered = repo.find_by_imdb_id(ImdbId("tt0804484")) assert recovered is not None assert recovered.seasons_count == 0 # --------------------------------------------------------------------------- # find_all # --------------------------------------------------------------------------- class TestFindAll: def test_returns_every_sidecar(self, tmp_path): (tmp_path / "Breaking.Bad").mkdir() (tmp_path / "Foundation").mkdir() repo = DotAlfredTVShowRepository(tmp_path) repo.save(_make_breaking_bad_episodic()) repo.save(_make_foundation_pack()) all_shows = repo.find_all() ids = {str(s.imdb_id) for s in all_shows} assert ids == {"tt0903747", "tt0804484"} def test_skips_folders_without_sidecar(self, tmp_path): (tmp_path / "Breaking.Bad").mkdir() (tmp_path / "ColdFolder").mkdir() repo = DotAlfredTVShowRepository(tmp_path) repo.save(_make_breaking_bad_episodic()) assert len(repo.find_all()) == 1 def test_skips_corrupted_sidecar(self, tmp_path, caplog): cold = tmp_path / "Garbage" cold.mkdir() (cold / SIDECAR_FILENAME).write_text("not: valid: yaml: :{[") repo = DotAlfredTVShowRepository(tmp_path) assert repo.find_all() == [] def test_skips_schema_violation(self, tmp_path): bad = tmp_path / "WrongSchema" bad.mkdir() (bad / SIDECAR_FILENAME).write_text( yaml.safe_dump({"schema_version": 99, "imdb_id": "tt0", "seasons": []}) ) repo = DotAlfredTVShowRepository(tmp_path) assert repo.find_all() == [] def test_empty_library(self, tmp_path): repo = DotAlfredTVShowRepository(tmp_path) assert repo.find_all() == [] # --------------------------------------------------------------------------- # delete + exists # --------------------------------------------------------------------------- class TestDeleteAndExists: def test_exists_after_save(self, tmp_path): (tmp_path / "Breaking.Bad").mkdir() repo = DotAlfredTVShowRepository(tmp_path) repo.save(_make_breaking_bad_episodic()) assert repo.exists(ImdbId("tt0903747")) is True def test_exists_false_before_save(self, tmp_path): repo = DotAlfredTVShowRepository(tmp_path) assert repo.exists(ImdbId("tt0903747")) is False def test_delete_removes_sidecar(self, tmp_path): show_dir = tmp_path / "Breaking.Bad" show_dir.mkdir() repo = DotAlfredTVShowRepository(tmp_path) repo.save(_make_breaking_bad_episodic()) assert (show_dir / SIDECAR_FILENAME).is_file() assert repo.delete(ImdbId("tt0903747")) is True assert not (show_dir / SIDECAR_FILENAME).exists() # The show folder itself stays — the repo only owns the sidecar. assert show_dir.is_dir() def test_delete_returns_false_when_unknown(self, tmp_path): repo = DotAlfredTVShowRepository(tmp_path) assert repo.delete(ImdbId("tt9999999")) is False # --------------------------------------------------------------------------- # save edge cases # --------------------------------------------------------------------------- class TestSaveEdgeCases: def test_save_raises_when_folder_missing(self, tmp_path): repo = DotAlfredTVShowRepository(tmp_path) with pytest.raises(ShowFolderUnknown): repo.save(_make_breaking_bad_episodic()) def test_save_atomic_no_tmp_file_left_behind(self, tmp_path): show_dir = tmp_path / "Breaking.Bad" show_dir.mkdir() repo = DotAlfredTVShowRepository(tmp_path) repo.save(_make_breaking_bad_episodic()) # Only the final .alfred should remain. leftovers = [p.name for p in show_dir.iterdir()] assert leftovers == [SIDECAR_FILENAME] def test_save_overwrites_existing(self, tmp_path): show_dir = tmp_path / "Breaking.Bad" show_dir.mkdir() repo = DotAlfredTVShowRepository(tmp_path) repo.save(_make_breaking_bad_episodic()) # Save a second time with a different aggregate — sidecar must # carry the new content. updated = TVShowBuilder( imdb_id="tt0903747", title="Breaking Bad", tmdb_id=1396, ).build() repo.save(updated) recovered = repo.find_by_imdb_id(ImdbId("tt0903747")) assert recovered is not None assert recovered.seasons_count == 0 # the second save dropped the season def test_save_finds_folder_via_get_folder_name(self, tmp_path): # show.get_folder_name() returns "Breaking.Bad" for title "Breaking Bad" (tmp_path / "Breaking.Bad").mkdir() repo = DotAlfredTVShowRepository(tmp_path) # No prior save → cache empty → falls back to get_folder_name(). repo.save(_make_breaking_bad_episodic()) assert (tmp_path / "Breaking.Bad" / SIDECAR_FILENAME).is_file() def test_save_falls_back_to_scan_when_folder_renamed(self, tmp_path): # Create a custom folder name (not the show's default). custom = tmp_path / "Breaking.Bad.1080p" custom.mkdir() repo = DotAlfredTVShowRepository(tmp_path) # Pre-populate the index by reading the sidecar — first we need # to put one there. Write a minimal one by hand. (custom / SIDECAR_FILENAME).write_text( yaml.safe_dump( { "schema_version": 1, "imdb_id": "tt0903747", "tmdb_id": 1396, "seasons": [], } ) ) # find_all primes the folder index. repo.find_all() # Now save through the repo — it must reuse the custom folder. repo.save(_make_breaking_bad_episodic()) assert (custom / SIDECAR_FILENAME).is_file() # Default folder was never created. assert not (tmp_path / "Breaking.Bad").exists() # --------------------------------------------------------------------------- # Sidecar content sanity # --------------------------------------------------------------------------- class TestSidecarContent: def test_written_yaml_matches_schema(self, tmp_path): (tmp_path / "Breaking.Bad").mkdir() repo = DotAlfredTVShowRepository(tmp_path) repo.save(_make_breaking_bad_episodic()) text = (tmp_path / "Breaking.Bad" / SIDECAR_FILENAME).read_text() data = yaml.safe_load(text) assert data["schema_version"] == 1 assert data["imdb_id"] == "tt0903747" assert data["tmdb_id"] == 1396 # One EPISODIC season with two episodes. assert len(data["seasons"]) == 1 season = data["seasons"][0] assert season["number"] == 5 assert len(season["episodes"]) == 2 # Episode paths come from FilePath. assert season["episodes"][0]["path"] == "Breaking.Bad.S05E01.mkv"