"""Unit tests for the VariableExtractor.""" import pytest from iac_reverse.models import ( CpuArchitecture, DiscoveredResource, ExtractedVariable, PlatformCategory, ProviderType, ) from iac_reverse.generator import VariableExtractor # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def make_resource( resource_type: str = "kubernetes_deployment", unique_id: str = "default/deployments/nginx", name: str = "nginx", provider: ProviderType = ProviderType.KUBERNETES, platform_category: PlatformCategory = PlatformCategory.CONTAINER_ORCHESTRATION, architecture: CpuArchitecture = CpuArchitecture.AARCH64, attributes: dict | None = None, ) -> DiscoveredResource: """Create a sample DiscoveredResource for testing.""" return DiscoveredResource( resource_type=resource_type, unique_id=unique_id, name=name, provider=provider, platform_category=platform_category, architecture=architecture, endpoint="https://k8s-api.local:6443", attributes=attributes or {}, raw_references=[], ) # --------------------------------------------------------------------------- # Tests: No shared values produces no variables # --------------------------------------------------------------------------- class TestNoSharedValues: """Tests that no variables are produced when values are not shared.""" def test_empty_resources_produces_no_variables(self): """An empty resource list produces no variables.""" extractor = VariableExtractor() result = extractor.extract_variables([]) assert result == [] def test_single_resource_produces_no_variables(self): """A single resource cannot have shared values.""" resource = make_resource(attributes={"namespace": "default", "replicas": 3}) extractor = VariableExtractor() result = extractor.extract_variables([resource]) assert result == [] def test_two_resources_no_common_values_produces_no_variables(self): """Two resources with completely different attribute values produce no variables.""" r1 = make_resource( unique_id="r1", name="app-a", attributes={"namespace": "alpha", "replicas": 1}, ) r2 = make_resource( unique_id="r2", name="app-b", attributes={"namespace": "beta", "replicas": 2}, ) extractor = VariableExtractor() result = extractor.extract_variables([r1, r2]) assert result == [] def test_two_resources_same_key_different_values_no_variable(self): """Two resources with the same key but different values produce no variable.""" r1 = make_resource( unique_id="r1", name="app-a", attributes={"environment": "staging"}, ) r2 = make_resource( unique_id="r2", name="app-b", attributes={"environment": "production"}, ) extractor = VariableExtractor() result = extractor.extract_variables([r1, r2]) assert result == [] # --------------------------------------------------------------------------- # Tests: Value appearing in 2 resources produces a variable # --------------------------------------------------------------------------- class TestSharedValueExtraction: """Tests that shared values are correctly extracted as variables.""" def test_same_value_in_two_resources_produces_variable(self): """A value appearing in 2 resources for the same key produces a variable.""" r1 = make_resource( unique_id="r1", name="app-a", attributes={"namespace": "production"}, ) r2 = make_resource( unique_id="r2", name="app-b", attributes={"namespace": "production"}, ) extractor = VariableExtractor() result = extractor.extract_variables([r1, r2]) assert len(result) == 1 assert result[0].name == "var_namespace" def test_same_value_in_three_resources_produces_one_variable(self): """A value appearing in 3 resources still produces exactly one variable.""" resources = [ make_resource( unique_id=f"r{i}", name=f"app-{i}", attributes={"region": "us-east-1"}, ) for i in range(3) ] extractor = VariableExtractor() result = extractor.extract_variables(resources) assert len(result) == 1 assert result[0].name == "var_region" def test_multiple_shared_keys_produce_multiple_variables(self): """Multiple shared attribute keys each produce their own variable.""" r1 = make_resource( unique_id="r1", name="app-a", attributes={"namespace": "default", "environment": "prod"}, ) r2 = make_resource( unique_id="r2", name="app-b", attributes={"namespace": "default", "environment": "prod"}, ) extractor = VariableExtractor() result = extractor.extract_variables([r1, r2]) var_names = {v.name for v in result} assert "var_namespace" in var_names assert "var_environment" in var_names # --------------------------------------------------------------------------- # Tests: Default is set to most common value # --------------------------------------------------------------------------- class TestDefaultValue: """Tests that the default value is set to the most common value.""" def test_default_is_most_common_value(self): """When only one value is shared (2+ resources), default is that value.""" r1 = make_resource( unique_id="r1", name="app-a", attributes={"namespace": "production"} ) r2 = make_resource( unique_id="r2", name="app-b", attributes={"namespace": "production"} ) r3 = make_resource( unique_id="r3", name="app-c", attributes={"namespace": "staging"} ) extractor = VariableExtractor() result = extractor.extract_variables([r1, r2, r3]) # "production" appears in 2 resources, "staging" in 1 (not shared) # The variable for "production" should have default = "production" assert len(result) == 1 assert result[0].default_value == '"production"' def test_default_with_equal_counts_picks_one(self): """When values have equal counts, each variable gets its own value as default.""" r1 = make_resource( unique_id="r1", name="app-a", attributes={"namespace": "alpha"} ) r2 = make_resource( unique_id="r2", name="app-b", attributes={"namespace": "alpha"} ) r3 = make_resource( unique_id="r3", name="app-c", attributes={"namespace": "beta"} ) r4 = make_resource( unique_id="r4", name="app-d", attributes={"namespace": "beta"} ) extractor = VariableExtractor() result = extractor.extract_variables([r1, r2, r3, r4]) # Both "alpha" and "beta" appear in 2 resources each # Both should produce variables; each with its own value as default assert len(result) == 2 defaults = {v.default_value for v in result} assert '"alpha"' in defaults assert '"beta"' in defaults # --------------------------------------------------------------------------- # Tests: Type expression matches value type # --------------------------------------------------------------------------- class TestTypeExpression: """Tests that type expressions match the Python value type.""" def test_string_value_has_string_type(self): """A string attribute value produces type = string.""" r1 = make_resource( unique_id="r1", name="app-a", attributes={"namespace": "default"} ) r2 = make_resource( unique_id="r2", name="app-b", attributes={"namespace": "default"} ) extractor = VariableExtractor() result = extractor.extract_variables([r1, r2]) assert result[0].type_expr == "string" def test_integer_value_has_number_type(self): """An integer attribute value produces type = number.""" r1 = make_resource( unique_id="r1", name="app-a", attributes={"replicas": 3} ) r2 = make_resource( unique_id="r2", name="app-b", attributes={"replicas": 3} ) extractor = VariableExtractor() result = extractor.extract_variables([r1, r2]) assert result[0].type_expr == "number" def test_boolean_value_has_bool_type(self): """A boolean attribute value produces type = bool.""" r1 = make_resource( unique_id="r1", name="app-a", attributes={"enabled": True} ) r2 = make_resource( unique_id="r2", name="app-b", attributes={"enabled": True} ) extractor = VariableExtractor() result = extractor.extract_variables([r1, r2]) assert result[0].type_expr == "bool" def test_number_default_format(self): """A number default is formatted without quotes.""" r1 = make_resource( unique_id="r1", name="app-a", attributes={"replicas": 3} ) r2 = make_resource( unique_id="r2", name="app-b", attributes={"replicas": 3} ) extractor = VariableExtractor() result = extractor.extract_variables([r1, r2]) assert result[0].default_value == "3" def test_bool_default_format(self): """A boolean default is formatted as true/false.""" r1 = make_resource( unique_id="r1", name="app-a", attributes={"enabled": False} ) r2 = make_resource( unique_id="r2", name="app-b", attributes={"enabled": False} ) extractor = VariableExtractor() result = extractor.extract_variables([r1, r2]) assert result[0].default_value == "false" # --------------------------------------------------------------------------- # Tests: used_by lists all resources using the variable # --------------------------------------------------------------------------- class TestUsedBy: """Tests that used_by correctly lists all resources using the variable.""" def test_used_by_contains_both_resource_ids(self): """used_by lists both resource unique_ids that share the value.""" r1 = make_resource( unique_id="ns/deployments/app-a", name="app-a", attributes={"namespace": "production"}, ) r2 = make_resource( unique_id="ns/deployments/app-b", name="app-b", attributes={"namespace": "production"}, ) extractor = VariableExtractor() result = extractor.extract_variables([r1, r2]) assert len(result) == 1 assert "ns/deployments/app-a" in result[0].used_by assert "ns/deployments/app-b" in result[0].used_by def test_used_by_contains_all_three_resource_ids(self): """used_by lists all three resource unique_ids when value is shared by 3.""" resources = [ make_resource( unique_id=f"ns/deployments/app-{i}", name=f"app-{i}", attributes={"environment": "prod"}, ) for i in range(3) ] extractor = VariableExtractor() result = extractor.extract_variables(resources) assert len(result[0].used_by) == 3 for i in range(3): assert f"ns/deployments/app-{i}" in result[0].used_by def test_used_by_excludes_resources_with_different_value(self): """used_by does not include resources that have a different value for the key.""" r1 = make_resource( unique_id="r1", name="app-a", attributes={"namespace": "production"} ) r2 = make_resource( unique_id="r2", name="app-b", attributes={"namespace": "production"} ) r3 = make_resource( unique_id="r3", name="app-c", attributes={"namespace": "staging"} ) extractor = VariableExtractor() result = extractor.extract_variables([r1, r2, r3]) # Only the "production" variable should exist (2 resources) assert len(result) == 1 assert "r1" in result[0].used_by assert "r2" in result[0].used_by assert "r3" not in result[0].used_by # --------------------------------------------------------------------------- # Tests: generate_variables_tf output # --------------------------------------------------------------------------- class TestGenerateVariablesTf: """Tests for the variables.tf file content generation.""" def test_empty_variables_produces_empty_string(self): """No variables produces an empty string.""" extractor = VariableExtractor() result = extractor.generate_variables_tf([]) assert result == "" def test_single_variable_produces_valid_block(self): """A single variable produces a valid Terraform variable block.""" var = ExtractedVariable( name="var_namespace", type_expr="string", default_value='"production"', description="Shared namespace value extracted from 2 resources", used_by=["r1", "r2"], ) extractor = VariableExtractor() result = extractor.generate_variables_tf([var]) assert 'variable "var_namespace"' in result assert "type = string" in result assert 'description = "Shared namespace value extracted from 2 resources"' in result assert 'default = "production"' in result def test_multiple_variables_separated_by_blank_line(self): """Multiple variables are separated by blank lines.""" vars_list = [ ExtractedVariable( name="var_namespace", type_expr="string", default_value='"default"', description="Shared namespace", used_by=["r1", "r2"], ), ExtractedVariable( name="var_replicas", type_expr="number", default_value="3", description="Shared replicas", used_by=["r1", "r2"], ), ] extractor = VariableExtractor() result = extractor.generate_variables_tf(vars_list) assert 'variable "var_namespace"' in result assert 'variable "var_replicas"' in result # Two blocks separated by a blank line assert "\n\n" in result