diff --git a/test/variables_test.go b/test/variables_test.go new file mode 100644 index 00000000..a3b78ab0 --- /dev/null +++ b/test/variables_test.go @@ -0,0 +1,368 @@ +package test + +import ( + "path/filepath" + "testing" + "os" + "fmt" + "strings" + + "github.com/gruntwork-io/terratest/modules/terraform" +) + +func writeFile(t *testing.T, dir, name, content string) string { + t.Helper() + full := filepath.Join(dir, name) + if err := os.WriteFile(full, []byte(content), 0o644); err \!= nil { + t.Fatalf("write %s: %v", full, err) + } + return full +} + +func moduleRoot(t *testing.T) string { + // Resolve repository root by walking up until we find any *.tf file that includes the variables under test. + // Fallback to current working directory. + wd, _ := os.Getwd() + cur := wd + for i := 0; i < 6; i++ { + entries, _ := os.ReadDir(cur) + hasTF := false + for _, e := range entries { + if strings.HasSuffix(e.Name(), ".tf") { + hasTF = true + break + } + } + if hasTF { + return cur + } + cur = filepath.Clean(filepath.Join(cur, "..")) + } + return wd +} + +func minimalMainTF() string { + return ` +terraform { + required_version = ">= 1.5.0" +} + +# Reference variables via locals to ensure type-checking but without provisioning resources. +locals { + # No-ops that touch variables to ensure object attribute defaults are processed. + _touch = [ + var.auto_scaler_profile_expander, + var.automatic_channel_upgrade, + var.green_field_application_gateway_for_ingress, + var.http_proxy_config, + var.identity_type, + var.kms_key_vault_network_access, + var.load_balancer_sku, + var.log_analytics_solution, + var.node_pools, + var.service_mesh_profile, + var.sku_tier, + var.support_plan, + var.upgrade_override, + ] +} +` +} + +// newFixture prepares an isolated temp dir with a main.tf that loads the module-under-test via 'module "uut"'. +func newFixture(t *testing.T) string { + t.Helper() + dir := t.TempDir() + // We source module from repo root so variable definitions are in scope. + root := moduleRoot(t) + main := fmt.Sprintf(` +%s + +module "uut" { + source = "%s" + # no inputs; each test may inject via -var / -var-file +} +`, minimalMainTF(), root) + writeFile(t, dir, "main.tf", main) + return dir +} + +func tfValidateWithVars(t *testing.T, dir string, extraVars map[string]interface{}, varFileContent string, expectValid bool) { + t.Helper() + opts := &terraform.Options{ + TerraformDir: dir, + NoColor: true, + } + // Write var-file if provided + if strings.TrimSpace(varFileContent) \!= "" { + writeFile(t, dir, "vars.auto.tfvars.json", varFileContent) + } + // Merge vars + if len(extraVars) > 0 { + opts.Vars = extraVars + } + + terraform.Init(t, opts) + if expectValid { + terraform.Validate(t, opts) + } else { + // Expect validate to fail + _, err := terraform.ValidateE(t, opts) + if err == nil { + t.Fatalf("expected terraform validate to fail, but it succeeded") + } + } +} + +func Test_AutoScalerProfileExpander_Validation(t *testing.T) { + dir := newFixture(t) + + // Valid values + valids := []string{"least-waste", "most-pods", "priority", "random"} + for _, v := range valids { + t.Run("valid_"+strings.ReplaceAll(v, "-", "_"), func(t *testing.T) { + tfValidateWithVars(t, dir, map[string]interface{}{ + "auto_scaler_profile_expander": v, + }, "", true) + }) + } + + // Invalid value + tfValidateWithVars(t, dir, map[string]interface{}{ + "auto_scaler_profile_expander": "round-robin", + }, "", false) +} + +func Test_AutomaticChannelUpgrade_AllowedValuesAndNull(t *testing.T) { + dir := newFixture(t) + // Null (default) should validate + tfValidateWithVars(t, dir, nil, "", true) + + // Valid values + for _, v := range []string{"patch", "stable", "rapid", "node-image"} { + t.Run("valid_"+strings.ReplaceAll(v, "-", "_"), func(t *testing.T) { + tfValidateWithVars(t, dir, map[string]interface{}{ + "automatic_channel_upgrade": v, + }, "", true) + }) + } + + // Invalid + tfValidateWithVars(t, dir, map[string]interface{}{ + "automatic_channel_upgrade": "weekly", + }, "", false) +} + +func Test_GreenFieldApplicationGatewayForIngress_AtLeastOneSubnetField(t *testing.T) { + dir := newFixture(t) + + // Null allowed + tfValidateWithVars(t, dir, map[string]interface{}{ + "green_field_application_gateway_for_ingress": nil, + }, "", true) + + // Only subnet_id + tfValidateWithVars(t, dir, nil, `{ + "green_field_application_gateway_for_ingress": { + "subnet_id": "/subscriptions/000/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet/subnets/snet" + } + }`, true) + + // Only subnet_cidr + tfValidateWithVars(t, dir, nil, `{ + "green_field_application_gateway_for_ingress": { + "subnet_cidr": "10.10.0.0/24" + } + }`, true) + + // Both + tfValidateWithVars(t, dir, nil, `{ + "green_field_application_gateway_for_ingress": { + "subnet_id": "/subscriptions/000/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet/subnets/snet", + "subnet_cidr": "10.10.0.0/24" + } + }`, true) + + // Neither -> fail + tfValidateWithVars(t, dir, nil, `{ + "green_field_application_gateway_for_ingress": { } + }`, false) +} + +func Test_HttpProxyConfig_RequireAtLeastOneProxy(t *testing.T) { + dir := newFixture(t) + + // Null allowed + tfValidateWithVars(t, dir, nil, `{ + "http_proxy_config": null + }`, true) + + // http_proxy only + tfValidateWithVars(t, dir, nil, `{ + "http_proxy_config": { "http_proxy": "http://proxy.local:8080" } + }`, true) + + // https_proxy only + tfValidateWithVars(t, dir, nil, `{ + "http_proxy_config": { "https_proxy": "https://proxy.local:8443" } + }`, true) + + // both + tfValidateWithVars(t, dir, nil, `{ + "http_proxy_config": { + "http_proxy": "http://proxy.local:8080", + "https_proxy": "https://proxy.local:8443", + "no_proxy": ["10.0.0.0/8","localhost"], + "trusted_ca": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCg==" + } + }`, true) + + // neither -> fail + tfValidateWithVars(t, dir, nil, `{ + "http_proxy_config": { + "no_proxy": ["example.com"] + } + }`, false) +} + +func Test_IdentityType_AllowedValues(t *testing.T) { + dir := newFixture(t) + + // Valid defaults (SystemAssigned) + tfValidateWithVars(t, dir, nil, "", true) + + // Explicit valid + for _, v := range []string{"SystemAssigned", "UserAssigned"} { + t.Run("valid_"+v, func(t *testing.T) { + tfValidateWithVars(t, dir, map[string]interface{}{ + "identity_type": v, + }, "", true) + }) + } + + // Invalid + tfValidateWithVars(t, dir, map[string]interface{}{ + "identity_type": "None", + }, "", false) +} + +func Test_KmsKeyVaultNetworkAccess_AllowedValues(t *testing.T) { + dir := newFixture(t) + for _, v := range []string{"Private", "Public"} { + tfValidateWithVars(t, dir, map[string]interface{}{ + "kms_key_vault_network_access": v, + }, "", true) + } + tfValidateWithVars(t, dir, map[string]interface{}{ + "kms_key_vault_network_access": "Internal", + }, "", false) +} + +func Test_LoadBalancerSku_AllowedValues(t *testing.T) { + dir := newFixture(t) + for _, v := range []string{"basic", "standard"} { + tfValidateWithVars(t, dir, map[string]interface{}{ + "load_balancer_sku": v, + }, "", true) + } + tfValidateWithVars(t, dir, map[string]interface{}{ + "load_balancer_sku": "premium", + }, "", false) +} + +func Test_LogAnalyticsSolution_NullOrNonEmptyID(t *testing.T) { + dir := newFixture(t) + + // null allowed (default) + tfValidateWithVars(t, dir, nil, "", true) + + // valid id provided + tfValidateWithVars(t, dir, nil, `{ + "log_analytics_solution": { "id": "/subscriptions/000/resourceGroups/rg/providers/Microsoft.OperationsManagement/solutions/containerinsights" } + }`, true) + + // empty id -> fail + tfValidateWithVars(t, dir, nil, `{ + "log_analytics_solution": { "id": "" } + }`, false) +} + +func Test_NodePools_NotNullable_DefaultsToEmptyMap(t *testing.T) { + dir := newFixture(t) + + // Default (empty map) should be valid because nullable=false but default is {} + tfValidateWithVars(t, dir, nil, "", true) + + // Explicit empty map valid + tfValidateWithVars(t, dir, map[string]interface{}{ + "node_pools": map[string]any{}, + }, "", true) +} + +func Test_ServiceMeshProfile_ShapeAndDefaults(t *testing.T) { + dir := newFixture(t) + + // Default null allowed + tfValidateWithVars(t, dir, nil, "", true) + + // Provide required fields only; optional bools should pick default true from type + tfValidateWithVars(t, dir, nil, `{ + "service_mesh_profile": { + "mode": "Istio", + "revisions": ["asm-1-20"] + } + }`, true) + + // Missing required field -> fail + tfValidateWithVars(t, dir, nil, `{ + "service_mesh_profile": { + "revisions": ["asm-1-20"] + } + }`, false) +} + +func Test_SKUTier_AllowedValues(t *testing.T) { + dir := newFixture(t) + for _, v := range []string{"Free", "Standard", "Premium"} { + tfValidateWithVars(t, dir, map[string]interface{}{ + "sku_tier": v, + }, "", true) + } + tfValidateWithVars(t, dir, map[string]interface{}{ + "sku_tier": "Paid", + }, "", false) +} + +func Test_SupportPlan_AllowedValues(t *testing.T) { + dir := newFixture(t) + for _, v := range []string{"KubernetesOfficial", "AKSLongTermSupport"} { + tfValidateWithVars(t, dir, map[string]interface{}{ + "support_plan": v, + }, "", true) + } + tfValidateWithVars(t, dir, map[string]interface{}{ + "support_plan": "Basic", + }, "", false) +} + +func Test_UpgradeOverride_ShapeAndTemporalHint(t *testing.T) { + dir := newFixture(t) + + // Null allowed + tfValidateWithVars(t, dir, nil, "", true) + + // Valid shape with both fields + tfValidateWithVars(t, dir, nil, `{ + "upgrade_override": { + "force_upgrade_enabled": true, + "effective_until": "2025-10-01T13:00:00Z" + } + }`, true) + + // Missing required force_upgrade_enabled -> fail + tfValidateWithVars(t, dir, nil, `{ + "upgrade_override": { + "effective_until": "2025-10-01T13:00:00Z" + } + }`, false) +} \ No newline at end of file