utils/crdt-sync/crdt_sync/tests/test_config.py
Krzysztof kuhy Rudnicki 9d897c13f4
Some checks are pending
crdt-sync pre-commit / pre-commit (push) Waiting to run
crdt-sync tests / test (3.10) (push) Waiting to run
crdt-sync tests / test (3.11) (push) Waiting to run
crdt-sync tests / test (3.12) (push) Waiting to run
gatelock pre-commit / pre-commit (push) Waiting to run
gatelock tests / test (3.10) (push) Waiting to run
gatelock tests / test (3.11) (push) Waiting to run
gatelock tests / test (3.12) (push) Waiting to run
palette drift / palette (push) Waiting to run
crdt-sync: split seven oversized test files by test group
test_firebase_auth.py    439 -> 170 + _google 159 + _tokens 159 + _session 175
  test_sync_revisions.py   406 -> 163 + _peers 162 + _backends 216
  test_firebase.py         369 -> 212 + _writes 221
  test_mirror.py           364 -> 178 + _fallback 229
  test_config.py           355 -> 139 + _credentials 112 + _client 233
  test_github.py           301 -> 127 + _writes 219
  test_sync.py             271 -> 195 + _skips 130

Cut at concern boundaries, not line counts: test_sync_skips.py holds the five
cases where a peer's pushed file is missing, corrupt, wrong-shaped or our own
— one story about what sync_log declines to merge — rather than an arbitrary
half of the original class.

259 passed at 100% branch coverage, verified against a clean HEAD worktree.

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

140 lines
4.6 KiB
Python

"""Tests for the shared Firebase configuration loader.
Every failure mode here is one that would otherwise surface as an
authentication error long after the real mistake (a placeholder left in, a
newline a text editor appended), so each is asserted to name the field at
fault rather than merely to raise.
"""
# 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
import pytest
from crdt_sync import (
ConfigError,
FirebaseConfig,
)
if TYPE_CHECKING:
from pathlib import Path
_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_loads_a_valid_config(tmp_path: Path) -> None:
"""Loads a valid config."""
config = FirebaseConfig.load(_write(tmp_path / "firebase.json", _VALID))
assert config.api_key == "AIzaSyExample"
assert config.project_id == "kuhy-syncs"
assert config.uid == "OvA2REQyLIhAHOEjzwS1o877rgG3"
assert config.email == "sync@example.com"
assert config.database_url.endswith("europe-west1.firebasedatabase.app")
def test_ignores_the_scaffold_comment_keys(tmp_path: Path) -> None:
"""The shipped scaffold explains each field inline; that is not config."""
annotated = {**_VALID, "_comment_apiKey": "where to find it", "_comment": "x"}
config = FirebaseConfig.load(_write(tmp_path / "firebase.json", annotated))
assert config.api_key == "AIzaSyExample"
def test_reports_a_missing_file(tmp_path: Path) -> None:
"""Reports a missing file."""
with pytest.raises(ConfigError, match="does not exist"):
FirebaseConfig.load(tmp_path / "absent.json")
def test_reports_unreadable_json(tmp_path: Path) -> None:
"""Reports unreadable JSON."""
path = tmp_path / "firebase.json"
path.write_text("{not json", encoding="utf-8")
with pytest.raises(ConfigError, match="not valid JSON"):
FirebaseConfig.load(path)
def test_reports_a_non_object_document(tmp_path: Path) -> None:
"""Reports a non object document."""
with pytest.raises(ConfigError, match="must contain a JSON object"):
FirebaseConfig.load(_write(tmp_path / "firebase.json", ["a list"]))
def test_reports_an_unreadable_path(tmp_path: Path) -> None:
"""A directory where a file belongs is an OSError, not a missing file."""
(tmp_path / "firebase.json").mkdir()
with pytest.raises(ConfigError, match="could not be read"):
FirebaseConfig.load(tmp_path / "firebase.json")
@pytest.mark.parametrize("field", sorted(_VALID))
def test_names_a_missing_field(tmp_path: Path, field: str) -> None:
"""Names a missing field."""
incomplete = {k: v for k, v in _VALID.items() if k != field}
with pytest.raises(ConfigError, match=field):
FirebaseConfig.load(_write(tmp_path / "firebase.json", incomplete))
def test_names_an_empty_field(tmp_path: Path) -> None:
"""Names an empty field."""
with pytest.raises(ConfigError, match="uid"):
FirebaseConfig.load(_write(tmp_path / "firebase.json", {**_VALID, "uid": " "}))
def test_names_an_unfilled_placeholder(tmp_path: Path) -> None:
"""The exact state the scaffold ships in must not load silently."""
unfilled = {**_VALID, "apiKey": "PASTE_WEB_API_KEY_HERE"}
with pytest.raises(ConfigError, match=r"placeholder for: apiKey"):
FirebaseConfig.load(_write(tmp_path / "firebase.json", unfilled))