"""A dead Firebase credential must heal itself, not wait for a human. On 2026-08-24 the PC could not read Firebase because ``screen_locker`` had no cached session and the shared password no longer authenticated. Every sibling app on the same machine -- ``diet_guard``, ``wake_alarm``, ``todo`` -- held a live refresh token for the *same* account and project the whole time. The credential was on disk, three directories away, and nothing looked for it. Recovery is therefore local and needs no human: borrow a sibling's refresh token, mint a session, and cache it under our own app name. """ from __future__ import annotations import json from typing import TYPE_CHECKING from screen_locker._credential_recovery import ( find_sibling_refresh_tokens, recover_session, ) from screen_locker.tests._credential_fixtures import write_cache as _write_cache if TYPE_CHECKING: from pathlib import Path import pytest class TestFindSiblingRefreshToken: """Locating a live credential already present on this machine.""" def test_finds_a_sibling_apps_token(self, tmp_path: Path) -> None: """Any sibling app's cache is a valid source -- one account, one project.""" _write_cache( tmp_path / "diet_guard" / "firebase_auth.json", "refresh-abc", expires_at="2026-08-24T12:00:00+00:00", ) found = find_sibling_refresh_tokens(tmp_path, skip="screen_locker") assert found == [("diet_guard", "refresh-abc")] def test_ignores_our_own_stale_cache(self, tmp_path: Path) -> None: """Our own dead cache is the thing being replaced, never the source.""" _write_cache( tmp_path / "screen_locker" / "firebase_auth.json", "refresh-ours", expires_at="2026-08-24T12:00:00+00:00", ) assert find_sibling_refresh_tokens(tmp_path, skip="screen_locker") == [] def test_skips_unreadable_and_malformed_caches(self, tmp_path: Path) -> None: """A corrupt sibling must not abort the search over the healthy ones.""" bad = tmp_path / "broken_app" / "firebase_auth.json" bad.parent.mkdir(parents=True) bad.write_text("{not json") _write_cache( tmp_path / "wake_alarm" / "firebase_auth.json", "refresh-xyz", expires_at="2026-08-24T12:00:00+00:00", ) found = find_sibling_refresh_tokens(tmp_path, skip="screen_locker") assert found == [("wake_alarm", "refresh-xyz")] def test_reports_nothing_when_no_sibling_has_one(self, tmp_path: Path) -> None: """No source is a real outcome; the caller must hear it, not guess.""" assert find_sibling_refresh_tokens(tmp_path, skip="screen_locker") == [] def test_missing_config_root_is_not_an_error(self, tmp_path: Path) -> None: """A machine with no ~/.config yet has no donor -- and must not crash. Recovery runs unattended inside a lock decision, so an exception here would take the screen for a reason that has nothing to do with working out. """ assert ( find_sibling_refresh_tokens(tmp_path / "nope", skip="screen_locker") == [] ) def test_cache_without_a_usable_token_is_skipped(self, tmp_path: Path) -> None: """Readable JSON is not the same as a usable credential. A sibling mid-logout leaves a well-formed cache whose refresh_token is missing or empty. Returning that would mint nothing and report success. """ for app, payload in ( ("no_token_app", {"expires_at": "2026-08-24T12:00:00+00:00"}), ("empty_token_app", {"refresh_token": ""}), ("wrong_type_app", {"refresh_token": 12345}), ): cache = tmp_path / app / "firebase_auth.json" cache.parent.mkdir(parents=True) cache.write_text(json.dumps(payload)) _write_cache( tmp_path / "zz_healthy" / "firebase_auth.json", "refresh-good", expires_at="2026-08-24T12:00:00+00:00", ) found = find_sibling_refresh_tokens(tmp_path, skip="screen_locker") assert found == [("zz_healthy", "refresh-good")] class TestRecoverSession: """Turning a borrowed refresh token into our own cached session.""" def test_recovers_and_reports_the_donor( self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """A successful recovery must say where the credential came from.""" _write_cache( tmp_path / "diet_guard" / "firebase_auth.json", "refresh-abc", expires_at="2026-08-24T12:00:00+00:00", ) minted: list[str] = [] def _mint(refresh_token: str) -> None: minted.append(refresh_token) result = recover_session( config_root=tmp_path, app_name="screen_locker", mint=_mint ) assert result.recovered is True assert result.donor == "diet_guard" assert minted == ["refresh-abc"] def test_failure_explains_itself( self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """A failed recovery must carry a reason a human can act on.""" def _mint(_refresh_token: str) -> None: message = "TOKEN_EXPIRED" raise RuntimeError(message) _write_cache( tmp_path / "diet_guard" / "firebase_auth.json", "refresh-abc", expires_at="2026-08-24T12:00:00+00:00", ) result = recover_session( config_root=tmp_path, app_name="screen_locker", mint=_mint ) assert result.recovered is False assert "TOKEN_EXPIRED" in result.reason def test_no_donor_is_reported_not_swallowed(self, tmp_path: Path) -> None: """ "Nothing to borrow" must be a stated reason, never a silent False.""" result = recover_session( config_root=tmp_path, app_name="screen_locker", mint=lambda _t: None ) assert result.recovered is False assert "no sibling" in result.reason.lower()