"""Tests for steam_api module.""" from __future__ import annotations from typing import Any from unittest.mock import MagicMock, patch import pytest import requests from steam_backlog_enforcer.steam_api import ( AchievementInfo, GameInfo, SteamAPIClient, SteamAPIError, ) class TestAchievementInfo: """Tests for AchievementInfo.""" def test_create(self) -> None: a = AchievementInfo( api_name="ACH_1", display_name="First", achieved=True, unlock_time=1000 ) assert a.api_name == "ACH_1" assert a.achieved is True class TestGameInfo: """Tests for GameInfo.""" def test_completion_pct_zero_achievements(self) -> None: g = GameInfo( app_id=1, name="G", total_achievements=0, unlocked_achievements=0, playtime_minutes=0, ) assert g.completion_pct == 100.0 def test_completion_pct_partial(self) -> None: g = GameInfo( app_id=1, name="G", total_achievements=10, unlocked_achievements=5, playtime_minutes=0, ) assert g.completion_pct == 50.0 def test_is_complete_true(self) -> None: g = GameInfo( app_id=1, name="G", total_achievements=5, unlocked_achievements=5, playtime_minutes=0, ) assert g.is_complete is True def test_is_complete_false(self) -> None: g = GameInfo( app_id=1, name="G", total_achievements=5, unlocked_achievements=3, playtime_minutes=0, ) assert g.is_complete is False def test_is_complete_zero(self) -> None: g = GameInfo( app_id=1, name="G", total_achievements=0, unlocked_achievements=0, playtime_minutes=0, ) assert g.is_complete is False def test_to_snapshot(self) -> None: ach = AchievementInfo( api_name="A1", display_name="Ach1", achieved=True, unlock_time=99 ) g = GameInfo( app_id=1, name="G", total_achievements=1, unlocked_achievements=1, playtime_minutes=60, achievements=[ach], completionist_hours=5.0, ) snap = g.to_snapshot() assert snap["app_id"] == 1 assert snap["achievements"][0]["api_name"] == "A1" assert snap["completionist_hours"] == 5.0 def test_from_snapshot(self) -> None: data: dict[str, Any] = { "app_id": 2, "name": "G2", "total_achievements": 3, "unlocked_achievements": 1, "playtime_minutes": 120, "completionist_hours": 10.0, "achievements": [ { "api_name": "A1", "display_name": "First", "achieved": False, "unlock_time": 0, }, ], } g = GameInfo.from_snapshot(data) assert g.app_id == 2 assert g.completionist_hours == 10.0 assert len(g.achievements) == 1 def test_from_snapshot_defaults(self) -> None: data: dict[str, Any] = { "app_id": 3, "name": "G3", "total_achievements": 0, "unlocked_achievements": 0, } g = GameInfo.from_snapshot(data) assert g.playtime_minutes == 0 assert g.completionist_hours == -1 assert g.achievements == [] def test_from_snapshot_achievement_defaults(self) -> None: data: dict[str, Any] = { "app_id": 4, "name": "G4", "total_achievements": 1, "unlocked_achievements": 0, "achievements": [{"api_name": "X", "achieved": False}], } g = GameInfo.from_snapshot(data) assert g.achievements[0].display_name == "X" assert g.achievements[0].unlock_time == 0 class TestSteamAPIClient: """Tests for SteamAPIClient.""" def test_init(self) -> None: client = SteamAPIClient("key", "id") assert client.api_key == "key" assert client.steam_id == "id" def test_rate_limit(self) -> None: client = SteamAPIClient("key", "id") # Should not block on first call client._rate_limit() def test_rate_limit_throttle(self) -> None: client = SteamAPIClient("key", "id") # Fill up the rate limit window client._request_times = [__import__("time").time()] * client._max_rps with patch( "steam_backlog_enforcer.steam_api.time.sleep", ) as mock_sleep: # Next call should trigger sleep then succeed client._rate_limit() mock_sleep.assert_called() def test_get_success(self) -> None: client = SteamAPIClient("key", "id") mock_resp = MagicMock() mock_resp.json.return_value = {"data": "value"} client.session.get = MagicMock(return_value=mock_resp) result = client._get("https://example.com/api") assert result == {"data": "value"} def test_get_with_params(self) -> None: client = SteamAPIClient("key", "id") mock_resp = MagicMock() mock_resp.json.return_value = {"data": "value"} client.session.get = MagicMock(return_value=mock_resp) result = client._get("https://example.com/api", params={"foo": "bar"}) assert result == {"data": "value"} # Verify key was added to existing params dict call_kwargs = client.session.get.call_args assert call_kwargs[1]["params"]["foo"] == "bar" assert call_kwargs[1]["params"]["key"] == "key" def test_get_failure(self) -> None: client = SteamAPIClient("key", "id") client.session.get = MagicMock(side_effect=requests.RequestException("fail")) with pytest.raises(SteamAPIError): client._get("https://example.com/api") def test_get_owned_games(self) -> None: client = SteamAPIClient("key", "id") with patch.object( client, "_get", return_value={"response": {"games": [{"appid": 440}]}}, ): games = client.get_owned_games() assert len(games) == 1 assert games[0]["appid"] == 440 def test_get_owned_games_empty(self) -> None: client = SteamAPIClient("key", "id") with patch.object(client, "_get", return_value={"response": {}}): games = client.get_owned_games() assert games == [] def test_get_achievement_details(self) -> None: client = SteamAPIClient("key", "id") with patch.object( client, "_get", return_value={ "playerstats": { "success": True, "achievements": [ { "apiname": "ACH_1", "name": "First", "achieved": 1, "unlocktime": 1000, }, ], }, }, ): result = client.get_achievement_details(440) assert len(result) == 1 assert result[0].achieved is True def test_get_achievement_details_failure(self) -> None: client = SteamAPIClient("key", "id") with patch.object(client, "_get", side_effect=SteamAPIError("fail")): result = client.get_achievement_details(440) assert result == [] def test_get_achievement_details_not_success(self) -> None: client = SteamAPIClient("key", "id") with patch.object( client, "_get", return_value={"playerstats": {"success": False}}, ): result = client.get_achievement_details(440) assert result == [] def test_fetch_one_game(self) -> None: client = SteamAPIClient("key", "id") ach = AchievementInfo("A1", "Ach1", achieved=True, unlock_time=100) with patch.object(client, "get_achievement_details", return_value=[ach]): result = client._fetch_one_game( {"appid": 440, "name": "TF2", "playtime_forever": 60}, ) assert result is not None assert result.app_id == 440 def test_fetch_one_game_no_achievements(self) -> None: client = SteamAPIClient("key", "id") with patch.object(client, "get_achievement_details", return_value=[]): result = client._fetch_one_game({"appid": 440}) assert result is None def test_build_game_list(self) -> None: client = SteamAPIClient("key", "id") ach = AchievementInfo("A1", "Ach1", achieved=True, unlock_time=100) with ( patch.object( client, "get_owned_games", return_value=[{"appid": 440, "name": "TF2", "playtime_forever": 60}], ), patch.object(client, "get_achievement_details", return_value=[ach]), ): progress_calls: list[tuple[int, int]] = [] def progress(c: int, t: int) -> None: progress_calls.append((c, t)) games = client.build_game_list(progress_callback=progress) assert len(games) == 1 assert len(progress_calls) > 0 def test_build_game_list_no_achievements_excluded(self) -> None: """Games without achievements are excluded from results.""" client = SteamAPIClient("key", "id") with ( patch.object( client, "get_owned_games", return_value=[{"appid": 440, "name": "TF2"}], ), patch.object(client, "get_achievement_details", return_value=[]), ): games = client.build_game_list() assert games == [] def test_build_game_list_exception_in_future(self) -> None: client = SteamAPIClient("key", "id") with ( patch.object( client, "get_owned_games", return_value=[{"appid": 440, "name": "TF2"}], ), patch.object( client, "get_achievement_details", side_effect=SteamAPIError("err"), ), ): games = client.build_game_list() assert games == [] def test_refresh_single_game(self) -> None: client = SteamAPIClient("key", "id") ach = AchievementInfo("A1", "Ach1", achieved=True, unlock_time=100) with patch.object(client, "get_achievement_details", return_value=[ach]): result = client.refresh_single_game(440, "TF2", 60) assert result is not None assert result.unlocked_achievements == 1 def test_refresh_single_game_no_achievements(self) -> None: client = SteamAPIClient("key", "id") with patch.object(client, "get_achievement_details", return_value=[]): result = client.refresh_single_game(440, "TF2") assert result is None