mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-11 18:13:49 +01:00
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
217 lines
9.3 KiB
Python
217 lines
9.3 KiB
Python
"""Tests for the main CLI: shared lock helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from steam_backlog_enforcer._total_block import TotalBlockStatus
|
|
from steam_backlog_enforcer.config import State
|
|
from steam_backlog_enforcer.main import (
|
|
_TOTAL_BLOCK_EXEMPT_COMMANDS,
|
|
_enforce_manual_pick_lock,
|
|
_enforce_total_block_lock,
|
|
_is_manual_pick_locked,
|
|
_show_manual_pick_lock_message,
|
|
_show_total_block_lock_message,
|
|
)
|
|
from steam_backlog_enforcer.tests._main_helpers import (
|
|
ACTIVE_STATUS,
|
|
EXPIRED_AT,
|
|
STARTED_AT,
|
|
locked_state,
|
|
)
|
|
|
|
PKG = "steam_backlog_enforcer.main._shared"
|
|
|
|
|
|
class TestIsManualPickLocked:
|
|
def test_no_manual_pick_not_locked(self) -> None:
|
|
assert _is_manual_pick_locked(State()) is False
|
|
|
|
def test_game_finished_not_locked(self) -> None:
|
|
state = locked_state(app_id=100)
|
|
state.finished_app_ids = [100]
|
|
assert _is_manual_pick_locked(state) is False
|
|
|
|
def test_deadline_passed_not_locked(self) -> None:
|
|
state = locked_state(started_at=EXPIRED_AT)
|
|
assert _is_manual_pick_locked(state) is False
|
|
|
|
def test_active_lock_returns_true(self) -> None:
|
|
state = locked_state(started_at=STARTED_AT)
|
|
assert _is_manual_pick_locked(state) is True
|
|
|
|
def test_no_started_at_stays_locked(self) -> None:
|
|
# Missing timestamp → cannot determine deadline → stays locked.
|
|
state = locked_state(started_at="")
|
|
assert _is_manual_pick_locked(state) is True
|
|
|
|
def test_invalid_started_at_stays_locked(self) -> None:
|
|
state = locked_state(started_at="not-a-date")
|
|
assert _is_manual_pick_locked(state) is True
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────
|
|
# _show_manual_pick_lock_message
|
|
# ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestShowManualPickLockMessage:
|
|
def test_shows_game_info(self) -> None:
|
|
state = locked_state(app_id=42, name="MyGame", started_at=STARTED_AT)
|
|
with patch(f"{PKG}._echo") as mock_echo:
|
|
_show_manual_pick_lock_message(state)
|
|
output = " ".join(str(c) for c in mock_echo.call_args_list)
|
|
assert "MyGame" in output
|
|
assert "42" in output
|
|
|
|
def test_shows_deadline_when_started_at_valid(self) -> None:
|
|
state = locked_state(started_at=STARTED_AT)
|
|
with patch(f"{PKG}._echo") as mock_echo:
|
|
_show_manual_pick_lock_message(state)
|
|
output = " ".join(str(c) for c in mock_echo.call_args_list)
|
|
assert "Deadline" in output
|
|
|
|
def test_no_crash_on_invalid_started_at(self) -> None:
|
|
state = locked_state(started_at="bad-date")
|
|
with patch(f"{PKG}._echo"):
|
|
_show_manual_pick_lock_message(state) # must not raise
|
|
|
|
def test_no_crash_on_empty_started_at(self) -> None:
|
|
state = locked_state(started_at="")
|
|
with patch(f"{PKG}._echo"):
|
|
_show_manual_pick_lock_message(state) # must not raise
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────
|
|
# _enforce_manual_pick_lock
|
|
# ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestEnforceManualPickLock:
|
|
def test_no_lock_passes(self) -> None:
|
|
_enforce_manual_pick_lock("scan", State()) # no exit
|
|
|
|
def test_exempt_command_passes_while_locked(self) -> None:
|
|
state = locked_state()
|
|
_enforce_manual_pick_lock("done", state) # no exit
|
|
_enforce_manual_pick_lock("status", state) # no exit
|
|
|
|
def test_blocked_command_exits(self) -> None:
|
|
state = locked_state()
|
|
with (
|
|
patch(f"{PKG}._show_manual_pick_lock_message"),
|
|
pytest.raises(SystemExit) as exc_info,
|
|
):
|
|
_enforce_manual_pick_lock("scan", state)
|
|
assert exc_info.value.code == 1
|
|
|
|
def test_add_exception_blocked_when_locked(self) -> None:
|
|
state = locked_state()
|
|
with (
|
|
patch(f"{PKG}._show_manual_pick_lock_message"),
|
|
pytest.raises(SystemExit),
|
|
):
|
|
_enforce_manual_pick_lock("add-exception", state)
|
|
|
|
def test_pick_manual_allowed_when_already_locked(self) -> None:
|
|
# A second pick must be reachable while the first holds the lock; the
|
|
# cap inside cmd_pick_manual is what limits it, not the lock check.
|
|
_enforce_manual_pick_lock("pick-manual", locked_state())
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────
|
|
# _resolve_game_name
|
|
# ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestShowTotalBlockLockMessage:
|
|
def test_shows_remaining_time(self) -> None:
|
|
with patch(f"{PKG}._echo") as mock_echo:
|
|
_show_total_block_lock_message(ACTIVE_STATUS)
|
|
output = " ".join(str(c) for c in mock_echo.call_args_list)
|
|
assert "TOTAL GAMING BLOCK ACTIVE" in output
|
|
assert "Days remaining" in output
|
|
|
|
def test_lists_exempt_commands(self) -> None:
|
|
with patch(f"{PKG}._echo") as mock_echo:
|
|
_show_total_block_lock_message(ACTIVE_STATUS)
|
|
output = " ".join(str(c) for c in mock_echo.call_args_list)
|
|
assert "status" in output
|
|
assert "enforce" in output
|
|
|
|
def test_no_crash_without_until(self) -> None:
|
|
status = TotalBlockStatus(
|
|
active=True, started_at=None, until=None, days=1, days_remaining=0.5
|
|
)
|
|
with patch(f"{PKG}._echo"):
|
|
_show_total_block_lock_message(status) # must not raise
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────
|
|
# _enforce_total_block_lock
|
|
# ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestEnforceTotalBlockLock:
|
|
def test_not_active_passes(self) -> None:
|
|
with patch(f"{PKG}.is_total_block_active", return_value=False):
|
|
_enforce_total_block_lock("scan") # no exit
|
|
|
|
def test_exempt_command_passes_while_active(self) -> None:
|
|
with patch(f"{PKG}.is_total_block_active", return_value=True):
|
|
_enforce_total_block_lock("status") # no exit
|
|
_enforce_total_block_lock("enforce") # no exit
|
|
|
|
def test_blocked_command_exits(self) -> None:
|
|
with (
|
|
patch(f"{PKG}.is_total_block_active", return_value=True),
|
|
patch(f"{PKG}.get_total_block_status", return_value=ACTIVE_STATUS),
|
|
patch(f"{PKG}._show_total_block_lock_message"),
|
|
pytest.raises(SystemExit) as exc_info,
|
|
):
|
|
_enforce_total_block_lock("scan")
|
|
assert exc_info.value.code == 1
|
|
|
|
def test_done_blocked_while_active(self) -> None:
|
|
"""Stricter than the manual-pick lock: even 'done' is blocked."""
|
|
with (
|
|
patch(f"{PKG}.is_total_block_active", return_value=True),
|
|
patch(f"{PKG}.get_total_block_status", return_value=ACTIVE_STATUS),
|
|
patch(f"{PKG}._show_total_block_lock_message"),
|
|
pytest.raises(SystemExit),
|
|
):
|
|
_enforce_total_block_lock("done")
|
|
|
|
def test_add_exception_blocked_while_active(self) -> None:
|
|
with (
|
|
patch(f"{PKG}.is_total_block_active", return_value=True),
|
|
patch(f"{PKG}.get_total_block_status", return_value=ACTIVE_STATUS),
|
|
patch(f"{PKG}._show_total_block_lock_message"),
|
|
pytest.raises(SystemExit),
|
|
):
|
|
_enforce_total_block_lock("add-exception")
|
|
|
|
def test_exempt_set_is_stricter_than_manual_pick(self) -> None:
|
|
"""Pinned exactly: widening this set weakens the total block.
|
|
|
|
gaming-unblock is present because a playtime bind mount makes the
|
|
total block's own `pacman -R steam` fail EBUSY - it has to stay
|
|
reachable exactly when the two collide. gaming-reset is deliberately
|
|
absent: it would shorten enforcement. serve changes nothing itself
|
|
(each job it starts is lock-checked) and refusing it took the web UI
|
|
down for the whole block.
|
|
"""
|
|
assert (
|
|
frozenset({"status", "enforce", "gaming-status", "gaming-unblock", "serve"})
|
|
== _TOTAL_BLOCK_EXEMPT_COMMANDS
|
|
)
|
|
assert "gaming-reset" not in _TOTAL_BLOCK_EXEMPT_COMMANDS
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────
|
|
# cmd_block_gaming
|
|
# ──────────────────────────────────────────────────────────────
|