steam-backlog-enforcer/steam_backlog_enforcer/tests/test_enforcer.py
Krzysztof kuhy Rudnicki d2565416aa Pylint gate covers tests, no suppressions
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>
2026-11-11 12:17:07 +01:00

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