utils/crdt-sync/crdt_sync/tests/test_config.py
Krzysztof kuhy Rudnicki 6154dc09bf Pin pre-commit's ruff to 0.16.7 and satisfy the CPY001 it brings
The three Python packages pinned ruff twice at different versions:
requirements.txt at 0.16.7, .pre-commit-config.yaml at v0.15.2. Only the
older one ever ran, so `select = ["ALL"]` was enforced two minor versions
behind. Measured before aligning: v0.15.2 reports 0 findings, every 0.16.x
reports CPY001 on each file -- 70 crdt-sync, 23 freedays, 71 gatelock, and
no other rule.

Resolved by adding the notice rather than ignoring the rule, so the gate
keeps its teeth.

Two gatelock modules sat at exactly the 250-line cap, so a one-line header
tipped them to 251. Both are split along a real seam rather than shaved:

  _widget_fills.py    colour arithmetic over LockConfig, no tkinter import
  _surfaces_focus.py  selects among existing surfaces, never mutates them

The focus split keeps _surfaces.py's documented containment (it stays the
only module that creates, moves or destroys windows), and the static guard
in test_recovery_invariants.py globs _surfaces*.py, so the new sibling is
covered with no list to extend. widgets.py re-exports the fills helpers,
which keeps gatelock.widgets the single public surface its tests import.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019kMvSMtsVMB3d2uoZ56pbp
2026-09-12 18:11:24 +02:00

141 lines
4.6 KiB
Python

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