mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-11 22:53:48 +01:00
Tests are now linted by the 10.00 gate (ignore-paths dropped), with no per-test disables. CtlServer/DaemonControl members the tests drive are public (renamed, behaviour unchanged); SteamAPIClient._get takes its params explicitly, as every caller already did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
175 lines
6.3 KiB
Python
175 lines
6.3 KiB
Python
"""Tests for enforcer module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
from unittest.mock import patch
|
|
|
|
from steam_backlog_enforcer.enforcer import (
|
|
enforce_allowed_game,
|
|
kill_process,
|
|
send_notification,
|
|
)
|
|
|
|
|
|
class TestEnforceAllowedGame:
|
|
"""Tests for enforce_allowed_game."""
|
|
|
|
def test_no_violations(self) -> None:
|
|
"""Only the allowed game running means no violations."""
|
|
with patch(
|
|
"steam_backlog_enforcer.enforcer.get_running_steam_game_pids",
|
|
return_value={100: 440},
|
|
):
|
|
result = enforce_allowed_game({440})
|
|
assert result == []
|
|
|
|
def test_kills_unauthorized(self) -> None:
|
|
"""An unauthorized game is reported and killed."""
|
|
with (
|
|
patch(
|
|
"steam_backlog_enforcer.enforcer.get_running_steam_game_pids",
|
|
return_value={100: 570, 200: 440},
|
|
),
|
|
patch("steam_backlog_enforcer.enforcer.kill_process") as mock_kill,
|
|
):
|
|
result = enforce_allowed_game({440}, kill_unauthorized=True)
|
|
assert result == [(100, 570)]
|
|
mock_kill.assert_called_once_with(100, 570)
|
|
|
|
def test_skips_app_id_zero(self) -> None:
|
|
"""Processes with app id 0 are not games and are ignored."""
|
|
with patch(
|
|
"steam_backlog_enforcer.enforcer.get_running_steam_game_pids",
|
|
return_value={100: 0},
|
|
):
|
|
result = enforce_allowed_game({440})
|
|
assert result == []
|
|
|
|
def test_detects_without_killing(self) -> None:
|
|
"""With killing off, violations are reported but left running."""
|
|
with patch(
|
|
"steam_backlog_enforcer.enforcer.get_running_steam_game_pids",
|
|
return_value={100: 570},
|
|
):
|
|
result = enforce_allowed_game({440}, kill_unauthorized=False)
|
|
assert result == [(100, 570)]
|
|
|
|
def test_allowed_none(self) -> None:
|
|
"""With nothing running, an empty allow set finds no violations."""
|
|
result = enforce_allowed_game(set(), kill_unauthorized=True)
|
|
assert result == []
|
|
|
|
def test_skips_protected_app_id(self) -> None:
|
|
"""Protected IDs must never be killed even if not the assigned game."""
|
|
with (
|
|
patch(
|
|
"steam_backlog_enforcer.enforcer.get_running_steam_game_pids",
|
|
return_value={100: 1331550, 200: 440},
|
|
),
|
|
patch(
|
|
"steam_backlog_enforcer.enforcer.is_protected_app",
|
|
side_effect=lambda aid: aid == 1331550,
|
|
),
|
|
patch("steam_backlog_enforcer.enforcer.kill_process") as mock_kill,
|
|
):
|
|
result = enforce_allowed_game({440}, kill_unauthorized=True)
|
|
assert result == []
|
|
mock_kill.assert_not_called()
|
|
|
|
|
|
class TestKillProcess:
|
|
"""Tests for kill_process."""
|
|
|
|
def test_kill_success(self) -> None:
|
|
"""A live process gets signalled once."""
|
|
with patch("steam_backlog_enforcer.enforcer.os.kill") as mock_kill:
|
|
kill_process(123, 440)
|
|
mock_kill.assert_called_once()
|
|
|
|
def test_process_already_gone(self) -> None:
|
|
"""An already-exited process does not raise."""
|
|
with patch(
|
|
"steam_backlog_enforcer.enforcer.os.kill",
|
|
side_effect=ProcessLookupError,
|
|
):
|
|
kill_process(123, 440) # Should not raise
|
|
|
|
def test_permission_error(self) -> None:
|
|
"""A permission error while killing does not raise."""
|
|
with patch(
|
|
"steam_backlog_enforcer.enforcer.os.kill",
|
|
side_effect=PermissionError,
|
|
):
|
|
kill_process(123, 440) # Should not raise
|
|
|
|
|
|
class TestSendNotification:
|
|
"""Tests for send_notification."""
|
|
|
|
def test_sends(self) -> None:
|
|
"""A notification runs notify-send once."""
|
|
with patch("steam_backlog_enforcer.enforcer.subprocess.run") as mock_run:
|
|
send_notification("Title", "Body")
|
|
mock_run.assert_called_once()
|
|
|
|
def test_handles_missing_notify_send(self) -> None:
|
|
"""A missing notify-send binary does not raise."""
|
|
with patch(
|
|
"steam_backlog_enforcer.enforcer.subprocess.run",
|
|
side_effect=FileNotFoundError,
|
|
):
|
|
send_notification("Title", "Body") # Should not raise
|
|
|
|
def test_handles_os_error(self) -> None:
|
|
"""An OS error from notify-send does not raise."""
|
|
with patch(
|
|
"steam_backlog_enforcer.enforcer.subprocess.run",
|
|
side_effect=OSError,
|
|
):
|
|
send_notification("Title", "Body") # Should not raise
|
|
|
|
|
|
class TestSendNotificationPrivilegeDrop:
|
|
"""As root, notifications must be delivered into the user's session bus.
|
|
|
|
A bare root ``notify-send`` has no DBUS_SESSION_BUS_ADDRESS and silently
|
|
reached nobody, for every caller of send_notification.
|
|
"""
|
|
|
|
_PKG = "steam_backlog_enforcer.enforcer"
|
|
_ENV = "steam_backlog_enforcer._desktop_env"
|
|
|
|
def test_root_drops_to_desktop_user(self) -> None:
|
|
"""As root, notify-send runs as the desktop user on their session bus."""
|
|
with (
|
|
patch(f"{self._ENV}.os.geteuid", return_value=0),
|
|
patch(f"{self._ENV}.desktop_uid", return_value=1000),
|
|
patch(f"{self._PKG}.resolve_desktop_user", return_value="kuhy"),
|
|
patch(f"{self._PKG}.subprocess.run") as mock_run,
|
|
):
|
|
send_notification("Title", "Body")
|
|
|
|
argv = mock_run.call_args[0][0]
|
|
assert argv[:4] == ["sudo", "-u", "kuhy", "env"]
|
|
assert "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus" in argv
|
|
|
|
def test_non_root_does_not_wrap(self) -> None:
|
|
"""A non-root caller runs notify-send directly."""
|
|
with (
|
|
patch(f"{self._ENV}.os.geteuid", return_value=1000),
|
|
patch(f"{self._PKG}.resolve_desktop_user", return_value="kuhy"),
|
|
patch(f"{self._PKG}.subprocess.run") as mock_run,
|
|
):
|
|
send_notification("Title", "Body")
|
|
|
|
assert mock_run.call_args[0][0][0] != "sudo"
|
|
|
|
def test_handles_timeout(self) -> None:
|
|
"""sudo can block where a bare notify-send would not."""
|
|
with patch(
|
|
f"{self._PKG}.subprocess.run",
|
|
side_effect=subprocess.TimeoutExpired("cmd", 5),
|
|
):
|
|
send_notification("Title", "Body") # Should not raise
|