"""Unit tests for the CodeGenerator.""" import pytest from iac_reverse.models import ( CodeGenerationResult, CpuArchitecture, DependencyGraph, DiscoveredResource, GeneratedFile, PlatformCategory, ProviderType, ResourceRelationship, ScanProfile, ) from iac_reverse.generator import CodeGenerator # --------------------------------------------------------------------------- # Helpers / Fixtures # --------------------------------------------------------------------------- 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, raw_references: list[str] | 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=raw_references or [], ) def make_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=[], ) def make_profiles() -> list[ScanProfile]: """Create a default list of scan profiles for testing.""" return [ ScanProfile( provider=ProviderType.KUBERNETES, credentials={"kubeconfig_path": "/home/user/.kube/config"}, ) ] # --------------------------------------------------------------------------- # Tests: Single resource generates valid HCL # --------------------------------------------------------------------------- class TestSingleResourceGeneration: """Tests for generating HCL from a single resource.""" def test_single_resource_produces_one_file(self): """A single resource produces exactly one resource file.""" resource = make_resource( attributes={"replicas": 3, "image": "nginx:1.25"}, ) graph = make_graph([resource]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) assert len(result.resource_files) == 1 def test_single_resource_file_has_correct_filename(self): """The generated file is named after the resource type.""" resource = make_resource(resource_type="kubernetes_deployment") graph = make_graph([resource]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) assert result.resource_files[0].filename == "kubernetes_deployment.tf" def test_single_resource_file_contains_resource_block(self): """The generated file contains a resource block with the correct type.""" resource = make_resource( resource_type="kubernetes_deployment", name="nginx", attributes={"replicas": 3}, ) graph = make_graph([resource]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) content = result.resource_files[0].content assert 'resource "kubernetes_deployment" "nginx"' in content def test_single_resource_includes_attributes(self): """The generated resource block includes all attributes.""" resource = make_resource( attributes={"replicas": 3, "image": "nginx:1.25"}, ) graph = make_graph([resource]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) content = result.resource_files[0].content assert "replicas = 3" in content assert 'image = "nginx:1.25"' in content def test_single_resource_resource_count_is_one(self): """The resource_count for a single resource file is 1.""" resource = make_resource() graph = make_graph([resource]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) assert result.resource_files[0].resource_count == 1 # --------------------------------------------------------------------------- # Tests: Multiple resources of same type go in one file # --------------------------------------------------------------------------- class TestSameTypeGrouping: """Tests for grouping multiple resources of the same type into one file.""" def test_two_resources_same_type_produce_one_file(self): """Two resources of the same type produce exactly one file.""" resource_a = make_resource( unique_id="default/deployments/app-a", name="app-a", attributes={"replicas": 2}, ) resource_b = make_resource( unique_id="default/deployments/app-b", name="app-b", attributes={"replicas": 1}, ) graph = make_graph([resource_a, resource_b]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) assert len(result.resource_files) == 1 def test_two_resources_same_type_both_in_file(self): """Both resources appear in the same file.""" resource_a = make_resource( unique_id="default/deployments/app-a", name="app-a", attributes={"replicas": 2}, ) resource_b = make_resource( unique_id="default/deployments/app-b", name="app-b", attributes={"replicas": 1}, ) graph = make_graph([resource_a, resource_b]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) content = result.resource_files[0].content assert 'resource "kubernetes_deployment" "app_a"' in content assert 'resource "kubernetes_deployment" "app_b"' in content def test_resource_count_matches_number_of_resources(self): """The resource_count reflects the number of resources in the file.""" resources = [ make_resource( unique_id=f"default/deployments/app-{i}", name=f"app-{i}", ) for i in range(3) ] graph = make_graph(resources) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) assert result.resource_files[0].resource_count == 3 # --------------------------------------------------------------------------- # Tests: Different resource types go in separate files # --------------------------------------------------------------------------- class TestDifferentTypesSeparateFiles: """Tests for separating different resource types into different files.""" def test_two_different_types_produce_two_files(self): """Two resources of different types produce two files.""" deployment = make_resource( resource_type="kubernetes_deployment", unique_id="default/deployments/nginx", name="nginx", ) service = make_resource( resource_type="kubernetes_service", unique_id="default/services/nginx-svc", name="nginx-svc", ) graph = make_graph([deployment, service]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) assert len(result.resource_files) == 2 def test_different_types_have_correct_filenames(self): """Each file is named after its resource type.""" deployment = make_resource( resource_type="kubernetes_deployment", unique_id="default/deployments/nginx", name="nginx", ) service = make_resource( resource_type="kubernetes_service", unique_id="default/services/nginx-svc", name="nginx-svc", ) graph = make_graph([deployment, service]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) filenames = {f.filename for f in result.resource_files} assert "kubernetes_deployment.tf" in filenames assert "kubernetes_service.tf" in filenames def test_each_file_contains_only_its_type(self): """Each file contains only resource blocks of its designated type.""" deployment = make_resource( resource_type="kubernetes_deployment", unique_id="default/deployments/nginx", name="nginx", ) service = make_resource( resource_type="kubernetes_service", unique_id="default/services/nginx-svc", name="nginx-svc", ) graph = make_graph([deployment, service]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) for f in result.resource_files: if f.filename == "kubernetes_deployment.tf": assert "kubernetes_deployment" in f.content assert 'resource "kubernetes_service"' not in f.content elif f.filename == "kubernetes_service.tf": assert "kubernetes_service" in f.content assert 'resource "kubernetes_deployment"' not in f.content def test_three_types_produce_three_files(self): """Three distinct resource types produce three files.""" resources = [ make_resource( resource_type="kubernetes_deployment", unique_id="default/deployments/app", name="app", ), make_resource( resource_type="kubernetes_service", unique_id="default/services/app-svc", name="app-svc", ), make_resource( resource_type="windows_service", unique_id="win/services/iis", name="iis", provider=ProviderType.WINDOWS, platform_category=PlatformCategory.WINDOWS, architecture=CpuArchitecture.AMD64, ), ] graph = make_graph(resources) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) assert len(result.resource_files) == 3 # --------------------------------------------------------------------------- # Tests: Traceability comments are present # --------------------------------------------------------------------------- class TestTraceabilityComments: """Tests for traceability comments in generated HCL.""" def test_resource_block_has_source_comment(self): """Each resource block is preceded by a comment with the unique_id.""" resource = make_resource( unique_id="apps/v1/deployments/default/nginx", name="nginx", ) graph = make_graph([resource]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) content = result.resource_files[0].content assert "# Source: apps/v1/deployments/default/nginx" in content def test_multiple_resources_each_have_source_comment(self): """Each resource in a multi-resource file has its own source comment.""" resource_a = make_resource( unique_id="default/deployments/app-a", name="app-a", ) resource_b = make_resource( unique_id="default/deployments/app-b", name="app-b", ) graph = make_graph([resource_a, resource_b]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) content = result.resource_files[0].content assert "# Source: default/deployments/app-a" in content assert "# Source: default/deployments/app-b" in content def test_windows_resource_has_source_comment(self): """Windows resources also have traceability comments.""" resource = make_resource( resource_type="windows_service", unique_id="win-server-01/services/W3SVC", name="W3SVC", provider=ProviderType.WINDOWS, platform_category=PlatformCategory.WINDOWS, architecture=CpuArchitecture.AMD64, ) graph = make_graph([resource]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) content = result.resource_files[0].content assert "# Source: win-server-01/services/W3SVC" in content # --------------------------------------------------------------------------- # Tests: Architecture tags are included # --------------------------------------------------------------------------- class TestArchitectureTags: """Tests for architecture-specific tags/labels on resources.""" def test_aarch64_resource_has_arch_tag(self): """An AArch64 resource includes arch = aarch64 in tags.""" resource = make_resource( architecture=CpuArchitecture.AARCH64, attributes={"replicas": 1}, ) graph = make_graph([resource]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) content = result.resource_files[0].content assert '"arch" = "aarch64"' in content def test_amd64_resource_has_arch_tag(self): """An AMD64 resource includes arch = amd64 in tags.""" resource = make_resource( resource_type="windows_service", unique_id="win/services/svc", name="svc", architecture=CpuArchitecture.AMD64, provider=ProviderType.WINDOWS, platform_category=PlatformCategory.WINDOWS, ) graph = make_graph([resource]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) content = result.resource_files[0].content assert '"arch" = "amd64"' in content def test_arm_resource_has_arch_tag(self): """An ARM resource includes arch = arm in tags.""" resource = make_resource( architecture=CpuArchitecture.ARM, ) graph = make_graph([resource]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) content = result.resource_files[0].content assert '"arch" = "arm"' in content def test_managed_by_tag_is_present(self): """All resources include a managed_by = iac-reverse tag.""" resource = make_resource() graph = make_graph([resource]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) content = result.resource_files[0].content assert '"managed_by" = "iac-reverse"' in content # --------------------------------------------------------------------------- # Tests: Dependencies use Terraform references # --------------------------------------------------------------------------- class TestTerraformReferences: """Tests for Terraform resource references in generated HCL.""" def test_dependency_uses_terraform_reference(self): """A resource referencing another uses a Terraform reference expression.""" namespace = make_resource( resource_type="kubernetes_namespace", unique_id="ns/default", name="default", ) deployment = make_resource( resource_type="kubernetes_deployment", unique_id="default/deployments/nginx", name="nginx", attributes={"namespace": "default"}, raw_references=["ns/default"], ) relationships = [ ResourceRelationship( source_id="default/deployments/nginx", target_id="ns/default", relationship_type="parent-child", source_attribute="namespace", ) ] graph = make_graph([namespace, deployment], relationships) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) # Find the deployment file deploy_file = next( f for f in result.resource_files if f.filename == "kubernetes_deployment.tf" ) content = deploy_file.content # Should use Terraform reference, not hardcoded "default" assert "kubernetes_namespace.default.id" in content def test_reference_by_unique_id_in_attribute(self): """An attribute containing a target's unique_id is replaced with a reference.""" app_pool = make_resource( resource_type="windows_iis_app_pool", unique_id="win/iis/app_pools/DefaultAppPool", name="DefaultAppPool", provider=ProviderType.WINDOWS, platform_category=PlatformCategory.WINDOWS, architecture=CpuArchitecture.AMD64, ) site = make_resource( resource_type="windows_iis_site", unique_id="win/iis/sites/MySite", name="MySite", attributes={"app_pool": "win/iis/app_pools/DefaultAppPool"}, provider=ProviderType.WINDOWS, platform_category=PlatformCategory.WINDOWS, architecture=CpuArchitecture.AMD64, ) relationships = [ ResourceRelationship( source_id="win/iis/sites/MySite", target_id="win/iis/app_pools/DefaultAppPool", relationship_type="dependency", source_attribute="app_pool", ) ] graph = make_graph([app_pool, site], relationships) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) site_file = next( f for f in result.resource_files if f.filename == "windows_iis_site.tf" ) content = site_file.content assert "windows_iis_app_pool.DefaultAppPool.id" in content def test_non_reference_attributes_remain_literal(self): """Attributes that don't reference other resources remain as literals.""" resource = make_resource( attributes={"replicas": 3, "image": "nginx:1.25"}, ) graph = make_graph([resource]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) content = result.resource_files[0].content assert "replicas = 3" in content assert 'image = "nginx:1.25"' in content def test_multiple_dependencies_all_use_references(self): """A resource with multiple dependencies uses references for all.""" namespace = make_resource( resource_type="kubernetes_namespace", unique_id="ns/prod", name="prod", ) config_map = make_resource( resource_type="kubernetes_config_map", unique_id="prod/configmaps/app-config", name="app-config", ) deployment = make_resource( resource_type="kubernetes_deployment", unique_id="prod/deployments/app", name="app", attributes={ "namespace": "prod", "config_map": "app-config", "replicas": 2, }, raw_references=["ns/prod", "prod/configmaps/app-config"], ) relationships = [ ResourceRelationship( source_id="prod/deployments/app", target_id="ns/prod", relationship_type="parent-child", source_attribute="namespace", ), ResourceRelationship( source_id="prod/deployments/app", target_id="prod/configmaps/app-config", relationship_type="dependency", source_attribute="config_map", ), ] graph = make_graph([namespace, config_map, deployment], relationships) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) deploy_file = next( f for f in result.resource_files if f.filename == "kubernetes_deployment.tf" ) content = deploy_file.content assert "kubernetes_namespace.prod.id" in content assert "kubernetes_config_map.app_config.id" in content assert "replicas = 2" in content # --------------------------------------------------------------------------- # Tests: Result structure # --------------------------------------------------------------------------- class TestResultStructure: """Tests for the CodeGenerationResult structure.""" def test_result_has_variables_file(self): """The result includes a variables_file (empty placeholder for now).""" resource = make_resource() graph = make_graph([resource]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) assert result.variables_file is not None assert result.variables_file.filename == "variables.tf" def test_result_has_provider_file(self): """The result includes a provider_file (empty placeholder for now).""" resource = make_resource() graph = make_graph([resource]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) assert result.provider_file is not None assert result.provider_file.filename == "providers.tf" def test_empty_graph_produces_no_resource_files(self): """An empty graph produces no resource files.""" graph = make_graph([]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) assert result.resource_files == [] def test_name_sanitization_replaces_special_chars(self): """Resource names with special characters are sanitized for Terraform.""" resource = make_resource( name="my-app.v2", unique_id="default/deployments/my-app.v2", ) graph = make_graph([resource]) generator = CodeGenerator() result = generator.generate(graph, make_profiles()) content = result.resource_files[0].content # Should use sanitized name (hyphens and dots replaced with underscores) assert 'resource "kubernetes_deployment" "my_app_v2"' in content