mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 18:23:07 +02:00
- Remove skip_app_ids from user-editable Config; callers updated - Split PROTECTED_APP_IDS: only Steam infra/Proton IDs remain; game IDs moved to a new time-locked exception system - Add _whitelist.py: 24-hour cooldown on new exceptions, entropy- checked justification (>= 5 words), append-only audit log, chattr +i immutability on enforcement-critical config files - Add is_protected_app() in game_install.py; used everywhere instead of direct PROTECTED_APP_IDS membership checks - Add 'add-exception' CLI command (cmd_add_exception in main.py) - Call promote_pending_exceptions() and lock_enforcement_files() in each _enforce_loop_iteration - 590 tests, 100% branch coverage on all steam_backlog_enforcer modules - Add .worktrees to .gitignore
497 lines
16 KiB
Python
497 lines
16 KiB
Python
"""Tests for main CLI module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import time
|
|
from typing import Any
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from python_pkg.steam_backlog_enforcer._whitelist import WHITELIST_COOLDOWN_SECONDS
|
|
from python_pkg.steam_backlog_enforcer.config import Config, State
|
|
from python_pkg.steam_backlog_enforcer.main import (
|
|
cmd_add_exception,
|
|
cmd_buy_dlc,
|
|
cmd_hide,
|
|
cmd_install,
|
|
cmd_installed,
|
|
cmd_list,
|
|
cmd_reset,
|
|
cmd_setup,
|
|
cmd_status,
|
|
cmd_unblock,
|
|
cmd_unhide,
|
|
cmd_uninstall,
|
|
main,
|
|
)
|
|
|
|
PKG = "python_pkg.steam_backlog_enforcer.main"
|
|
|
|
|
|
def _snap(
|
|
app_id: int = 1,
|
|
name: str = "G",
|
|
total: int = 10,
|
|
unlocked: int = 0,
|
|
hours: float = -1,
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"app_id": app_id,
|
|
"name": name,
|
|
"total_achievements": total,
|
|
"unlocked_achievements": unlocked,
|
|
"playtime_minutes": 60,
|
|
"completionist_hours": hours,
|
|
}
|
|
|
|
|
|
class TestCmdStatus:
|
|
"""Tests for cmd_status."""
|
|
|
|
def test_with_game(self) -> None:
|
|
state = State(current_app_id=440, current_game_name="TF2")
|
|
with (
|
|
patch(f"{PKG}.is_store_blocked", return_value=True),
|
|
patch(f"{PKG}.get_installed_games", return_value=[(440, "TF2")]),
|
|
patch(f"{PKG}._echo"),
|
|
):
|
|
cmd_status(Config(), state)
|
|
|
|
def test_no_game(self) -> None:
|
|
with (
|
|
patch(f"{PKG}.is_store_blocked", return_value=False),
|
|
patch(f"{PKG}.get_installed_games", return_value=[]),
|
|
patch(f"{PKG}._echo"),
|
|
):
|
|
cmd_status(Config(), State())
|
|
|
|
|
|
class TestCmdList:
|
|
"""Tests for cmd_list."""
|
|
|
|
def test_no_snapshot(self) -> None:
|
|
with (
|
|
patch(f"{PKG}.load_snapshot", return_value=None),
|
|
patch(f"{PKG}._echo") as mock_echo,
|
|
):
|
|
cmd_list(Config(), State())
|
|
assert any("No snapshot" in str(c) for c in mock_echo.call_args_list)
|
|
|
|
def test_with_games(self) -> None:
|
|
snap = [
|
|
_snap(1, "A", 10, 5, 20.0),
|
|
_snap(2, "B", 10, 10, 10.0),
|
|
_snap(3, "C", 10, 3, -1),
|
|
]
|
|
state = State(current_app_id=1)
|
|
with (
|
|
patch(f"{PKG}.load_snapshot", return_value=snap),
|
|
patch(f"{PKG}._echo"),
|
|
):
|
|
cmd_list(Config(), state)
|
|
|
|
def test_many_games(self) -> None:
|
|
snap = [_snap(i, f"Game{i}") for i in range(60)]
|
|
with (
|
|
patch(f"{PKG}.load_snapshot", return_value=snap),
|
|
patch(f"{PKG}._echo") as mock_echo,
|
|
):
|
|
cmd_list(Config(), State())
|
|
assert any("more" in str(c) for c in mock_echo.call_args_list)
|
|
|
|
|
|
class TestCmdUnblock:
|
|
"""Tests for cmd_unblock."""
|
|
|
|
def test_success(self) -> None:
|
|
with (
|
|
patch(f"{PKG}.unblock_store", return_value=True),
|
|
patch(f"{PKG}._echo"),
|
|
):
|
|
cmd_unblock(Config(), State())
|
|
|
|
def test_fail(self) -> None:
|
|
with (
|
|
patch(f"{PKG}.unblock_store", return_value=False),
|
|
patch(f"{PKG}._echo") as mock_echo,
|
|
):
|
|
cmd_unblock(Config(), State())
|
|
assert any("Failed" in str(c) for c in mock_echo.call_args_list)
|
|
|
|
|
|
class TestCmdBuyDlc:
|
|
"""Tests for cmd_buy_dlc."""
|
|
|
|
def test_no_game(self) -> None:
|
|
with patch(f"{PKG}._echo") as mock_echo:
|
|
cmd_buy_dlc(Config(), State())
|
|
assert any("No game" in str(c) for c in mock_echo.call_args_list)
|
|
|
|
def test_unblock_fails(self) -> None:
|
|
state = State(current_app_id=1, current_game_name="G")
|
|
with (
|
|
patch(f"{PKG}.unblock_store", return_value=False),
|
|
patch(f"{PKG}._echo"),
|
|
):
|
|
cmd_buy_dlc(Config(), state)
|
|
|
|
def test_success_reblock(self) -> None:
|
|
state = State(current_app_id=1, current_game_name="G")
|
|
config = Config(block_store=True)
|
|
with (
|
|
patch(f"{PKG}.unblock_store", return_value=True),
|
|
patch(f"{PKG}.block_store", return_value=True),
|
|
patch(f"{PKG}.restart_steam"),
|
|
patch(f"{PKG}._echo"),
|
|
patch("builtins.input", return_value=""),
|
|
):
|
|
cmd_buy_dlc(config, state)
|
|
|
|
def test_reblock_fails(self) -> None:
|
|
state = State(current_app_id=1, current_game_name="G")
|
|
config = Config(block_store=True)
|
|
with (
|
|
patch(f"{PKG}.unblock_store", return_value=True),
|
|
patch(f"{PKG}.block_store", return_value=False),
|
|
patch(f"{PKG}._echo") as mock_echo,
|
|
patch("builtins.input", return_value=""),
|
|
):
|
|
cmd_buy_dlc(config, state)
|
|
assert any("Warning" in str(c) for c in mock_echo.call_args_list)
|
|
|
|
def test_no_reblock(self) -> None:
|
|
state = State(current_app_id=1, current_game_name="G")
|
|
config = Config(block_store=False)
|
|
with (
|
|
patch(f"{PKG}.unblock_store", return_value=True),
|
|
patch(f"{PKG}._echo"),
|
|
patch("builtins.input", return_value=""),
|
|
):
|
|
cmd_buy_dlc(config, state)
|
|
|
|
|
|
class TestCmdReset:
|
|
"""Tests for cmd_reset."""
|
|
|
|
def test_normal_reset(self) -> None:
|
|
state = State(current_app_id=1, current_game_name="G", finished_app_ids=[1])
|
|
with (
|
|
patch(f"{PKG}.unblock_store"),
|
|
patch(f"{PKG}.get_all_owned_app_ids", return_value=[1, 2]),
|
|
patch(f"{PKG}.unhide_all_games", return_value=2),
|
|
patch(f"{PKG}._echo"),
|
|
patch.object(State, "save"),
|
|
):
|
|
cmd_reset(Config(), state)
|
|
assert state.current_app_id is None
|
|
assert state.finished_app_ids == []
|
|
|
|
def test_unhide_fails(self) -> None:
|
|
state = State(current_app_id=1)
|
|
with (
|
|
patch(f"{PKG}.unblock_store"),
|
|
patch(
|
|
f"{PKG}.get_all_owned_app_ids",
|
|
side_effect=OSError("fail"),
|
|
),
|
|
patch(f"{PKG}._echo"),
|
|
patch.object(State, "save"),
|
|
):
|
|
cmd_reset(Config(), state)
|
|
|
|
def test_unhide_returns_zero(self) -> None:
|
|
state = State(current_app_id=1)
|
|
with (
|
|
patch(f"{PKG}.unblock_store"),
|
|
patch(f"{PKG}.get_all_owned_app_ids", return_value=[1, 2]),
|
|
patch(f"{PKG}.unhide_all_games", return_value=0),
|
|
patch(f"{PKG}._echo"),
|
|
patch.object(State, "save"),
|
|
):
|
|
cmd_reset(Config(), state)
|
|
|
|
def test_no_owned_ids(self) -> None:
|
|
state = State(current_app_id=1)
|
|
with (
|
|
patch(f"{PKG}.unblock_store"),
|
|
patch(f"{PKG}.get_all_owned_app_ids", return_value=[]),
|
|
patch(f"{PKG}._echo"),
|
|
patch.object(State, "save"),
|
|
):
|
|
cmd_reset(Config(), state)
|
|
|
|
|
|
class TestCmdInstalled:
|
|
"""Tests for cmd_installed."""
|
|
|
|
def test_shows_games(self) -> None:
|
|
with (
|
|
patch(
|
|
f"{PKG}.get_installed_games",
|
|
return_value=[(440, "TF2"), (228980, "RT")],
|
|
),
|
|
patch(f"{PKG}.is_protected_app", side_effect=lambda aid: aid == 228980),
|
|
patch(f"{PKG}._echo"),
|
|
):
|
|
cmd_installed(Config(), State(current_app_id=440))
|
|
|
|
|
|
class TestCmdUninstall:
|
|
"""Tests for cmd_uninstall."""
|
|
|
|
def test_no_game(self) -> None:
|
|
with patch(f"{PKG}._echo") as mock_echo:
|
|
cmd_uninstall(Config(), State())
|
|
assert any("No game" in str(c) for c in mock_echo.call_args_list)
|
|
|
|
def test_nothing_to_remove(self) -> None:
|
|
state = State(current_app_id=440)
|
|
with (
|
|
patch(f"{PKG}.get_installed_games", return_value=[(440, "TF2")]),
|
|
patch(f"{PKG}._echo"),
|
|
):
|
|
cmd_uninstall(Config(), state)
|
|
|
|
def test_confirms_yes(self) -> None:
|
|
state = State(current_app_id=440)
|
|
with (
|
|
patch(
|
|
f"{PKG}.get_installed_games",
|
|
return_value=[(440, "TF2"), (730, "CS")],
|
|
),
|
|
patch(f"{PKG}.uninstall_other_games", return_value=1),
|
|
patch("builtins.input", return_value="YES"),
|
|
patch(f"{PKG}._echo"),
|
|
):
|
|
cmd_uninstall(Config(), state)
|
|
|
|
def test_aborts(self) -> None:
|
|
state = State(current_app_id=440)
|
|
with (
|
|
patch(
|
|
f"{PKG}.get_installed_games",
|
|
return_value=[(440, "TF2"), (730, "CS")],
|
|
),
|
|
patch("builtins.input", return_value="no"),
|
|
patch(f"{PKG}._echo") as mock_echo,
|
|
):
|
|
cmd_uninstall(Config(), state)
|
|
assert any("Aborted" in str(c) for c in mock_echo.call_args_list)
|
|
|
|
|
|
class TestCmdSetup:
|
|
"""Tests for cmd_setup."""
|
|
|
|
def test_calls_interactive(self) -> None:
|
|
with patch(f"{PKG}.interactive_setup") as mock_setup:
|
|
cmd_setup(Config(), State())
|
|
mock_setup.assert_called_once()
|
|
|
|
|
|
class TestCmdInstall:
|
|
"""Tests for cmd_install."""
|
|
|
|
def test_no_game(self) -> None:
|
|
with patch(f"{PKG}._echo") as mock_echo:
|
|
cmd_install(Config(), State())
|
|
assert any("No game" in str(c) for c in mock_echo.call_args_list)
|
|
|
|
def test_already_installed(self) -> None:
|
|
state = State(current_app_id=1, current_game_name="G")
|
|
with (
|
|
patch(f"{PKG}.is_game_installed", return_value=True),
|
|
patch(f"{PKG}._echo"),
|
|
):
|
|
cmd_install(Config(), state)
|
|
|
|
def test_installs_ok(self) -> None:
|
|
state = State(current_app_id=1, current_game_name="G")
|
|
with (
|
|
patch(f"{PKG}.is_game_installed", return_value=False),
|
|
patch(f"{PKG}.install_game", return_value=True),
|
|
patch(f"{PKG}._echo"),
|
|
):
|
|
cmd_install(Config(steam_id="i"), state)
|
|
|
|
def test_install_fails(self) -> None:
|
|
state = State(current_app_id=1, current_game_name="G")
|
|
with (
|
|
patch(f"{PKG}.is_game_installed", return_value=False),
|
|
patch(f"{PKG}.install_game", return_value=False),
|
|
patch(f"{PKG}._echo"),
|
|
):
|
|
cmd_install(Config(steam_id="i"), state)
|
|
|
|
|
|
class TestCmdHide:
|
|
"""Tests for cmd_hide."""
|
|
|
|
def test_no_game(self) -> None:
|
|
with patch(f"{PKG}._echo"):
|
|
cmd_hide(Config(), State())
|
|
|
|
def test_no_owned(self) -> None:
|
|
state = State(current_app_id=1, current_game_name="G")
|
|
with (
|
|
patch(f"{PKG}.get_all_owned_app_ids", return_value=[]),
|
|
patch(f"{PKG}._echo"),
|
|
):
|
|
cmd_hide(Config(), state)
|
|
|
|
def test_hides(self) -> None:
|
|
state = State(current_app_id=1, current_game_name="G")
|
|
with (
|
|
patch(f"{PKG}.get_all_owned_app_ids", return_value=[1, 2]),
|
|
patch(f"{PKG}.hide_other_games", return_value=1),
|
|
patch(f"{PKG}._echo"),
|
|
):
|
|
cmd_hide(Config(), state)
|
|
|
|
def test_hides_zero(self) -> None:
|
|
state = State(current_app_id=1, current_game_name="G")
|
|
with (
|
|
patch(f"{PKG}.get_all_owned_app_ids", return_value=[1]),
|
|
patch(f"{PKG}.hide_other_games", return_value=0),
|
|
patch(f"{PKG}._echo"),
|
|
):
|
|
cmd_hide(Config(), state)
|
|
|
|
|
|
class TestCmdUnhide:
|
|
"""Tests for cmd_unhide."""
|
|
|
|
def test_no_owned(self) -> None:
|
|
with (
|
|
patch(f"{PKG}.get_all_owned_app_ids", return_value=[]),
|
|
patch(f"{PKG}._echo"),
|
|
):
|
|
cmd_unhide(Config(), State())
|
|
|
|
def test_unhides(self) -> None:
|
|
with (
|
|
patch(f"{PKG}.get_all_owned_app_ids", return_value=[1]),
|
|
patch(f"{PKG}.unhide_all_games", return_value=1),
|
|
patch(f"{PKG}._echo"),
|
|
):
|
|
cmd_unhide(Config(), State())
|
|
|
|
def test_unhides_zero(self) -> None:
|
|
with (
|
|
patch(f"{PKG}.get_all_owned_app_ids", return_value=[1]),
|
|
patch(f"{PKG}.unhide_all_games", return_value=0),
|
|
patch(f"{PKG}._echo"),
|
|
):
|
|
cmd_unhide(Config(), State())
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────
|
|
# cmd_add_exception
|
|
# ──────────────────────────────────────────────────────────────
|
|
|
|
_VALID_REASON = "I need this game installed for a work presentation this week."
|
|
|
|
|
|
class TestCmdAddException:
|
|
def test_no_args_prints_usage_and_exits(self) -> None:
|
|
with (
|
|
patch(f"{PKG}._echo"),
|
|
pytest.raises(SystemExit, match="1"),
|
|
):
|
|
cmd_add_exception([])
|
|
|
|
def test_missing_reason_flag_exits(self) -> None:
|
|
with (
|
|
patch(f"{PKG}._echo"),
|
|
pytest.raises(SystemExit, match="1"),
|
|
):
|
|
cmd_add_exception(["440", "no", "flag"])
|
|
|
|
def test_non_numeric_app_id_exits(self) -> None:
|
|
with (
|
|
patch(f"{PKG}._echo"),
|
|
pytest.raises(SystemExit, match="1"),
|
|
):
|
|
cmd_add_exception(["notanumber", "--reason", _VALID_REASON])
|
|
|
|
def test_reason_flag_with_no_value_exits(self) -> None:
|
|
with (
|
|
patch(f"{PKG}._echo"),
|
|
pytest.raises(SystemExit, match="1"),
|
|
):
|
|
cmd_add_exception(["440", "--reason"])
|
|
|
|
def test_reason_flag_last_position_with_no_value_exits(self) -> None:
|
|
# 3 args passes the len/flag guard but --reason is last so reason_parts=[]
|
|
with (
|
|
patch(f"{PKG}._echo"),
|
|
pytest.raises(SystemExit, match="1"),
|
|
):
|
|
cmd_add_exception(["440", "extra", "--reason"])
|
|
|
|
def test_invalid_reason_exits(self) -> None:
|
|
with (
|
|
patch(f"{PKG}._echo"),
|
|
pytest.raises(SystemExit, match="1"),
|
|
):
|
|
cmd_add_exception(["440", "--reason", "too short"])
|
|
|
|
def test_add_pending_exception_raises_value_error(self) -> None:
|
|
with (
|
|
patch(f"{PKG}._echo"),
|
|
patch(
|
|
f"{PKG}.add_pending_exception",
|
|
side_effect=ValueError("already approved"),
|
|
),
|
|
pytest.raises(SystemExit, match="1"),
|
|
):
|
|
cmd_add_exception(["440", "--reason", _VALID_REASON])
|
|
|
|
def test_happy_path_no_pending(self) -> None:
|
|
with (
|
|
patch(f"{PKG}._echo") as mock_echo,
|
|
patch(
|
|
f"{PKG}.add_pending_exception",
|
|
return_value="Exception requested for AppID 440.",
|
|
),
|
|
patch(f"{PKG}.list_pending_exceptions", return_value=[]),
|
|
):
|
|
cmd_add_exception(["440", "--reason", _VALID_REASON])
|
|
mock_echo.assert_called()
|
|
|
|
def test_happy_path_with_pending_list(self) -> None:
|
|
now = time.time()
|
|
pending = [
|
|
{"app_id": 440, "requested_at": now - WHITELIST_COOLDOWN_SECONDS - 1},
|
|
{"app_id": 730, "requested_at": now},
|
|
]
|
|
with (
|
|
patch(f"{PKG}._echo") as mock_echo,
|
|
patch(
|
|
f"{PKG}.add_pending_exception",
|
|
return_value="Exception requested for AppID 440.",
|
|
),
|
|
patch(f"{PKG}.list_pending_exceptions", return_value=pending),
|
|
):
|
|
cmd_add_exception(["440", "--reason", _VALID_REASON])
|
|
# At least the "Pending exceptions" line should be echoed
|
|
calls = [str(c) for c in mock_echo.call_args_list]
|
|
assert any("Pending" in s for s in calls)
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────
|
|
# main() dispatch to add-exception
|
|
# ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestMainDispatchAddException:
|
|
def test_dispatches_add_exception(self) -> None:
|
|
argv = ["prog", "add-exception", "440", "--reason", _VALID_REASON]
|
|
with (
|
|
patch.object(sys, "argv", argv),
|
|
patch(f"{PKG}.cmd_add_exception") as mock_cmd,
|
|
):
|
|
main()
|
|
mock_cmd.assert_called_once_with(["440", "--reason", _VALID_REASON])
|