Files
SnarfCode/tests/unit/test_state_builder.py
2026-05-22 00:19:30 -04:00

682 lines
25 KiB
Python

"""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"