406 lines
15 KiB
Python
406 lines
15 KiB
Python
"""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
|