steam-backlog-enforcer/steam_backlog_enforcer/tests/conftest.py
Krzysztof kuhy Rudnicki d8ebbf978b Split _actions, _whitelist, _cmd_done and hltb under the cap
Each keeps its name and serves the moved names through a module __getattr__
(PEP 562) rather than a top-level re-export: every child imports back from
its parent, so a plain import would be circular. The lookup goes through
importlib.import_module, which is a call rather than an import statement,
so it needs no linter suppression. Return type is a _Reexport alias -- mypy
rejects `object` as not callable at the call sites, ruff rejects a bare Any.

Also fixes two leaks where tests wrote to the user's real config, both
caused by a redirected constant moving to a new module while conftest kept
patching the old one:

- APPROVED_EXCEPTIONS_FILE / EXCEPTION_AUDIT_LOG moved to _whitelist_locking;
  the suite had appended 27 fixture entries to the live exception_audit.log.
- _OWNED_IDS_CACHE_FILE was never redirected at all: it is derived from
  CONFIG_DIR at import time, so it captured the real path before the fixture
  could patch it, and the suite overwrote the user's owned-games cache.

Both are now verified by md5summing ~/.config/steam_backlog_enforcer before
and after a full run, which is the only check that actually catches this --
the tests pass either way.

1347 tests, 100.00% coverage.

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

204 lines
8.1 KiB
Python

"""Safety conftest: prevent tests from touching real Steam/config files.
Redirects all filesystem paths used by the steam_backlog_enforcer package
to temporary directories. This stops tests from accidentally:
- Deleting real game files via uninstall_other_games / uninstall_game
- Overwriting ~/.config/steam_backlog_enforcer/state.json (losing the
user's current assignment)
- Reading real appmanifest files from ~/.local/share/Steam/steamapps
- Modifying /etc/hosts via the store blocker
- Corrupting the HLTB cache on disk
- Launching real Steam or calling real subprocess commands
- Deleting real ~/.steam, ~/.local/share/Steam, etc. via the total-block
Steam/Proton remnant purge
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
import pytest
# Imported for its autouse side effect: naming it here registers it.
from steam_backlog_enforcer.tests._isolate_playtime import _isolate_playtime
from steam_backlog_enforcer.tests._no_subprocess import _block_real_subprocesses
# Re-exported so ruff --fix does not delete the imports above: pytest
# registers autouse fixtures by name, so they look unused to the linter.
__all__ = ["_block_real_subprocesses", "_isolate_playtime"]
from steam_backlog_enforcer.tests._total_block_paths import (
BLOCK,
HOSTS,
IPTABLES,
PURGE,
Paths,
build_paths,
)
if TYPE_CHECKING:
from collections.abc import Iterator
from pathlib import Path
@pytest.fixture(autouse=True)
def _isolate_filesystem(tmp_path: Path) -> Iterator[None]:
"""Redirect all real filesystem paths to a temporary directory.
Individual tests that also patch these paths will simply override
this fixture's patches for the duration of their own ``with`` block.
"""
fake_config = tmp_path / "config"
fake_config.mkdir()
fake_steamapps = tmp_path / "steamapps"
fake_steamapps.mkdir()
fake_hosts = tmp_path / "hosts"
with (
# Config / state / snapshot paths (used by State.save, Config.save, etc.)
patch(
"steam_backlog_enforcer.config.CONFIG_DIR",
fake_config,
),
patch(
"steam_backlog_enforcer.config.CONFIG_FILE",
fake_config / "config.json",
),
patch(
"steam_backlog_enforcer.config.STATE_FILE",
fake_config / "state.json",
),
patch(
"steam_backlog_enforcer.config.SNAPSHOT_FILE",
fake_config / "snapshot.json",
),
# Steam game manifests / install dirs. STEAMAPPS_PATH is canonical in
# _steam_state; game_install imports it, so both bindings need the
# patch (same gotcha as HOSTS_FILE below).
patch(
"steam_backlog_enforcer._steam_state.STEAMAPPS_PATH",
fake_steamapps,
),
patch(
"steam_backlog_enforcer.game_install.STEAMAPPS_PATH",
fake_steamapps,
),
# HLTB cache file (computed at import time from CONFIG_DIR, so
# patching CONFIG_DIR alone does not redirect it)
patch(
"steam_backlog_enforcer._hltb_types.HLTB_CACHE_FILE",
fake_config / "hltb_cache.json",
),
# /etc/hosts (store blocker + total block - each module has its own
# `from ... import HOSTS_FILE` binding, so each needs its own patch)
patch(
"steam_backlog_enforcer.store_blocker.HOSTS_FILE",
fake_hosts,
),
patch(
"steam_backlog_enforcer._total_block_hosts.HOSTS_FILE",
fake_hosts,
),
patch(
"steam_backlog_enforcer.config.HOSTS_FILE",
fake_hosts,
),
# Total-block lock + IP cache (computed at import time from
# CONFIG_DIR, so patching CONFIG_DIR alone does not redirect them -
# same gotcha as HLTB_CACHE_FILE above. A real total-block lock may
# be active on the host machine; tests must never touch it.)
patch(
"steam_backlog_enforcer._total_block.TOTAL_BLOCK_LOCK_FILE",
fake_config / "total_block_lock.json",
),
patch(
"steam_backlog_enforcer._total_block_iptables._IPTABLES_IP_CACHE_FILE",
fake_config / "total_block_ip_cache.json",
),
patch(
"steam_backlog_enforcer._total_block_purge._STEAM_PURGE_LOG_FILE",
fake_config / "total_block_purge_log.json",
),
# Steam/Proton remnant paths (real ~/.steam, ~/.local/share/Steam,
# etc. - _remove_steam_remnants() deletes these, so tests must never
# see the real ones)
patch(
"steam_backlog_enforcer._total_block_purge._STEAM_REMNANT_PATHS",
(
tmp_path / "fake_home" / ".steam",
tmp_path / "fake_home" / "steam",
tmp_path / "fake_home" / ".local" / "share" / "Steam",
tmp_path / "fake_home" / ".steampath",
tmp_path / "fake_home" / ".steampid",
tmp_path / "fake_home" / ".config" / "steamtinkerlaunch",
tmp_path / "fake_home" / ".config" / "CSDSteamBuild",
),
),
# Derived from CONFIG_DIR at import time, so it captures the REAL path
# before this fixture redirects CONFIG_DIR. Without it the suite
# rewrites the user's owned-games cache.
patch(
"steam_backlog_enforcer._owned_apps_cache._OWNED_IDS_CACHE_FILE",
fake_config / "owned_app_ids_cache.json",
),
# Whitelist exception files (module-level constants; these live in
# _whitelist_locking, which is where the code that reads them is)
patch(
"steam_backlog_enforcer._whitelist_locking.APPROVED_EXCEPTIONS_FILE",
fake_config / "approved_exceptions.json",
),
patch(
"steam_backlog_enforcer._whitelist_locking.EXCEPTION_AUDIT_LOG",
fake_config / "exception_audit.log",
),
# _enforce_loop imports CONFIG_FILE directly; patch the local binding so
# lock_enforcement_files() uses the tmp path instead of the real one.
patch(
"steam_backlog_enforcer._enforce_loop.CONFIG_FILE",
fake_config / "config.json",
),
):
yield
@pytest.fixture(autouse=True)
def _no_real_sleep() -> Iterator[None]:
"""No-op every ``time.sleep`` used by the package.
Several modules call ``time.sleep`` for Steam-launch / install-retry /
rate-limit pacing. Individual tests that need to observe sleep
behaviour can override these patches inside their own ``with`` block.
"""
noop = MagicMock()
with (
patch("steam_backlog_enforcer._steam_client.time.sleep", noop),
patch("steam_backlog_enforcer._steam_launch.time.sleep", noop),
patch("steam_backlog_enforcer._steam_api_client.time.sleep", noop),
patch("steam_backlog_enforcer._enforce_loop.time.sleep", noop),
):
yield
# ──────────────────────────────────────────────────────────────
# Total-block path redirection
#
# Lives here rather than in a helper module because pytest resolves
# fixtures by name: an imported fixture looks unused to ruff, and this
# repo runs `ruff --fix --unsafe-fixes`, which deletes the import.
# ──────────────────────────────────────────────────────────────
@pytest.fixture(autouse=True)
def total_block_paths(tmp_path: Path) -> Iterator[Paths]:
"""Redirect every total-block path constant to tmp_path for one test."""
built = build_paths(tmp_path)
with (
patch(f"{BLOCK}.TOTAL_BLOCK_LOCK_FILE", built.lock_file),
patch(f"{IPTABLES}._IPTABLES_IP_CACHE_FILE", built.ip_cache_file),
patch(f"{HOSTS}.HOSTS_FILE", built.hosts_file),
patch(f"{PURGE}._STEAM_PURGE_LOG_FILE", built.purge_log_file),
patch(f"{PURGE}._STEAM_REMNANT_PATHS", built.remnant_paths),
):
yield built