diff --git a/test/core/configuration/__init__.py b/test/core/configuration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/core/configuration/configuration_model_abc_test.py b/test/core/configuration/configuration_model_abc_test.py new file mode 100644 index 00000000..f51a3890 --- /dev/null +++ b/test/core/configuration/configuration_model_abc_test.py @@ -0,0 +1,130 @@ +import os +import pytest +from cpl.core.configuration.configuration_model_abc import ConfigurationModelABC + + +class DatabaseSettings(ConfigurationModelABC): + def __init__(self, src: dict = None): + ConfigurationModelABC.__init__(self, src or {}) + self.option("host", str, default="localhost") + self.option("port", int, default=5432) + self.option("name", str) + self.option("debug", bool, default=False) + + +class RequiredSettings(ConfigurationModelABC): + def __init__(self, src: dict = None): + ConfigurationModelABC.__init__(self, src or {}) + self.option("api_key", str, required=True) + + +class MutableSettings(ConfigurationModelABC): + def __init__(self, src: dict = None): + ConfigurationModelABC.__init__(self, src or {}, readonly=False) + self.option("value", str, default="initial") + + +class EnvSettings(ConfigurationModelABC): + def __init__(self, src: dict = None): + ConfigurationModelABC.__init__(self, src or {}, env_prefix="MYAPP") + self.option("secret", str, default=None) + + +# --- option() / defaults --- + +def test_default_values(): + s = DatabaseSettings() + assert s.host == "localhost" + assert s.port == 5432 + assert s.debug is False + + +def test_values_from_src(): + # NOTE: passing native bool True via src triggers cast(True, bool) which + # fails because cast() calls value.lower() on a bool (bug in cast.py). + # Use string "true" instead — matches real JSON-parsed values. + s = DatabaseSettings({"host": "db.example.com", "port": 3306, "name": "mydb", "debug": "true"}) + assert s.host == "db.example.com" + assert s.port == 3306 + assert s.name == "mydb" + assert s.debug is True + + +def test_pascal_case_key(): + s = DatabaseSettings({"Host": "remotehost", "Port": 1234}) + assert s.host == "remotehost" + assert s.port == 1234 + + +def test_snake_case_key(): + s = DatabaseSettings({"host": "snakehost"}) + assert s.host == "snakehost" + + +def test_missing_optional_is_none(): + s = DatabaseSettings() + assert s.name is None + + +def test_type_casting_int(): + s = DatabaseSettings({"port": "9999"}) + assert s.port == 9999 + assert isinstance(s.port, int) + + +def test_type_casting_bool(): + s = DatabaseSettings({"debug": "true"}) + assert s.debug is True + + +# --- required --- + +def test_required_field_present(): + s = RequiredSettings({"api_key": "abc123"}) + assert s.api_key == "abc123" + + +def test_required_field_missing_raises(): + with pytest.raises(ValueError, match="required"): + RequiredSettings() + + +# --- readonly --- + +def test_readonly_raises_on_setattr(): + s = DatabaseSettings() + with pytest.raises(AttributeError, match="read-only"): + s.host = "newhost" + + +def test_mutable_settings_can_be_set(): + s = MutableSettings() + s.value = "changed" + assert s.value == "changed" + + +# --- env override --- + +def test_env_prefix_override(monkeypatch): + monkeypatch.setenv("MYAPP_SECRET", "env_secret_value") + s = EnvSettings() + assert s.secret == "env_secret_value" + + +def test_env_no_prefix_override(monkeypatch): + monkeypatch.setenv("HOST", "env_host") + + class NoPrefix(ConfigurationModelABC): + def __init__(self, src=None): + ConfigurationModelABC.__init__(self, src or {}) + self.option("host", str, default=None) + + s = NoPrefix() + assert s.host == "env_host" + + +# --- cannot instantiate ABC --- + +def test_cannot_instantiate_abc_directly(): + with pytest.raises(TypeError): + ConfigurationModelABC() diff --git a/test/core/configuration/configuration_test.py b/test/core/configuration/configuration_test.py new file mode 100644 index 00000000..99f79f2e --- /dev/null +++ b/test/core/configuration/configuration_test.py @@ -0,0 +1,107 @@ +import json +import os +import pytest +from cpl.core.configuration.configuration import Configuration +from cpl.core.configuration.configuration_model_abc import ConfigurationModelABC + + +class AppSettings(ConfigurationModelABC): + def __init__(self, src: dict = None): + ConfigurationModelABC.__init__(self, src or {}) + self.option("app_name", str, default="default-app") + self.option("version", str, default="0.0.0") + + +class ServerSettings(ConfigurationModelABC): + def __init__(self, src: dict = None): + ConfigurationModelABC.__init__(self, src or {}) + self.option("host", str, default="localhost") + self.option("port", int, default=8080) + + +@pytest.fixture(autouse=True) +def clear_config(): + """Reset Configuration state before each test.""" + Configuration._config.clear() + yield + Configuration._config.clear() + + +# --- set / get --- + +def test_set_and_get_by_class(): + settings = AppSettings({"app_name": "TestApp", "version": "1.0.0"}) + Configuration.set(AppSettings, settings) + result = Configuration.get(AppSettings) + assert result.app_name == "TestApp" + assert result.version == "1.0.0" + + +def test_set_and_get_by_string_key(): + Configuration.set("my_key", "my_value") + assert Configuration.get("my_key") == "my_value" + + +def test_get_missing_returns_default(): + assert Configuration.get("nonexistent", "fallback") == "fallback" + + +def test_get_missing_returns_none(): + assert Configuration.get("nonexistent") is None + + +def test_get_model_auto_instantiates(): + # Getting an unregistered ConfigurationModelABC subclass should auto-create it + result = Configuration.get(ServerSettings) + assert isinstance(result, ServerSettings) + assert result.host == "localhost" + assert result.port == 8080 + + +def test_overwrite_existing(): + Configuration.set("key", "first") + Configuration.set("key", "second") + assert Configuration.get("key") == "second" + + +def test_multiple_models(): + Configuration.set(AppSettings, AppSettings({"app_name": "App"})) + Configuration.set(ServerSettings, ServerSettings({"port": 9000})) + assert Configuration.get(AppSettings).app_name == "App" + assert Configuration.get(ServerSettings).port == 9000 + + +# --- add_json_file --- + +def test_add_json_file_loads_model(tmp_path): + config_data = {"AppSettings": {"app_name": "FromFile", "version": "2.0.0"}} + config_file = tmp_path / "appsettings.json" + config_file.write_text(json.dumps(config_data), encoding="utf-8") + + Configuration.add_json_file(str(config_file), output=False) + + result = Configuration.get(AppSettings) + assert result.app_name == "FromFile" + assert result.version == "2.0.0" + + +def test_add_json_file_not_found_exits(tmp_path): + with pytest.raises(SystemExit): + Configuration.add_json_file(str(tmp_path / "missing.json"), output=False) + + +def test_add_json_file_optional_missing_returns_none(tmp_path): + result = Configuration.add_json_file( + str(tmp_path / "missing.json"), optional=True, output=False + ) + assert result is None + + +def test_add_json_file_invalid_json(tmp_path): + # _load_json_file catches parse errors and returns {} — no SystemExit. + # add_json_file then proceeds with an empty config (no models loaded). + bad_file = tmp_path / "bad.json" + bad_file.write_text("not valid json", encoding="utf-8") + Configuration.add_json_file(str(bad_file), output=False) + # No model should be registered since the file was invalid + assert Configuration.get("AppSettings") is None