mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-11 19:34:01 +01:00
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
234 lines
7.6 KiB
Python
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
|