"""Unit tests for the StateBuilder.""" import json import uuid import pytest from iac_reverse.models import ( CodeGenerationResult, CpuArchitecture, DependencyGraph, DiscoveredResource, GeneratedFile, PlatformCategory, ProviderType, ResourceRelationship, StateEntry, StateFile, ) from iac_reverse.state_builder import StateBuilder # --------------------------------------------------------------------------- # Helpers / Fixtures # --------------------------------------------------------------------------- def make_resource( resource_type: str = "kubernetes_deployment", unique_id: str = "apps/v1/deployments/default/nginx", name: str = "nginx", raw_references: list[str] | None = None, attributes: dict | None = None, provider: ProviderType = ProviderType.KUBERNETES, platform_category: PlatformCategory = PlatformCategory.CONTAINER_ORCHESTRATION, ) -> 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=CpuArchitecture.AARCH64, endpoint="https://k8s-api.local:6443", attributes=attributes or {"replicas": 3, "image": "nginx:1.25"}, raw_references=raw_references or [], ) def make_code_generation_result() -> CodeGenerationResult: """Create a minimal CodeGenerationResult for testing.""" return CodeGenerationResult( resource_files=[ GeneratedFile( filename="kubernetes_deployment.tf", content='resource "kubernetes_deployment" "nginx" {}', resource_count=1, ) ], variables_file=GeneratedFile( filename="variables.tf", content="", resource_count=0 ), provider_file=GeneratedFile( filename="providers.tf", content="", resource_count=0 ), ) def make_dependency_graph( resources: list[DiscoveredResource], relationships: list[ResourceRelationship] | None = None, ) -> DependencyGraph: """Create a DependencyGraph from resources and optional relationships.""" return DependencyGraph( resources=resources, relationships=relationships or [], topological_order=[r.unique_id for r in resources], cycles=[], unresolved_references=[], ) # --------------------------------------------------------------------------- # Tests: Single resource produces valid state entry # --------------------------------------------------------------------------- class TestSingleResource: """Tests for state generation with a single resource.""" def test_single_resource_produces_one_state_entry(self): """A single resource in the graph produces exactly one state entry.""" resource = make_resource() graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="3.2.1") assert len(state.resources) == 1 def test_state_entry_has_correct_resource_type(self): """State entry resource_type matches the discovered resource type.""" resource = make_resource(resource_type="kubernetes_deployment") graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") assert state.resources[0].resource_type == "kubernetes_deployment" def test_state_entry_has_sanitized_resource_name(self): """State entry resource_name is a valid Terraform identifier.""" resource = make_resource(name="my-nginx-app") graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") # Hyphens should be replaced with underscores assert state.resources[0].resource_name == "my_nginx_app" def test_state_entry_provider_id_is_unique_id(self): """State entry provider_id is the live infrastructure unique_id.""" resource = make_resource(unique_id="apps/v1/deployments/default/nginx") graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") assert state.resources[0].provider_id == "apps/v1/deployments/default/nginx" def test_state_entry_attributes_from_discovery(self): """State entry attributes contain the full discovery attribute set.""" attrs = {"replicas": 3, "image": "nginx:1.25", "namespace": "default"} resource = make_resource(attributes=attrs) graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") assert state.resources[0].attributes == attrs # --------------------------------------------------------------------------- # Tests: Multiple resources produce multiple state entries # --------------------------------------------------------------------------- class TestMultipleResources: """Tests for state generation with multiple resources.""" def test_multiple_resources_produce_multiple_entries(self): """Each resource in the graph produces a corresponding state entry.""" resources = [ make_resource( resource_type="kubernetes_deployment", unique_id="deploy/nginx", name="nginx", ), make_resource( resource_type="kubernetes_service", unique_id="svc/nginx-svc", name="nginx-svc", ), make_resource( resource_type="kubernetes_namespace", unique_id="ns/default", name="default", ), ] graph = make_dependency_graph(resources) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="2.0.0") assert len(state.resources) == 3 def test_multiple_resources_have_distinct_entries(self): """Each state entry corresponds to a different resource.""" resources = [ make_resource( resource_type="kubernetes_deployment", unique_id="deploy/nginx", name="nginx", ), make_resource( resource_type="kubernetes_service", unique_id="svc/redis", name="redis", ), ] graph = make_dependency_graph(resources) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") types = {e.resource_type for e in state.resources} assert "kubernetes_deployment" in types assert "kubernetes_service" in types # --------------------------------------------------------------------------- # Tests: Lineage is a valid UUID # --------------------------------------------------------------------------- class TestLineage: """Tests for state file lineage UUID generation.""" def test_lineage_is_valid_uuid(self): """The state file lineage is a valid UUID string.""" resource = make_resource() graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") # Should not raise ValueError parsed = uuid.UUID(state.lineage) assert str(parsed) == state.lineage def test_lineage_is_unique_per_build(self): """Each build produces a different lineage UUID.""" resource = make_resource() graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state1 = builder.build(code_result, graph, provider_version="1.0.0") state2 = builder.build(code_result, graph, provider_version="1.0.0") assert state1.lineage != state2.lineage # --------------------------------------------------------------------------- # Tests: Dependencies are included as Terraform resource addresses # --------------------------------------------------------------------------- class TestDependencies: """Tests for dependency references in state entries.""" def test_dependencies_as_terraform_addresses(self): """Dependencies are formatted as resource_type.resource_name.""" namespace = make_resource( resource_type="kubernetes_namespace", unique_id="ns/default", name="default", ) deployment = make_resource( resource_type="kubernetes_deployment", unique_id="deploy/nginx", name="nginx", raw_references=["ns/default"], ) relationship = ResourceRelationship( source_id="deploy/nginx", target_id="ns/default", relationship_type="dependency", source_attribute="namespace", ) graph = make_dependency_graph( [namespace, deployment], relationships=[relationship] ) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") # Find the deployment entry deploy_entry = next( e for e in state.resources if e.resource_type == "kubernetes_deployment" ) assert "kubernetes_namespace.default" in deploy_entry.dependencies def test_resource_without_dependencies_has_empty_list(self): """A resource with no relationships has an empty dependencies list.""" resource = make_resource() graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") assert state.resources[0].dependencies == [] def test_multiple_dependencies_all_included(self): """All dependency relationships are included in the state entry.""" ns = make_resource( resource_type="kubernetes_namespace", unique_id="ns/default", name="default", ) svc = make_resource( resource_type="kubernetes_service", unique_id="svc/nginx-svc", name="nginx-svc", ) deployment = make_resource( resource_type="kubernetes_deployment", unique_id="deploy/nginx", name="nginx", ) relationships = [ ResourceRelationship( source_id="deploy/nginx", target_id="ns/default", relationship_type="dependency", source_attribute="namespace", ), ResourceRelationship( source_id="deploy/nginx", target_id="svc/nginx-svc", relationship_type="reference", source_attribute="service", ), ] graph = make_dependency_graph([ns, svc, deployment], relationships=relationships) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") deploy_entry = next( e for e in state.resources if e.resource_type == "kubernetes_deployment" ) assert len(deploy_entry.dependencies) == 2 assert "kubernetes_namespace.default" in deploy_entry.dependencies assert "kubernetes_service.nginx_svc" in deploy_entry.dependencies # --------------------------------------------------------------------------- # Tests: Sensitive attributes are marked # --------------------------------------------------------------------------- class TestSensitiveAttributes: """Tests for sensitive attribute detection.""" def test_password_attribute_marked_sensitive(self): """Attributes containing 'password' are marked sensitive.""" resource = make_resource( attributes={"db_password": "secret123", "name": "mydb"} ) graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") assert "db_password" in state.resources[0].sensitive_attributes def test_token_attribute_marked_sensitive(self): """Attributes containing 'token' are marked sensitive.""" resource = make_resource( attributes={"api_token": "abc123", "endpoint": "https://api.local"} ) graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") assert "api_token" in state.resources[0].sensitive_attributes def test_secret_attribute_marked_sensitive(self): """Attributes containing 'secret' are marked sensitive.""" resource = make_resource( attributes={"client_secret": "xyz", "client_id": "app1"} ) graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") assert "client_secret" in state.resources[0].sensitive_attributes def test_key_attribute_marked_sensitive(self): """Attributes containing 'key' are marked sensitive.""" resource = make_resource( attributes={"private_key": "-----BEGIN RSA KEY-----", "name": "cert1"} ) graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") assert "private_key" in state.resources[0].sensitive_attributes def test_certificate_attribute_marked_sensitive(self): """Attributes containing 'certificate' are marked sensitive.""" resource = make_resource( attributes={"tls_certificate": "cert-data", "port": 443} ) graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") assert "tls_certificate" in state.resources[0].sensitive_attributes def test_non_sensitive_attributes_not_marked(self): """Attributes without sensitive patterns are not marked.""" resource = make_resource( attributes={"replicas": 3, "image": "nginx:1.25", "namespace": "default"} ) graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") assert state.resources[0].sensitive_attributes == [] def test_nested_sensitive_attributes_detected(self): """Sensitive attributes in nested dicts are detected.""" resource = make_resource( attributes={ "config": {"database_password": "secret", "host": "localhost"} } ) graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") assert "config.database_password" in state.resources[0].sensitive_attributes # --------------------------------------------------------------------------- # Tests: Schema version is set from provider_version # --------------------------------------------------------------------------- class TestSchemaVersion: """Tests for schema_version setting from provider_version.""" def test_schema_version_from_major_version(self): """Schema version is the major version number from provider_version.""" resource = make_resource() graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="3.2.1") assert state.resources[0].schema_version == 3 def test_schema_version_single_digit(self): """Schema version works with a single digit version string.""" resource = make_resource() graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1") assert state.resources[0].schema_version == 1 def test_schema_version_defaults_to_zero_on_invalid(self): """Schema version defaults to 0 if provider_version is unparseable.""" resource = make_resource() graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="invalid") assert state.resources[0].schema_version == 0 # --------------------------------------------------------------------------- # Tests: to_json() produces valid JSON with correct structure # --------------------------------------------------------------------------- class TestToJson: """Tests for state file JSON serialization.""" def test_to_json_produces_valid_json(self): """to_json() output is valid JSON.""" resource = make_resource( attributes={"replicas": 3, "image": "nginx:1.25"} ) graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") json_str = state.to_json() parsed = json.loads(json_str) assert isinstance(parsed, dict) def test_to_json_has_version_4(self): """JSON output has version field set to 4.""" resource = make_resource() graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") parsed = json.loads(state.to_json()) assert parsed["version"] == 4 def test_to_json_has_serial_1(self): """JSON output has serial field set to 1.""" resource = make_resource() graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") parsed = json.loads(state.to_json()) assert parsed["serial"] == 1 def test_to_json_has_terraform_version(self): """JSON output includes the terraform_version.""" resource = make_resource() graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder(terraform_version="1.7.0") state = builder.build(code_result, graph, provider_version="1.0.0") parsed = json.loads(state.to_json()) assert parsed["terraform_version"] == "1.7.0" def test_to_json_has_valid_lineage_uuid(self): """JSON output lineage is a valid UUID.""" resource = make_resource() graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") parsed = json.loads(state.to_json()) # Should not raise ValueError uuid.UUID(parsed["lineage"]) def test_to_json_resources_have_correct_structure(self): """JSON resources have mode, type, name, provider, and instances.""" resource = make_resource( resource_type="kubernetes_deployment", name="nginx", attributes={"replicas": 3}, ) graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="2.0.0") parsed = json.loads(state.to_json()) assert len(parsed["resources"]) == 1 res = parsed["resources"][0] assert res["mode"] == "managed" assert res["type"] == "kubernetes_deployment" assert res["name"] == "nginx" assert "provider" in res assert len(res["instances"]) == 1 def test_to_json_instance_has_attributes_with_id(self): """JSON instance attributes include the provider_id as 'id'.""" resource = make_resource( unique_id="apps/v1/deployments/default/nginx", attributes={"replicas": 3}, ) graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") parsed = json.loads(state.to_json()) instance = parsed["resources"][0]["instances"][0] assert instance["attributes"]["id"] == "apps/v1/deployments/default/nginx" assert instance["attributes"]["replicas"] == 3 def test_to_json_instance_has_schema_version(self): """JSON instance includes schema_version.""" resource = make_resource() graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="3.0.0") parsed = json.loads(state.to_json()) instance = parsed["resources"][0]["instances"][0] assert instance["schema_version"] == 3 def test_to_json_instance_has_dependencies(self): """JSON instance includes dependencies list.""" ns = make_resource( resource_type="kubernetes_namespace", unique_id="ns/default", name="default", ) deployment = make_resource( resource_type="kubernetes_deployment", unique_id="deploy/nginx", name="nginx", ) relationship = ResourceRelationship( source_id="deploy/nginx", target_id="ns/default", relationship_type="dependency", source_attribute="namespace", ) graph = make_dependency_graph([ns, deployment], relationships=[relationship]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") parsed = json.loads(state.to_json()) deploy_res = next( r for r in parsed["resources"] if r["type"] == "kubernetes_deployment" ) instance = deploy_res["instances"][0] assert "kubernetes_namespace.default" in instance["dependencies"] # --------------------------------------------------------------------------- # Tests: State file metadata # --------------------------------------------------------------------------- class TestStateFileMetadata: """Tests for state file top-level metadata.""" def test_version_is_4(self): """State file version is always 4.""" resource = make_resource() graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") assert state.version == 4 def test_serial_is_1(self): """State file serial is 1 for initial generation.""" resource = make_resource() graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder() state = builder.build(code_result, graph, provider_version="1.0.0") assert state.serial == 1 def test_custom_terraform_version(self): """StateBuilder accepts a custom terraform_version.""" resource = make_resource() graph = make_dependency_graph([resource]) code_result = make_code_generation_result() builder = StateBuilder(terraform_version="1.8.0") state = builder.build(code_result, graph, provider_version="1.0.0") assert state.terraform_version == "1.8.0"