"""Tests for protondb module.""" from __future__ import annotations import asyncio import json from typing import TYPE_CHECKING, Any from unittest.mock import AsyncMock, MagicMock, patch import aiohttp from python_pkg.steam_backlog_enforcer.protondb import ( HTTP_NOT_FOUND, ProtonDBRating, _fetch_batch, _fetch_one, _load_cache, _rating_from_cache, _rating_to_dict, _save_cache, fetch_protondb_ratings, ) if TYPE_CHECKING: from pathlib import Path class TestProtonDBRating: """Tests for ProtonDBRating.""" def test_playable_native(self) -> None: r = ProtonDBRating(app_id=1, tier="native") assert r.is_playable is True def test_playable_platinum(self) -> None: r = ProtonDBRating(app_id=1, tier="platinum") assert r.is_playable is True def test_playable_gold(self) -> None: r = ProtonDBRating(app_id=1, tier="gold") assert r.is_playable is True def test_not_playable_silver(self) -> None: r = ProtonDBRating(app_id=1, tier="silver") assert r.is_playable is False def test_not_playable_bronze(self) -> None: r = ProtonDBRating(app_id=1, tier="bronze") assert r.is_playable is False def test_not_playable_borked(self) -> None: r = ProtonDBRating(app_id=1, tier="borked") assert r.is_playable is False def test_playable_no_data(self) -> None: r = ProtonDBRating(app_id=1, tier="") assert r.is_playable is True def test_playable_pending(self) -> None: r = ProtonDBRating(app_id=1, tier="pending") assert r.is_playable is True def test_gold_trending_silver(self) -> None: r = ProtonDBRating(app_id=1, tier="gold", trending_tier="silver") assert r.is_playable is True def test_gold_trending_gold(self) -> None: r = ProtonDBRating(app_id=1, tier="gold", trending_tier="gold") assert r.is_playable is True def test_silver_trending_gold(self) -> None: r = ProtonDBRating(app_id=1, tier="silver", trending_tier="gold") assert r.is_playable is True def test_gold_no_trending(self) -> None: r = ProtonDBRating(app_id=1, tier="gold", trending_tier="") assert r.is_playable is True def test_gold_trending_platinum(self) -> None: r = ProtonDBRating(app_id=1, tier="gold", trending_tier="platinum") assert r.is_playable is True def test_gold_trending_unknown(self) -> None: r = ProtonDBRating(app_id=1, tier="gold", trending_tier="unknown") assert r.is_playable is False def test_gold_trending_bronze(self) -> None: r = ProtonDBRating(app_id=1, tier="gold", trending_tier="bronze") assert r.is_playable is False def test_unknown_tier(self) -> None: r = ProtonDBRating(app_id=1, tier="unknown_tier") assert r.is_playable is False def test_unplayable_reason_no_trending_tier(self) -> None: r = ProtonDBRating(app_id=1, tier="borked") assert "tier<" in r.unplayable_reason def test_unplayable_reason_for_silver_silver(self) -> None: r = ProtonDBRating(app_id=1, tier="silver", trending_tier="silver") assert "no gold tier" in r.unplayable_reason def test_unplayable_reason_for_gold_bronze(self) -> None: r = ProtonDBRating(app_id=1, tier="gold", trending_tier="bronze") assert "below silver" in r.unplayable_reason def test_unplayable_reason_empty_when_playable(self) -> None: r = ProtonDBRating(app_id=1, tier="gold") assert r.unplayable_reason == "" class TestProtonDBCache: """Tests for cache I/O.""" def test_load_cache_exists(self, tmp_path: Path) -> None: cache_file = tmp_path / "protondb_cache.json" cache_file.write_text(json.dumps({"440": {"tier": "gold"}}), encoding="utf-8") with patch( "python_pkg.steam_backlog_enforcer.protondb.PROTONDB_CACHE_FILE", cache_file, ): result = _load_cache() assert result == {"440": {"tier": "gold"}} def test_load_cache_missing(self, tmp_path: Path) -> None: cache_file = tmp_path / "nonexistent.json" with patch( "python_pkg.steam_backlog_enforcer.protondb.PROTONDB_CACHE_FILE", cache_file, ): assert _load_cache() == {} def test_save_cache(self, tmp_path: Path) -> None: cache_file = tmp_path / "protondb_cache.json" config_dir = tmp_path with ( patch( "python_pkg.steam_backlog_enforcer.protondb.PROTONDB_CACHE_FILE", cache_file, ), patch("python_pkg.steam_backlog_enforcer.protondb.CONFIG_DIR", config_dir), ): _save_cache({"440": {"tier": "gold"}}) assert cache_file.exists() class TestRatingConversion: """Tests for rating serialization.""" def test_to_dict(self) -> None: r = ProtonDBRating( app_id=1, tier="gold", trending_tier="platinum", score=0.9, confidence="high", total_reports=100, ) d = _rating_to_dict(r) assert d["tier"] == "gold" assert d["total_reports"] == 100 def test_from_cache(self) -> None: data: dict[str, Any] = { "tier": "silver", "trending_tier": "bronze", "score": 0.5, } r = _rating_from_cache(440, data) assert r.app_id == 440 assert r.tier == "silver" assert r.trending_tier == "bronze" def test_from_cache_defaults(self) -> None: r = _rating_from_cache(440, {}) assert r.tier == "" assert r.total_reports == 0 class TestFetchOne: """Tests for _fetch_one.""" def test_success(self) -> None: mock_resp = AsyncMock() mock_resp.status = 200 mock_resp.raise_for_status = MagicMock() mock_resp.json = AsyncMock( return_value={"tier": "gold", "trendingTier": "platinum"} ) mock_resp.__aenter__ = AsyncMock(return_value=mock_resp) mock_resp.__aexit__ = AsyncMock(return_value=False) mock_session = MagicMock() mock_session.get = MagicMock(return_value=mock_resp) sem = asyncio.Semaphore(1) result = asyncio.run(_fetch_one(mock_session, sem, 440)) assert result.tier == "gold" def test_not_found(self) -> None: mock_resp = AsyncMock() mock_resp.status = HTTP_NOT_FOUND mock_resp.__aenter__ = AsyncMock(return_value=mock_resp) mock_resp.__aexit__ = AsyncMock(return_value=False) mock_session = MagicMock() mock_session.get = MagicMock(return_value=mock_resp) sem = asyncio.Semaphore(1) result = asyncio.run(_fetch_one(mock_session, sem, 440)) assert result.tier == "" def test_client_error(self) -> None: mock_resp = AsyncMock() mock_resp.status = 200 mock_resp.raise_for_status = MagicMock(side_effect=aiohttp.ClientError) mock_resp.__aenter__ = AsyncMock(return_value=mock_resp) mock_resp.__aexit__ = AsyncMock(return_value=False) mock_session = MagicMock() mock_session.get = MagicMock(return_value=mock_resp) sem = asyncio.Semaphore(1) result = asyncio.run(_fetch_one(mock_session, sem, 440)) assert result.tier == "" class TestFetchBatch: """Tests for _fetch_batch.""" def test_returns_ratings(self) -> None: rating = ProtonDBRating(app_id=440, tier="gold") with patch( "python_pkg.steam_backlog_enforcer.protondb._fetch_one", new_callable=AsyncMock, return_value=rating, ): result = asyncio.run(_fetch_batch([440])) assert len(result) == 1 assert result[0].tier == "gold" class TestFetchProtondbRatings: """Tests for fetch_protondb_ratings.""" def test_all_cached(self, tmp_path: Path) -> None: cache_file = tmp_path / "protondb_cache.json" cache_file.write_text(json.dumps({"440": {"tier": "gold"}}), encoding="utf-8") with patch( "python_pkg.steam_backlog_enforcer.protondb.PROTONDB_CACHE_FILE", cache_file, ): result = fetch_protondb_ratings([440]) assert 440 in result assert result[440].tier == "gold" def test_fetch_uncached(self, tmp_path: Path) -> None: cache_file = tmp_path / "protondb_cache.json" config_dir = tmp_path with ( patch( "python_pkg.steam_backlog_enforcer.protondb.PROTONDB_CACHE_FILE", cache_file, ), patch("python_pkg.steam_backlog_enforcer.protondb.CONFIG_DIR", config_dir), patch( "python_pkg.steam_backlog_enforcer.protondb._fetch_batch", return_value=[ProtonDBRating(app_id=440, tier="platinum")], ), ): result = fetch_protondb_ratings([440]) assert result[440].tier == "platinum"