github.com/opentofu/opentofu@v1.7.1/internal/tofu/node_root_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 "testing" 10 11 "github.com/hashicorp/hcl/v2" 12 "github.com/hashicorp/hcl/v2/hcltest" 13 "github.com/zclconf/go-cty/cty" 14 15 "github.com/opentofu/opentofu/internal/addrs" 16 "github.com/opentofu/opentofu/internal/checks" 17 "github.com/opentofu/opentofu/internal/configs" 18 "github.com/opentofu/opentofu/internal/lang" 19 ) 20 21 func TestNodeRootVariableExecute(t *testing.T) { 22 t.Run("type conversion", func(t *testing.T) { 23 ctx := new(MockEvalContext) 24 25 n := &NodeRootVariable{ 26 Addr: addrs.InputVariable{Name: "foo"}, 27 Config: &configs.Variable{ 28 Name: "foo", 29 Type: cty.String, 30 ConstraintType: cty.String, 31 }, 32 RawValue: &InputValue{ 33 Value: cty.True, 34 SourceType: ValueFromUnknown, 35 }, 36 } 37 38 ctx.ChecksState = checks.NewState(&configs.Config{ 39 Module: &configs.Module{ 40 Variables: map[string]*configs.Variable{ 41 "foo": n.Config, 42 }, 43 }, 44 }) 45 46 diags := n.Execute(ctx, walkApply) 47 if diags.HasErrors() { 48 t.Fatalf("unexpected error: %s", diags.Err()) 49 } 50 51 if !ctx.SetRootModuleArgumentCalled { 52 t.Fatalf("ctx.SetRootModuleArgument wasn't called") 53 } 54 if got, want := ctx.SetRootModuleArgumentAddr.String(), "var.foo"; got != want { 55 t.Errorf("wrong address for ctx.SetRootModuleArgument\ngot: %s\nwant: %s", got, want) 56 } 57 if got, want := ctx.SetRootModuleArgumentValue, cty.StringVal("true"); !want.RawEquals(got) { 58 // NOTE: The given value was cty.Bool but the type constraint was 59 // cty.String, so it was NodeRootVariable's responsibility to convert 60 // as part of preparing the "final value". 61 t.Errorf("wrong value for ctx.SetRootModuleArgument\ngot: %#v\nwant: %#v", got, want) 62 } 63 }) 64 t.Run("validation", func(t *testing.T) { 65 ctx := new(MockEvalContext) 66 67 // The variable validation function gets called with OpenTofu's 68 // built-in functions available, so we need a minimal scope just for 69 // it to get the functions from. 70 ctx.EvaluationScopeScope = &lang.Scope{} 71 72 // We need to reimplement a _little_ bit of EvalContextBuiltin logic 73 // here to get a similar effect with EvalContextMock just to get the 74 // value to flow through here in a realistic way that'll make this test 75 // useful. 76 var finalVal cty.Value 77 ctx.SetRootModuleArgumentFunc = func(addr addrs.InputVariable, v cty.Value) { 78 if addr.Name == "foo" { 79 t.Logf("set %s to %#v", addr.String(), v) 80 finalVal = v 81 } 82 } 83 ctx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value { 84 if addr.String() != "var.foo" { 85 return cty.NilVal 86 } 87 t.Logf("reading final val for %s (%#v)", addr.String(), finalVal) 88 return finalVal 89 } 90 91 n := &NodeRootVariable{ 92 Addr: addrs.InputVariable{Name: "foo"}, 93 Config: &configs.Variable{ 94 Name: "foo", 95 Type: cty.Number, 96 ConstraintType: cty.Number, 97 Validations: []*configs.CheckRule{ 98 { 99 Condition: fakeHCLExpressionFunc(func(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { 100 // This returns true only if the given variable value 101 // is exactly cty.Number, which allows us to verify 102 // that we were given the value _after_ type 103 // conversion. 104 // This had previously not been handled correctly, 105 // as reported in: 106 // https://github.com/hashicorp/terraform/issues/29899 107 vars := ctx.Variables["var"] 108 if vars == cty.NilVal || !vars.Type().IsObjectType() || !vars.Type().HasAttribute("foo") { 109 t.Logf("var.foo isn't available") 110 return cty.False, nil 111 } 112 val := vars.GetAttr("foo") 113 if val == cty.NilVal || val.Type() != cty.Number { 114 t.Logf("var.foo is %#v; want a number", val) 115 return cty.False, nil 116 } 117 return cty.True, nil 118 }), 119 ErrorMessage: hcltest.MockExprLiteral(cty.StringVal("Must be a number.")), 120 }, 121 }, 122 }, 123 RawValue: &InputValue{ 124 // Note: This is a string, but the variable's type constraint 125 // is number so it should be converted before use. 126 Value: cty.StringVal("5"), 127 SourceType: ValueFromUnknown, 128 }, 129 } 130 131 ctx.ChecksState = checks.NewState(&configs.Config{ 132 Module: &configs.Module{ 133 Variables: map[string]*configs.Variable{ 134 "foo": n.Config, 135 }, 136 }, 137 }) 138 139 diags := n.Execute(ctx, walkApply) 140 if diags.HasErrors() { 141 t.Fatalf("unexpected error: %s", diags.Err()) 142 } 143 144 if !ctx.SetRootModuleArgumentCalled { 145 t.Fatalf("ctx.SetRootModuleArgument wasn't called") 146 } 147 if got, want := ctx.SetRootModuleArgumentAddr.String(), "var.foo"; got != want { 148 t.Errorf("wrong address for ctx.SetRootModuleArgument\ngot: %s\nwant: %s", got, want) 149 } 150 if got, want := ctx.SetRootModuleArgumentValue, cty.NumberIntVal(5); !want.RawEquals(got) { 151 // NOTE: The given value was cty.Bool but the type constraint was 152 // cty.String, so it was NodeRootVariable's responsibility to convert 153 // as part of preparing the "final value". 154 t.Errorf("wrong value for ctx.SetRootModuleArgument\ngot: %#v\nwant: %#v", got, want) 155 } 156 if status := ctx.Checks().ObjectCheckStatus(n.Addr.Absolute(addrs.RootModuleInstance)); status != checks.StatusPass { 157 t.Errorf("expected checks to pass but go %s instead", status) 158 } 159 }) 160 } 161 162 // fakeHCLExpressionFunc is a fake implementation of hcl.Expression that just 163 // directly produces a value with direct Go code. 164 // 165 // An expression of this type has no references and so it cannot access any 166 // variables from the EvalContext unless something else arranges for them 167 // to be guaranteed available. For example, custom variable validations just 168 // unconditionally have access to the variable they are validating regardless 169 // of references. 170 type fakeHCLExpressionFunc func(*hcl.EvalContext) (cty.Value, hcl.Diagnostics) 171 172 var _ hcl.Expression = fakeHCLExpressionFunc(nil) 173 174 func (f fakeHCLExpressionFunc) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { 175 return f(ctx) 176 } 177 178 func (f fakeHCLExpressionFunc) Variables() []hcl.Traversal { 179 return nil 180 } 181 182 func (f fakeHCLExpressionFunc) Functions() []hcl.Traversal { 183 return nil 184 } 185 186 func (f fakeHCLExpressionFunc) Range() hcl.Range { 187 return hcl.Range{ 188 Filename: "fake", 189 Start: hcl.InitialPos, 190 End: hcl.InitialPos, 191 } 192 } 193 194 func (f fakeHCLExpressionFunc) StartRange() hcl.Range { 195 return f.Range() 196 }