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  }