steam-backlog-enforcer/steam_backlog_enforcer/tests/test_web_library.py
2026-10-10 16:07:42 +02:00

192 lines
7.3 KiB
Python

"""Tests for _web_library — the owned-games join behind ``GET /api/library``."""
from __future__ import annotations
import json
from typing import Any
from unittest.mock import patch
import pytest
from steam_backlog_enforcer import _web_library
from steam_backlog_enforcer import config as _config
from steam_backlog_enforcer.config import Config, State
from steam_backlog_enforcer.steam_api import GameInfo
from steam_backlog_enforcer.tests._web_request import make_request, payload_of
_PKG = "steam_backlog_enforcer._web_library"
@pytest.fixture(autouse=True)
def _fresh_memos(monkeypatch: pytest.MonkeyPatch) -> None:
"""The module keeps process-wide memos; every test starts without them."""
monkeypatch.setattr(_web_library, "_MEMOS", _web_library._Memos())
def _write_snapshot(rows: list[dict[str, Any]]) -> None:
_config.SNAPSHOT_FILE.write_text(json.dumps(rows), encoding="utf-8")
def _row(app_id: int, **extra: object) -> dict[str, Any]:
return {"app_id": app_id, "name": f"G{app_id}", **extra}
class TestSnapshotGames:
def test_no_snapshot_file(self) -> None:
assert _web_library._snapshot_games() == {}
def test_rows_become_games_and_are_memoised(self) -> None:
_write_snapshot(
[
_row(1, total_achievements=10, unlocked_achievements=4),
{"name": "no id"},
_row(2, completionist_hours=7.5, playtime_minutes=30),
]
)
games = _web_library._snapshot_games()
assert sorted(games) == [1, 2]
assert games[1].total_achievements == 10
assert games[2].completionist_hours == 7.5
assert games[1].completionist_hours == -1
with patch(f"{_PKG}.load_snapshot") as load:
assert _web_library._snapshot_games() is games
load.assert_not_called()
def test_a_changed_file_is_reparsed(self) -> None:
_write_snapshot([_row(1)])
assert sorted(_web_library._snapshot_games()) == [1]
_write_snapshot([_row(1), _row(2, name=None)])
got = _web_library._snapshot_games()
assert sorted(got) == [1, 2]
assert got[2].name == ""
def test_missing_snapshot_content_is_empty(self) -> None:
_write_snapshot([_row(1)])
with patch(f"{_PKG}.load_snapshot", return_value=None):
assert _web_library._snapshot_games() == {}
class TestOwnedRecords:
def test_cached_records(self) -> None:
records = [{"app_id": 1}, {"name": "no id"}]
with patch(f"{_PKG}.load_owned_records", return_value=records):
got = _web_library._owned_records(Config(steam_id="s"))
assert got == {1: {"app_id": 1}}
def test_fetched_once_when_nothing_is_cached(self) -> None:
with (
patch(f"{_PKG}.load_owned_records", return_value=None),
patch(f"{_PKG}.refresh_owned_records", return_value=[{"app_id": 2}]),
):
assert list(_web_library._owned_records(Config())) == [2]
def test_steam_unreachable_is_empty(self) -> None:
with (
patch(f"{_PKG}.load_owned_records", return_value=None),
patch(f"{_PKG}.refresh_owned_records", return_value=None),
):
assert _web_library._owned_records(Config()) == {}
class TestHltbHours:
def test_prefers_the_hltb_cache(self) -> None:
game = GameInfo(1, "g", 1, 0, 0, completionist_hours=9.0)
assert _web_library._hltb_hours(1, game, {1: {"hours": 12.34}}) == 12.3
def test_falls_back_to_the_snapshot(self) -> None:
game = GameInfo(1, "g", 1, 0, 0, completionist_hours=9.0)
assert _web_library._hltb_hours(1, game, {}) == 9.0
def test_unknown_without_either(self) -> None:
assert _web_library._hltb_hours(1, None, {}) is None
game = GameInfo(1, "g", 1, 0, 0)
assert _web_library._hltb_hours(1, game, {}) is None
class TestOwnedAppIds:
def test_union_of_snapshot_and_records_memoised_by_stamp(self) -> None:
_write_snapshot([_row(1)])
records = [{"app_id": 2}, {"x": 1}]
with (
patch(f"{_PKG}.owned_cache_stamp", return_value=(1, 1)),
patch(f"{_PKG}.load_owned_records", return_value=records) as load,
):
assert _web_library.owned_app_ids() == {1, 2}
assert _web_library.owned_app_ids() == {1, 2}
load.assert_called_once()
def test_a_new_stamp_reloads_and_none_means_empty(self) -> None:
with (
patch(f"{_PKG}.owned_cache_stamp", side_effect=[(1, 1), (2, 2)]),
patch(f"{_PKG}.load_owned_records", side_effect=[[{"app_id": 5}], None]),
):
assert _web_library.owned_app_ids() == {5}
assert _web_library.owned_app_ids() == set()
class TestReason:
def test_no_achievements_flag(self) -> None:
got = _web_library._reason(1, None, {"has_stats": False}, State(), set())
assert got == "No achievements"
def test_not_scanned_yet(self) -> None:
got = _web_library._reason(1, None, {"has_stats": True}, State(), set())
assert got == "Not scanned yet"
def test_otherwise_the_pick_rule(self) -> None:
assert _web_library._reason(1, None, {}, State(), set()) == (
"No achievement data"
)
game = GameInfo(1, "g", 2, 0, 0)
assert _web_library._reason(1, game, {}, State(), set()) is None
class TestLibraryRows:
def test_joins_every_source(self) -> None:
_write_snapshot(
[_row(1, total_achievements=4, unlocked_achievements=1, playtime_minutes=5)]
)
records = [
{"app_id": 1, "name": "One", "playtime_minutes": 60, "last_played": 99},
{"app_id": 2, "has_stats": False},
]
with (
patch(f"{_PKG}.load_owned_records", return_value=records),
patch(f"{_PKG}._read_raw_cache", return_value={1: {"hours": 3.0}}),
patch(f"{_PKG}.get_installed_games", return_value=[(1, "One")]),
patch(f"{_PKG}.allowed_app_ids", return_value={1}),
):
rows = {
r["app_id"]: r for r in _web_library.library_rows(State(), Config())
}
assert rows[1] == {
"app_id": 1,
"name": "One",
"achievements_total": 4,
"achievements_unlocked": 1,
"hltb_hours": 3.0,
"playtime_minutes": 60,
"last_played": 99,
"installed": True,
"assigned": True,
"ineligible_reason": None,
}
assert rows[2]["name"] == "2"
assert rows[2]["last_played"] is None
assert rows[2]["ineligible_reason"] == "No achievements"
def test_snapshot_only_game_uses_its_own_name_and_playtime(self) -> None:
_write_snapshot([_row(3, playtime_minutes=8)])
with (
patch(f"{_PKG}.load_owned_records", return_value=[]),
patch(f"{_PKG}._read_raw_cache", return_value={}),
patch(f"{_PKG}.get_installed_games", return_value=[]),
):
(row,) = _web_library.library_rows(State(), Config())
assert (row["name"], row["playtime_minutes"]) == ("G3", 8)
def test_view_wraps_the_rows(self) -> None:
with patch(f"{_PKG}.library_rows", return_value=[{"app_id": 1}]):
reply = _web_library.library_view(make_request())
assert payload_of(reply) == {"games": [{"app_id": 1}]}