utils/crdt-sync/crdt_sync/tests/test_config_client.py
Krzysztof kuhy Rudnicki 9f271b0d37 Fix the cap hook in CI, and the re-exports its --all-files run exposed
The hook entry was an absolute /home/kuhy/utils path, copied from a config
written for ~/.claude where it only ever ran locally. On an Actions runner
that is exit code 127 — "No such file or directory" — so gatelock-precommit
and crdt-sync-precommit failed on every commit of this refactor while the
hook silently checked nothing. Now repo-relative, matching the sibling hooks
(`entry: scripts/check_subproject_scope.sh`): pre-commit always runs from the
git root, so it resolves the same in both places.

Fixing that made the CI configs runnable locally, and `--all-files` then found
what per-file runs had missed — ruff pruning imports that exist *for* their
re-export. `import crdt_sync` was broken three separate ways (FileSyncStateStore,
encode_key, FileCredentialStore). Each affected module now states an explicit
__all__ so the autofixer cannot take them again, and the two files that pushed
past the cap once __all__ was added are split further:

  _firebase.py 260 -> 247 + _fberrors 25
  _sync.py     257 -> 188 + _syncargs 91

Test patch targets followed the code they patch: gatelock._outputs.shutil ->
_xrandr.shutil, gatelock._arbiter.Path -> _claims.Path.

gatelock and crdt-sync both pass `pre-commit run --all-files` in full;
389 and 259 tests at 100% branch coverage; the root gate exits 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 15:52:59 +02:00

234 lines
7.6 KiB
Python

"""Tests for building a sync client from a config file.
Split from ``test_config.py`` (250-line cap);
``test_config_credentials.py`` keeps reading and validating the password.
"""
# These tests assert on private attributes on purpose: `_timeout_seconds`
# and `_path` are exactly what the fixes under test set, and there is no
# public accessor for either.
# pylint: disable=protected-access
from __future__ import annotations
import json
from typing import TYPE_CHECKING
from crdt_sync import (
FirebaseConfig,
FirebaseSyncClient,
credential_store_for,
mirror_client_for,
)
from crdt_sync._config import _DEFAULT_TIMEOUT_SECONDS, firebase_client_for
if TYPE_CHECKING:
from pathlib import Path
import pytest
_VALID = {
"apiKey": "AIzaSyExample",
"databaseUrl": "https://kuhy-syncs-default-rtdb.europe-west1.firebasedatabase.app",
"projectId": "kuhy-syncs",
"uid": "OvA2REQyLIhAHOEjzwS1o877rgG3",
"email": "sync@example.com",
}
class _StubRemote:
"""A do-nothing :class:`~crdt_sync.RemoteStore`, standing in for GitHub.
A real implementation rather than a bare ``object()`` so the type checker
accepts it without a suppression.
"""
def list_directory(self, _path: str) -> list[str]:
"""Report an empty remote directory."""
return []
def get_file_text(self, _path: str) -> str | None:
"""Report the file as absent."""
return None
def put_file_text(self, path: str, text: str, *, message: str) -> None:
"""Accept and discard a write."""
def delete_file(self, path: str, *, message: str = "") -> None:
"""Accept and discard a delete."""
def can_access_remote(self) -> bool:
"""Report the remote as reachable."""
return True
def _write(path: Path, data: object) -> Path:
"""Write ``data`` as JSON to ``path`` and return it."""
path.write_text(json.dumps(data), encoding="utf-8")
return path
def test_credential_store_is_per_app() -> None:
"""Two apps must not share one cache file; concurrent writes corrupt it."""
diet = credential_store_for("diet_guard")
alarm = credential_store_for("wake_alarm")
assert diet._path != alarm._path
assert diet._path.name == "firebase_auth.json"
assert "diet_guard" in str(diet._path)
def test_client_signs_in_only_without_a_cached_session(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The common path is a cached refresh token, costing no round trip."""
config = FirebaseConfig.load(_write(tmp_path / "firebase.json", _VALID))
sign_ins: list[tuple[str, str]] = []
class _Auth:
def __init__(self, api_key: str, _store: object) -> None:
self.api_key = api_key
def has_session(self) -> bool:
"""Pretend a cached session exists."""
return True
def sign_in(self, email: str, password: str) -> None:
"""Record the sign-in attempt."""
sign_ins.append((email, password)) # pragma: no cover - must not run
monkeypatch.setattr("crdt_sync._config.FirebaseTokenProvider", _Auth)
client = firebase_client_for("diet_guard", config=config)
assert not sign_ins
assert client is not None
def test_client_signs_in_when_there_is_no_session(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Client signs in when there is no session."""
config = FirebaseConfig.load(_write(tmp_path / "firebase.json", _VALID))
password = tmp_path / "password"
password.write_text("hunter2", encoding="utf-8")
monkeypatch.setattr("crdt_sync._config.PASSWORD_FILE", password)
sign_ins: list[tuple[str, str]] = []
class _Auth:
def __init__(self, api_key: str, _store: object) -> None:
self.api_key = api_key
def has_session(self) -> bool:
"""Pretend no cached session exists."""
return False
def sign_in(self, email: str, password_value: str) -> None:
"""Record the credentials the caller signed in with."""
sign_ins.append((email, password_value))
monkeypatch.setattr("crdt_sync._config.FirebaseTokenProvider", _Auth)
firebase_client_for("diet_guard", config=config)
assert sign_ins == [("sync@example.com", "hunter2")]
def test_mirror_client_makes_firebase_primary(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Firebase authoritative, GitHub best-effort -- the cutover shape."""
config = FirebaseConfig.load(_write(tmp_path / "firebase.json", _VALID))
github = _StubRemote()
class _Auth:
def __init__(self, api_key: str, _store: object) -> None:
self.api_key = api_key
def has_session(self) -> bool:
"""Pretend a cached session exists."""
return True
def sign_in(self, email: str, password: str) -> None:
"""Record the sign-in attempt."""
raise AssertionError # pragma: no cover - must not run
monkeypatch.setattr("crdt_sync._config.FirebaseTokenProvider", _Auth)
client = mirror_client_for("diet_guard", github, config=config)
# Reaching into the wrapper is the point: which backend is authoritative
# is the one thing a rollback depends on getting right.
assert client.mirror is github
assert isinstance(client.primary, FirebaseSyncClient)
def test_client_loads_the_shared_config_when_none_is_given(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""The default path: apps name themselves and nothing else."""
monkeypatch.setattr(
"crdt_sync._config.CONFIG_FILE",
_write(tmp_path / "firebase.json", _VALID),
)
class _Auth:
def __init__(self, api_key: str, _store: object) -> None:
self.api_key = api_key
def has_session(self) -> bool:
"""Pretend a cached session exists."""
return True
def sign_in(self, email: str, password: str) -> None:
"""Record the sign-in attempt."""
raise AssertionError # pragma: no cover - must not run
monkeypatch.setattr("crdt_sync._config.FirebaseTokenProvider", _Auth)
assert firebase_client_for("wake_alarm") is not None
def _client_with_stub_auth(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
**kwargs: float,
) -> FirebaseSyncClient:
"""Return a client built with stubbed auth, forwarding ``kwargs``."""
config = FirebaseConfig.load(_write(tmp_path / "firebase.json", _VALID))
class _Auth:
def __init__(self, api_key: str, _store: object) -> None:
"""Record the key; the credential store is irrelevant here."""
self.api_key = api_key
def has_session(self) -> bool:
"""Pretend a cached session exists, so no sign-in is attempted."""
return True
monkeypatch.setattr("crdt_sync._config.FirebaseTokenProvider", _Auth)
return firebase_client_for("diet_guard", config=config, **kwargs)
def test_client_uses_the_requested_timeout(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A caller's timeout must reach the client, not just the GitHub half."""
# A keyword with a default adds no branch, so coverage alone cannot tell
# whether it is actually threaded through to the client.
client = _client_with_stub_auth(tmp_path, monkeypatch, timeout_seconds=3)
assert client._timeout_seconds == 3
def test_client_timeout_defaults_to_the_shared_value(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Omitting the keyword keeps the library-wide default."""
client = _client_with_stub_auth(tmp_path, monkeypatch)
assert client._timeout_seconds == _DEFAULT_TIMEOUT_SECONDS