github.com/opentofu/opentofu@v1.7.1/internal/tofu/node_module_variable_test.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package tofu 7 8 import ( 9 "errors" 10 "reflect" 11 "testing" 12 13 "github.com/go-test/deep" 14 "github.com/hashicorp/hcl/v2" 15 "github.com/hashicorp/hcl/v2/hclsyntax" 16 "github.com/zclconf/go-cty/cty" 17 18 "github.com/opentofu/opentofu/internal/addrs" 19 "github.com/opentofu/opentofu/internal/checks" 20 "github.com/opentofu/opentofu/internal/configs" 21 "github.com/opentofu/opentofu/internal/configs/configschema" 22 "github.com/opentofu/opentofu/internal/plans" 23 "github.com/opentofu/opentofu/internal/providers" 24 "github.com/opentofu/opentofu/internal/states" 25 "github.com/opentofu/opentofu/internal/tfdiags" 26 ) 27 28 func TestNodeModuleVariablePath(t *testing.T) { 29 n := &nodeModuleVariable{ 30 Addr: addrs.RootModuleInstance.InputVariable("foo"), 31 Config: &configs.Variable{ 32 Name: "foo", 33 Type: cty.String, 34 ConstraintType: cty.String, 35 }, 36 } 37 38 want := addrs.RootModuleInstance 39 got := n.Path() 40 if got.String() != want.String() { 41 t.Fatalf("wrong module address %s; want %s", got, want) 42 } 43 } 44 45 func TestNodeModuleVariableReferenceableName(t *testing.T) { 46 n := &nodeExpandModuleVariable{ 47 Addr: addrs.InputVariable{Name: "foo"}, 48 Config: &configs.Variable{ 49 Name: "foo", 50 Type: cty.String, 51 ConstraintType: cty.String, 52 }, 53 } 54 55 { 56 expected := []addrs.Referenceable{ 57 addrs.InputVariable{Name: "foo"}, 58 } 59 actual := n.ReferenceableAddrs() 60 if !reflect.DeepEqual(actual, expected) { 61 t.Fatalf("%#v != %#v", actual, expected) 62 } 63 } 64 65 { 66 gotSelfPath, gotReferencePath := n.ReferenceOutside() 67 wantSelfPath := addrs.RootModuleInstance 68 wantReferencePath := addrs.RootModuleInstance 69 if got, want := gotSelfPath.String(), wantSelfPath.String(); got != want { 70 t.Errorf("wrong self path\ngot: %s\nwant: %s", got, want) 71 } 72 if got, want := gotReferencePath.String(), wantReferencePath.String(); got != want { 73 t.Errorf("wrong reference path\ngot: %s\nwant: %s", got, want) 74 } 75 } 76 77 } 78 79 func TestNodeModuleVariableReference(t *testing.T) { 80 n := &nodeExpandModuleVariable{ 81 Addr: addrs.InputVariable{Name: "foo"}, 82 Module: addrs.RootModule.Child("bar"), 83 Config: &configs.Variable{ 84 Name: "foo", 85 Type: cty.String, 86 ConstraintType: cty.String, 87 }, 88 Expr: &hclsyntax.ScopeTraversalExpr{ 89 Traversal: hcl.Traversal{ 90 hcl.TraverseRoot{Name: "var"}, 91 hcl.TraverseAttr{Name: "foo"}, 92 }, 93 }, 94 } 95 96 want := []*addrs.Reference{ 97 { 98 Subject: addrs.InputVariable{Name: "foo"}, 99 }, 100 } 101 got := n.References() 102 for _, problem := range deep.Equal(got, want) { 103 t.Error(problem) 104 } 105 } 106 107 func TestNodeModuleVariableReference_grandchild(t *testing.T) { 108 n := &nodeExpandModuleVariable{ 109 Addr: addrs.InputVariable{Name: "foo"}, 110 Module: addrs.RootModule.Child("bar"), 111 Config: &configs.Variable{ 112 Name: "foo", 113 Type: cty.String, 114 ConstraintType: cty.String, 115 }, 116 Expr: &hclsyntax.ScopeTraversalExpr{ 117 Traversal: hcl.Traversal{ 118 hcl.TraverseRoot{Name: "var"}, 119 hcl.TraverseAttr{Name: "foo"}, 120 }, 121 }, 122 } 123 124 want := []*addrs.Reference{ 125 { 126 Subject: addrs.InputVariable{Name: "foo"}, 127 }, 128 } 129 got := n.References() 130 for _, problem := range deep.Equal(got, want) { 131 t.Error(problem) 132 } 133 } 134 135 func TestNodeModuleVariableConstraints(t *testing.T) { 136 // This is a little extra convoluted to poke at some edge cases that have cropped up in the past around 137 // evaluating dependent nodes between the plan -> apply and destroy cycle. 138 m := testModuleInline(t, map[string]string{ 139 "main.tf": ` 140 variable "input" { 141 type = string 142 validation { 143 condition = var.input != "" 144 error_message = "Input must not be empty." 145 } 146 } 147 148 module "child" { 149 source = "./child" 150 input = var.input 151 } 152 153 provider "test" { 154 alias = "secondary" 155 test_string = module.child.output 156 } 157 158 resource "test_object" "resource" { 159 provider = test.secondary 160 test_string = "test string" 161 } 162 163 `, 164 "child/main.tf": ` 165 variable "input" { 166 type = string 167 validation { 168 condition = var.input != "" 169 error_message = "Input must not be empty." 170 } 171 } 172 provider "test" { 173 test_string = "foo" 174 } 175 resource "test_object" "resource" { 176 test_string = var.input 177 } 178 output "output" { 179 value = test_object.resource.id 180 } 181 `, 182 }) 183 184 checkableObjects := []addrs.Checkable{ 185 addrs.InputVariable{Name: "input"}.Absolute(addrs.RootModuleInstance), 186 addrs.InputVariable{Name: "input"}.Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey)), 187 } 188 189 p := &MockProvider{ 190 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 191 Provider: providers.Schema{Block: simpleTestSchema()}, 192 ResourceTypes: map[string]providers.Schema{ 193 "test_object": providers.Schema{Block: &configschema.Block{ 194 Attributes: map[string]*configschema.Attribute{ 195 "id": { 196 Type: cty.String, 197 Computed: true, 198 }, 199 "test_string": { 200 Type: cty.String, 201 Required: true, 202 }, 203 }, 204 }}, 205 }, 206 }, 207 } 208 p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { 209 if req.Config.GetAttr("test_string").IsNull() { 210 resp.Diagnostics = resp.Diagnostics.Append(errors.New("missing test_string value")) 211 } 212 return resp 213 } 214 215 ctxOpts := &ContextOpts{ 216 Providers: map[addrs.Provider]providers.Factory{ 217 addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), 218 }, 219 } 220 221 t.Run("pass", func(t *testing.T) { 222 ctx := testContext2(t, ctxOpts) 223 plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 224 Mode: plans.NormalMode, 225 SetVariables: InputValues{ 226 "input": &InputValue{ 227 Value: cty.StringVal("beep"), 228 SourceType: ValueFromCLIArg, 229 }, 230 }, 231 }) 232 assertNoDiagnostics(t, diags) 233 234 for _, addr := range checkableObjects { 235 result := plan.Checks.GetObjectResult(addr) 236 if result == nil { 237 t.Fatalf("no check result for %s in the plan", addr) 238 } 239 if got, want := result.Status, checks.StatusPass; got != want { 240 t.Fatalf("wrong check status for %s during planning\ngot: %s\nwant: %s", addr, got, want) 241 } 242 } 243 244 state, diags := ctx.Apply(plan, m) 245 assertNoDiagnostics(t, diags) 246 for _, addr := range checkableObjects { 247 result := state.CheckResults.GetObjectResult(addr) 248 if result == nil { 249 t.Fatalf("no check result for %s in the final state", addr) 250 } 251 if got, want := result.Status, checks.StatusPass; got != want { 252 t.Errorf("wrong check status for %s after apply\ngot: %s\nwant: %s", addr, got, want) 253 } 254 } 255 256 plan, diags = ctx.Plan(m, state, &PlanOpts{ 257 Mode: plans.DestroyMode, 258 SetVariables: InputValues{ 259 "input": &InputValue{ 260 Value: cty.StringVal("beep"), 261 SourceType: ValueFromCLIArg, 262 }, 263 }, 264 }) 265 assertNoDiagnostics(t, diags) 266 267 state, diags = ctx.Apply(plan, m) 268 assertNoDiagnostics(t, diags) 269 for _, addr := range checkableObjects { 270 result := state.CheckResults.GetObjectResult(addr) 271 if result == nil { 272 t.Fatalf("no check result for %s in the final state", addr) 273 } 274 if got, want := result.Status, checks.StatusPass; got != want { 275 t.Errorf("wrong check status for %s after apply\ngot: %s\nwant: %s", addr, got, want) 276 } 277 } 278 }) 279 280 t.Run("fail", func(t *testing.T) { 281 ctx := testContext2(t, ctxOpts) 282 _, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ 283 Mode: plans.NormalMode, 284 SetVariables: InputValues{ 285 "input": &InputValue{ 286 Value: cty.StringVal(""), 287 SourceType: ValueFromCLIArg, 288 }, 289 }, 290 }) 291 if !diags.HasErrors() { 292 t.Fatalf("succeeded; want error") 293 } 294 295 const wantSummary = "Invalid value for variable" 296 found := false 297 for _, diag := range diags { 298 if diag.Severity() == tfdiags.Error && diag.Description().Summary == wantSummary { 299 found = true 300 break 301 } 302 } 303 304 if !found { 305 t.Fatalf("missing expected error\nwant summary: %s\ngot: %s", wantSummary, diags.Err().Error()) 306 } 307 }) 308 }