mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-11 13:47:39 +01:00
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
141 lines
4.9 KiB
Python
141 lines
4.9 KiB
Python
"""Recovery keeps trying when a donor credential has gone stale.
|
|
|
|
Split from ``test_credential_recovery.py`` (250-line cap). Covers the two
|
|
behaviours added after 2026-08-27: falling through to the next sibling when
|
|
one refresh token is rejected, and the real token-exchange closure that
|
|
rewrites this machine's cached session.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
from typing import TYPE_CHECKING
|
|
|
|
from screen_locker._credential_recovery import recover_session
|
|
from screen_locker.tests._credential_fixtures import write_cache as _write_cache
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
class TestDonorFallback:
|
|
"""One stale sibling must not defeat a recovery the others could finish."""
|
|
|
|
def test_tries_the_next_sibling_when_the_first_is_rejected(
|
|
self, tmp_path: Path
|
|
) -> None:
|
|
"""A dead donor is skipped, not treated as the final answer.
|
|
|
|
The caches on a real machine are of mixed vintages, so the
|
|
alphabetically-first one is not necessarily live. Stopping at it is
|
|
the same "one dead source blocks the healthy ones" bug this module
|
|
exists to fix.
|
|
"""
|
|
_write_cache(
|
|
tmp_path / "aaa_stale" / "firebase_auth.json",
|
|
"refresh-stale",
|
|
expires_at="2026-08-24T12:00:00+00:00",
|
|
)
|
|
_write_cache(
|
|
tmp_path / "zzz_live" / "firebase_auth.json",
|
|
"refresh-live",
|
|
expires_at="2026-08-24T12:00:00+00:00",
|
|
)
|
|
|
|
def _mint(refresh_token: str) -> None:
|
|
if refresh_token.endswith("-stale"):
|
|
message = "TOKEN_EXPIRED"
|
|
raise RuntimeError(message)
|
|
|
|
result = recover_session(
|
|
config_root=tmp_path, app_name="screen_locker", mint=_mint
|
|
)
|
|
|
|
assert result.recovered is True
|
|
assert result.donor == "zzz_live"
|
|
|
|
def test_reports_every_rejection_when_all_donors_are_dead(
|
|
self, tmp_path: Path
|
|
) -> None:
|
|
"""Exhausting the donors must name them, not just say "failed"."""
|
|
for app in ("aaa_stale", "zzz_stale"):
|
|
_write_cache(
|
|
tmp_path / app / "firebase_auth.json",
|
|
f"refresh-{app}",
|
|
expires_at="2026-08-24T12:00:00+00:00",
|
|
)
|
|
|
|
def _mint(_refresh_token: str) -> None:
|
|
message = "TOKEN_EXPIRED"
|
|
raise RuntimeError(message)
|
|
|
|
result = recover_session(
|
|
config_root=tmp_path, app_name="screen_locker", mint=_mint
|
|
)
|
|
|
|
assert result.recovered is False
|
|
assert "aaa_stale" in result.reason
|
|
assert "zzz_stale" in result.reason
|
|
|
|
|
|
class TestTryRecoverFirebaseSession:
|
|
"""The one impure step: exchanging a borrowed token for our own session."""
|
|
|
|
def test_mints_and_caches_a_session_from_a_sibling(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
"""A successful exchange must persist the session under OUR app name.
|
|
|
|
Exercised end to end (minus the HTTP call) because this closure is
|
|
what actually rewrites ``~/.config/screen_locker/firebase_auth.json``.
|
|
The autouse HOME redirect is what keeps that write inside tmp_path
|
|
instead of clobbering the machine's live credential -- the exact
|
|
accident this suite caused on 2026-08-27.
|
|
"""
|
|
from screen_locker import _sync_client
|
|
|
|
_write_cache(
|
|
tmp_path / ".config" / "diet_guard" / "firebase_auth.json",
|
|
"refresh-from-sibling",
|
|
expires_at="2026-08-27T12:00:00+00:00",
|
|
)
|
|
posted: list[dict[str, str]] = []
|
|
|
|
class _Response:
|
|
def raise_for_status(self) -> None:
|
|
"""Report the exchange as successful."""
|
|
|
|
def json(self) -> dict[str, str]:
|
|
"""Return a freshly minted session."""
|
|
minted = "minted"
|
|
return {
|
|
"id_token": f"{minted}-id-token",
|
|
"refresh_token": f"{minted}-refresh-token",
|
|
"expires_in": "3600",
|
|
}
|
|
|
|
def _post(
|
|
_url: str, *, data: dict[str, str], timeout: float = 0.0
|
|
) -> _Response:
|
|
del timeout
|
|
posted.append(dict(data))
|
|
return _Response()
|
|
|
|
monkeypatch.setattr(_sync_client.requests, "post", _post)
|
|
monkeypatch.setattr(
|
|
_sync_client.FirebaseConfig,
|
|
"load",
|
|
classmethod(lambda _cls: SimpleNamespace(api_key="test-api-key")),
|
|
)
|
|
|
|
result = _sync_client.try_recover_firebase_session()
|
|
|
|
assert result.recovered is True
|
|
assert result.donor == "diet_guard"
|
|
assert posted[0].get("grant_type") == "refresh_token"
|
|
assert "refresh-from-sibling" in posted[0].values()
|
|
written = tmp_path / ".config" / "screen_locker" / "firebase_auth.json"
|
|
assert "minted-refresh-token" in written.read_text()
|