Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
368 changes: 368 additions & 0 deletions test/variables_test.go
Original file line number Diff line number Diff line change
@@ -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)
}