screen-locker/screen_locker/tests/test_workout_sync_part2.py
Krzysztof kuhy Rudnicki 965b9c4ebb Sync: Firebase only — drop the GitHub transport (kuhyx/syncs stays a frozen archive)
The PC push aborted before it ever tried Firebase whenever the GitHub PAT
was missing ("Workouts NOT synced: no sync token"), so once the PAT was lost
this machine published nothing although Firebase was configured and live.
The phone has not written the GitHub repo since 2026-08-21.

- sync_client() / sync_client_or_reason(): firebase_client_for("screen_locker")
  only, proven with can_access_remote(), healed once from a sibling app's
  session, else a warning naming what will not sync (push AND pull) plus a
  recorded degraded source. No config -> warning, no fallback.
- _manual_push and scripts/tombstone_sync_record.py take their client from
  sync_client_or_reason(); the push result carries the reason.
- Deleted: read_sync_token, remote_client, the mirror client, _push_outcome
  (the half-landed-push case no longer exists), SYNC_TOKEN_FILE and
  SYNC_REPO_OWNER/NAME, the sync_token test fixture. Status backend is
  "firebase" or "none".
- crdt-sync pinned to v0.9.2 in pyproject, requirements.txt and
  install_systemd.sh (the installer still said v0.9.0).

kuhyx/syncs/screen-locker-sync is left untouched as an archive; no code
reads it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018RHwVPyiiQMPh4M3JQwQku
2026-10-09 19:18:56 +02:00

108 lines
3.8 KiB
Python

"""Backend selection and the two public pulls, split from ``test_workout_sync``.
``sync_client`` decides WHICH backend is read; ``pull_synced_workout`` and
``pull_all_manual_records`` are what the locker actually calls.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
from crdt_sync import (
ConfigError,
FirebaseAuthError,
FirebaseSyncError,
)
import pytest
from screen_locker import _sync_client, _workout_sync
from screen_locker.tests._workout_sync_fixtures import (
_firebase_config,
)
if TYPE_CHECKING:
from pathlib import Path
class TestSyncClient:
"""Firebase is the only backend: configured, missing, or unusable."""
def test_returns_none_when_nothing_is_configured(self) -> None:
"""Returns none when nothing is configured."""
assert _workout_sync.sync_client() is None
def test_warns_when_nothing_is_configured(
self, caplog: pytest.LogCaptureFixture
) -> None:
"""The warning names BOTH directions that stop syncing."""
with caplog.at_level("WARNING"):
_workout_sync.sync_client()
assert "Sync is OFF" in caplog.text
assert "will NOT be pushed" in caplog.text
assert "NOT be pulled" in caplog.text
def test_reason_names_the_missing_config(self) -> None:
"""The reason is a sentence a caller can put into its own result."""
client, reason = _workout_sync.sync_client_or_reason()
assert client is None
assert reason.startswith("no Firebase config at ")
def test_never_builds_a_client_without_config(self) -> None:
"""An unconfigured machine must not reach the network at all."""
with patch.object(_sync_client, "firebase_client_for") as build:
_workout_sync.sync_client()
build.assert_not_called()
def test_uses_firebase_when_the_config_exists(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A configured machine syncs over Firebase -- no PAT anywhere."""
_firebase_config(_sync_client, tmp_path, monkeypatch)
firebase = MagicMock()
monkeypatch.setattr(
_sync_client, "firebase_client_for", lambda *_a, **_k: firebase
)
assert _workout_sync.sync_client() is firebase
assert _workout_sync.sync_client_or_reason() == (firebase, "")
@pytest.mark.parametrize(
"error",
[
ConfigError("no password"),
FirebaseAuthError("bad credentials"),
FirebaseSyncError("backend down"),
],
)
def test_returns_none_when_firebase_only_is_unusable(
self, error: Exception, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""No fallback exists: report it loudly, do not raise."""
_firebase_config(_sync_client, tmp_path, monkeypatch)
def _boom(*_args: object, **_kwargs: object) -> None:
raise error
monkeypatch.setattr(_sync_client, "firebase_client_for", _boom)
client, reason = _workout_sync.sync_client_or_reason()
assert client is None
assert reason.startswith("Firebase unusable: ")
def test_warns_when_firebase_only_is_unusable(
self,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Warns when firebase only is unusable."""
_firebase_config(_sync_client, tmp_path, monkeypatch)
def _boom(*_args: object, **_kwargs: object) -> None:
message = "backend down"
raise FirebaseSyncError(message)
monkeypatch.setattr(_sync_client, "firebase_client_for", _boom)
with caplog.at_level("WARNING"):
_workout_sync.sync_client()
assert "backend down" in caplog.text