Files
rose-ash/shared/tests/test_config.py
giles 00efbc2a35 Add unit test coverage for shared pure-logic modules (240 tests)
Track 1.1 of master plan: expand from sexp-only tests to cover
DTOs, HTTP signatures, HMAC auth, URL utilities, Jinja filters,
calendar helpers, config freeze, activity bus registry, parse
utilities, sexp helpers, error classes, and jinja bridge render API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 19:34:37 +00:00

153 lines
5.0 KiB
Python

"""Tests for config freeze/readonly enforcement."""
from __future__ import annotations
import asyncio
import os
import tempfile
from types import MappingProxyType
import pytest
from shared.config import _freeze
# ---------------------------------------------------------------------------
# _freeze
# ---------------------------------------------------------------------------
class TestFreeze:
def test_freezes_dict(self):
result = _freeze({"a": 1, "b": 2})
assert isinstance(result, MappingProxyType)
assert result["a"] == 1
def test_frozen_dict_is_immutable(self):
result = _freeze({"a": 1})
with pytest.raises(TypeError):
result["a"] = 2
with pytest.raises(TypeError):
result["new"] = 3
def test_freezes_list_to_tuple(self):
result = _freeze([1, 2, 3])
assert isinstance(result, tuple)
assert result == (1, 2, 3)
def test_freezes_set_to_frozenset(self):
result = _freeze({1, 2, 3})
assert isinstance(result, frozenset)
assert result == frozenset({1, 2, 3})
def test_freezes_nested_dict(self):
result = _freeze({"a": {"b": {"c": 1}}})
assert isinstance(result, MappingProxyType)
assert isinstance(result["a"], MappingProxyType)
assert isinstance(result["a"]["b"], MappingProxyType)
assert result["a"]["b"]["c"] == 1
def test_freezes_dict_with_list(self):
result = _freeze({"items": [1, 2, 3]})
assert isinstance(result["items"], tuple)
def test_freezes_list_of_dicts(self):
result = _freeze([{"a": 1}, {"b": 2}])
assert isinstance(result, tuple)
assert isinstance(result[0], MappingProxyType)
def test_preserves_scalars(self):
assert _freeze(42) == 42
assert _freeze("hello") == "hello"
assert _freeze(3.14) == 3.14
assert _freeze(True) is True
assert _freeze(None) is None
def test_freezes_tuple_recursively(self):
result = _freeze(({"a": 1}, [2, 3]))
assert isinstance(result, tuple)
assert isinstance(result[0], MappingProxyType)
assert isinstance(result[1], tuple)
def test_complex_config_structure(self):
"""Simulates a real app-config.yaml structure."""
raw = {
"app_urls": {
"blog": "https://blog.rose-ash.com",
"market": "https://market.rose-ash.com",
},
"features": ["sexp", "federation"],
"limits": {"max_upload": 10485760},
}
frozen = _freeze(raw)
assert frozen["app_urls"]["blog"] == "https://blog.rose-ash.com"
assert frozen["features"] == ("sexp", "federation")
with pytest.raises(TypeError):
frozen["app_urls"]["blog"] = "changed"
# ---------------------------------------------------------------------------
# init_config / config / as_plain / pretty
# ---------------------------------------------------------------------------
class TestConfigInit:
def test_init_and_read(self):
"""Test full init_config → config() → as_plain() → pretty() cycle."""
import shared.config as cfg
# Save original state
orig_frozen = cfg._data_frozen
orig_plain = cfg._data_plain
try:
# Reset state
cfg._data_frozen = None
cfg._data_plain = None
# Write a temp YAML file
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
f.write("app_urls:\n blog: https://blog.example.com\nport: 8001\n")
path = f.name
try:
asyncio.run(cfg.init_config(path, force=True))
c = cfg.config()
assert c["app_urls"]["blog"] == "https://blog.example.com"
assert c["port"] == 8001
assert isinstance(c, MappingProxyType)
plain = cfg.as_plain()
assert isinstance(plain, dict)
assert plain["port"] == 8001
# Modifying plain should not affect config
plain["port"] = 9999
assert cfg.config()["port"] == 8001
pretty_str = cfg.pretty()
assert "blog" in pretty_str
finally:
os.unlink(path)
finally:
# Restore original state
cfg._data_frozen = orig_frozen
cfg._data_plain = orig_plain
def test_config_raises_before_init(self):
import shared.config as cfg
orig = cfg._data_frozen
try:
cfg._data_frozen = None
with pytest.raises(RuntimeError, match="init_config"):
cfg.config()
finally:
cfg._data_frozen = orig
def test_file_not_found(self):
import shared.config as cfg
orig = cfg._data_frozen
try:
cfg._data_frozen = None
with pytest.raises(FileNotFoundError):
asyncio.run(cfg.init_config("/nonexistent/path.yaml", force=True))
finally:
cfg._data_frozen = orig