github.com/jpreese/tflint@v0.19.2-0.20200908152133-b01686250fb6/tflint/runner_test.go (about) 1 package tflint 2 3 import ( 4 "errors" 5 "path/filepath" 6 "testing" 7 8 "github.com/google/go-cmp/cmp" 9 "github.com/google/go-cmp/cmp/cmpopts" 10 hcl "github.com/hashicorp/hcl/v2" 11 "github.com/hashicorp/hcl/v2/hclsyntax" 12 "github.com/hashicorp/terraform/configs" 13 "github.com/zclconf/go-cty/cty" 14 ) 15 16 func Test_NewModuleRunners_noModules(t *testing.T) { 17 withinFixtureDir(t, "no_modules", func() { 18 runner := testRunnerWithOsFs(t, moduleConfig()) 19 20 runners, err := NewModuleRunners(runner) 21 if err != nil { 22 t.Fatalf("Unexpected error occurred: %s", err) 23 } 24 25 if len(runners) > 0 { 26 t.Fatal("`NewModuleRunners` must not return runners when there is no module") 27 } 28 }) 29 } 30 31 func Test_NewModuleRunners_nestedModules(t *testing.T) { 32 withinFixtureDir(t, "nested_modules", func() { 33 runner := testRunnerWithOsFs(t, moduleConfig()) 34 35 runners, err := NewModuleRunners(runner) 36 if err != nil { 37 t.Fatalf("Unexpected error occurred: %s", err) 38 } 39 40 if len(runners) != 2 { 41 t.Fatal("This function must return 2 runners because the config has 2 modules") 42 } 43 44 expectedVars := map[string]map[string]*configs.Variable{ 45 "module.root": { 46 "override": { 47 Name: "override", 48 Default: cty.StringVal("foo"), 49 Type: cty.DynamicPseudoType, 50 ParsingMode: configs.VariableParseLiteral, 51 DeclRange: hcl.Range{ 52 Filename: filepath.Join("module", "module.tf"), 53 Start: hcl.Pos{Line: 1, Column: 1}, 54 End: hcl.Pos{Line: 1, Column: 20}, 55 }, 56 }, 57 "no_default": { 58 Name: "no_default", 59 Default: cty.StringVal("bar"), 60 Type: cty.DynamicPseudoType, 61 ParsingMode: configs.VariableParseLiteral, 62 DeclRange: hcl.Range{ 63 Filename: filepath.Join("module", "module.tf"), 64 Start: hcl.Pos{Line: 4, Column: 1}, 65 End: hcl.Pos{Line: 4, Column: 22}, 66 }, 67 }, 68 "unknown": { 69 Name: "unknown", 70 Default: cty.UnknownVal(cty.DynamicPseudoType), 71 Type: cty.DynamicPseudoType, 72 ParsingMode: configs.VariableParseLiteral, 73 DeclRange: hcl.Range{ 74 Filename: filepath.Join("module", "module.tf"), 75 Start: hcl.Pos{Line: 5, Column: 1}, 76 End: hcl.Pos{Line: 5, Column: 19}, 77 }, 78 }, 79 }, 80 "module.root.module.test": { 81 "override": { 82 Name: "override", 83 Default: cty.StringVal("foo"), 84 Type: cty.DynamicPseudoType, 85 ParsingMode: configs.VariableParseLiteral, 86 DeclRange: hcl.Range{ 87 Filename: filepath.Join("module", "module1", "resource.tf"), 88 Start: hcl.Pos{Line: 1, Column: 1}, 89 End: hcl.Pos{Line: 1, Column: 20}, 90 }, 91 }, 92 "no_default": { 93 Name: "no_default", 94 Default: cty.StringVal("bar"), 95 Type: cty.DynamicPseudoType, 96 ParsingMode: configs.VariableParseLiteral, 97 DeclRange: hcl.Range{ 98 Filename: filepath.Join("module", "module1", "resource.tf"), 99 Start: hcl.Pos{Line: 4, Column: 1}, 100 End: hcl.Pos{Line: 4, Column: 22}, 101 }, 102 }, 103 "unknown": { 104 Name: "unknown", 105 Default: cty.UnknownVal(cty.DynamicPseudoType), 106 Type: cty.DynamicPseudoType, 107 ParsingMode: configs.VariableParseLiteral, 108 DeclRange: hcl.Range{ 109 Filename: filepath.Join("module", "module1", "resource.tf"), 110 Start: hcl.Pos{Line: 5, Column: 1}, 111 End: hcl.Pos{Line: 5, Column: 19}, 112 }, 113 }, 114 }, 115 } 116 117 for _, runner := range runners { 118 expected, exists := expectedVars[runner.TFConfig.Path.String()] 119 if !exists { 120 t.Fatalf("`%s` is not found in module runners", runner.TFConfig.Path) 121 } 122 123 opts := []cmp.Option{ 124 cmpopts.IgnoreUnexported(cty.Type{}, cty.Value{}), 125 cmpopts.IgnoreFields(hcl.Pos{}, "Byte"), 126 } 127 if !cmp.Equal(expected, runner.TFConfig.Module.Variables, opts...) { 128 t.Fatalf("`%s` module variables are unmatched: Diff=%s", runner.TFConfig.Path, cmp.Diff(expected, runner.TFConfig.Module.Variables, opts...)) 129 } 130 } 131 }) 132 } 133 134 func Test_NewModuleRunners_modVars(t *testing.T) { 135 withinFixtureDir(t, "nested_module_vars", func() { 136 runner := testRunnerWithOsFs(t, moduleConfig()) 137 138 runners, err := NewModuleRunners(runner) 139 if err != nil { 140 t.Fatalf("Unexpected error occurred: %s", err) 141 } 142 143 if len(runners) != 2 { 144 t.Fatal("This function must return 2 runners because the config has 2 modules") 145 } 146 147 child := runners[0] 148 if child.TFConfig.Path.String() != "module.module1" { 149 t.Fatalf("Expected child config path name is `module.module1`, but get `%s`", child.TFConfig.Path.String()) 150 } 151 152 expected := map[string]*moduleVariable{ 153 "foo": { 154 Root: true, 155 DeclRange: hcl.Range{ 156 Filename: "main.tf", 157 Start: hcl.Pos{Line: 4, Column: 9}, 158 End: hcl.Pos{Line: 4, Column: 14}, 159 }, 160 }, 161 "bar": { 162 Root: true, 163 DeclRange: hcl.Range{ 164 Filename: "main.tf", 165 Start: hcl.Pos{Line: 5, Column: 9}, 166 End: hcl.Pos{Line: 5, Column: 14}, 167 }, 168 }, 169 } 170 opts := []cmp.Option{cmpopts.IgnoreFields(hcl.Pos{}, "Byte")} 171 if !cmp.Equal(expected, child.modVars, opts...) { 172 t.Fatalf("`%s` module variables are unmatched: Diff=%s", child.TFConfig.Path.String(), cmp.Diff(expected, child.modVars, opts...)) 173 } 174 175 grandchild := runners[1] 176 if grandchild.TFConfig.Path.String() != "module.module1.module.module2" { 177 t.Fatalf("Expected child config path name is `module.module1.module.module2`, but get `%s`", grandchild.TFConfig.Path.String()) 178 } 179 180 expected = map[string]*moduleVariable{ 181 "red": { 182 Root: false, 183 Parents: []*moduleVariable{expected["foo"], expected["bar"]}, 184 DeclRange: hcl.Range{ 185 Filename: filepath.Join("module", "main.tf"), 186 Start: hcl.Pos{Line: 8, Column: 11}, 187 End: hcl.Pos{Line: 8, Column: 34}, 188 }, 189 }, 190 "blue": { 191 Root: false, 192 Parents: []*moduleVariable{}, 193 DeclRange: hcl.Range{ 194 Filename: filepath.Join("module", "main.tf"), 195 Start: hcl.Pos{Line: 9, Column: 11}, 196 End: hcl.Pos{Line: 9, Column: 17}, 197 }, 198 }, 199 "green": { 200 Root: false, 201 Parents: []*moduleVariable{expected["foo"]}, 202 DeclRange: hcl.Range{ 203 Filename: filepath.Join("module", "main.tf"), 204 Start: hcl.Pos{Line: 10, Column: 11}, 205 End: hcl.Pos{Line: 10, Column: 49}, 206 }, 207 }, 208 } 209 opts = []cmp.Option{cmpopts.IgnoreFields(hcl.Pos{}, "Byte")} 210 if !cmp.Equal(expected, grandchild.modVars, opts...) { 211 t.Fatalf("`%s` module variables are unmatched: Diff=%s", grandchild.TFConfig.Path.String(), cmp.Diff(expected, grandchild.modVars, opts...)) 212 } 213 }) 214 } 215 216 func Test_NewModuleRunners_ignoreModules(t *testing.T) { 217 withinFixtureDir(t, "nested_modules", func() { 218 config := moduleConfig() 219 config.IgnoreModules["./module"] = true 220 runner := testRunnerWithOsFs(t, config) 221 222 runners, err := NewModuleRunners(runner) 223 if err != nil { 224 t.Fatalf("Unexpected error occurred: %s", err) 225 } 226 227 if len(runners) != 0 { 228 t.Fatalf("This function must not return runners because `ignore_module` is set. Got `%d` runner(s)", len(runners)) 229 } 230 }) 231 } 232 233 func Test_NewModuleRunners_withInvalidExpression(t *testing.T) { 234 withinFixtureDir(t, "invalid_module_attribute", func() { 235 runner := testRunnerWithOsFs(t, moduleConfig()) 236 237 _, err := NewModuleRunners(runner) 238 239 expected := Error{ 240 Code: EvaluationError, 241 Level: ErrorLevel, 242 Message: "Failed to eval an expression in module.tf:4; Invalid \"terraform\" attribute: The terraform.env attribute was deprecated in v0.10 and removed in v0.12. The \"state environment\" concept was rename to \"workspace\" in v0.12, and so the workspace name can now be accessed using the terraform.workspace attribute.", 243 } 244 AssertAppError(t, expected, err) 245 }) 246 } 247 248 func Test_NewModuleRunners_withNotAllowedAttributes(t *testing.T) { 249 withinFixtureDir(t, "not_allowed_module_attribute", func() { 250 runner := testRunnerWithOsFs(t, moduleConfig()) 251 252 _, err := NewModuleRunners(runner) 253 254 expected := Error{ 255 Code: UnexpectedAttributeError, 256 Level: ErrorLevel, 257 Message: "Attribute of module not allowed was found in module.tf:1; module.tf:4,3-10: Unexpected \"invalid\" block; Blocks are not allowed here.", 258 } 259 AssertAppError(t, expected, err) 260 }) 261 } 262 263 func Test_RunnerFiles(t *testing.T) { 264 runner := TestRunner(t, map[string]string{ 265 "main.tf": "", 266 }) 267 runner.files["child/main.tf"] = &hcl.File{} 268 269 expected := map[string]*hcl.File{ 270 "main.tf": { 271 Body: hcl.EmptyBody(), 272 Bytes: []byte{}, 273 }, 274 } 275 276 files := runner.Files() 277 278 opt := cmpopts.IgnoreFields(hcl.File{}, "Body", "Nav") 279 if !cmp.Equal(expected, files, opt) { 280 t.Fatalf("Failed test: diff: %s", cmp.Diff(expected, files, opt)) 281 } 282 } 283 284 func Test_LookupResourcesByType(t *testing.T) { 285 content := ` 286 resource "aws_instance" "web" { 287 ami = "${data.aws_ami.ubuntu.id}" 288 instance_type = "t2.micro" 289 290 tags { 291 Name = "HelloWorld" 292 } 293 } 294 295 resource "aws_route53_zone" "primary" { 296 name = "example.com" 297 } 298 299 resource "aws_route" "r" { 300 route_table_id = "rtb-4fbb3ac4" 301 destination_cidr_block = "10.0.1.0/22" 302 vpc_peering_connection_id = "pcx-45ff3dc1" 303 depends_on = ["aws_route_table.testing"] 304 }` 305 306 runner := TestRunner(t, map[string]string{"resource.tf": content}) 307 resources := runner.LookupResourcesByType("aws_instance") 308 309 if len(resources) != 1 { 310 t.Fatalf("Expected resources size is `1`, but get `%d`", len(resources)) 311 } 312 if resources[0].Type != "aws_instance" { 313 t.Fatalf("Expected resource type is `aws_instance`, but get `%s`", resources[0].Type) 314 } 315 } 316 317 func Test_LookupIssues(t *testing.T) { 318 runner := TestRunner(t, map[string]string{}) 319 runner.Issues = Issues{ 320 { 321 Rule: &testRule{}, 322 Message: "This is test rule", 323 Range: hcl.Range{ 324 Filename: "template.tf", 325 Start: hcl.Pos{Line: 1}, 326 }, 327 }, 328 { 329 Rule: &testRule{}, 330 Message: "This is test rule", 331 Range: hcl.Range{ 332 Filename: "resource.tf", 333 Start: hcl.Pos{Line: 1}, 334 }, 335 }, 336 } 337 338 ret := runner.LookupIssues("template.tf") 339 expected := Issues{ 340 { 341 Rule: &testRule{}, 342 Message: "This is test rule", 343 Range: hcl.Range{ 344 Filename: "template.tf", 345 Start: hcl.Pos{Line: 1}, 346 }, 347 }, 348 } 349 350 if !cmp.Equal(expected, ret) { 351 t.Fatalf("Failed test: diff: %s", cmp.Diff(expected, ret)) 352 } 353 } 354 355 func Test_EnsureNoError(t *testing.T) { 356 cases := []struct { 357 Name string 358 Error error 359 ErrorText string 360 }{ 361 { 362 Name: "no error", 363 Error: nil, 364 ErrorText: "function called", 365 }, 366 { 367 Name: "native error", 368 Error: errors.New("Error occurred"), 369 ErrorText: "Error occurred", 370 }, 371 { 372 Name: "warning error", 373 Error: &Error{ 374 Code: UnknownValueError, 375 Level: WarningLevel, 376 Message: "Warning error", 377 }, 378 }, 379 { 380 Name: "app error", 381 Error: &Error{ 382 Code: TypeMismatchError, 383 Level: ErrorLevel, 384 Message: "App error", 385 }, 386 ErrorText: "App error", 387 }, 388 } 389 390 for _, tc := range cases { 391 runner := TestRunner(t, map[string]string{}) 392 393 err := runner.EnsureNoError(tc.Error, func() error { 394 return errors.New("function called") 395 }) 396 if err == nil { 397 if tc.ErrorText != "" { 398 t.Fatalf("Failed `%s` test: expected error is not occurred `%s`", tc.Name, tc.ErrorText) 399 } 400 } else if err.Error() != tc.ErrorText { 401 t.Fatalf("Failed `%s` test: expected error is %s, but get %s", tc.Name, tc.ErrorText, err) 402 } 403 } 404 } 405 406 func Test_IsNullExpr(t *testing.T) { 407 cases := []struct { 408 Name string 409 Content string 410 Expected bool 411 Error string 412 }{ 413 { 414 Name: "non null literal", 415 Content: ` 416 resource "null_resource" "test" { 417 key = "string" 418 }`, 419 Expected: false, 420 }, 421 { 422 Name: "non null variable", 423 Content: ` 424 variable "value" { 425 default = "string" 426 } 427 428 resource "null_resource" "test" { 429 key = var.value 430 }`, 431 Expected: false, 432 }, 433 { 434 Name: "null literal", 435 Content: ` 436 resource "null_resource" "test" { 437 key = null 438 }`, 439 Expected: true, 440 }, 441 { 442 Name: "null variable", 443 Content: ` 444 variable "value" { 445 default = null 446 } 447 448 resource "null_resource" "test" { 449 key = var.value 450 }`, 451 Expected: true, 452 }, 453 { 454 Name: "unknown variable", 455 Content: ` 456 variable "value" {} 457 458 resource "null_resource" "test" { 459 key = var.value 460 }`, 461 Expected: false, 462 }, 463 { 464 Name: "unevaluable reference", 465 Content: ` 466 resource "null_resource" "test" { 467 key = aws_instance.id 468 }`, 469 Expected: false, 470 }, 471 { 472 Name: "including null literal", 473 Content: ` 474 resource "null_resource" "test" { 475 key = "${null}-1" 476 }`, 477 Expected: false, 478 Error: "Invalid template interpolation value: The expression result is null. Cannot include a null value in a string template.", 479 }, 480 { 481 Name: "invalid references", 482 Content: ` 483 resource "null_resource" "test" { 484 key = invalid 485 }`, 486 Expected: false, 487 Error: "Invalid reference: A reference to a resource type must be followed by at least one attribute access, specifying the resource name.", 488 }, 489 } 490 491 for _, tc := range cases { 492 runner := TestRunner(t, map[string]string{"main.tf": tc.Content}) 493 494 err := runner.WalkResourceAttributes("null_resource", "key", func(attribute *hcl.Attribute) error { 495 ret, err := runner.IsNullExpr(attribute.Expr) 496 if err != nil && tc.Error == "" { 497 t.Fatalf("Failed `%s` test: unexpected error occurred: %s", tc.Name, err) 498 } 499 if err == nil && tc.Error != "" { 500 t.Fatalf("Failed `%s` test: expected error is %s, but no errors", tc.Name, tc.Error) 501 } 502 if err != nil && tc.Error != "" && err.Error() != tc.Error { 503 t.Fatalf("Failed `%s` test: expected error is %s, but got %s", tc.Name, tc.Error, err) 504 } 505 if tc.Expected != ret { 506 t.Fatalf("Failed `%s` test: expected value is %t, but get %t", tc.Name, tc.Expected, ret) 507 } 508 return nil 509 }) 510 511 if err != nil { 512 t.Fatalf("Failed `%s` test: `%s` occurred", tc.Name, err) 513 } 514 } 515 } 516 517 func Test_EachStringSliceExprs(t *testing.T) { 518 cases := []struct { 519 Name string 520 Content string 521 Vals []string 522 Lines []int 523 }{ 524 { 525 Name: "literal list", 526 Content: ` 527 resource "null_resource" "test" { 528 value = [ 529 "text", 530 "element", 531 ] 532 }`, 533 Vals: []string{"text", "element"}, 534 Lines: []int{4, 5}, 535 }, 536 { 537 Name: "literal list", 538 Content: ` 539 variable "list" { 540 default = [ 541 "text", 542 "element", 543 ] 544 } 545 546 resource "null_resource" "test" { 547 value = var.list 548 }`, 549 Vals: []string{"text", "element"}, 550 Lines: []int{10, 10}, 551 }, 552 { 553 Name: "for expressions", 554 Content: ` 555 variable "list" { 556 default = ["text", "element", "ignored"] 557 } 558 559 resource "null_resource" "test" { 560 value = [ 561 for e in var.list: 562 e 563 if e != "ignored" 564 ] 565 }`, 566 Vals: []string{"text", "element"}, 567 Lines: []int{7, 7}, 568 }, 569 } 570 571 for _, tc := range cases { 572 runner := TestRunner(t, map[string]string{"main.tf": tc.Content}) 573 574 vals := []string{} 575 lines := []int{} 576 err := runner.WalkResourceAttributes("null_resource", "value", func(attribute *hcl.Attribute) error { 577 return runner.EachStringSliceExprs(attribute.Expr, func(val string, expr hcl.Expression) { 578 vals = append(vals, val) 579 lines = append(lines, expr.Range().Start.Line) 580 }) 581 }) 582 if err != nil { 583 t.Fatalf("Failed `%s` test: %s", tc.Name, err) 584 } 585 586 if !cmp.Equal(vals, tc.Vals) { 587 t.Fatalf("Failed `%s` test: diff=%s", tc.Name, cmp.Diff(vals, tc.Vals)) 588 } 589 if !cmp.Equal(lines, tc.Lines) { 590 t.Fatalf("Failed `%s` test: diff=%s", tc.Name, cmp.Diff(lines, tc.Lines)) 591 } 592 } 593 } 594 595 type testRule struct{} 596 597 func (r *testRule) Name() string { 598 return "test_rule" 599 } 600 func (r *testRule) Severity() string { 601 return ERROR 602 } 603 func (r *testRule) Link() string { 604 return "" 605 } 606 607 func Test_EmitIssue(t *testing.T) { 608 cases := []struct { 609 Name string 610 Rule Rule 611 Message string 612 Location hcl.Range 613 Annotations map[string]Annotations 614 Expected Issues 615 }{ 616 { 617 Name: "basic", 618 Rule: &testRule{}, 619 Message: "This is test message", 620 Location: hcl.Range{ 621 Filename: "test.tf", 622 Start: hcl.Pos{Line: 1}, 623 }, 624 Annotations: map[string]Annotations{}, 625 Expected: Issues{ 626 { 627 Rule: &testRule{}, 628 Message: "This is test message", 629 Range: hcl.Range{ 630 Filename: "test.tf", 631 Start: hcl.Pos{Line: 1}, 632 }, 633 }, 634 }, 635 }, 636 { 637 Name: "ignore", 638 Rule: &testRule{}, 639 Message: "This is test message", 640 Location: hcl.Range{ 641 Filename: "test.tf", 642 Start: hcl.Pos{Line: 1}, 643 }, 644 Annotations: map[string]Annotations{ 645 "test.tf": { 646 { 647 Content: "test_rule", 648 Token: hclsyntax.Token{ 649 Type: hclsyntax.TokenComment, 650 Range: hcl.Range{ 651 Filename: "test.tf", 652 Start: hcl.Pos{Line: 1}, 653 }, 654 }, 655 }, 656 }, 657 }, 658 Expected: Issues{}, 659 }, 660 } 661 662 for _, tc := range cases { 663 runner := testRunnerWithAnnotations(t, map[string]string{}, tc.Annotations) 664 665 runner.EmitIssue(tc.Rule, tc.Message, tc.Location) 666 667 if !cmp.Equal(runner.Issues, tc.Expected) { 668 t.Fatalf("Failed `%s` test: diff=%s", tc.Name, cmp.Diff(runner.Issues, tc.Expected)) 669 } 670 } 671 } 672 673 func Test_DecodeRuleConfig(t *testing.T) { 674 type ruleSchema struct { 675 Foo string `hcl:"foo"` 676 } 677 options := ruleSchema{} 678 679 file, diags := hclsyntax.ParseConfig([]byte(`foo = "bar"`), "test.hcl", hcl.Pos{}) 680 if diags.HasErrors() { 681 t.Fatalf("Failed to parse test config: %s", diags) 682 } 683 684 cfg := EmptyConfig() 685 cfg.Rules["test"] = &RuleConfig{ 686 Name: "test", 687 Enabled: true, 688 Body: file.Body, 689 } 690 691 runner := TestRunnerWithConfig(t, map[string]string{}, cfg) 692 if err := runner.DecodeRuleConfig("test", &options); err != nil { 693 t.Fatalf("Failed to decode rule config: %s", err) 694 } 695 696 expected := ruleSchema{Foo: "bar"} 697 if !cmp.Equal(options, expected) { 698 t.Fatalf("Failed to decode rule config: diff=%s", cmp.Diff(options, expected)) 699 } 700 } 701 702 func Test_DecodeRuleConfig_emptyBody(t *testing.T) { 703 type ruleSchema struct { 704 Foo string `hcl:"foo"` 705 } 706 options := ruleSchema{} 707 708 cfg := EmptyConfig() 709 cfg.Rules["test"] = &RuleConfig{ 710 Name: "test", 711 Enabled: true, 712 Body: hcl.EmptyBody(), 713 } 714 715 runner := TestRunnerWithConfig(t, map[string]string{}, cfg) 716 err := runner.DecodeRuleConfig("test", &options) 717 if err == nil { 718 t.Fatal("Expected to fail to decode rule config, but not") 719 } 720 721 expected := "This rule cannot be enabled with the `--enable-rule` option because it lacks the required configuration" 722 if err.Error() != expected { 723 t.Fatalf("Expected error message is %s, but got %s", expected, err.Error()) 724 } 725 }