steam-backlog-enforcer/steam_backlog_enforcer/tests/test_scanning_part2.py
Krzysztof kuhy Rudnicki dd480f3208 Bring _stats.py and scanning.py under the 250-line cap
Leaf-set extraction only, so every new import is one-way and no cycle is
possible:

  _stats.py    523 -> 233   + _stats_types (the shared _GameTimes dataclass,
                              imported by parent and helpers alike),
                              _stats_gathering, _stats_display
  scanning.py  521 -> 248   + _scanning_candidates, _scanning_assign,
                              _scanning_tampering

_GameTimes and the four moved scanning helpers are re-exported from their
original modules via __all__ (mypy --no-implicit-reexport needs them listed),
so callers and tests keep their existing import paths.

Test patch targets were retargeted per file from the pytest failure list —
the target is the module whose source now contains the function under test.
Note TestPrintScenario and TestDoCheck did NOT move, so they keep the
original target while the rest of their files change.

1344 tests, 100.00% coverage. ./run.sh status and ./run.sh stats verified
against the live install; service at 0 restarts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016A1EYiv7e7HGhET84oa9M3
2026-08-24 16:00:54 +02:00

135 lines
4.3 KiB
Python

"""Tests for scanning module — part 2 (missing coverage)."""
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock, patch
from steam_backlog_enforcer.config import Config, State
from steam_backlog_enforcer.scanning import (
_check_game_tampering,
detect_tampering,
)
PKG = "steam_backlog_enforcer._scanning_tampering"
def _entry(
app_id: int = 1,
name: str = "G",
total: int = 10,
unlocked: int = 5,
playtime: int = 60,
) -> dict[str, Any]:
return {
"app_id": app_id,
"name": name,
"total_achievements": total,
"unlocked_achievements": unlocked,
"playtime_minutes": playtime,
}
class TestCheckGameTampering:
"""Tests for _check_game_tampering."""
def test_current_game_skipped(self) -> None:
state = State(current_app_id=1)
result = _check_game_tampering(MagicMock(), _entry(app_id=1), state)
assert result is None
def test_already_complete_skipped(self) -> None:
state = State()
result = _check_game_tampering(
MagicMock(),
_entry(unlocked=10, total=10),
state,
)
assert result is None
def test_zero_playtime_skipped(self) -> None:
state = State()
result = _check_game_tampering(
MagicMock(),
_entry(playtime=0),
state,
)
assert result is None
def test_no_new_achievements(self) -> None:
client = MagicMock()
game = MagicMock()
game.unlocked_achievements = 5
client.refresh_single_game.return_value = game
state = State()
result = _check_game_tampering(client, _entry(unlocked=5), state)
assert result is None
def test_tampering_detected(self) -> None:
client = MagicMock()
game = MagicMock()
game.unlocked_achievements = 8
client.refresh_single_game.return_value = game
state = State()
entry = _entry(app_id=99, name="Cheated", unlocked=5)
result = _check_game_tampering(client, entry, state)
assert result is not None
assert result == ("Cheated", 99, 3)
def test_refresh_returns_none(self) -> None:
client = MagicMock()
client.refresh_single_game.return_value = None
state = State()
result = _check_game_tampering(client, _entry(), state)
assert result is None
class TestDetectTampering:
"""Tests for detect_tampering."""
def test_no_snapshot(self) -> None:
with patch(f"{PKG}.load_snapshot", return_value=None):
detect_tampering(Config(steam_api_key="k", steam_id="i"), State())
def test_no_tampering(self) -> None:
entries = [_entry(app_id=1)]
with (
patch(f"{PKG}.load_snapshot", return_value=entries),
patch(f"{PKG}.SteamAPIClient"),
patch(f"{PKG}._check_game_tampering", return_value=None),
patch(f"{PKG}._echo"),
):
detect_tampering(Config(steam_api_key="k", steam_id="i"), State())
def test_tampering_found(self) -> None:
entries = [_entry(app_id=1, name="BadGame")]
with (
patch(f"{PKG}.load_snapshot", return_value=entries),
patch(f"{PKG}.SteamAPIClient"),
patch(
f"{PKG}._check_game_tampering",
return_value=("BadGame", 1, 3),
),
patch(f"{PKG}._echo") as mock_echo,
patch(f"{PKG}.send_notification"),
):
detect_tampering(Config(steam_api_key="k", steam_id="i"), State())
assert any("TAMPERING" in str(c) for c in mock_echo.call_args_list)
def test_stops_at_limit(self) -> None:
"""Stops after _TAMPER_CHECK_LIMIT suspicious games."""
entries = [_entry(app_id=i, name=f"G{i}") for i in range(10)]
with (
patch(f"{PKG}.load_snapshot", return_value=entries),
patch(f"{PKG}.SteamAPIClient"),
patch(
f"{PKG}._check_game_tampering",
return_value=("Game", 1, 1),
) as mock_check,
patch(f"{PKG}._echo"),
patch(f"{PKG}.send_notification"),
):
detect_tampering(Config(steam_api_key="k", steam_id="i"), State())
# Should stop after 3 (_TAMPER_CHECK_LIMIT)
assert mock_check.call_count == 3