@ -5,10 +5,15 @@ package stackeval
import (
"context"
"fmt"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/hcl/v2/hcltest"
"github.com/zclconf/go-cty-debug/ctydebug"
"github.com/zclconf/go-cty/cty"
"google.golang.org/protobuf/encoding/prototext"
@ -17,9 +22,13 @@ import (
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/providers"
testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
"github.com/hashicorp/terraform/internal/stacks/stackplan"
"github.com/hashicorp/terraform/internal/stacks/stackstate"
"github.com/hashicorp/terraform/internal/tfdiags"
)
func TestInputVariableValue ( t * testing . T ) {
@ -487,3 +496,604 @@ func TestInputVariablePlanChanges(t *testing.T) {
} )
}
}
// TestEvalVariableValidation tests the evalVariableValidation function directly,
// covering all the "invalid" cases: sensitive/ephemeral values in the error message,
// unknown/null condition results, and unknown error messages. These tests
// exercise the logic independently of the full stack-evaluator machinery.
func TestEvalVariableValidation ( t * testing . T ) {
// parseExpr parses a real HCL expression from a source string.
parseExpr := func ( t * testing . T , src string ) hcl . Expression {
t . Helper ( )
expr , diags := hclsyntax . ParseExpression ( [ ] byte ( src ) , "test.hcl" , hcl . Pos { Line : 1 , Column : 1 } )
if diags . HasErrors ( ) {
t . Fatalf ( "failed to parse expression %q: %s" , src , diags . Error ( ) )
}
return expr
}
// makeFakeRule builds a minimal stackconfig.CheckRule from two expressions.
makeFakeRule := func ( condition , errorMessage hcl . Expression ) * stackconfig . CheckRule {
return & stackconfig . CheckRule {
Condition : condition ,
ErrorMessage : errorMessage ,
DeclRange : hcl . Range {
Filename : "test.hcl" ,
Start : hcl . Pos { Line : 2 , Column : 1 } ,
End : hcl . Pos { Line : 5 , Column : 1 } ,
} ,
}
}
// makeVarCtx builds an HCL evaluation context that exposes var.foo = val.
makeVarCtx := func ( val cty . Value ) * hcl . EvalContext {
return & hcl . EvalContext {
Variables : map [ string ] cty . Value {
"var" : cty . ObjectVal ( map [ string ] cty . Value {
"foo" : val ,
} ) ,
} ,
}
}
valueRange := hcl . Range {
Filename : "test.hcl" ,
Start : hcl . Pos { Line : 1 , Column : 1 } ,
End : hcl . Pos { Line : 1 , Column : 10 } ,
}
// --- Basic pass/fail ---
t . Run ( "condition passes, clean message → no diagnostics" , func ( t * testing . T ) {
rule := makeFakeRule (
hcltest . MockExprLiteral ( cty . True ) ,
hcltest . MockExprLiteral ( cty . StringVal ( "Value is invalid." ) ) ,
)
diags := evalVariableValidation ( rule , makeVarCtx ( cty . StringVal ( "good" ) ) , valueRange )
assertNoDiags ( t , diags )
} )
t . Run ( "condition fails, clean message → Invalid value for variable" , func ( t * testing . T ) {
rule := makeFakeRule (
hcltest . MockExprLiteral ( cty . False ) ,
hcltest . MockExprLiteral ( cty . StringVal ( "Value must be 'good'." ) ) ,
)
diags := evalVariableValidation ( rule , makeVarCtx ( cty . StringVal ( "bad" ) ) , valueRange )
assertMatchingDiag ( t , diags , func ( d tfdiags . Diagnostic ) bool {
return d . Severity ( ) == tfdiags . Error &&
d . Description ( ) . Summary == "Invalid value for variable"
} )
} )
// --- Sensitive error message ---
t . Run ( "condition passes, sensitive error_message → flagged even on success" , func ( t * testing . T ) {
// The error_message evaluates to a sensitive string even though the
// condition passes. This structural problem must always be reported.
rule := makeFakeRule (
hcltest . MockExprLiteral ( cty . True ) ,
hcltest . MockExprLiteral ( cty . StringVal ( "Contains secret" ) . Mark ( marks . Sensitive ) ) ,
)
diags := evalVariableValidation ( rule , makeVarCtx ( cty . StringVal ( "good" ) ) , valueRange )
assertMatchingDiag ( t , diags , func ( d tfdiags . Diagnostic ) bool {
return d . Severity ( ) == tfdiags . Error &&
d . Description ( ) . Summary == "Error message refers to sensitive values"
} )
// Condition passed, so there must be no "Invalid value for variable".
for _ , d := range diags {
if d . Description ( ) . Summary == "Invalid value for variable" {
t . Errorf ( "unexpected 'Invalid value for variable' diagnostic when condition passed" )
}
}
} )
t . Run ( "condition fails, sensitive error_message → both diagnostics" , func ( t * testing . T ) {
rule := makeFakeRule (
hcltest . MockExprLiteral ( cty . False ) ,
hcltest . MockExprLiteral ( cty . StringVal ( "Contains secret" ) . Mark ( marks . Sensitive ) ) ,
)
diags := evalVariableValidation ( rule , makeVarCtx ( cty . StringVal ( "bad" ) ) , valueRange )
assertMatchingDiag ( t , diags , func ( d tfdiags . Diagnostic ) bool {
return d . Severity ( ) == tfdiags . Error &&
d . Description ( ) . Summary == "Error message refers to sensitive values"
} )
assertMatchingDiag ( t , diags , func ( d tfdiags . Diagnostic ) bool {
return d . Severity ( ) == tfdiags . Error &&
d . Description ( ) . Summary == "Invalid value for variable"
} )
} )
// --- Ephemeral error message ---
t . Run ( "condition passes, ephemeral error_message → flagged even on success" , func ( t * testing . T ) {
rule := makeFakeRule (
hcltest . MockExprLiteral ( cty . True ) ,
hcltest . MockExprLiteral ( cty . StringVal ( "Contains ephemeral" ) . Mark ( marks . Ephemeral ) ) ,
)
diags := evalVariableValidation ( rule , makeVarCtx ( cty . StringVal ( "good" ) ) , valueRange )
assertMatchingDiag ( t , diags , func ( d tfdiags . Diagnostic ) bool {
return d . Severity ( ) == tfdiags . Error &&
d . Description ( ) . Summary == "Error message refers to ephemeral values"
} )
// Condition passed, so there must be no "Invalid value for variable".
for _ , d := range diags {
if d . Description ( ) . Summary == "Invalid value for variable" {
t . Errorf ( "unexpected 'Invalid value for variable' diagnostic when condition passed" )
}
}
} )
t . Run ( "condition fails, ephemeral error_message → both diagnostics" , func ( t * testing . T ) {
rule := makeFakeRule (
hcltest . MockExprLiteral ( cty . False ) ,
hcltest . MockExprLiteral ( cty . StringVal ( "Contains ephemeral" ) . Mark ( marks . Ephemeral ) ) ,
)
diags := evalVariableValidation ( rule , makeVarCtx ( cty . StringVal ( "bad" ) ) , valueRange )
assertMatchingDiag ( t , diags , func ( d tfdiags . Diagnostic ) bool {
return d . Severity ( ) == tfdiags . Error &&
d . Description ( ) . Summary == "Error message refers to ephemeral values"
} )
assertMatchingDiag ( t , diags , func ( d tfdiags . Diagnostic ) bool {
return d . Severity ( ) == tfdiags . Error &&
d . Description ( ) . Summary == "Invalid value for variable"
} )
} )
// --- Unknown / null condition results ---
t . Run ( "condition result unknown → no diagnostics" , func ( t * testing . T ) {
// Unknown condition means we cannot determine validity yet; skip quietly.
rule := makeFakeRule (
hcltest . MockExprLiteral ( cty . UnknownVal ( cty . Bool ) ) ,
hcltest . MockExprLiteral ( cty . StringVal ( "Value is invalid." ) ) ,
)
diags := evalVariableValidation ( rule , makeVarCtx ( cty . UnknownVal ( cty . String ) ) , valueRange )
assertNoDiags ( t , diags )
} )
t . Run ( "condition result null → Invalid variable validation result" , func ( t * testing . T ) {
rule := makeFakeRule (
hcltest . MockExprLiteral ( cty . NullVal ( cty . Bool ) ) ,
hcltest . MockExprLiteral ( cty . StringVal ( "Value is invalid." ) ) ,
)
diags := evalVariableValidation ( rule , makeVarCtx ( cty . StringVal ( "anything" ) ) , valueRange )
assertMatchingDiag ( t , diags , func ( d tfdiags . Diagnostic ) bool {
return d . Severity ( ) == tfdiags . Error &&
d . Description ( ) . Summary == "Invalid variable validation result"
} )
} )
// --- Unknown error message ---
t . Run ( "error message unknown, condition fails → Invalid error message (no failure diag)" , func ( t * testing . T ) {
// We must flag the unknown error message but must NOT also emit
// "Invalid value for variable" because we return early.
rule := makeFakeRule (
hcltest . MockExprLiteral ( cty . False ) ,
hcltest . MockExprLiteral ( cty . UnknownVal ( cty . String ) ) ,
)
diags := evalVariableValidation ( rule , makeVarCtx ( cty . StringVal ( "bad" ) ) , valueRange )
assertMatchingDiag ( t , diags , func ( d tfdiags . Diagnostic ) bool {
return d . Severity ( ) == tfdiags . Error &&
d . Description ( ) . Summary == "Invalid error message"
} )
for _ , d := range diags {
if d . Description ( ) . Summary == "Invalid value for variable" {
t . Errorf ( "unexpected 'Invalid value for variable' when error message is unknown" )
}
}
} )
t . Run ( "error message unknown, condition passes → no diagnostic about error message" , func ( t * testing . T ) {
// Unknown error message is only a problem when the condition actually
// fails (because we'd need to display it). When the condition passes
// the unknown error message must be silently ignored.
rule := makeFakeRule (
hcltest . MockExprLiteral ( cty . True ) ,
hcltest . MockExprLiteral ( cty . UnknownVal ( cty . String ) ) ,
)
diags := evalVariableValidation ( rule , makeVarCtx ( cty . StringVal ( "good" ) ) , valueRange )
assertNoDiags ( t , diags )
} )
// --- Sensitive variable value in condition expression ---
t . Run ( "sensitive variable value, plain error message, condition fails → only Invalid value for variable" , func ( t * testing . T ) {
// var.foo carries a sensitive mark. The condition expression
// (var.foo == "good") evaluates to a sensitive bool; Unmark() peels
// off the mark so the check works correctly. The error_message is a
// plain literal → no "Error message refers to sensitive values" diag.
hclCtx := makeVarCtx ( cty . StringVal ( "bad" ) . Mark ( marks . Sensitive ) )
rule := makeFakeRule (
parseExpr ( t , ` var.foo == "good" ` ) ,
hcltest . MockExprLiteral ( cty . StringVal ( "Value is not allowed." ) ) ,
)
diags := evalVariableValidation ( rule , hclCtx , valueRange )
assertMatchingDiag ( t , diags , func ( d tfdiags . Diagnostic ) bool {
return d . Severity ( ) == tfdiags . Error &&
d . Description ( ) . Summary == "Invalid value for variable"
} )
for _ , d := range diags {
if d . Description ( ) . Summary == "Error message refers to sensitive values" {
t . Errorf ( "unexpected 'Error message refers to sensitive values' when error message is plain text" )
}
}
} )
t . Run ( "sensitive variable referenced in error message, condition fails → both diagnostics" , func ( t * testing . T ) {
// When the error_message interpolates a sensitive variable the
// evaluated message is itself sensitive — both the sensitive-value
// diagnostic and the generic failure diagnostic must be emitted.
hclCtx := makeVarCtx ( cty . StringVal ( "secret" ) . Mark ( marks . Sensitive ) )
rule := makeFakeRule (
parseExpr ( t , ` var.foo == "good" ` ) ,
parseExpr ( t , ` "Value '$ { var.foo}' is not allowed." ` ) ,
)
diags := evalVariableValidation ( rule , hclCtx , valueRange )
assertMatchingDiag ( t , diags , func ( d tfdiags . Diagnostic ) bool {
return d . Severity ( ) == tfdiags . Error &&
d . Description ( ) . Summary == "Error message refers to sensitive values"
} )
assertMatchingDiag ( t , diags , func ( d tfdiags . Diagnostic ) bool {
return d . Severity ( ) == tfdiags . Error &&
d . Description ( ) . Summary == "Invalid value for variable"
} )
} )
t . Run ( "ephemeral variable referenced in error message, condition passes → flagged even on success" , func ( t * testing . T ) {
// The condition passes but the error_message references an ephemeral
// variable, making the message itself ephemeral. This structural
// problem must still be reported.
hclCtx := makeVarCtx ( cty . StringVal ( "good" ) . Mark ( marks . Ephemeral ) )
rule := makeFakeRule (
hcltest . MockExprLiteral ( cty . True ) ,
parseExpr ( t , ` "Value '$ { var.foo}' is not allowed." ` ) ,
)
diags := evalVariableValidation ( rule , hclCtx , valueRange )
assertMatchingDiag ( t , diags , func ( d tfdiags . Diagnostic ) bool {
return d . Severity ( ) == tfdiags . Error &&
d . Description ( ) . Summary == "Error message refers to ephemeral values"
} )
for _ , d := range diags {
if d . Description ( ) . Summary == "Invalid value for variable" {
t . Errorf ( "unexpected 'Invalid value for variable' when condition passed" )
}
}
} )
// --- Condition evaluation error ---
t . Run ( "condition evaluation error → early return with HCL error" , func ( t * testing . T ) {
// When the condition expression itself fails to evaluate (e.g. it references
// an undefined variable), evalVariableValidation must return early with the
// evaluation error and must NOT emit "Invalid value for variable" or
// "Invalid variable validation result".
rule := makeFakeRule (
parseExpr ( t , "undefined_var.foo" ) ,
hcltest . MockExprLiteral ( cty . StringVal ( "Value is invalid." ) ) ,
)
// hclCtx only has "var", so "undefined_var" is unknown → evaluation error.
diags := evalVariableValidation ( rule , makeVarCtx ( cty . StringVal ( "anything" ) ) , valueRange )
if ! diags . HasErrors ( ) {
t . Fatal ( "expected at least one error diagnostic, got none" )
}
for _ , d := range diags {
if d . Description ( ) . Summary == "Invalid value for variable" {
t . Errorf ( "unexpected 'Invalid value for variable' on condition evaluation error" )
}
if d . Description ( ) . Summary == "Invalid variable validation result" {
t . Errorf ( "unexpected 'Invalid variable validation result' on condition evaluation error" )
}
}
} )
// --- Non-bool condition result ---
t . Run ( "condition result is non-bool (list) → Invalid variable validation result" , func ( t * testing . T ) {
// A condition that returns a list (or any value that cannot be converted
// to bool) hits the convert.Convert(result, cty.Bool) failure path.
rule := makeFakeRule (
hcltest . MockExprLiteral ( cty . ListValEmpty ( cty . String ) ) ,
hcltest . MockExprLiteral ( cty . StringVal ( "Value is invalid." ) ) ,
)
diags := evalVariableValidation ( rule , makeVarCtx ( cty . StringVal ( "anything" ) ) , valueRange )
assertMatchingDiag ( t , diags , func ( d tfdiags . Diagnostic ) bool {
return d . Severity ( ) == tfdiags . Error &&
d . Description ( ) . Summary == "Invalid variable validation result"
} )
// Must return early — no "Invalid value for variable" should follow.
for _ , d := range diags {
if d . Description ( ) . Summary == "Invalid value for variable" {
t . Errorf ( "unexpected 'Invalid value for variable' when condition type conversion failed" )
}
}
} )
// --- Null error message ---
t . Run ( "null error message, condition fails → Invalid value for variable with fallback text" , func ( t * testing . T ) {
// A null error_message is skipped during string conversion; the framework
// falls back to "Failed to evaluate condition error message." in the
// detail of the "Invalid value for variable" diagnostic.
rule := makeFakeRule (
hcltest . MockExprLiteral ( cty . False ) ,
hcltest . MockExprLiteral ( cty . NullVal ( cty . String ) ) ,
)
diags := evalVariableValidation ( rule , makeVarCtx ( cty . StringVal ( "bad" ) ) , valueRange )
assertMatchingDiag ( t , diags , func ( d tfdiags . Diagnostic ) bool {
return d . Severity ( ) == tfdiags . Error &&
d . Description ( ) . Summary == "Invalid value for variable" &&
strings . Contains ( d . Description ( ) . Detail , "Failed to evaluate condition error message." )
} )
} )
// --- Non-string error message ---
t . Run ( "non-string error message (list), condition fails → Invalid error message + fallback in failure diag" , func ( t * testing . T ) {
// An error_message that evaluates to a list (unconvertible to string)
// hits the convert.Convert(errorValue, cty.String) failure path. Both
// "Invalid error message" and "Invalid value for variable" (with the
// fallback text) must be emitted.
rule := makeFakeRule (
hcltest . MockExprLiteral ( cty . False ) ,
hcltest . MockExprLiteral ( cty . ListValEmpty ( cty . String ) ) ,
)
diags := evalVariableValidation ( rule , makeVarCtx ( cty . StringVal ( "bad" ) ) , valueRange )
assertMatchingDiag ( t , diags , func ( d tfdiags . Diagnostic ) bool {
return d . Severity ( ) == tfdiags . Error &&
d . Description ( ) . Summary == "Invalid error message"
} )
assertMatchingDiag ( t , diags , func ( d tfdiags . Diagnostic ) bool {
return d . Severity ( ) == tfdiags . Error &&
d . Description ( ) . Summary == "Invalid value for variable" &&
strings . Contains ( d . Description ( ) . Detail , "Failed to evaluate condition error message." )
} )
} )
}
// TestInputVariableValidation exercises evalVariableValidations end-to-end
// through CheckValue, using the "validation" fixture that declares variables
// with validation blocks.
func TestInputVariableValidation ( t * testing . T ) {
cfg := testStackConfig ( t , "input_variable" , "validation" )
tests := map [ string ] struct {
varName string
inputVal cty . Value
wantSummaries [ ] string // diagnostics that MUST be present (by Summary)
wantNoErrors bool // if true, no error diagnostics are expected
} {
// --- validated (plain error message) ---
"validated: clean pass" : {
varName : "validated" ,
inputVal : cty . StringVal ( "good" ) ,
wantNoErrors : true ,
} ,
"validated: clean fail" : {
varName : "validated" ,
inputVal : cty . StringVal ( "bad" ) ,
wantSummaries : [ ] string { "Invalid value for variable" } ,
} ,
// --- with_msg_ref (error message interpolates var.with_msg_ref) ---
"with_msg_ref: clean pass" : {
varName : "with_msg_ref" ,
inputVal : cty . StringVal ( "good" ) ,
wantNoErrors : true ,
} ,
"with_msg_ref: clean fail" : {
varName : "with_msg_ref" ,
inputVal : cty . StringVal ( "bad" ) ,
wantSummaries : [ ] string { "Invalid value for variable" } ,
} ,
"with_msg_ref: sensitive value passes → error message diag only" : {
// Condition passes, but the interpolated error_message is sensitive
// → we should still flag the structural problem.
varName : "with_msg_ref" ,
inputVal : cty . StringVal ( "good" ) . Mark ( marks . Sensitive ) ,
wantSummaries : [ ] string { "Error message refers to sensitive values" } ,
} ,
"with_msg_ref: sensitive value fails → both diags" : {
varName : "with_msg_ref" ,
inputVal : cty . StringVal ( "bad" ) . Mark ( marks . Sensitive ) ,
wantSummaries : [ ] string {
"Error message refers to sensitive values" ,
"Invalid value for variable" ,
} ,
} ,
"with_msg_ref: ephemeral value passes → error message diag only" : {
varName : "with_msg_ref" ,
inputVal : cty . StringVal ( "good" ) . Mark ( marks . Ephemeral ) ,
wantSummaries : [ ] string { "Error message refers to ephemeral values" } ,
} ,
"with_msg_ref: ephemeral value fails → both diags" : {
varName : "with_msg_ref" ,
inputVal : cty . StringVal ( "bad" ) . Mark ( marks . Ephemeral ) ,
wantSummaries : [ ] string {
"Error message refers to ephemeral values" ,
"Invalid value for variable" ,
} ,
} ,
}
for name , tc := range tests {
t . Run ( name , func ( t * testing . T ) {
main := testEvaluator ( t , testEvaluatorOpts {
Config : cfg ,
InputVariableValues : map [ string ] cty . Value {
tc . varName : tc . inputVal ,
} ,
} )
inPromisingTask ( t , func ( ctx context . Context , t * testing . T ) {
mainStack := main . MainStack ( )
rootVar := mainStack . InputVariable ( stackaddrs . InputVariable { Name : tc . varName } )
_ , diags := rootVar . CheckValue ( ctx , InspectPhase )
if tc . wantNoErrors {
if diags . HasErrors ( ) {
t . Errorf ( "unexpected errors: %s" , diags . Err ( ) )
}
return
}
for _ , wantSummary := range tc . wantSummaries {
wantSummary := wantSummary // capture for closure
assertMatchingDiag ( t , diags , func ( d tfdiags . Diagnostic ) bool {
return d . Severity ( ) == tfdiags . Error &&
d . Description ( ) . Summary == wantSummary
} )
}
} )
} )
}
}
// TestInputVariableValidationWithProviderFunction verifies that provider-defined
// functions can be called inside a variable validation condition expression.
// It uses the "validation_provider_function" fixture together with a mock provider
// that exposes a simple "upper" string function.
func TestInputVariableValidationWithProviderFunction ( t * testing . T ) {
cfg := testStackConfig ( t , "input_variable" , "validation_provider_function" )
providerTypeAddr := addrs . MustParseProviderSourceString ( "terraform.io/builtin/testing" )
newMockProvider := func ( t * testing . T ) ( * testing_provider . MockProvider , providers . Factory ) {
t . Helper ( )
mockProvider := & testing_provider . MockProvider {
GetProviderSchemaResponse : & providers . GetProviderSchemaResponse {
Functions : map [ string ] providers . FunctionDecl {
"upper" : {
Parameters : [ ] providers . FunctionParam {
{ Name : "input" , Type : cty . String } ,
} ,
ReturnType : cty . String ,
Summary : "Converts a string to upper-case." ,
} ,
} ,
} ,
CallFunctionFn : func ( req providers . CallFunctionRequest ) providers . CallFunctionResponse {
if req . FunctionName != "upper" {
return providers . CallFunctionResponse {
Err : fmt . Errorf ( "unexpected function call: %s" , req . FunctionName ) ,
}
}
input , _ := req . Arguments [ 0 ] . Unmark ( )
return providers . CallFunctionResponse {
Result : cty . StringVal ( strings . ToUpper ( input . AsString ( ) ) ) ,
}
} ,
}
return mockProvider , providers . FactoryFixed ( mockProvider )
}
t . Run ( "passes validation" , func ( t * testing . T ) {
_ , providerFactory := newMockProvider ( t )
main := testEvaluator ( t , testEvaluatorOpts {
Config : cfg ,
InputVariableValues : map [ string ] cty . Value {
"foo" : cty . StringVal ( "hello" ) , // upper("hello") == "HELLO" → condition passes
} ,
ProviderFactories : ProviderFactories {
providerTypeAddr : providerFactory ,
} ,
} )
inPromisingTask ( t , func ( ctx context . Context , t * testing . T ) {
mainStack := main . MainStack ( )
rootVar := mainStack . InputVariable ( stackaddrs . InputVariable { Name : "foo" } )
_ , diags := rootVar . CheckValue ( ctx , InspectPhase )
assertNoDiags ( t , diags )
} )
} )
t . Run ( "fails validation" , func ( t * testing . T ) {
_ , providerFactory := newMockProvider ( t )
main := testEvaluator ( t , testEvaluatorOpts {
Config : cfg ,
InputVariableValues : map [ string ] cty . Value {
"foo" : cty . StringVal ( "world" ) , // upper("world") == "WORLD" ≠ "HELLO" → condition fails
} ,
ProviderFactories : ProviderFactories {
providerTypeAddr : providerFactory ,
} ,
} )
inPromisingTask ( t , func ( ctx context . Context , t * testing . T ) {
mainStack := main . MainStack ( )
rootVar := mainStack . InputVariable ( stackaddrs . InputVariable { Name : "foo" } )
_ , diags := rootVar . CheckValue ( ctx , InspectPhase )
assertMatchingDiag ( t , diags , func ( d tfdiags . Diagnostic ) bool {
return d . Severity ( ) == tfdiags . Error &&
d . Description ( ) . Summary == "Invalid value for variable"
} )
} )
} )
}
// TestInputVariableMultipleValidationRules verifies that when a variable has
// more than one validation block, every failing rule produces its own
// diagnostic — i.e., all rules are evaluated and none are short-circuited.
//
// The "multi_rule" variable in the "validation" fixture has two rules:
//
// Rule 1: length(var.multi_rule) >= 5
// Rule 2: var.multi_rule != "bad"
//
// The value "bad" has length 3 (< 5) and equals "bad", so it violates both
// rules simultaneously, giving us exactly two "Invalid value for variable"
// diagnostics.
func TestInputVariableMultipleValidationRules ( t * testing . T ) {
cfg := testStackConfig ( t , "input_variable" , "validation" )
tests := map [ string ] struct {
inputVal cty . Value
wantErrCount int // expected number of "Invalid value for variable" diagnostics
} {
"passes both rules" : {
inputVal : cty . StringVal ( "hello" ) , // length 5 >= 5, != "bad"
wantErrCount : 0 ,
} ,
"fails first rule only" : {
inputVal : cty . StringVal ( "hi" ) , // length 2 < 5, != "bad"
wantErrCount : 1 ,
} ,
"fails second rule only" : {
// length("hello!") = 6 >= 5 → first passes; "hello!" != "bad" → second passes.
// To fail only the second rule we need length >= 5 AND value == "bad".
// "bad" itself has length 3, so the only way to isolate rule 2 failure
// is with a longer value that equals "bad" — impossible for a plain
// string. We therefore omit this sub-case and rely on the unit-level
// TestEvalVariableValidation coverage instead.
inputVal : cty . StringVal ( "hello" ) , // deliberately a pass case
wantErrCount : 0 ,
} ,
"fails both rules" : {
inputVal : cty . StringVal ( "bad" ) , // length 3 < 5 AND == "bad"
wantErrCount : 2 ,
} ,
}
for name , tc := range tests {
t . Run ( name , func ( t * testing . T ) {
main := testEvaluator ( t , testEvaluatorOpts {
Config : cfg ,
InputVariableValues : map [ string ] cty . Value {
"multi_rule" : tc . inputVal ,
} ,
} )
inPromisingTask ( t , func ( ctx context . Context , t * testing . T ) {
mainStack := main . MainStack ( )
rootVar := mainStack . InputVariable ( stackaddrs . InputVariable { Name : "multi_rule" } )
_ , diags := rootVar . CheckValue ( ctx , InspectPhase )
var failCount int
for _ , d := range diags {
if d . Severity ( ) == tfdiags . Error && d . Description ( ) . Summary == "Invalid value for variable" {
failCount ++
}
}
if failCount != tc . wantErrCount {
t . Errorf ( "expected %d 'Invalid value for variable' diagnostic(s), got %d; diags:\n%s" ,
tc . wantErrCount , failCount , diags . ErrWithWarnings ( ) )
}
} )
} )
}
}