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  }