mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:27:13 +01:00
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018RHwVPyiiQMPh4M3JQwQku
111 lines
4.7 KiB
Python
111 lines
4.7 KiB
Python
"""Autouse fixtures whose one job is keeping tests off real user state.
|
|
|
|
Split out of ``conftest.py`` for the 250-line cap, and grouped because they
|
|
answer the same question in two different ways: what stops this suite from
|
|
reading or overwriting the developer's own files? Loaded through
|
|
``pytest_plugins`` in ``conftest.py``, so both stay autouse for every test.
|
|
|
|
The two seams are genuinely different, which is why neither subsumes the
|
|
other. ``_isolate_home`` covers paths resolved at *call* time from
|
|
``Path.home()``; ``_no_free_days_by_default`` covers paths bound into module
|
|
constants at *import* time, which redirecting home afterwards cannot reach.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterator
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _no_free_days_by_default(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Never let the lock chain read the developer's real free-day pool.
|
|
|
|
``_isolate_home`` cannot cover this one. freedays binds its paths from
|
|
``Path.home()`` at *import* time into module constants, so redirecting
|
|
home afterwards leaves them pointing at the real
|
|
``~/.local/share/freedays``. Every writer resolves through
|
|
``resolve_paths``, so that is the seam.
|
|
|
|
Without it, a real free day on this machine would short-circuit the lock
|
|
chain in every test here -- it is the very first check in the chain --
|
|
and the failures would name early-bird and sick-day logic instead.
|
|
"""
|
|
import freedays._api
|
|
|
|
monkeypatch.setattr(
|
|
freedays._api,
|
|
"resolve_paths",
|
|
lambda paths: paths or freedays.Paths.under(tmp_path / "freedays"),
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolate_home(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
|
|
"""Point ``$HOME`` and ``Path.home()`` at tmp_path for every test.
|
|
|
|
``ISOLATED_STATE`` cannot cover this. Everything under ``~/.config`` is
|
|
resolved at *call* time by ``crdt_sync.credential_store_for``
|
|
(``Path.home() / ".config" / app / "firebase_auth.json"``), not bound to a
|
|
module-level constant, so there is no attribute to patch -- the only seam
|
|
is home itself.
|
|
|
|
What this protects is a live credential, not a scratch file. A test that
|
|
reaches ``credential_store_for("screen_locker")`` and saves would overwrite
|
|
the machine's real Firebase session with fixture values, and the damage is
|
|
invisible until the next sync fails with HTTP 401. The same redirect also
|
|
covers the sync PAT and the shared ``~/.config/crdt-sync/`` password and
|
|
OAuth secret, which are read the same way.
|
|
|
|
``Path.home()`` is patched as well as ``$HOME``: it consults ``os.environ``
|
|
on POSIX, but only via ``expanduser``, and a test that patches the
|
|
environment differently must not silently regain access to the real one.
|
|
"""
|
|
monkeypatch.setenv("HOME", str(tmp_path))
|
|
with patch.object(Path, "home", lambda: tmp_path):
|
|
yield
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _no_real_runnerup_webdav_dir(tmp_path: Path) -> Iterator[None]:
|
|
"""Never let the RunnerUp scan read the developer's real WebDAV drop dir.
|
|
|
|
``RUNNERUP_WEBDAV_DIRS`` is bound from ``Path.home()`` at *import* time,
|
|
so ``_isolate_home`` cannot reach it. On this machine the real directory
|
|
holds today's run, which made every "phone absent → not verified" test
|
|
pass verification the day the source was added (2026-09-16). Tests that
|
|
want the local source point the tuple at directories they populate.
|
|
"""
|
|
absent = (tmp_path / "no-runnerup-webdav",)
|
|
with patch("screen_locker._runnerup_verification.RUNNERUP_WEBDAV_DIRS", absent):
|
|
yield
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _no_desktop_notifications(tmp_path: Path) -> Iterator[None]:
|
|
"""Keep the credit path off the real desktop and the real lock file.
|
|
|
|
Every workout credit ends in ``_credit_notify.send_notification``, which
|
|
runs ``notify-send`` -- without this the suite would pop hundreds of
|
|
"Workout credited" bubbles on the developer's screen, and its budget
|
|
sum would read ledgers and log fail-closed warnings into caplog -- so
|
|
``notify_workout_credit`` itself is stubbed. The credit also takes
|
|
``bonus_lock`` on ``_workout_credit.SHUTDOWN_BASE_FILE``, bound at import
|
|
time, so that is redirected too. Tests of the notifier itself capture the
|
|
real function at import time.
|
|
"""
|
|
with (
|
|
patch("screen_locker._credit_notify.notify_workout_credit", return_value=True),
|
|
patch(
|
|
"screen_locker._workout_credit.SHUTDOWN_BASE_FILE",
|
|
tmp_path / "credit_shutdown_base.json",
|
|
),
|
|
):
|
|
yield
|