"""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