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  }