|
1 | 1 | import os |
2 | 2 | from types import SimpleNamespace |
3 | 3 |
|
| 4 | +import pytest |
| 5 | +from dependency_injector import containers, errors, providers |
4 | 6 | from loguru import logger |
5 | | -from pydantic import BaseModel |
| 7 | +from pydantic import BaseModel, ValidationError |
6 | 8 |
|
7 | | -from zodiac_core import ConfigManagement, Environment |
| 9 | +from zodiac_core import ConfigManagement, Environment, StrictConfig |
| 10 | +from zodiac_core.utils import strtobool |
8 | 11 |
|
9 | 12 |
|
10 | 13 | def test_get_config_files_base_only(tmp_path): |
@@ -144,3 +147,74 @@ class AllDefaultConfig(BaseModel): |
144 | 147 | config = ConfigManagement.provide_config({}, AllDefaultConfig) |
145 | 148 | assert config.name == "app" |
146 | 149 | assert config.version == "1.0.0" |
| 150 | + |
| 151 | + |
| 152 | +class TestConfigIntegration: |
| 153 | + """Integration: dependency-injector Configuration with strict+required + strtobool.""" |
| 154 | + |
| 155 | + def _make_container(self, tmp_path, base_ini, override_ini=None): |
| 156 | + (tmp_path / "app.ini").write_text(base_ini) |
| 157 | + if override_ini: |
| 158 | + (tmp_path / "app.develop.ini").write_text(override_ini) |
| 159 | + |
| 160 | + class C(containers.DeclarativeContainer): |
| 161 | + config = providers.Configuration(strict=True) |
| 162 | + |
| 163 | + c = C() |
| 164 | + for path in ConfigManagement.get_config_files([tmp_path], default_env="develop"): |
| 165 | + c.config.from_ini(path, required=True) |
| 166 | + return c |
| 167 | + |
| 168 | + @pytest.mark.parametrize("echo_val,expected", [("false", False), ("true", True)]) |
| 169 | + def test_strtobool_as_callback(self, tmp_path, echo_val, expected): |
| 170 | + """as_(strtobool) correctly converts 'false'/'true' from ini.""" |
| 171 | + c = self._make_container(tmp_path, f"[db]\necho = {echo_val}\n") |
| 172 | + assert c.config.db.echo.as_(strtobool)() is expected |
| 173 | + |
| 174 | + def test_strict_rejects_undefined_key(self, tmp_path): |
| 175 | + """strict=True raises on accessing a key not in any loaded file.""" |
| 176 | + c = self._make_container(tmp_path, "[db]\nurl = sqlite://\n") |
| 177 | + with pytest.raises(errors.Error, match="Undefined"): |
| 178 | + c.config.db.nonexistent() |
| 179 | + |
| 180 | + def test_required_rejects_missing_file(self): |
| 181 | + """from_ini(required=True) raises when the file does not exist.""" |
| 182 | + |
| 183 | + class C(containers.DeclarativeContainer): |
| 184 | + config = providers.Configuration(strict=True) |
| 185 | + |
| 186 | + c = C() |
| 187 | + with pytest.raises(FileNotFoundError): |
| 188 | + c.config.from_ini("/tmp/nonexistent.ini", required=True) |
| 189 | + |
| 190 | + def test_override_merges_with_strict(self, tmp_path): |
| 191 | + """Base + override merge works under strict mode; override key wins.""" |
| 192 | + c = self._make_container( |
| 193 | + tmp_path, |
| 194 | + "[db]\nurl = sqlite://\necho = false\n\n[cache]\nprefix = myapp\n", |
| 195 | + "[db]\necho = true\n", |
| 196 | + ) |
| 197 | + assert c.config.db.url() == "sqlite://" |
| 198 | + assert c.config.db.echo.as_(strtobool)() is True |
| 199 | + assert c.config.cache.prefix() == "myapp" |
| 200 | + |
| 201 | + |
| 202 | +class TestStrictConfig: |
| 203 | + """StrictConfig enforces extra='forbid' and frozen=True.""" |
| 204 | + |
| 205 | + def test_typo_field_rejected(self): |
| 206 | + class DbConfig(StrictConfig): |
| 207 | + url: str |
| 208 | + echo: bool = False |
| 209 | + |
| 210 | + with pytest.raises(Exception, match="ech0"): |
| 211 | + DbConfig(url="sqlite://", ech0="true") |
| 212 | + |
| 213 | + def test_immutable_after_creation(self): |
| 214 | + class DbConfig(StrictConfig): |
| 215 | + url: str |
| 216 | + echo: bool = False |
| 217 | + |
| 218 | + cfg = DbConfig(url="sqlite://") |
| 219 | + with pytest.raises(ValidationError): |
| 220 | + cfg.echo = True |
0 commit comments