"""Tests for settings bootstrap.""" import pytest from alfred.settings_bootstrap import ( ConfigSource, SettingsBootstrap, extract_python_version, generate_secret, get_nested_value, ) @pytest.fixture def test_toml_content(): """Test TOML content with schema.""" return """ [tool.poetry] name = "test" version = "1.0.0" [tool.poetry.dependencies] python = "==3.14.2" [tool.alfred.settings] runner = "poetry" image_name = "test_image" [tool.alfred.settings_schema.TEST_FROM_TOML] type = "string" source = "toml" toml_path = "tool.poetry.version" description = "Version from TOML" category = "test" [tool.alfred.settings_schema.TEST_FROM_ENV] type = "string" source = "env" default = "default_value" description = "Value from env" category = "test" [tool.alfred.settings_schema.TEST_SECRET] type = "secret" source = "generated" secret_rule = "16:hex" description = "Generated secret" category = "security" [tool.alfred.settings_schema.TEST_COMPUTED] type = "computed" source = "computed" compute_from = ["TEST_FROM_TOML", "TEST_FROM_ENV"] compute_template = "{TEST_FROM_TOML}-{TEST_FROM_ENV}" description = "Computed value" category = "test" [tool.alfred.settings_schema.PYTHON_VERSION] type = "string" source = "toml" toml_path = "tool.poetry.dependencies.python" transform = "extract_python_version_full" description = "Python version" category = "build" """ @pytest.fixture def config_source(tmp_path): """Create a ConfigSource for testing.""" return ConfigSource.from_base_dir(tmp_path) @pytest.fixture def create_test_env(tmp_path, test_toml_content): """Create a complete test environment.""" # Create pyproject.toml toml_path = tmp_path / "pyproject.toml" toml_path.write_text(test_toml_content) # Create .env.example env_example = tmp_path / ".env.example" env_example.write_text(""" TEST_FROM_TOML= TEST_FROM_ENV= TEST_SECRET= TEST_COMPUTED= PYTHON_VERSION= """) return ConfigSource.from_base_dir(tmp_path) class TestExtractPythonVersion: """Test Python version extraction.""" def test_exact_version(self): """Test exact version format.""" full, short = extract_python_version("==3.14.2") assert full == "3.14.2" assert short == "3.14" def test_caret_version(self): """Test caret version format.""" full, short = extract_python_version("^3.14.2") assert full == "3.14.2" assert short == "3.14" def test_invalid_version(self): """Test invalid version raises error.""" with pytest.raises(ValueError, match="Invalid Python version"): extract_python_version("3") class TestGenerateSecret: """Test secret generation.""" def test_generate_b64(self): """Test base64 secret generation.""" secret = generate_secret("32:b64") assert isinstance(secret, str) assert len(secret) > 0 def test_generate_hex(self): """Test hex secret generation.""" secret = generate_secret("16:hex") assert isinstance(secret, str) assert len(secret) == 32 # 16 bytes = 32 hex chars assert all(c in "0123456789abcdef" for c in secret) def test_invalid_format(self): """Test invalid format raises error.""" with pytest.raises(ValueError, match="Invalid security format"): generate_secret("32:invalid") def test_invalid_rule(self): """Test invalid rule format raises error.""" with pytest.raises(ValueError, match="Invalid security rule format"): generate_secret("32") class TestGetNestedValue: """Test nested value extraction.""" def test_simple_path(self): """Test simple path.""" data = {"key": "value"} assert get_nested_value(data, "key") == "value" def test_nested_path(self): """Test nested path.""" data = {"a": {"b": {"c": "value"}}} assert get_nested_value(data, "a.b.c") == "value" def test_missing_key(self): """Test missing key raises error.""" data = {"a": {"b": "value"}} with pytest.raises(KeyError): get_nested_value(data, "a.c") def test_non_dict_value(self): """Test accessing non-dict raises error.""" data = {"a": "not a dict"} with pytest.raises(KeyError, match="non-dict"): get_nested_value(data, "a.b") class TestSettingsBootstrap: """Test settings bootstrap.""" def test_load_sources(self, create_test_env): """Test loading TOML and env sources.""" bootstrapper = SettingsBootstrap(create_test_env) bootstrapper._load_sources() assert bootstrapper.toml_data is not None assert "tool" in bootstrapper.toml_data assert isinstance(bootstrapper.existing_env, dict) def test_resolve_from_toml(self, create_test_env): """Test resolving setting from TOML.""" # Load schema from test env from alfred.settings_schema import load_schema schema = load_schema(create_test_env.base_dir) bootstrapper = SettingsBootstrap(create_test_env, schema) bootstrapper._load_sources() # Get definition for TEST_FROM_TOML definition = bootstrapper.schema.get("TEST_FROM_TOML") value = bootstrapper._resolve_from_toml(definition) assert value == "1.0.0" # From tool.poetry.version def test_resolve_from_env_with_default(self, create_test_env): """Test resolving from env with default.""" from alfred.settings_schema import load_schema schema = load_schema(create_test_env.base_dir) bootstrapper = SettingsBootstrap(create_test_env, schema) bootstrapper._load_sources() definition = bootstrapper.schema.get("TEST_FROM_ENV") value = bootstrapper._resolve_from_env(definition) assert value == "default_value" def test_resolve_from_env_existing(self, create_test_env): """Test resolving from existing env.""" # Create existing .env create_test_env.env_path.write_text("TEST_FROM_ENV=existing_value\n") from alfred.settings_schema import load_schema schema = load_schema(create_test_env.base_dir) bootstrapper = SettingsBootstrap(create_test_env, schema) bootstrapper._load_sources() definition = bootstrapper.schema.get("TEST_FROM_ENV") value = bootstrapper._resolve_from_env(definition) assert value == "existing_value" def test_resolve_generated_new(self, create_test_env): """Test generating new secret.""" from alfred.settings_schema import load_schema schema = load_schema(create_test_env.base_dir) bootstrapper = SettingsBootstrap(create_test_env, schema) bootstrapper._load_sources() definition = bootstrapper.schema.get("TEST_SECRET") value = bootstrapper._resolve_generated(definition) assert isinstance(value, str) assert len(value) == 32 # 16 hex = 32 chars def test_resolve_generated_preserve_existing(self, create_test_env): """Test preserving existing secret.""" # Create existing .env with secret create_test_env.env_path.write_text("TEST_SECRET=existing_secret\n") from alfred.settings_schema import load_schema schema = load_schema(create_test_env.base_dir) bootstrapper = SettingsBootstrap(create_test_env, schema) bootstrapper._load_sources() definition = bootstrapper.schema.get("TEST_SECRET") value = bootstrapper._resolve_generated(definition) assert value == "existing_secret" def test_resolve_computed(self, create_test_env): """Test resolving computed setting.""" from alfred.settings_schema import load_schema schema = load_schema(create_test_env.base_dir) bootstrapper = SettingsBootstrap(create_test_env, schema) bootstrapper._load_sources() # Resolve dependencies first bootstrapper.resolved_settings["TEST_FROM_TOML"] = "1.0.0" bootstrapper.resolved_settings["TEST_FROM_ENV"] = "test" definition = bootstrapper.schema.get("TEST_COMPUTED") value = bootstrapper._resolve_computed(definition) assert value == "1.0.0-test" def test_resolve_with_transform(self, create_test_env): """Test resolving with transform function.""" from alfred.settings_schema import load_schema schema = load_schema(create_test_env.base_dir) bootstrapper = SettingsBootstrap(create_test_env, schema) bootstrapper._load_sources() definition = bootstrapper.schema.get("PYTHON_VERSION") value = bootstrapper._resolve_from_toml(definition) assert value == "3.14.2" # Transformed from "==3.14.2" def test_full_bootstrap(self, create_test_env): """Test complete bootstrap process.""" from alfred.settings_schema import load_schema schema = load_schema(create_test_env.base_dir) bootstrapper = SettingsBootstrap(create_test_env, schema) bootstrapper.bootstrap() # Check .env was created assert create_test_env.env_path.exists() # Check .env.make was created env_make_path = create_test_env.base_dir / ".env.make" assert env_make_path.exists() # Check content env_content = create_test_env.env_path.read_text() assert "TEST_FROM_TOML=1.0.0" in env_content assert "TEST_FROM_ENV=default_value" in env_content assert "TEST_SECRET=" in env_content assert "TEST_COMPUTED=1.0.0-default_value" in env_content def test_bootstrap_preserves_secrets(self, create_test_env): """Test that bootstrap preserves existing secrets.""" # Create existing .env with secret create_test_env.env_path.write_text(""" TEST_FROM_ENV=old_value TEST_SECRET=my_secret_123 """) from alfred.settings_schema import load_schema schema = load_schema(create_test_env.base_dir) bootstrapper = SettingsBootstrap(create_test_env, schema) bootstrapper.bootstrap() # Check secret was preserved env_content = create_test_env.env_path.read_text() assert "TEST_SECRET=my_secret_123" in env_content def test_validation_error(self, tmp_path, test_toml_content): """Test validation error is raised.""" # Add a setting with validation toml_with_validation = ( test_toml_content + """ [tool.alfred.settings_schema.TEST_VALIDATED] type = "integer" source = "env" default = 150 validator = "range:1:100" description = "Validated setting" category = "test" """ ) toml_path = tmp_path / "pyproject.toml" toml_path.write_text(toml_with_validation) env_example = tmp_path / ".env.example" env_example.write_text("TEST_VALIDATED=\n") source = ConfigSource.from_base_dir(tmp_path) from alfred.settings_schema import load_schema schema = load_schema(tmp_path) bootstrapper = SettingsBootstrap(source, schema) with pytest.raises(ValueError, match="Validation errors"): bootstrapper.bootstrap() def test_write_env_make_only_exports(self, create_test_env): """Test that .env.make only contains export_to_env_make settings.""" # Add a setting with export_to_env_make toml_content = create_test_env.toml_path.read_text() toml_content += """ [tool.alfred.settings_schema.EXPORTED_VAR] type = "string" source = "env" default = "exported" export_to_env_make = true category = "build" """ create_test_env.toml_path.write_text(toml_content) # Recreate schema from alfred.settings_schema import load_schema schema = load_schema(create_test_env.base_dir) bootstrapper = SettingsBootstrap(create_test_env, schema) bootstrapper.bootstrap() env_make_content = (create_test_env.base_dir / ".env.make").read_text() assert "EXPORTED_VAR=exported" in env_make_content # Non-exported vars should not be in .env.make assert "TEST_FROM_ENV" not in env_make_content class TestConfigSource: """Test ConfigSource dataclass.""" def test_from_base_dir(self, tmp_path): """Test creating ConfigSource from base dir.""" source = ConfigSource.from_base_dir(tmp_path) assert source.base_dir == tmp_path assert source.toml_path == tmp_path / "pyproject.toml" assert source.env_path == tmp_path / ".env" assert source.env_example_path == tmp_path / ".env.example" def test_from_base_dir_default(self): """Test creating ConfigSource with default base dir.""" source = ConfigSource.from_base_dir() assert source.base_dir.exists() assert source.toml_path.name == "pyproject.toml"