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>
153 lines
5.0 KiB
Python
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
|