2024-02-08 04:48:59 -05:00
// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
2023-05-02 11:33:06 -04:00
// SPDX-License-Identifier: MPL-2.0
2023-09-20 08:16:53 -04:00
package tofu
2016-09-16 17:18:43 -04:00
import (
2024-11-12 18:19:45 -05:00
"context"
2023-11-08 18:07:09 -05:00
"errors"
2016-09-16 17:18:43 -04:00
"reflect"
"testing"
2025-09-16 18:19:46 -04:00
"github.com/google/go-cmp/cmp"
2019-09-09 18:58:44 -04:00
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
2025-09-16 18:19:46 -04:00
"github.com/zclconf/go-cty-debug/ctydebug"
2021-09-10 10:58:44 -04:00
"github.com/zclconf/go-cty/cty"
2018-05-04 22:24:06 -04:00
2023-09-20 07:35:35 -04:00
"github.com/opentofu/opentofu/internal/addrs"
2023-11-08 18:07:09 -05:00
"github.com/opentofu/opentofu/internal/checks"
2023-09-20 07:35:35 -04:00
"github.com/opentofu/opentofu/internal/configs"
2023-11-08 18:07:09 -05:00
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/plans"
2026-01-26 09:20:04 -05:00
"github.com/opentofu/opentofu/internal/plugins"
2023-11-08 18:07:09 -05:00
"github.com/opentofu/opentofu/internal/providers"
"github.com/opentofu/opentofu/internal/states"
"github.com/opentofu/opentofu/internal/tfdiags"
2016-09-16 17:18:43 -04:00
)
2020-10-14 09:10:37 -04:00
func TestNodeModuleVariablePath ( t * testing . T ) {
2020-04-07 17:11:32 -04:00
n := & nodeModuleVariable {
Addr : addrs . RootModuleInstance . InputVariable ( "foo" ) ,
2018-05-04 22:24:06 -04:00
Config : & configs . Variable {
2021-09-10 10:58:44 -04:00
Name : "foo" ,
Type : cty . String ,
ConstraintType : cty . String ,
2018-05-04 22:24:06 -04:00
} ,
2016-09-16 17:18:43 -04:00
}
2018-05-10 19:20:34 -04:00
want := addrs . RootModuleInstance
got := n . Path ( )
if got . String ( ) != want . String ( ) {
t . Fatalf ( "wrong module address %s; want %s" , got , want )
2016-09-16 17:18:43 -04:00
}
}
2020-10-14 09:10:37 -04:00
func TestNodeModuleVariableReferenceableName ( t * testing . T ) {
2020-04-07 17:11:32 -04:00
n := & nodeExpandModuleVariable {
Addr : addrs . InputVariable { Name : "foo" } ,
2018-05-04 22:24:06 -04:00
Config : & configs . Variable {
2021-09-10 10:58:44 -04:00
Name : "foo" ,
Type : cty . String ,
ConstraintType : cty . String ,
2018-05-04 22:24:06 -04:00
} ,
2016-09-16 17:18:43 -04:00
}
2018-05-04 22:24:06 -04:00
{
expected := [ ] addrs . Referenceable {
addrs . InputVariable { Name : "foo" } ,
}
actual := n . ReferenceableAddrs ( )
if ! reflect . DeepEqual ( actual , expected ) {
t . Fatalf ( "%#v != %#v" , actual , expected )
}
}
{
gotSelfPath , gotReferencePath := n . ReferenceOutside ( )
2020-10-14 09:10:37 -04:00
wantSelfPath := addrs . RootModuleInstance
2018-05-04 22:24:06 -04:00
wantReferencePath := addrs . RootModuleInstance
if got , want := gotSelfPath . String ( ) , wantSelfPath . String ( ) ; got != want {
t . Errorf ( "wrong self path\ngot: %s\nwant: %s" , got , want )
}
if got , want := gotReferencePath . String ( ) , wantReferencePath . String ( ) ; got != want {
t . Errorf ( "wrong reference path\ngot: %s\nwant: %s" , got , want )
}
2016-09-16 17:18:43 -04:00
}
2018-05-04 22:24:06 -04:00
2016-09-16 17:18:43 -04:00
}
2020-10-14 09:10:37 -04:00
func TestNodeModuleVariableReference ( t * testing . T ) {
2020-04-07 17:11:32 -04:00
n := & nodeExpandModuleVariable {
2020-10-14 09:10:37 -04:00
Addr : addrs . InputVariable { Name : "foo" } ,
Module : addrs . RootModule . Child ( "bar" ) ,
2018-05-04 22:24:06 -04:00
Config : & configs . Variable {
2021-09-10 10:58:44 -04:00
Name : "foo" ,
Type : cty . String ,
ConstraintType : cty . String ,
2018-05-04 22:24:06 -04:00
} ,
Expr : & hclsyntax . ScopeTraversalExpr {
Traversal : hcl . Traversal {
hcl . TraverseRoot { Name : "var" } ,
hcl . TraverseAttr { Name : "foo" } ,
} ,
} ,
2016-09-16 17:18:43 -04:00
}
2018-05-10 18:59:47 -04:00
want := [ ] * addrs . Reference {
2018-05-04 22:24:06 -04:00
{
Subject : addrs . InputVariable { Name : "foo" } ,
} ,
}
2018-05-10 18:59:47 -04:00
got := n . References ( )
2025-10-10 14:01:11 -04:00
if diff := cmp . Diff ( want , got , addrs . CmpOptionsForTesting ) ; diff != "" {
t . Error ( "wrong references\n" + diff )
2016-09-16 17:18:43 -04:00
}
}
2020-10-14 09:10:37 -04:00
func TestNodeModuleVariableReference_grandchild ( t * testing . T ) {
2020-04-07 17:11:32 -04:00
n := & nodeExpandModuleVariable {
2020-10-14 09:10:37 -04:00
Addr : addrs . InputVariable { Name : "foo" } ,
Module : addrs . RootModule . Child ( "bar" ) ,
2018-05-04 22:24:06 -04:00
Config : & configs . Variable {
2021-09-10 10:58:44 -04:00
Name : "foo" ,
Type : cty . String ,
ConstraintType : cty . String ,
2018-05-04 22:24:06 -04:00
} ,
Expr : & hclsyntax . ScopeTraversalExpr {
Traversal : hcl . Traversal {
hcl . TraverseRoot { Name : "var" } ,
hcl . TraverseAttr { Name : "foo" } ,
} ,
} ,
2016-09-16 17:18:43 -04:00
}
2018-05-10 18:59:47 -04:00
want := [ ] * addrs . Reference {
2018-05-04 22:24:06 -04:00
{
Subject : addrs . InputVariable { Name : "foo" } ,
} ,
}
2018-05-10 18:59:47 -04:00
got := n . References ( )
2025-10-10 14:01:11 -04:00
if diff := cmp . Diff ( want , got , addrs . CmpOptionsForTesting ) ; diff != "" {
t . Error ( "wrong references\n" + diff )
2016-09-16 17:18:43 -04:00
}
}
2023-11-08 18:07:09 -05:00
func TestNodeModuleVariableConstraints ( t * testing . T ) {
// This is a little extra convoluted to poke at some edge cases that have cropped up in the past around
// evaluating dependent nodes between the plan -> apply and destroy cycle.
m := testModuleInline ( t , map [ string ] string {
"main.tf" : `
variable "input" {
type = string
validation {
condition = var . input != ""
error_message = "Input must not be empty."
}
}
module "child" {
source = "./child"
input = var . input
}
provider "test" {
alias = "secondary"
test_string = module . child . output
}
resource "test_object" "resource" {
provider = test . secondary
test_string = "test string"
}
` ,
"child/main.tf" : `
variable "input" {
type = string
validation {
condition = var . input != ""
error_message = "Input must not be empty."
}
}
provider "test" {
test_string = "foo"
}
resource "test_object" "resource" {
test_string = var . input
}
output "output" {
value = test_object . resource . id
}
` ,
} )
checkableObjects := [ ] addrs . Checkable {
addrs . InputVariable { Name : "input" } . Absolute ( addrs . RootModuleInstance ) ,
addrs . InputVariable { Name : "input" } . Absolute ( addrs . RootModuleInstance . Child ( "child" , addrs . NoKey ) ) ,
}
p := & MockProvider {
GetProviderSchemaResponse : & providers . GetProviderSchemaResponse {
Provider : providers . Schema { Block : simpleTestSchema ( ) } ,
ResourceTypes : map [ string ] providers . Schema {
"test_object" : providers . Schema { Block : & configschema . Block {
Attributes : map [ string ] * configschema . Attribute {
"id" : {
Type : cty . String ,
Computed : true ,
} ,
"test_string" : {
Type : cty . String ,
Required : true ,
} ,
} ,
} } ,
} ,
} ,
}
p . ConfigureProviderFn = func ( req providers . ConfigureProviderRequest ) ( resp providers . ConfigureProviderResponse ) {
if req . Config . GetAttr ( "test_string" ) . IsNull ( ) {
resp . Diagnostics = resp . Diagnostics . Append ( errors . New ( "missing test_string value" ) )
}
return resp
}
ctxOpts := & ContextOpts {
2026-01-26 09:20:04 -05:00
Plugins : plugins . NewLibrary ( map [ addrs . Provider ] providers . Factory {
2023-11-08 18:07:09 -05:00
addrs . NewDefaultProvider ( "test" ) : testProviderFuncFixed ( p ) ,
2026-01-26 09:20:04 -05:00
} , nil ) ,
2023-11-08 18:07:09 -05:00
}
t . Run ( "pass" , func ( t * testing . T ) {
ctx := testContext2 ( t , ctxOpts )
2024-11-12 18:19:45 -05:00
plan , diags := ctx . Plan ( context . Background ( ) , m , states . NewState ( ) , & PlanOpts {
2023-11-08 18:07:09 -05:00
Mode : plans . NormalMode ,
SetVariables : InputValues {
"input" : & InputValue {
Value : cty . StringVal ( "beep" ) ,
SourceType : ValueFromCLIArg ,
} ,
} ,
} )
assertNoDiagnostics ( t , diags )
for _ , addr := range checkableObjects {
result := plan . Checks . GetObjectResult ( addr )
if result == nil {
t . Fatalf ( "no check result for %s in the plan" , addr )
}
if got , want := result . Status , checks . StatusPass ; got != want {
t . Fatalf ( "wrong check status for %s during planning\ngot: %s\nwant: %s" , addr , got , want )
}
}
2025-09-22 02:31:19 -04:00
state , diags := ctx . Apply ( context . Background ( ) , plan , m , nil )
2023-11-08 18:07:09 -05:00
assertNoDiagnostics ( t , diags )
for _ , addr := range checkableObjects {
result := state . CheckResults . GetObjectResult ( addr )
if result == nil {
t . Fatalf ( "no check result for %s in the final state" , addr )
}
if got , want := result . Status , checks . StatusPass ; got != want {
t . Errorf ( "wrong check status for %s after apply\ngot: %s\nwant: %s" , addr , got , want )
}
}
2024-11-12 18:19:45 -05:00
plan , diags = ctx . Plan ( context . Background ( ) , m , state , & PlanOpts {
2023-11-08 18:07:09 -05:00
Mode : plans . DestroyMode ,
SetVariables : InputValues {
"input" : & InputValue {
Value : cty . StringVal ( "beep" ) ,
SourceType : ValueFromCLIArg ,
} ,
} ,
} )
assertNoDiagnostics ( t , diags )
2025-09-22 02:31:19 -04:00
state , diags = ctx . Apply ( context . Background ( ) , plan , m , nil )
2023-11-08 18:07:09 -05:00
assertNoDiagnostics ( t , diags )
for _ , addr := range checkableObjects {
result := state . CheckResults . GetObjectResult ( addr )
if result == nil {
t . Fatalf ( "no check result for %s in the final state" , addr )
}
if got , want := result . Status , checks . StatusPass ; got != want {
t . Errorf ( "wrong check status for %s after apply\ngot: %s\nwant: %s" , addr , got , want )
}
}
} )
t . Run ( "fail" , func ( t * testing . T ) {
ctx := testContext2 ( t , ctxOpts )
2024-11-12 18:19:45 -05:00
_ , diags := ctx . Plan ( context . Background ( ) , m , states . NewState ( ) , & PlanOpts {
2023-11-08 18:07:09 -05:00
Mode : plans . NormalMode ,
SetVariables : InputValues {
"input" : & InputValue {
Value : cty . StringVal ( "" ) ,
SourceType : ValueFromCLIArg ,
} ,
} ,
} )
if ! diags . HasErrors ( ) {
t . Fatalf ( "succeeded; want error" )
}
const wantSummary = "Invalid value for variable"
found := false
for _ , diag := range diags {
if diag . Severity ( ) == tfdiags . Error && diag . Description ( ) . Summary == wantSummary {
found = true
break
}
}
if ! found {
t . Fatalf ( "missing expected error\nwant summary: %s\ngot: %s" , wantSummary , diags . Err ( ) . Error ( ) )
}
} )
}
2025-09-16 18:19:46 -04:00
func TestNodeModuleVariable_warningDiags ( t * testing . T ) {
t . Run ( "unused object attribute" , func ( t * testing . T ) {
n := & nodeModuleVariable {
Addr : addrs . InputVariable { Name : "foo" } . Absolute ( addrs . RootModuleInstance ) ,
Config : & configs . Variable {
Name : "foo" ,
ConstraintType : cty . Object ( map [ string ] cty . Type {
"foo" : cty . String ,
2025-09-19 14:36:18 -04:00
"bar" : cty . Object ( map [ string ] cty . Type { "nested" : cty . EmptyObject } ) ,
2025-09-16 18:19:46 -04:00
} ) ,
} ,
Expr : & hclsyntax . ObjectConsExpr {
2025-09-22 12:34:51 -04:00
SrcRange : hcl . Range { Filename : "context1.tofu" } ,
2025-09-16 18:19:46 -04:00
Items : [ ] hclsyntax . ObjectConsItem {
{
KeyExpr : & hclsyntax . LiteralValueExpr {
Val : cty . StringVal ( "baz" ) ,
SrcRange : hcl . Range {
2025-09-22 12:34:51 -04:00
Filename : "test1.tofu" ,
2025-09-16 18:19:46 -04:00
} ,
} ,
ValueExpr : & hclsyntax . LiteralValueExpr {
Val : cty . StringVal ( "..." ) ,
} ,
} ,
2025-09-19 14:36:18 -04:00
{
KeyExpr : & hclsyntax . LiteralValueExpr {
Val : cty . StringVal ( "bar" ) ,
SrcRange : hcl . Range {
Filename : "test.tofu" ,
} ,
} ,
ValueExpr : & hclsyntax . ObjectConsExpr {
2025-09-22 12:34:51 -04:00
SrcRange : hcl . Range { Filename : "context2.tofu" } ,
2025-09-19 14:36:18 -04:00
Items : [ ] hclsyntax . ObjectConsItem {
{
KeyExpr : & hclsyntax . LiteralValueExpr {
Val : cty . StringVal ( "beep" ) ,
SrcRange : hcl . Range {
2025-09-22 12:34:51 -04:00
Filename : "test2.tofu" ,
2025-09-19 14:36:18 -04:00
} ,
} ,
ValueExpr : & hclsyntax . LiteralValueExpr {
Val : cty . StringVal ( "..." ) ,
} ,
} ,
} ,
} ,
} ,
2025-09-16 18:19:46 -04:00
} ,
} ,
ModuleInstance : addrs . RootModuleInstance ,
}
// We use the "ForRPC" representation of the diagnostics just because
// it's more friendly for comparison and we care only about the
// user-facing information in the diagnostics, not their concrete types.
2025-09-19 14:36:18 -04:00
gotDiags := n . warningDiags ( ) . ForRPC ( )
2025-09-16 18:19:46 -04:00
var wantDiags tfdiags . Diagnostics
wantDiags = wantDiags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagWarning ,
Summary : "Object attribute is ignored" ,
Detail : ` The object type for input variable "foo" does not include an attribute named "baz", so this definition is unused. Did you mean to set attribute "bar" instead? ` ,
Subject : & hcl . Range {
2025-09-22 12:34:51 -04:00
Filename : "test1.tofu" , // from synthetic source range in constructed expression above
} ,
Context : & hcl . Range {
Filename : "context1.tofu" , // from synthetic source range in constructed expression above
2025-09-16 18:19:46 -04:00
} ,
} )
2025-09-19 14:36:18 -04:00
wantDiags = wantDiags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagWarning ,
Summary : "Object attribute is ignored" ,
Detail : ` The object type for input variable "foo" nested value .bar does not include an attribute named "beep", so this definition is unused. ` ,
Subject : & hcl . Range {
2025-09-22 12:34:51 -04:00
Filename : "test2.tofu" , // from synthetic source range in constructed expression above
} ,
Context : & hcl . Range {
Filename : "context2.tofu" , // from synthetic source range in constructed expression above
2025-09-19 14:36:18 -04:00
} ,
} )
2025-09-16 18:19:46 -04:00
wantDiags = wantDiags . ForRPC ( )
if diff := cmp . Diff ( wantDiags , gotDiags , ctydebug . CmpOptions ) ; diff != "" {
t . Error ( "wrong diagnostics\n" + diff )
}
} )
}