From b423cd8af0d7d1f137fe0ce257df26925ff7437b Mon Sep 17 00:00:00 2001 From: Teagan Glenn Date: Sun, 5 Oct 2025 23:21:24 -0600 Subject: [PATCH 1/2] test: isolate enrichment helper tests --- docker/pyproject.deps.toml | 2 +- pyproject.toml | 2 +- tests/test_enrichment_helpers.py | 205 ++++++++++++++++++++++++++++++ tests/test_loader_unit.py | 212 ------------------------------- uv.lock | 2 +- 5 files changed, 208 insertions(+), 215 deletions(-) create mode 100644 tests/test_enrichment_helpers.py diff --git a/docker/pyproject.deps.toml b/docker/pyproject.deps.toml index eb2f4de..ef7289f 100644 --- a/docker/pyproject.deps.toml +++ b/docker/pyproject.deps.toml @@ -1,6 +1,6 @@ [project] name = "mcp-plex" -version = "0.26.76" +version = "0.26.77" requires-python = ">=3.11,<3.13" dependencies = [ "fastmcp>=2.11.2", diff --git a/pyproject.toml b/pyproject.toml index 491475a..20c3866 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mcp-plex" -version = "0.26.76" +version = "0.26.77" description = "Plex-Oriented Model Context Protocol Server" requires-python = ">=3.11,<3.13" diff --git a/tests/test_enrichment_helpers.py b/tests/test_enrichment_helpers.py new file mode 100644 index 0000000..98830b5 --- /dev/null +++ b/tests/test_enrichment_helpers.py @@ -0,0 +1,205 @@ +import asyncio +import types + +import httpx + +from mcp_plex.loader.pipeline.enrichment import ( + _build_plex_item, + _extract_external_ids, + _fetch_tmdb_episode, + _fetch_tmdb_movie, + _fetch_tmdb_show, + resolve_tmdb_season_number, +) +from mcp_plex.common.types import ( + TMDBSeason, + TMDBShow, +) + + +def test_extract_external_ids(): + guid_objs = [ + types.SimpleNamespace(id="imdb://tt0133093"), + types.SimpleNamespace(id="tmdb://603"), + ] + item = types.SimpleNamespace(guids=guid_objs) + ids = _extract_external_ids(item) + assert ids.imdb == "tt0133093" + assert ids.tmdb == "603" + + +def test_extract_external_ids_missing_values(): + item = types.SimpleNamespace(guids=None) + ids = _extract_external_ids(item) + assert ids.imdb is None + assert ids.tmdb is None + + +def test_build_plex_item_handles_full_metadata(): + guid_objs = [ + types.SimpleNamespace(id="imdb://tt0133093"), + types.SimpleNamespace(id="tmdb://603"), + ] + raw = types.SimpleNamespace( + ratingKey="603", + guid="plex://movie/603", + type="movie", + title="The Matrix", + summary="A hacker discovers the nature of his reality.", + year=1999, + guids=guid_objs, + thumb="matrix.jpg", + art="matrix_art.jpg", + tagline="Welcome to the Real World", + contentRating="R", + directors=[types.SimpleNamespace(id=1, tag="Lana Wachowski", thumb="lana.jpg")], + writers=[types.SimpleNamespace(id=2, tag="Lilly Wachowski", thumb="lilly.jpg")], + actors=[types.SimpleNamespace(id=3, tag="Keanu Reeves", thumb="neo.jpg", role="Neo")], + ) + + item = _build_plex_item(raw) + assert item.rating_key == "603" + assert item.directors[0].tag == "Lana Wachowski" + assert item.actors[0].role == "Neo" + + +def test_build_plex_item_missing_metadata_defaults(): + raw = types.SimpleNamespace(ratingKey="1", guid="g", type="movie", title="T") + item = _build_plex_item(raw) + assert item.directors == [] + assert item.writers == [] + assert item.actors == [] + + +def test_build_plex_item_converts_string_indices(): + raw = types.SimpleNamespace( + ratingKey="1", + guid="g", + type="episode", + title="Episode", + parentIndex="02", + index="03", + ) + + item = _build_plex_item(raw) + assert item.season_number == 2 + assert item.episode_number == 3 + + +def test_fetch_functions_success_and_failure(): + async def tmdb_movie_mock(request): + assert request.headers.get("Authorization") == "Bearer k" + if "good" in str(request.url): + return httpx.Response(200, json={"id": 1, "title": "M"}) + return httpx.Response(404) + + async def tmdb_show_mock(request): + assert request.headers.get("Authorization") == "Bearer k" + if "good" in str(request.url): + return httpx.Response(200, json={"id": 1, "name": "S"}) + return httpx.Response(404) + + async def tmdb_episode_mock(request): + assert request.headers.get("Authorization") == "Bearer k" + if "/tv/1/season/2/episode/3" in str(request.url): + return httpx.Response(200, json={"id": 1, "name": "E"}) + return httpx.Response(404) + + async def main(): + movie_transport = httpx.MockTransport(tmdb_movie_mock) + show_transport = httpx.MockTransport(tmdb_show_mock) + episode_transport = httpx.MockTransport(tmdb_episode_mock) + + async with httpx.AsyncClient(transport=movie_transport) as client: + assert (await _fetch_tmdb_movie(client, "good", "k")) is not None + assert (await _fetch_tmdb_movie(client, "bad", "k")) is None + + async with httpx.AsyncClient(transport=show_transport) as client: + assert (await _fetch_tmdb_show(client, "good", "k")) is not None + assert (await _fetch_tmdb_show(client, "bad", "k")) is None + + async with httpx.AsyncClient(transport=episode_transport) as client: + assert (await _fetch_tmdb_episode(client, 1, 2, 3, "k")) is not None + assert (await _fetch_tmdb_episode(client, 1, 2, 4, "k")) is None + + asyncio.run(main()) + + +def test_fetch_functions_handle_http_error(): + def raise_error(request: httpx.Request) -> httpx.Response: # type: ignore[override] + raise httpx.ConnectError("boom", request=request) + + async def main() -> None: + transport = httpx.MockTransport(raise_error) + async with httpx.AsyncClient(transport=transport) as client: + assert await _fetch_tmdb_movie(client, "1", "k") is None + async with httpx.AsyncClient(transport=transport) as client: + assert await _fetch_tmdb_show(client, "1", "k") is None + async with httpx.AsyncClient(transport=transport) as client: + assert await _fetch_tmdb_episode(client, 1, 1, 1, "k") is None + + asyncio.run(main()) + + +def test_resolve_tmdb_season_number_matches_name(): + episode = types.SimpleNamespace(parentIndex=2018, parentTitle="2018") + show = TMDBShow( + id=1, + name="Show", + seasons=[TMDBSeason(season_number=14, name="2018")], + ) + assert resolve_tmdb_season_number(show, episode) == 14 + + +def test_resolve_tmdb_season_number_matches_air_date(): + episode = types.SimpleNamespace(parentIndex=2018, parentTitle="Season 2018") + show = TMDBShow( + id=1, + name="Show", + seasons=[TMDBSeason(season_number=16, name="Season 16", air_date="2018-01-03")], + ) + assert resolve_tmdb_season_number(show, episode) == 16 + + +def test_resolve_tmdb_season_number_parent_year_fallback(): + episode = types.SimpleNamespace( + parentIndex="Special", + parentTitle="Special", + parentYear=2018, + ) + show = TMDBShow( + id=1, + name="Show", + seasons=[TMDBSeason(season_number=5, name="Season 5", air_date="2018-06-01")], + ) + assert resolve_tmdb_season_number(show, episode) == 5 + + +def test_resolve_tmdb_season_number_numeric_match(): + episode = types.SimpleNamespace(parentIndex=2, parentTitle="Season 2") + show = TMDBShow( + id=1, + name="Show", + seasons=[TMDBSeason(season_number=2, name="Season 2")], + ) + assert resolve_tmdb_season_number(show, episode) == 2 + + +def test_resolve_tmdb_season_number_title_year(): + episode = types.SimpleNamespace(parentTitle="2018") + show = TMDBShow( + id=1, + name="Show", + seasons=[TMDBSeason(season_number=7, name="Season 7", air_date="2018-02-03")], + ) + assert resolve_tmdb_season_number(show, episode) == 7 + + +def test_resolve_tmdb_season_number_parent_index_str(): + episode = types.SimpleNamespace(parentIndex="3") + assert resolve_tmdb_season_number(None, episode) == 3 + + +def test_resolve_tmdb_season_number_parent_title_digit(): + episode = types.SimpleNamespace(parentTitle="4") + assert resolve_tmdb_season_number(None, episode) == 4 diff --git a/tests/test_loader_unit.py b/tests/test_loader_unit.py index 60da806..3faf281 100644 --- a/tests/test_loader_unit.py +++ b/tests/test_loader_unit.py @@ -13,19 +13,13 @@ from mcp_plex import loader from mcp_plex.loader.imdb_cache import IMDbCache from mcp_plex.loader import ( - _build_plex_item, - _extract_external_ids, _fetch_imdb, - _fetch_tmdb_episode, - _fetch_tmdb_movie, - _fetch_tmdb_show, _load_from_sample, _load_imdb_retry_queue, _persist_imdb_retry_queue, _process_imdb_retry_queue, _resolve_dense_model_params, build_point, - resolve_tmdb_season_number, ) from mcp_plex.common.types import ( AggregatedItem, @@ -36,8 +30,6 @@ PlexItem, PlexPerson, TMDBMovie, - TMDBSeason, - TMDBShow, ) @@ -54,26 +46,6 @@ def fake_import(name, globals=None, locals=None, fromlist=(), level=0): assert module.PlexServer is None assert module.PlexPartialObject is object importlib.reload(loader) - - -def test_extract_external_ids(): - guid_objs = [ - types.SimpleNamespace(id="imdb://tt0133093"), - types.SimpleNamespace(id="tmdb://603"), - ] - item = types.SimpleNamespace(guids=guid_objs) - ids = _extract_external_ids(item) - assert ids.imdb == "tt0133093" - assert ids.tmdb == "603" - - -def test_extract_external_ids_missing_values(): - item = types.SimpleNamespace(guids=None) - ids = _extract_external_ids(item) - assert ids.imdb is None - assert ids.tmdb is None - - def test_load_from_sample_returns_items(): sample_dir = Path(__file__).resolve().parents[1] / "sample-data" items = _load_from_sample(sample_dir) @@ -81,109 +53,6 @@ def test_load_from_sample_returns_items(): assert {i.plex.type for i in items} == {"movie", "episode"} -def test_build_plex_item_handles_full_metadata(): - guid_objs = [ - types.SimpleNamespace(id="imdb://tt0133093"), - types.SimpleNamespace(id="tmdb://603"), - ] - raw = types.SimpleNamespace( - ratingKey="603", - guid="plex://movie/603", - type="movie", - title="The Matrix", - summary="A hacker discovers the nature of his reality.", - year=1999, - guids=guid_objs, - thumb="matrix.jpg", - art="matrix_art.jpg", - tagline="Welcome to the Real World", - contentRating="R", - directors=[types.SimpleNamespace(id=1, tag="Lana Wachowski", thumb="lana.jpg")], - writers=[types.SimpleNamespace(id=2, tag="Lilly Wachowski", thumb="lilly.jpg")], - actors=[types.SimpleNamespace(id=3, tag="Keanu Reeves", thumb="neo.jpg", role="Neo")], - ) - - item = _build_plex_item(raw) - assert item.rating_key == "603" - assert item.directors[0].tag == "Lana Wachowski" - assert item.actors[0].role == "Neo" - - -def test_build_plex_item_missing_metadata_defaults(): - raw = types.SimpleNamespace(ratingKey="1", guid="g", type="movie", title="T") - item = _build_plex_item(raw) - assert item.directors == [] - assert item.writers == [] - assert item.actors == [] - - -def test_fetch_functions_success_and_failure(): - async def imdb_mock(request): - if "good" in str(request.url): - return httpx.Response(200, json={"id": "tt1", "type": "movie", "primaryTitle": "T"}) - return httpx.Response(404) - - async def tmdb_movie_mock(request): - assert request.headers.get("Authorization") == "Bearer k" - if "good" in str(request.url): - return httpx.Response(200, json={"id": 1, "title": "M"}) - return httpx.Response(404) - - async def tmdb_show_mock(request): - assert request.headers.get("Authorization") == "Bearer k" - if "good" in str(request.url): - return httpx.Response(200, json={"id": 1, "name": "S"}) - return httpx.Response(404) - - async def tmdb_episode_mock(request): - assert request.headers.get("Authorization") == "Bearer k" - if "/tv/1/season/2/episode/3" in str(request.url): - return httpx.Response(200, json={"id": 1, "name": "E"}) - return httpx.Response(404) - - async def main(): - imdb_transport = httpx.MockTransport(imdb_mock) - movie_transport = httpx.MockTransport(tmdb_movie_mock) - show_transport = httpx.MockTransport(tmdb_show_mock) - episode_transport = httpx.MockTransport(tmdb_episode_mock) - - async with httpx.AsyncClient(transport=imdb_transport) as client: - assert (await _fetch_imdb(client, "good")) is not None - assert (await _fetch_imdb(client, "bad")) is None - - async with httpx.AsyncClient(transport=movie_transport) as client: - assert (await _fetch_tmdb_movie(client, "good", "k")) is not None - assert (await _fetch_tmdb_movie(client, "bad", "k")) is None - - async with httpx.AsyncClient(transport=show_transport) as client: - assert (await _fetch_tmdb_show(client, "good", "k")) is not None - assert (await _fetch_tmdb_show(client, "bad", "k")) is None - - async with httpx.AsyncClient(transport=episode_transport) as client: - assert (await _fetch_tmdb_episode(client, 1, 2, 3, "k")) is not None - assert (await _fetch_tmdb_episode(client, 1, 2, 4, "k")) is None - - asyncio.run(main()) - - -def test_fetch_functions_handle_http_error(): - def raise_error(request: httpx.Request) -> httpx.Response: # type: ignore[override] - raise httpx.ConnectError("boom", request=request) - - async def main() -> None: - transport = httpx.MockTransport(raise_error) - async with httpx.AsyncClient(transport=transport) as client: - assert await _fetch_imdb(client, "tt1") is None - async with httpx.AsyncClient(transport=transport) as client: - assert await _fetch_tmdb_movie(client, "1", "k") is None - async with httpx.AsyncClient(transport=transport) as client: - assert await _fetch_tmdb_show(client, "1", "k") is None - async with httpx.AsyncClient(transport=transport) as client: - assert await _fetch_tmdb_episode(client, 1, 1, 1, "k") is None - - asyncio.run(main()) - - def test_fetch_imdb_cache_miss(tmp_path, monkeypatch): cache_path = tmp_path / "cache.json" monkeypatch.setattr(loader, "_imdb_cache", IMDbCache(cache_path)) @@ -329,72 +198,6 @@ async def run_test(): asyncio.run(run_test()) assert queue.qsize() == 1 assert queue.snapshot() == ["tt0111161"] - - -def test_resolve_tmdb_season_number_matches_name(): - episode = types.SimpleNamespace(parentIndex=2018, parentTitle="2018") - show = TMDBShow( - id=1, - name="Show", - seasons=[TMDBSeason(season_number=14, name="2018")], - ) - assert resolve_tmdb_season_number(show, episode) == 14 - - -def test_resolve_tmdb_season_number_matches_air_date(): - episode = types.SimpleNamespace(parentIndex=2018, parentTitle="Season 2018") - show = TMDBShow( - id=1, - name="Show", - seasons=[TMDBSeason(season_number=16, name="Season 16", air_date="2018-01-03")], - ) - assert resolve_tmdb_season_number(show, episode) == 16 - - -def test_resolve_tmdb_season_number_parent_year_fallback(): - episode = types.SimpleNamespace( - parentIndex="Special", - parentTitle="Special", - parentYear=2018, - ) - show = TMDBShow( - id=1, - name="Show", - seasons=[TMDBSeason(season_number=5, name="Season 5", air_date="2018-06-01")], - ) - assert resolve_tmdb_season_number(show, episode) == 5 - - -def test_resolve_tmdb_season_number_numeric_match(): - episode = types.SimpleNamespace(parentIndex=2, parentTitle="Season 2") - show = TMDBShow( - id=1, - name="Show", - seasons=[TMDBSeason(season_number=2, name="Season 2")], - ) - assert resolve_tmdb_season_number(show, episode) == 2 - - -def test_resolve_tmdb_season_number_title_year(): - episode = types.SimpleNamespace(parentTitle="2018") - show = TMDBShow( - id=1, - name="Show", - seasons=[TMDBSeason(season_number=7, name="Season 7", air_date="2018-02-03")], - ) - assert resolve_tmdb_season_number(show, episode) == 7 - - -def test_resolve_tmdb_season_number_parent_index_str(): - episode = types.SimpleNamespace(parentIndex="3") - assert resolve_tmdb_season_number(None, episode) == 3 - - -def test_resolve_tmdb_season_number_parent_title_digit(): - episode = types.SimpleNamespace(parentTitle="4") - assert resolve_tmdb_season_number(None, episode) == 4 - - def test_upsert_in_batches_handles_errors(monkeypatch): class DummyClient: def __init__(self): @@ -525,21 +328,6 @@ async def create_payload_index(self, *args, **kwargs): ) -def test_build_plex_item_converts_string_indices(): - raw = types.SimpleNamespace( - ratingKey="1", - guid="g", - type="episode", - title="Episode", - parentIndex="02", - index="03", - ) - - item = _build_plex_item(raw) - assert item.season_number == 2 - assert item.episode_number == 3 - - def test_build_point_includes_metadata(): plex_item = PlexItem( rating_key="1", diff --git a/uv.lock b/uv.lock index 324ea5e..096953c 100644 --- a/uv.lock +++ b/uv.lock @@ -730,7 +730,7 @@ wheels = [ [[package]] name = "mcp-plex" -version = "0.26.76" +version = "0.26.77" source = { editable = "." } dependencies = [ { name = "fastapi" }, From b59efeb118674453c022c4a78e0d14eac4979354 Mon Sep 17 00:00:00 2001 From: Teagan Glenn Date: Sun, 5 Oct 2025 23:27:00 -0600 Subject: [PATCH 2/2] chore: fix loader test lint issues --- docker/pyproject.deps.toml | 2 +- pyproject.toml | 2 +- tests/test_loader_unit.py | 1 - uv.lock | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docker/pyproject.deps.toml b/docker/pyproject.deps.toml index ef7289f..6fb4ca7 100644 --- a/docker/pyproject.deps.toml +++ b/docker/pyproject.deps.toml @@ -1,6 +1,6 @@ [project] name = "mcp-plex" -version = "0.26.77" +version = "0.26.78" requires-python = ">=3.11,<3.13" dependencies = [ "fastmcp>=2.11.2", diff --git a/pyproject.toml b/pyproject.toml index 20c3866..c3a53d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mcp-plex" -version = "0.26.77" +version = "0.26.78" description = "Plex-Oriented Model Context Protocol Server" requires-python = ">=3.11,<3.13" diff --git a/tests/test_loader_unit.py b/tests/test_loader_unit.py index 3faf281..8c02cef 100644 --- a/tests/test_loader_unit.py +++ b/tests/test_loader_unit.py @@ -2,7 +2,6 @@ import builtins import importlib import json -import types from datetime import datetime from pathlib import Path diff --git a/uv.lock b/uv.lock index 096953c..ba8e51e 100644 --- a/uv.lock +++ b/uv.lock @@ -730,7 +730,7 @@ wheels = [ [[package]] name = "mcp-plex" -version = "0.26.77" +version = "0.26.78" source = { editable = "." } dependencies = [ { name = "fastapi" },