github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/terraform/eval_variable_test.go (about) 1 package terraform 2 3 import ( 4 "fmt" 5 "strings" 6 "testing" 7 8 "github.com/hashicorp/hcl/v2" 9 "github.com/zclconf/go-cty/cty" 10 11 "github.com/hashicorp/terraform/internal/addrs" 12 "github.com/hashicorp/terraform/internal/lang" 13 "github.com/hashicorp/terraform/internal/lang/marks" 14 "github.com/hashicorp/terraform/internal/tfdiags" 15 ) 16 17 func TestPrepareFinalInputVariableValue(t *testing.T) { 18 // This is just a concise way to define a bunch of *configs.Variable 19 // objects to use in our tests below. We're only going to decode this 20 // config, not fully evaluate it. 21 cfgSrc := ` 22 variable "nullable_required" { 23 } 24 variable "nullable_optional_default_string" { 25 default = "hello" 26 } 27 variable "nullable_optional_default_null" { 28 default = null 29 } 30 variable "constrained_string_nullable_required" { 31 type = string 32 } 33 variable "constrained_string_nullable_optional_default_string" { 34 type = string 35 default = "hello" 36 } 37 variable "constrained_string_nullable_optional_default_bool" { 38 type = string 39 default = true 40 } 41 variable "constrained_string_nullable_optional_default_null" { 42 type = string 43 default = null 44 } 45 variable "required" { 46 nullable = false 47 } 48 variable "optional_default_string" { 49 nullable = false 50 default = "hello" 51 } 52 variable "constrained_string_required" { 53 nullable = false 54 type = string 55 } 56 variable "constrained_string_optional_default_string" { 57 nullable = false 58 type = string 59 default = "hello" 60 } 61 variable "constrained_string_optional_default_bool" { 62 nullable = false 63 type = string 64 default = true 65 } 66 variable "constrained_string_sensitive_required" { 67 sensitive = true 68 nullable = false 69 type = string 70 } 71 ` 72 cfg := testModuleInline(t, map[string]string{ 73 "main.tf": cfgSrc, 74 }) 75 variableConfigs := cfg.Module.Variables 76 77 // Because we loaded our pseudo-module from a temporary file, the 78 // declaration source ranges will have unpredictable filenames. We'll 79 // fix that here just to make things easier below. 80 for _, vc := range variableConfigs { 81 vc.DeclRange.Filename = "main.tf" 82 } 83 84 tests := []struct { 85 varName string 86 given cty.Value 87 want cty.Value 88 wantErr string 89 }{ 90 // nullable_required 91 { 92 "nullable_required", 93 cty.NilVal, 94 cty.UnknownVal(cty.DynamicPseudoType), 95 `Required variable not set: The variable "nullable_required" is required, but is not set.`, 96 }, 97 { 98 "nullable_required", 99 cty.NullVal(cty.DynamicPseudoType), 100 cty.NullVal(cty.DynamicPseudoType), 101 ``, // "required" for a nullable variable means only that it must be set, even if it's set to null 102 }, 103 { 104 "nullable_required", 105 cty.StringVal("ahoy"), 106 cty.StringVal("ahoy"), 107 ``, 108 }, 109 { 110 "nullable_required", 111 cty.UnknownVal(cty.String), 112 cty.UnknownVal(cty.String), 113 ``, 114 }, 115 116 // nullable_optional_default_string 117 { 118 "nullable_optional_default_string", 119 cty.NilVal, 120 cty.StringVal("hello"), // the declared default value 121 ``, 122 }, 123 { 124 "nullable_optional_default_string", 125 cty.NullVal(cty.DynamicPseudoType), 126 cty.NullVal(cty.DynamicPseudoType), // nullable variables can be really set to null, masking the default 127 ``, 128 }, 129 { 130 "nullable_optional_default_string", 131 cty.StringVal("ahoy"), 132 cty.StringVal("ahoy"), 133 ``, 134 }, 135 { 136 "nullable_optional_default_string", 137 cty.UnknownVal(cty.String), 138 cty.UnknownVal(cty.String), 139 ``, 140 }, 141 142 // nullable_optional_default_null 143 { 144 "nullable_optional_default_null", 145 cty.NilVal, 146 cty.NullVal(cty.DynamicPseudoType), // the declared default value 147 ``, 148 }, 149 { 150 "nullable_optional_default_null", 151 cty.NullVal(cty.String), 152 cty.NullVal(cty.String), // nullable variables can be really set to null, masking the default 153 ``, 154 }, 155 { 156 "nullable_optional_default_null", 157 cty.StringVal("ahoy"), 158 cty.StringVal("ahoy"), 159 ``, 160 }, 161 { 162 "nullable_optional_default_null", 163 cty.UnknownVal(cty.String), 164 cty.UnknownVal(cty.String), 165 ``, 166 }, 167 168 // constrained_string_nullable_required 169 { 170 "constrained_string_nullable_required", 171 cty.NilVal, 172 cty.UnknownVal(cty.String), 173 `Required variable not set: The variable "constrained_string_nullable_required" is required, but is not set.`, 174 }, 175 { 176 "constrained_string_nullable_required", 177 cty.NullVal(cty.DynamicPseudoType), 178 cty.NullVal(cty.String), // the null value still gets converted to match the type constraint 179 ``, // "required" for a nullable variable means only that it must be set, even if it's set to null 180 }, 181 { 182 "constrained_string_nullable_required", 183 cty.StringVal("ahoy"), 184 cty.StringVal("ahoy"), 185 ``, 186 }, 187 { 188 "constrained_string_nullable_required", 189 cty.UnknownVal(cty.String), 190 cty.UnknownVal(cty.String), 191 ``, 192 }, 193 194 // constrained_string_nullable_optional_default_string 195 { 196 "constrained_string_nullable_optional_default_string", 197 cty.NilVal, 198 cty.StringVal("hello"), // the declared default value 199 ``, 200 }, 201 { 202 "constrained_string_nullable_optional_default_string", 203 cty.NullVal(cty.DynamicPseudoType), 204 cty.NullVal(cty.String), // nullable variables can be really set to null, masking the default 205 ``, 206 }, 207 { 208 "constrained_string_nullable_optional_default_string", 209 cty.StringVal("ahoy"), 210 cty.StringVal("ahoy"), 211 ``, 212 }, 213 { 214 "constrained_string_nullable_optional_default_string", 215 cty.UnknownVal(cty.String), 216 cty.UnknownVal(cty.String), 217 ``, 218 }, 219 220 // constrained_string_nullable_optional_default_bool 221 { 222 "constrained_string_nullable_optional_default_bool", 223 cty.NilVal, 224 cty.StringVal("true"), // the declared default value, automatically converted to match type constraint 225 ``, 226 }, 227 { 228 "constrained_string_nullable_optional_default_bool", 229 cty.NullVal(cty.DynamicPseudoType), 230 cty.NullVal(cty.String), // nullable variables can be really set to null, masking the default 231 ``, 232 }, 233 { 234 "constrained_string_nullable_optional_default_bool", 235 cty.StringVal("ahoy"), 236 cty.StringVal("ahoy"), 237 ``, 238 }, 239 { 240 "constrained_string_nullable_optional_default_bool", 241 cty.UnknownVal(cty.String), 242 cty.UnknownVal(cty.String), 243 ``, 244 }, 245 246 // constrained_string_nullable_optional_default_null 247 { 248 "constrained_string_nullable_optional_default_null", 249 cty.NilVal, 250 cty.NullVal(cty.String), 251 ``, 252 }, 253 { 254 "constrained_string_nullable_optional_default_null", 255 cty.NullVal(cty.DynamicPseudoType), 256 cty.NullVal(cty.String), 257 ``, 258 }, 259 { 260 "constrained_string_nullable_optional_default_null", 261 cty.StringVal("ahoy"), 262 cty.StringVal("ahoy"), 263 ``, 264 }, 265 { 266 "constrained_string_nullable_optional_default_null", 267 cty.UnknownVal(cty.String), 268 cty.UnknownVal(cty.String), 269 ``, 270 }, 271 272 // required 273 { 274 "required", 275 cty.NilVal, 276 cty.UnknownVal(cty.DynamicPseudoType), 277 `Required variable not set: The variable "required" is required, but is not set.`, 278 }, 279 { 280 "required", 281 cty.NullVal(cty.DynamicPseudoType), 282 cty.UnknownVal(cty.DynamicPseudoType), 283 `Required variable not set: Unsuitable value for var.required set from outside of the configuration: required variable may not be set to null.`, 284 }, 285 { 286 "required", 287 cty.StringVal("ahoy"), 288 cty.StringVal("ahoy"), 289 ``, 290 }, 291 { 292 "required", 293 cty.UnknownVal(cty.String), 294 cty.UnknownVal(cty.String), 295 ``, 296 }, 297 298 // optional_default_string 299 { 300 "optional_default_string", 301 cty.NilVal, 302 cty.StringVal("hello"), // the declared default value 303 ``, 304 }, 305 { 306 "optional_default_string", 307 cty.NullVal(cty.DynamicPseudoType), 308 cty.StringVal("hello"), // the declared default value 309 ``, 310 }, 311 { 312 "optional_default_string", 313 cty.StringVal("ahoy"), 314 cty.StringVal("ahoy"), 315 ``, 316 }, 317 { 318 "optional_default_string", 319 cty.UnknownVal(cty.String), 320 cty.UnknownVal(cty.String), 321 ``, 322 }, 323 324 // constrained_string_required 325 { 326 "constrained_string_required", 327 cty.NilVal, 328 cty.UnknownVal(cty.String), 329 `Required variable not set: The variable "constrained_string_required" is required, but is not set.`, 330 }, 331 { 332 "constrained_string_required", 333 cty.NullVal(cty.DynamicPseudoType), 334 cty.UnknownVal(cty.String), 335 `Required variable not set: Unsuitable value for var.constrained_string_required set from outside of the configuration: required variable may not be set to null.`, 336 }, 337 { 338 "constrained_string_required", 339 cty.StringVal("ahoy"), 340 cty.StringVal("ahoy"), 341 ``, 342 }, 343 { 344 "constrained_string_required", 345 cty.UnknownVal(cty.String), 346 cty.UnknownVal(cty.String), 347 ``, 348 }, 349 350 // constrained_string_optional_default_string 351 { 352 "constrained_string_optional_default_string", 353 cty.NilVal, 354 cty.StringVal("hello"), // the declared default value 355 ``, 356 }, 357 { 358 "constrained_string_optional_default_string", 359 cty.NullVal(cty.DynamicPseudoType), 360 cty.StringVal("hello"), // the declared default value 361 ``, 362 }, 363 { 364 "constrained_string_optional_default_string", 365 cty.StringVal("ahoy"), 366 cty.StringVal("ahoy"), 367 ``, 368 }, 369 { 370 "constrained_string_optional_default_string", 371 cty.UnknownVal(cty.String), 372 cty.UnknownVal(cty.String), 373 ``, 374 }, 375 376 // constrained_string_optional_default_bool 377 { 378 "constrained_string_optional_default_bool", 379 cty.NilVal, 380 cty.StringVal("true"), // the declared default value, automatically converted to match type constraint 381 ``, 382 }, 383 { 384 "constrained_string_optional_default_bool", 385 cty.NullVal(cty.DynamicPseudoType), 386 cty.StringVal("true"), // the declared default value, automatically converted to match type constraint 387 ``, 388 }, 389 { 390 "constrained_string_optional_default_bool", 391 cty.StringVal("ahoy"), 392 cty.StringVal("ahoy"), 393 ``, 394 }, 395 { 396 "constrained_string_optional_default_bool", 397 cty.UnknownVal(cty.String), 398 cty.UnknownVal(cty.String), 399 ``, 400 }, 401 402 // sensitive 403 { 404 "constrained_string_sensitive_required", 405 cty.UnknownVal(cty.String), 406 cty.UnknownVal(cty.String), 407 ``, 408 }, 409 } 410 411 for _, test := range tests { 412 t.Run(fmt.Sprintf("%s %#v", test.varName, test.given), func(t *testing.T) { 413 varAddr := addrs.InputVariable{Name: test.varName}.Absolute(addrs.RootModuleInstance) 414 varCfg := variableConfigs[test.varName] 415 if varCfg == nil { 416 t.Fatalf("invalid variable name %q", test.varName) 417 } 418 419 t.Logf( 420 "test case\nvariable: %s\nconstraint: %#v\ndefault: %#v\nnullable: %#v\ngiven value: %#v", 421 varAddr, 422 varCfg.Type, 423 varCfg.Default, 424 varCfg.Nullable, 425 test.given, 426 ) 427 428 rawVal := &InputValue{ 429 Value: test.given, 430 SourceType: ValueFromCaller, 431 } 432 433 got, diags := prepareFinalInputVariableValue( 434 varAddr, rawVal, varCfg, 435 ) 436 437 if test.wantErr != "" { 438 if !diags.HasErrors() { 439 t.Errorf("unexpected success\nwant error: %s", test.wantErr) 440 } else if got, want := diags.Err().Error(), test.wantErr; got != want { 441 t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) 442 } 443 } else { 444 if diags.HasErrors() { 445 t.Errorf("unexpected error\ngot: %s", diags.Err().Error()) 446 } 447 } 448 449 // NOTE: should still have returned some reasonable value even if there was an error 450 if !test.want.RawEquals(got) { 451 t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, test.want) 452 } 453 }) 454 } 455 456 t.Run("SourceType error message variants", func(t *testing.T) { 457 tests := []struct { 458 SourceType ValueSourceType 459 SourceRange tfdiags.SourceRange 460 WantTypeErr string 461 WantNullErr string 462 }{ 463 { 464 ValueFromUnknown, 465 tfdiags.SourceRange{}, 466 `Invalid value for input variable: Unsuitable value for var.constrained_string_required set from outside of the configuration: string required.`, 467 `Required variable not set: Unsuitable value for var.constrained_string_required set from outside of the configuration: required variable may not be set to null.`, 468 }, 469 { 470 ValueFromConfig, 471 tfdiags.SourceRange{ 472 Filename: "example.tf", 473 Start: tfdiags.SourcePos(hcl.InitialPos), 474 End: tfdiags.SourcePos(hcl.InitialPos), 475 }, 476 `Invalid value for input variable: The given value is not suitable for var.constrained_string_required declared at main.tf:32,3-41: string required.`, 477 `Required variable not set: The given value is not suitable for var.constrained_string_required defined at main.tf:32,3-41: required variable may not be set to null.`, 478 }, 479 { 480 ValueFromAutoFile, 481 tfdiags.SourceRange{ 482 Filename: "example.auto.tfvars", 483 Start: tfdiags.SourcePos(hcl.InitialPos), 484 End: tfdiags.SourcePos(hcl.InitialPos), 485 }, 486 `Invalid value for input variable: The given value is not suitable for var.constrained_string_required declared at main.tf:32,3-41: string required.`, 487 `Required variable not set: The given value is not suitable for var.constrained_string_required defined at main.tf:32,3-41: required variable may not be set to null.`, 488 }, 489 { 490 ValueFromNamedFile, 491 tfdiags.SourceRange{ 492 Filename: "example.tfvars", 493 Start: tfdiags.SourcePos(hcl.InitialPos), 494 End: tfdiags.SourcePos(hcl.InitialPos), 495 }, 496 `Invalid value for input variable: The given value is not suitable for var.constrained_string_required declared at main.tf:32,3-41: string required.`, 497 `Required variable not set: The given value is not suitable for var.constrained_string_required defined at main.tf:32,3-41: required variable may not be set to null.`, 498 }, 499 { 500 ValueFromCLIArg, 501 tfdiags.SourceRange{}, 502 `Invalid value for input variable: Unsuitable value for var.constrained_string_required set using -var="constrained_string_required=...": string required.`, 503 `Required variable not set: Unsuitable value for var.constrained_string_required set using -var="constrained_string_required=...": required variable may not be set to null.`, 504 }, 505 { 506 ValueFromEnvVar, 507 tfdiags.SourceRange{}, 508 `Invalid value for input variable: Unsuitable value for var.constrained_string_required set using the TF_VAR_constrained_string_required environment variable: string required.`, 509 `Required variable not set: Unsuitable value for var.constrained_string_required set using the TF_VAR_constrained_string_required environment variable: required variable may not be set to null.`, 510 }, 511 { 512 ValueFromInput, 513 tfdiags.SourceRange{}, 514 `Invalid value for input variable: Unsuitable value for var.constrained_string_required set using an interactive prompt: string required.`, 515 `Required variable not set: Unsuitable value for var.constrained_string_required set using an interactive prompt: required variable may not be set to null.`, 516 }, 517 { 518 // NOTE: This isn't actually a realistic case for this particular 519 // function, because if we have a value coming from a plan then 520 // we must be in the apply step, and we shouldn't be able to 521 // get past the plan step if we have invalid variable values, 522 // and during planning we'll always have other source types. 523 ValueFromPlan, 524 tfdiags.SourceRange{}, 525 `Invalid value for input variable: Unsuitable value for var.constrained_string_required set from outside of the configuration: string required.`, 526 `Required variable not set: Unsuitable value for var.constrained_string_required set from outside of the configuration: required variable may not be set to null.`, 527 }, 528 { 529 ValueFromCaller, 530 tfdiags.SourceRange{}, 531 `Invalid value for input variable: Unsuitable value for var.constrained_string_required set from outside of the configuration: string required.`, 532 `Required variable not set: Unsuitable value for var.constrained_string_required set from outside of the configuration: required variable may not be set to null.`, 533 }, 534 } 535 536 for _, test := range tests { 537 t.Run(fmt.Sprintf("%s %s", test.SourceType, test.SourceRange.StartString()), func(t *testing.T) { 538 varAddr := addrs.InputVariable{Name: "constrained_string_required"}.Absolute(addrs.RootModuleInstance) 539 varCfg := variableConfigs[varAddr.Variable.Name] 540 t.Run("type error", func(t *testing.T) { 541 rawVal := &InputValue{ 542 Value: cty.EmptyObjectVal, 543 SourceType: test.SourceType, 544 SourceRange: test.SourceRange, 545 } 546 547 _, diags := prepareFinalInputVariableValue( 548 varAddr, rawVal, varCfg, 549 ) 550 if !diags.HasErrors() { 551 t.Fatalf("unexpected success; want error") 552 } 553 554 if got, want := diags.Err().Error(), test.WantTypeErr; got != want { 555 t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) 556 } 557 }) 558 t.Run("null error", func(t *testing.T) { 559 rawVal := &InputValue{ 560 Value: cty.NullVal(cty.DynamicPseudoType), 561 SourceType: test.SourceType, 562 SourceRange: test.SourceRange, 563 } 564 565 _, diags := prepareFinalInputVariableValue( 566 varAddr, rawVal, varCfg, 567 ) 568 if !diags.HasErrors() { 569 t.Fatalf("unexpected success; want error") 570 } 571 572 if got, want := diags.Err().Error(), test.WantNullErr; got != want { 573 t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) 574 } 575 }) 576 }) 577 } 578 }) 579 580 t.Run("SensitiveVariable error message variants, with source variants", func(t *testing.T) { 581 tests := []struct { 582 SourceType ValueSourceType 583 SourceRange tfdiags.SourceRange 584 WantTypeErr string 585 HideSubject bool 586 }{ 587 { 588 ValueFromUnknown, 589 tfdiags.SourceRange{}, 590 "Invalid value for input variable: Unsuitable value for var.constrained_string_sensitive_required set from outside of the configuration: string required.", 591 false, 592 }, 593 { 594 ValueFromConfig, 595 tfdiags.SourceRange{ 596 Filename: "example.tfvars", 597 Start: tfdiags.SourcePos(hcl.InitialPos), 598 End: tfdiags.SourcePos(hcl.InitialPos), 599 }, 600 `Invalid value for input variable: The given value is not suitable for var.constrained_string_sensitive_required, which is sensitive: string required. Invalid value defined at example.tfvars:1,1-1.`, 601 true, 602 }, 603 } 604 605 for _, test := range tests { 606 t.Run(fmt.Sprintf("%s %s", test.SourceType, test.SourceRange.StartString()), func(t *testing.T) { 607 varAddr := addrs.InputVariable{Name: "constrained_string_sensitive_required"}.Absolute(addrs.RootModuleInstance) 608 varCfg := variableConfigs[varAddr.Variable.Name] 609 t.Run("type error", func(t *testing.T) { 610 rawVal := &InputValue{ 611 Value: cty.EmptyObjectVal, 612 SourceType: test.SourceType, 613 SourceRange: test.SourceRange, 614 } 615 616 _, diags := prepareFinalInputVariableValue( 617 varAddr, rawVal, varCfg, 618 ) 619 if !diags.HasErrors() { 620 t.Fatalf("unexpected success; want error") 621 } 622 623 if got, want := diags.Err().Error(), test.WantTypeErr; got != want { 624 t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) 625 } 626 627 if test.HideSubject { 628 if got, want := diags[0].Source().Subject.StartString(), test.SourceRange.StartString(); got == want { 629 t.Errorf("Subject start should have been hidden, but was %s", got) 630 } 631 } 632 }) 633 }) 634 } 635 }) 636 } 637 638 // These tests cover the JSON syntax configuration edge case handling, 639 // the background of which is described in detail in comments in the 640 // evalVariableValidations function. Future versions of Terraform may 641 // be able to remove this behaviour altogether. 642 func TestEvalVariableValidations_jsonErrorMessageEdgeCase(t *testing.T) { 643 cfgSrc := `{ 644 "variable": { 645 "valid": { 646 "type": "string", 647 "validation": { 648 "condition": "${var.valid != \"bar\"}", 649 "error_message": "Valid template string ${var.valid}" 650 } 651 }, 652 "invalid": { 653 "type": "string", 654 "validation": { 655 "condition": "${var.invalid != \"bar\"}", 656 "error_message": "Invalid template string ${" 657 } 658 } 659 } 660 } 661 ` 662 cfg := testModuleInline(t, map[string]string{ 663 "main.tf.json": cfgSrc, 664 }) 665 variableConfigs := cfg.Module.Variables 666 667 // Because we loaded our pseudo-module from a temporary file, the 668 // declaration source ranges will have unpredictable filenames. We'll 669 // fix that here just to make things easier below. 670 for _, vc := range variableConfigs { 671 vc.DeclRange.Filename = "main.tf.json" 672 for _, v := range vc.Validations { 673 v.DeclRange.Filename = "main.tf.json" 674 } 675 } 676 677 tests := []struct { 678 varName string 679 given cty.Value 680 wantErr []string 681 wantWarn []string 682 }{ 683 // Valid variable validation declaration, assigned value which passes 684 // the condition generates no diagnostics. 685 { 686 varName: "valid", 687 given: cty.StringVal("foo"), 688 }, 689 // Assigning a value which fails the condition generates an error 690 // message with the expression successfully evaluated. 691 { 692 varName: "valid", 693 given: cty.StringVal("bar"), 694 wantErr: []string{ 695 "Invalid value for variable", 696 "Valid template string bar", 697 }, 698 }, 699 // Invalid variable validation declaration due to an unparseable 700 // template string. Assigning a value which passes the condition 701 // results in a warning about the error message. 702 { 703 varName: "invalid", 704 given: cty.StringVal("foo"), 705 wantWarn: []string{ 706 "Validation error message expression is invalid", 707 "Missing expression; Expected the start of an expression, but found the end of the file.", 708 }, 709 }, 710 // Assigning a value which fails the condition generates an error 711 // message including the configured string interpreted as a literal 712 // value, and the same warning diagnostic as above. 713 { 714 varName: "invalid", 715 given: cty.StringVal("bar"), 716 wantErr: []string{ 717 "Invalid value for variable", 718 "Invalid template string ${", 719 }, 720 wantWarn: []string{ 721 "Validation error message expression is invalid", 722 "Missing expression; Expected the start of an expression, but found the end of the file.", 723 }, 724 }, 725 } 726 727 for _, test := range tests { 728 t.Run(fmt.Sprintf("%s %#v", test.varName, test.given), func(t *testing.T) { 729 varAddr := addrs.InputVariable{Name: test.varName}.Absolute(addrs.RootModuleInstance) 730 varCfg := variableConfigs[test.varName] 731 if varCfg == nil { 732 t.Fatalf("invalid variable name %q", test.varName) 733 } 734 735 // Build a mock context to allow the function under test to 736 // retrieve the variable value and evaluate the expressions 737 ctx := &MockEvalContext{} 738 739 // We need a minimal scope to allow basic functions to be passed to 740 // the HCL scope 741 ctx.EvaluationScopeScope = &lang.Scope{} 742 ctx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value { 743 if got, want := addr.String(), varAddr.String(); got != want { 744 t.Errorf("incorrect argument to GetVariableValue: got %s, want %s", got, want) 745 } 746 return test.given 747 } 748 749 gotDiags := evalVariableValidations( 750 varAddr, varCfg, nil, ctx, 751 ) 752 753 if len(test.wantErr) == 0 && len(test.wantWarn) == 0 { 754 if len(gotDiags) > 0 { 755 t.Errorf("no diags expected, got %s", gotDiags.Err().Error()) 756 } 757 } else { 758 wantErrs: 759 for _, want := range test.wantErr { 760 for _, diag := range gotDiags { 761 if diag.Severity() != tfdiags.Error { 762 continue 763 } 764 desc := diag.Description() 765 if strings.Contains(desc.Summary, want) || strings.Contains(desc.Detail, want) { 766 continue wantErrs 767 } 768 } 769 t.Errorf("no error diagnostics found containing %q\ngot: %s", want, gotDiags.Err().Error()) 770 } 771 772 wantWarns: 773 for _, want := range test.wantWarn { 774 for _, diag := range gotDiags { 775 if diag.Severity() != tfdiags.Warning { 776 continue 777 } 778 desc := diag.Description() 779 if strings.Contains(desc.Summary, want) || strings.Contains(desc.Detail, want) { 780 continue wantWarns 781 } 782 } 783 t.Errorf("no warning diagnostics found containing %q\ngot: %s", want, gotDiags.Err().Error()) 784 } 785 } 786 }) 787 } 788 } 789 790 func TestEvalVariableValidations_sensitiveValues(t *testing.T) { 791 cfgSrc := ` 792 variable "foo" { 793 type = string 794 sensitive = true 795 default = "boop" 796 797 validation { 798 condition = length(var.foo) == 4 799 error_message = "Foo must be 4 characters, not ${length(var.foo)}" 800 } 801 } 802 803 variable "bar" { 804 type = string 805 sensitive = true 806 default = "boop" 807 808 validation { 809 condition = length(var.bar) == 4 810 error_message = "Bar must be 4 characters, not ${nonsensitive(length(var.bar))}." 811 } 812 } 813 ` 814 cfg := testModuleInline(t, map[string]string{ 815 "main.tf": cfgSrc, 816 }) 817 variableConfigs := cfg.Module.Variables 818 819 // Because we loaded our pseudo-module from a temporary file, the 820 // declaration source ranges will have unpredictable filenames. We'll 821 // fix that here just to make things easier below. 822 for _, vc := range variableConfigs { 823 vc.DeclRange.Filename = "main.tf" 824 for _, v := range vc.Validations { 825 v.DeclRange.Filename = "main.tf" 826 } 827 } 828 829 tests := []struct { 830 varName string 831 given cty.Value 832 wantErr []string 833 }{ 834 // Validations pass on a sensitive variable with an error message which 835 // would generate a sensitive value 836 { 837 varName: "foo", 838 given: cty.StringVal("boop"), 839 }, 840 // Assigning a value which fails the condition generates a sensitive 841 // error message, which is elided and generates another error 842 { 843 varName: "foo", 844 given: cty.StringVal("bap"), 845 wantErr: []string{ 846 "Invalid value for variable", 847 "The error message included a sensitive value, so it will not be displayed.", 848 "Error message refers to sensitive values", 849 }, 850 }, 851 // Validations pass on a sensitive variable with a correctly defined 852 // error message 853 { 854 varName: "bar", 855 given: cty.StringVal("boop"), 856 }, 857 // Assigning a value which fails the condition generates a nonsensitive 858 // error message, which is displayed 859 { 860 varName: "bar", 861 given: cty.StringVal("bap"), 862 wantErr: []string{ 863 "Invalid value for variable", 864 "Bar must be 4 characters, not 3.", 865 }, 866 }, 867 } 868 869 for _, test := range tests { 870 t.Run(fmt.Sprintf("%s %#v", test.varName, test.given), func(t *testing.T) { 871 varAddr := addrs.InputVariable{Name: test.varName}.Absolute(addrs.RootModuleInstance) 872 varCfg := variableConfigs[test.varName] 873 if varCfg == nil { 874 t.Fatalf("invalid variable name %q", test.varName) 875 } 876 877 // Build a mock context to allow the function under test to 878 // retrieve the variable value and evaluate the expressions 879 ctx := &MockEvalContext{} 880 881 // We need a minimal scope to allow basic functions to be passed to 882 // the HCL scope 883 ctx.EvaluationScopeScope = &lang.Scope{} 884 ctx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value { 885 if got, want := addr.String(), varAddr.String(); got != want { 886 t.Errorf("incorrect argument to GetVariableValue: got %s, want %s", got, want) 887 } 888 if varCfg.Sensitive { 889 return test.given.Mark(marks.Sensitive) 890 } else { 891 return test.given 892 } 893 } 894 895 gotDiags := evalVariableValidations( 896 varAddr, varCfg, nil, ctx, 897 ) 898 899 if len(test.wantErr) == 0 { 900 if len(gotDiags) > 0 { 901 t.Errorf("no diags expected, got %s", gotDiags.Err().Error()) 902 } 903 } else { 904 wantErrs: 905 for _, want := range test.wantErr { 906 for _, diag := range gotDiags { 907 if diag.Severity() != tfdiags.Error { 908 continue 909 } 910 desc := diag.Description() 911 if strings.Contains(desc.Summary, want) || strings.Contains(desc.Detail, want) { 912 continue wantErrs 913 } 914 } 915 t.Errorf("no error diagnostics found containing %q\ngot: %s", want, gotDiags.Err().Error()) 916 } 917 } 918 }) 919 } 920 }