mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-11 12:47:41 +01:00
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
The previous session reported the new code green, but measured it with --no-cov (step 1 of finish_2026_08_24.sh passes that flag), so seven branches in the recovery path had never been exercised. Coverage was 99.78%. Most of the gap was the auto-recovery itself: whether healing succeeded, whether the retry after healing succeeded, and what gets recorded when it does not. That is the code which ran in production for the first time today, and none of its outcomes were pinned. Also covers the donor search on a machine with no ~/.config, siblings whose cache is readable but holds no usable token, and the two healthy paths that must stay silent — an always-on "this may be wrong" panel is one nobody reads. Writing the UI test surfaced that collect_source_findings is never empty for a fresh tmp_path, because an empty sync is itself a finding; the stub is there so the branch under test is "nothing to report". 1115 passed, coverage back to 100%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJmPLyT8NU4DzDJPV1KEX5
179 lines
6.4 KiB
Python
179 lines
6.4 KiB
Python
"""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_token,
|
|
recover_session,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
def _write_cache(path: Path, refresh_token: str, *, expires_at: str) -> None:
|
|
"""Write a credential cache shaped like ``FileCredentialStore``'s."""
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(
|
|
json.dumps(
|
|
{
|
|
"id_token": "stale-id-token",
|
|
"refresh_token": refresh_token,
|
|
"expires_at": expires_at,
|
|
"local_id": "uid-1",
|
|
}
|
|
)
|
|
)
|
|
|
|
|
|
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_token(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_token(tmp_path, skip="screen_locker") is None
|
|
|
|
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_token(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_token(tmp_path, skip="screen_locker") is None
|
|
|
|
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_token(tmp_path / "nope", skip="screen_locker") is None
|
|
)
|
|
|
|
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_token(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()
|