"""Tests for the Jellyseerr stats provider + generic stat widget source.""" from __future__ import annotations import asyncio from unittest.mock import MagicMock, patch from media_library_viewer_api.widgets.jellyseerr_stats import JellyseerrStatsProvider from media_library_viewer_api.widgets.sources import ServiceRecord, StatsWidgetSource from media_library_viewer_api.widgets.stats_provider import ( StatValue, get_stats_provider, ) def _service( *, jellyseerr_url: str = "https://js.example.com", jellyseerr_api_key: str = "key", ) -> ServiceRecord: return ServiceRecord( id="svc-jf", service_type="jellyfin", name="Jellyfin", config={"jellyseerr_url": jellyseerr_url, "jellyseerr_api_key": jellyseerr_api_key}, secrets={}, enabled=True, ) _COUNTS = {"total": 5, "pending": 2, "approved": 1, "declined": 0, "processing": 1, "available": 1} _RECENT = [{"id": 1, "name": "Inception", "status": "pending", "media_status": "available"}] def test_provider_registered_for_jellyfin(): assert isinstance(get_stats_provider("jellyfin"), JellyseerrStatsProvider) def test_provider_returns_normalized_stats_and_recent(): provider = JellyseerrStatsProvider(ttl=0) fake = MagicMock() fake.request_count.return_value = _COUNTS fake.recent_requests.return_value = _RECENT with patch("media_library_viewer_api.widgets.jellyseerr_stats._jellyseer_client", return_value=fake): result = provider.fetch_stats(_service()) assert [s.key for s in result.stats] == [ "total", "pending", "approved", "declined", "processing", "available", ] by_key = {s.key: s.value for s in result.stats} assert by_key["pending"] == 2 and by_key["total"] == 5 assert result.recent == _RECENT def test_provider_not_configured_returns_detail(): provider = JellyseerrStatsProvider() result = provider.fetch_stats(_service(jellyseerr_url="", jellyseerr_api_key="")) assert result.stats == [] assert "not configured" in (result.detail or "").lower() def test_provider_caches_within_ttl(): provider = JellyseerrStatsProvider(ttl=10) fake = MagicMock() fake.request_count.return_value = _COUNTS fake.recent_requests.return_value = _RECENT with patch("media_library_viewer_api.widgets.jellyseerr_stats._jellyseer_client", return_value=fake): provider.fetch_stats(_service()) provider.fetch_stats(_service()) # served from cache assert fake.request_count.call_count == 1 def test_stat_widget_returns_selected_value(): src = StatsWidgetSource() result = MagicMock() result.detail = None result.stats = [StatValue("total", "Total", 5), StatValue("pending", "Pending", 2)] result.recent = [] provider = MagicMock() provider.fetch_stats.return_value = result with patch("media_library_viewer_api.widgets.sources.get_stats_provider", return_value=provider): data = asyncio.run(src.fetch(_service(), "stat", {"stat": "pending"})) assert data == {"key": "pending", "label": "Pending", "value": 2} def test_stats_overview_widget_returns_all_stats_and_recent(): src = StatsWidgetSource() result = MagicMock() result.detail = None result.stats = [StatValue("total", "Total", 5), StatValue("pending", "Pending", 2)] result.recent = _RECENT provider = MagicMock() provider.fetch_stats.return_value = result with patch("media_library_viewer_api.widgets.sources.get_stats_provider", return_value=provider): data = asyncio.run(src.fetch(_service(), "stats_overview", {})) assert data["stats"] == [ {"key": "total", "label": "Total", "value": 5}, {"key": "pending", "label": "Pending", "value": 2}, ] assert data["recent"] == _RECENT def test_stat_widget_unknown_stat_returns_error(): src = StatsWidgetSource() result = MagicMock() result.detail = None result.stats = [StatValue("total", "Total", 5)] result.recent = [] provider = MagicMock() provider.fetch_stats.return_value = result with patch("media_library_viewer_api.widgets.sources.get_stats_provider", return_value=provider): data = asyncio.run(src.fetch(_service(), "stat", {"stat": "nope"})) assert "error" in data def test_jellyseer_client_open_requests_resolves_titles(): """open_requests() fetches pending+approved via filter and resolves titles.""" from media_library_viewer_api.clients.jellyseerr import JellyseerrClient c = JellyseerrClient("https://js.example.com", "key") c.session = MagicMock() def mock_get(url, **kw): resp = MagicMock() resp.raise_for_status.return_value = None resp.status_code = 200 resp.text = "" params = kw.get("params", {}) if "/movie/" in url or "/tv/" in url: resp.json.return_value = {"title": "Inception"} # title resolution elif params.get("filter") == "pending": resp.json.return_value = { "results": [ { "id": 7, "type": 1, "status": 1, "media": {"tmdbId": 123, "status": 5}, "createdAt": 1_700_000_000, } ] } else: resp.json.return_value = {"results": []} return resp c.session.get.side_effect = mock_get out = c.open_requests() assert len(out) == 1 r = out[0] assert r["id"] == 7 assert r["type"] == "movie" assert r["name"] == "Inception" # resolved via /movie/123 assert r["status"] == "pending" assert r["media_status"] == "available" assert r["created_at"] == 1_700_000_000 # Title is cached: a second call doesn't re-fetch /movie/123. movie_calls_before = sum(1 for call in c.session.get.call_args_list if "/movie/" in call.args[0]) assert movie_calls_before == 1 c.open_requests() # second poll movie_calls_after = sum(1 for call in c.session.get.call_args_list if "/movie/" in call.args[0]) assert movie_calls_after == 1 # cached, no new /movie call def test_fetch_jellyseer_requests_not_configured_returns_empty(): """No jellyseerr config -> empty list (tab shows 'No requests').""" from media_library_viewer_api.widgets.jellyseerr_stats import fetch_jellyseer_requests service = ServiceRecord(id="s", service_type="jellyfin", name="JF", config={}, secrets={}) assert fetch_jellyseer_requests(service) == []