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