# Copyright (c) 2026 Krzysztof Rudnicki """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