mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-07-04 11:43:12 +02:00
Extracted from testsAndMisc monorepo. Changes: - Rewrote imports from python_pkg.steam_backlog_enforcer.* → steam_backlog_enforcer.* - Moved run.sh, install.sh, README.md, service file to repo root - Added standalone pyproject.toml, requirements.txt, .pre-commit-config.yaml, .gitignore - Added GitHub Actions CI workflows (tests + pre-commit) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
334 lines
11 KiB
Python
334 lines
11 KiB
Python
"""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
|