github.com/terraform-linters/tflint-plugin-sdk@v0.22.0/helper/runner_test.go (about) 1 package helper 2 3 import ( 4 "errors" 5 "testing" 6 7 "github.com/google/go-cmp/cmp" 8 "github.com/google/go-cmp/cmp/cmpopts" 9 "github.com/hashicorp/hcl/v2" 10 "github.com/hashicorp/hcl/v2/hclsyntax" 11 "github.com/terraform-linters/tflint-plugin-sdk/hclext" 12 "github.com/terraform-linters/tflint-plugin-sdk/tflint" 13 "github.com/zclconf/go-cty/cty" 14 ) 15 16 func Test_GetResourceContent(t *testing.T) { 17 cases := []struct { 18 Name string 19 Src string 20 Resource string 21 Schema *hclext.BodySchema 22 Expected *hclext.BodyContent 23 }{ 24 { 25 Name: "attribute", 26 Src: ` 27 resource "aws_instance" "foo" { 28 ami = "ami-123456" 29 instance_type = "t2.micro" 30 } 31 32 resource "aws_s3_bucket" "bar" { 33 bucket = "my-tf-test-bucket" 34 acl = "private" 35 }`, 36 Resource: "aws_instance", 37 Schema: &hclext.BodySchema{ 38 Attributes: []hclext.AttributeSchema{{Name: "instance_type"}}, 39 }, 40 Expected: &hclext.BodyContent{ 41 Blocks: hclext.Blocks{ 42 { 43 Type: "resource", 44 Labels: []string{"aws_instance", "foo"}, 45 Body: &hclext.BodyContent{ 46 Attributes: hclext.Attributes{ 47 "instance_type": { 48 Name: "instance_type", 49 Expr: &hclsyntax.TemplateExpr{ 50 Parts: []hclsyntax.Expression{ 51 &hclsyntax.LiteralValueExpr{ 52 SrcRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 20}, End: hcl.Pos{Line: 4, Column: 28}}, 53 }, 54 }, 55 SrcRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 19}, End: hcl.Pos{Line: 4, Column: 29}}, 56 }, 57 Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 3}, End: hcl.Pos{Line: 4, Column: 29}}, 58 NameRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 3}, End: hcl.Pos{Line: 4, Column: 16}}, 59 }, 60 }, 61 Blocks: hclext.Blocks{}, 62 }, 63 DefRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 1}, End: hcl.Pos{Line: 2, Column: 30}}, 64 TypeRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 1}, End: hcl.Pos{Line: 2, Column: 9}}, 65 LabelRanges: []hcl.Range{ 66 {Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 10}, End: hcl.Pos{Line: 2, Column: 24}}, 67 {Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 25}, End: hcl.Pos{Line: 2, Column: 30}}, 68 }, 69 }, 70 }, 71 }, 72 }, 73 { 74 Name: "block", 75 Src: ` 76 resource "aws_instance" "foo" { 77 ami = "ami-123456" 78 ebs_block_device { 79 volume_size = 16 80 } 81 } 82 83 resource "aws_s3_bucket" "bar" { 84 bucket = "my-tf-test-bucket" 85 acl = "private" 86 }`, 87 Resource: "aws_instance", 88 Schema: &hclext.BodySchema{ 89 Blocks: []hclext.BlockSchema{ 90 {Type: "ebs_block_device", Body: &hclext.BodySchema{Attributes: []hclext.AttributeSchema{{Name: "volume_size"}}}}, 91 }, 92 }, 93 Expected: &hclext.BodyContent{ 94 Blocks: hclext.Blocks{ 95 { 96 Type: "resource", 97 Labels: []string{"aws_instance", "foo"}, 98 Body: &hclext.BodyContent{ 99 Attributes: hclext.Attributes{}, 100 Blocks: hclext.Blocks{ 101 { 102 Type: "ebs_block_device", 103 Body: &hclext.BodyContent{ 104 Attributes: hclext.Attributes{ 105 "volume_size": { 106 Name: "volume_size", 107 Expr: &hclsyntax.LiteralValueExpr{ 108 SrcRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 5, Column: 19}, End: hcl.Pos{Line: 5, Column: 21}}, 109 }, 110 Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 5, Column: 5}, End: hcl.Pos{Line: 5, Column: 21}}, 111 NameRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 5, Column: 5}, End: hcl.Pos{Line: 5, Column: 16}}, 112 }, 113 }, 114 Blocks: hclext.Blocks{}, 115 }, 116 DefRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 3}, End: hcl.Pos{Line: 4, Column: 19}}, 117 TypeRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 3}, End: hcl.Pos{Line: 4, Column: 19}}, 118 }, 119 }, 120 }, 121 DefRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 1}, End: hcl.Pos{Line: 2, Column: 30}}, 122 TypeRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 1}, End: hcl.Pos{Line: 2, Column: 9}}, 123 LabelRanges: []hcl.Range{ 124 {Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 10}, End: hcl.Pos{Line: 2, Column: 24}}, 125 {Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 25}, End: hcl.Pos{Line: 2, Column: 30}}, 126 }, 127 }, 128 }, 129 }, 130 }, 131 } 132 133 for _, tc := range cases { 134 t.Run(tc.Name, func(t *testing.T) { 135 runner := TestRunner(t, map[string]string{"main.tf": tc.Src}) 136 137 got, err := runner.GetResourceContent(tc.Resource, tc.Schema, nil) 138 if err != nil { 139 t.Error(err) 140 } else { 141 opts := cmp.Options{ 142 cmpopts.IgnoreFields(hclsyntax.LiteralValueExpr{}, "Val"), 143 cmpopts.IgnoreFields(hcl.Pos{}, "Byte"), 144 } 145 if diff := cmp.Diff(tc.Expected, got, opts...); diff != "" { 146 t.Error(diff) 147 } 148 } 149 }) 150 } 151 } 152 153 func Test_GetModuleContent(t *testing.T) { 154 cases := []struct { 155 Name string 156 Src string 157 Schema *hclext.BodySchema 158 Expected *hclext.BodyContent 159 }{ 160 { 161 Name: "backend", 162 Src: ` 163 terraform { 164 backend "s3" { 165 bucket = "mybucket" 166 key = "path/to/my/key" 167 region = "us-east-1" 168 } 169 }`, 170 Schema: &hclext.BodySchema{ 171 Blocks: []hclext.BlockSchema{ 172 { 173 Type: "terraform", 174 Body: &hclext.BodySchema{ 175 Blocks: []hclext.BlockSchema{ 176 { 177 Type: "backend", 178 LabelNames: []string{"name"}, 179 Body: &hclext.BodySchema{ 180 Attributes: []hclext.AttributeSchema{{Name: "bucket"}}, 181 }, 182 }, 183 }, 184 }, 185 }, 186 }, 187 }, 188 Expected: &hclext.BodyContent{ 189 Blocks: hclext.Blocks{ 190 { 191 Type: "terraform", 192 Body: &hclext.BodyContent{ 193 Attributes: hclext.Attributes{}, 194 Blocks: hclext.Blocks{ 195 { 196 Type: "backend", 197 Labels: []string{"s3"}, 198 Body: &hclext.BodyContent{ 199 Attributes: hclext.Attributes{ 200 "bucket": &hclext.Attribute{ 201 Name: "bucket", 202 Expr: &hclsyntax.TemplateExpr{ 203 Parts: []hclsyntax.Expression{ 204 &hclsyntax.LiteralValueExpr{ 205 SrcRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 12}, End: hcl.Pos{Line: 4, Column: 20}}, 206 }, 207 }, 208 SrcRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 11}, End: hcl.Pos{Line: 4, Column: 21}}, 209 }, 210 Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 2}, End: hcl.Pos{Line: 4, Column: 21}}, 211 NameRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4, Column: 2}, End: hcl.Pos{Line: 4, Column: 8}}, 212 }, 213 }, 214 Blocks: hclext.Blocks{}, 215 }, 216 DefRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 3, Column: 2}, End: hcl.Pos{Line: 3, Column: 14}}, 217 TypeRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 3, Column: 2}, End: hcl.Pos{Line: 3, Column: 9}}, 218 LabelRanges: []hcl.Range{ 219 {Filename: "main.tf", Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 14}}, 220 }, 221 }, 222 }, 223 }, 224 DefRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 1}, End: hcl.Pos{Line: 2, Column: 10}}, 225 TypeRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 2, Column: 1}, End: hcl.Pos{Line: 2, Column: 10}}, 226 }, 227 }, 228 }, 229 }, 230 } 231 232 for _, tc := range cases { 233 t.Run(tc.Name, func(t *testing.T) { 234 runner := TestRunner(t, map[string]string{"main.tf": tc.Src}) 235 236 got, err := runner.GetModuleContent(tc.Schema, nil) 237 if err != nil { 238 t.Error(err) 239 } else { 240 opts := cmp.Options{ 241 cmpopts.IgnoreFields(hclsyntax.LiteralValueExpr{}, "Val"), 242 cmpopts.IgnoreFields(hcl.Pos{}, "Byte"), 243 } 244 if diff := cmp.Diff(tc.Expected, got, opts...); diff != "" { 245 t.Error(diff) 246 } 247 } 248 }) 249 } 250 } 251 252 func Test_GetModuleContent_json(t *testing.T) { 253 files := map[string]string{ 254 "main.tf.json": `{"variable": {"foo": {"type": "string"}}}`, 255 } 256 257 runner := TestRunner(t, files) 258 259 schema := &hclext.BodySchema{ 260 Blocks: []hclext.BlockSchema{ 261 { 262 Type: "variable", 263 Body: &hclext.BodySchema{ 264 Blocks: []hclext.BlockSchema{ 265 { 266 Type: "type", 267 LabelNames: []string{"name"}, 268 Body: &hclext.BodySchema{}, 269 }, 270 }, 271 }, 272 }, 273 }, 274 } 275 got, err := runner.GetModuleContent(schema, nil) 276 if err != nil { 277 t.Error(err) 278 } else { 279 if len(got.Blocks) != 1 { 280 t.Errorf("got %d blocks, but 1 block is expected", len(got.Blocks)) 281 } 282 } 283 } 284 285 func TestWalkExpressions(t *testing.T) { 286 tests := []struct { 287 name string 288 files map[string]string 289 walked []hcl.Range 290 }{ 291 { 292 name: "resource", 293 files: map[string]string{ 294 "resource.tf": ` 295 resource "null_resource" "test" { 296 key = "foo" 297 }`, 298 }, 299 walked: []hcl.Range{ 300 {Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}}, 301 {Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}}, 302 }, 303 }, 304 { 305 name: "data source", 306 files: map[string]string{ 307 "data.tf": ` 308 data "null_dataresource" "test" { 309 key = "foo" 310 }`, 311 }, 312 walked: []hcl.Range{ 313 {Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}}, 314 {Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}}, 315 }, 316 }, 317 { 318 name: "module call", 319 files: map[string]string{ 320 "module.tf": ` 321 module "m" { 322 source = "./module" 323 key = "foo" 324 }`, 325 }, 326 walked: []hcl.Range{ 327 {Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 22}}, 328 {Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 21}}, 329 {Start: hcl.Pos{Line: 4, Column: 12}, End: hcl.Pos{Line: 4, Column: 17}}, 330 {Start: hcl.Pos{Line: 4, Column: 13}, End: hcl.Pos{Line: 4, Column: 16}}, 331 }, 332 }, 333 { 334 name: "provider config", 335 files: map[string]string{ 336 "provider.tf": ` 337 provider "p" { 338 key = "foo" 339 }`, 340 }, 341 walked: []hcl.Range{ 342 {Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}}, 343 {Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}}, 344 }, 345 }, 346 { 347 name: "locals", 348 files: map[string]string{ 349 "locals.tf": ` 350 locals { 351 key = "foo" 352 }`, 353 }, 354 walked: []hcl.Range{ 355 {Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}}, 356 {Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}}, 357 }, 358 }, 359 { 360 name: "output", 361 files: map[string]string{ 362 "output.tf": ` 363 output "o" { 364 value = "foo" 365 }`, 366 }, 367 walked: []hcl.Range{ 368 {Start: hcl.Pos{Line: 3, Column: 11}, End: hcl.Pos{Line: 3, Column: 16}}, 369 {Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 15}}, 370 }, 371 }, 372 { 373 name: "resource with block", 374 files: map[string]string{ 375 "resource.tf": ` 376 resource "null_resource" "test" { 377 key = "foo" 378 379 lifecycle { 380 ignore_changes = [key] 381 } 382 }`, 383 }, 384 walked: []hcl.Range{ 385 {Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}}, 386 {Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}}, 387 {Start: hcl.Pos{Line: 6, Column: 22}, End: hcl.Pos{Line: 6, Column: 27}}, 388 {Start: hcl.Pos{Line: 6, Column: 23}, End: hcl.Pos{Line: 6, Column: 26}}, 389 }, 390 }, 391 { 392 name: "resource json", 393 files: map[string]string{ 394 "resource.tf.json": ` 395 { 396 "resource": { 397 "null_resource": { 398 "test": { 399 "key": "foo", 400 "nested": { 401 "key": "foo" 402 }, 403 "list": [{ 404 "key": "foo" 405 }] 406 } 407 } 408 } 409 }`, 410 }, 411 walked: []hcl.Range{ 412 {Start: hcl.Pos{Line: 3, Column: 15}, End: hcl.Pos{Line: 15, Column: 4}}, 413 }, 414 }, 415 { 416 name: "multiple files", 417 files: map[string]string{ 418 "main.tf": ` 419 provider "aws" { 420 region = "us-east-1" 421 422 assume_role { 423 role_arn = "arn:aws:iam::123412341234:role/ExampleRole" 424 } 425 }`, 426 "main_override.tf": ` 427 provider "aws" { 428 region = "us-east-1" 429 430 assume_role { 431 role_arn = null 432 } 433 }`, 434 }, 435 walked: []hcl.Range{ 436 {Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 23}, Filename: "main.tf"}, 437 {Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 22}, Filename: "main.tf"}, 438 {Start: hcl.Pos{Line: 6, Column: 16}, End: hcl.Pos{Line: 6, Column: 60}, Filename: "main.tf"}, 439 {Start: hcl.Pos{Line: 6, Column: 17}, End: hcl.Pos{Line: 6, Column: 59}, Filename: "main.tf"}, 440 {Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 23}, Filename: "main_override.tf"}, 441 {Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 22}, Filename: "main_override.tf"}, 442 {Start: hcl.Pos{Line: 6, Column: 16}, End: hcl.Pos{Line: 6, Column: 20}, Filename: "main_override.tf"}, 443 }, 444 }, 445 { 446 name: "nested attributes", 447 files: map[string]string{ 448 "data.tf": ` 449 data "terraform_remote_state" "remote_state" { 450 backend = "remote" 451 452 config = { 453 organization = "Organization" 454 workspaces = { 455 name = "${var.environment}" 456 } 457 } 458 }`, 459 }, 460 walked: []hcl.Range{ 461 {Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 21}}, 462 {Start: hcl.Pos{Line: 3, Column: 14}, End: hcl.Pos{Line: 3, Column: 20}}, 463 {Start: hcl.Pos{Line: 5, Column: 12}, End: hcl.Pos{Line: 10, Column: 4}}, 464 {Start: hcl.Pos{Line: 6, Column: 5}, End: hcl.Pos{Line: 6, Column: 17}}, 465 {Start: hcl.Pos{Line: 6, Column: 20}, End: hcl.Pos{Line: 6, Column: 34}}, 466 {Start: hcl.Pos{Line: 6, Column: 21}, End: hcl.Pos{Line: 6, Column: 33}}, 467 {Start: hcl.Pos{Line: 7, Column: 5}, End: hcl.Pos{Line: 7, Column: 15}}, 468 {Start: hcl.Pos{Line: 7, Column: 18}, End: hcl.Pos{Line: 9, Column: 6}}, 469 {Start: hcl.Pos{Line: 8, Column: 7}, End: hcl.Pos{Line: 8, Column: 11}}, 470 {Start: hcl.Pos{Line: 8, Column: 14}, End: hcl.Pos{Line: 8, Column: 34}}, 471 {Start: hcl.Pos{Line: 8, Column: 17}, End: hcl.Pos{Line: 8, Column: 32}}, 472 }, 473 }, 474 } 475 476 for _, test := range tests { 477 t.Run(test.name, func(t *testing.T) { 478 runner := TestRunner(t, test.files) 479 480 walked := []hcl.Range{} 481 diags := runner.WalkExpressions(tflint.ExprWalkFunc(func(expr hcl.Expression) hcl.Diagnostics { 482 walked = append(walked, expr.Range()) 483 return nil 484 })) 485 if diags.HasErrors() { 486 t.Fatal(diags) 487 } 488 opts := cmp.Options{ 489 cmpopts.IgnoreFields(hcl.Range{}, "Filename"), 490 cmpopts.IgnoreFields(hcl.Pos{}, "Byte"), 491 cmpopts.SortSlices(func(x, y hcl.Range) bool { return x.String() > y.String() }), 492 } 493 if diff := cmp.Diff(walked, test.walked, opts); diff != "" { 494 t.Error(diff) 495 } 496 }) 497 } 498 } 499 500 func Test_DecodeRuleConfig(t *testing.T) { 501 files := map[string]string{ 502 ".tflint.hcl": ` 503 rule "test" { 504 enabled = true 505 foo = "bar" 506 }`, 507 } 508 509 runner := TestRunner(t, files) 510 511 type ruleConfig struct { 512 Foo string `hclext:"foo"` 513 } 514 target := &ruleConfig{} 515 if err := runner.DecodeRuleConfig("test", target); err != nil { 516 t.Fatal(err) 517 } 518 519 if target.Foo != "bar" { 520 t.Errorf("target.Foo should be `bar`, but got `%s`", target.Foo) 521 } 522 } 523 524 func Test_DecodeRuleConfig_config_not_found(t *testing.T) { 525 runner := TestRunner(t, map[string]string{}) 526 527 type ruleConfig struct { 528 Foo string `hclext:"foo"` 529 } 530 target := &ruleConfig{} 531 if err := runner.DecodeRuleConfig("test", target); err != nil { 532 t.Fatal(err) 533 } 534 535 if target.Foo != "" { 536 t.Errorf("target.Foo should be empty, but got `%s`", target.Foo) 537 } 538 } 539 540 func Test_EvaluateExpr_string(t *testing.T) { 541 tests := []struct { 542 Name string 543 Src string 544 Want string 545 }{ 546 { 547 Name: "string literal", 548 Src: ` 549 resource "aws_instance" "foo" { 550 instance_type = "t2.micro" 551 }`, 552 Want: "t2.micro", 553 }, 554 { 555 Name: "string interpolation", 556 Src: ` 557 variable "instance_type" { 558 type = string 559 default = "t2.micro" 560 } 561 562 resource "aws_instance" "foo" { 563 instance_type = var.instance_type 564 }`, 565 Want: "t2.micro", 566 }, 567 } 568 569 for _, test := range tests { 570 t.Run(test.Name, func(t *testing.T) { 571 runner := TestRunner(t, map[string]string{"main.tf": test.Src}) 572 573 resources, err := runner.GetResourceContent("aws_instance", &hclext.BodySchema{ 574 Attributes: []hclext.AttributeSchema{{Name: "instance_type"}}, 575 }, nil) 576 if err != nil { 577 t.Fatal(err) 578 } 579 580 for _, resource := range resources.Blocks { 581 // raw value 582 var instanceType string 583 if err := runner.EvaluateExpr(resource.Body.Attributes["instance_type"].Expr, &instanceType, nil); err != nil { 584 t.Fatal(err) 585 } 586 587 if instanceType != test.Want { 588 t.Fatalf(`"%s" is expected, but got "%s"`, test.Want, instanceType) 589 } 590 591 // callback 592 if err := runner.EvaluateExpr(resource.Body.Attributes["instance_type"].Expr, func(val string) error { 593 if instanceType != test.Want { 594 t.Fatalf(`"%s" is expected, but got "%s"`, test.Want, instanceType) 595 } 596 return nil 597 }, nil); err != nil { 598 t.Fatal(err) 599 } 600 } 601 }) 602 } 603 } 604 605 func Test_EvaluateExpr_value(t *testing.T) { 606 tests := []struct { 607 Name string 608 Src string 609 Want string 610 }{ 611 { 612 Name: "sensitive variable", 613 Src: ` 614 variable "instance_type" { 615 type = string 616 default = "secret" 617 sensitive = true 618 } 619 620 resource "aws_instance" "foo" { 621 instance_type = var.instance_type 622 }`, 623 Want: `cty.StringVal("secret").Mark(marks.Sensitive)`, 624 }, 625 { 626 Name: "ephemeral variable", 627 Src: ` 628 variable "instance_type" { 629 type = string 630 default = "secret" 631 ephemeral = true 632 } 633 634 resource "aws_instance" "foo" { 635 instance_type = var.instance_type 636 }`, 637 Want: `cty.StringVal("secret").Mark(marks.Ephemeral)`, 638 }, 639 } 640 641 for _, test := range tests { 642 t.Run(test.Name, func(t *testing.T) { 643 runner := TestRunner(t, map[string]string{"main.tf": test.Src}) 644 645 resources, err := runner.GetResourceContent("aws_instance", &hclext.BodySchema{ 646 Attributes: []hclext.AttributeSchema{{Name: "instance_type"}}, 647 }, nil) 648 if err != nil { 649 t.Fatal(err) 650 } 651 652 for _, resource := range resources.Blocks { 653 // raw value 654 var instanceType cty.Value 655 if err := runner.EvaluateExpr(resource.Body.Attributes["instance_type"].Expr, &instanceType, nil); err != nil { 656 t.Fatal(err) 657 } 658 659 if instanceType.GoString() != test.Want { 660 t.Fatalf(`"%s" is expected, but got "%s"`, test.Want, instanceType.GoString()) 661 } 662 663 // callback 664 if err := runner.EvaluateExpr(resource.Body.Attributes["instance_type"].Expr, func(val cty.Value) error { 665 if instanceType.GoString() != test.Want { 666 t.Fatalf(`"%s" is expected, but got "%s"`, test.Want, instanceType.GoString()) 667 } 668 return nil 669 }, nil); err != nil { 670 t.Fatal(err) 671 } 672 } 673 }) 674 } 675 } 676 677 type dummyRule struct { 678 tflint.DefaultRule 679 } 680 681 func (r *dummyRule) Name() string { return "dummy_rule" } 682 func (r *dummyRule) Enabled() bool { return true } 683 func (r *dummyRule) Severity() tflint.Severity { return tflint.ERROR } 684 func (r *dummyRule) Check(tflint.Runner) error { return nil } 685 686 func Test_EmitIssue(t *testing.T) { 687 src := ` 688 resource "aws_instance" "foo" { 689 instance_type = "t2.micro" 690 }` 691 692 runner := TestRunner(t, map[string]string{"main.tf": src}) 693 694 resources, err := runner.GetResourceContent("aws_instance", &hclext.BodySchema{ 695 Attributes: []hclext.AttributeSchema{{Name: "instance_type"}}, 696 }, nil) 697 if err != nil { 698 t.Fatal(err) 699 } 700 701 for _, resource := range resources.Blocks { 702 if err := runner.EmitIssue(&dummyRule{}, "issue found", resource.Body.Attributes["instance_type"].Expr.Range()); err != nil { 703 t.Fatal(err) 704 } 705 } 706 707 expected := Issues{ 708 { 709 Rule: &dummyRule{}, 710 Message: "issue found", 711 Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 3, Column: 19}, End: hcl.Pos{Line: 3, Column: 29}}, 712 }, 713 } 714 715 opt := cmpopts.IgnoreFields(hcl.Pos{}, "Byte") 716 if diff := cmp.Diff(expected, runner.Issues, opt); diff != "" { 717 t.Fatal(diff) 718 } 719 } 720 721 func Test_EmitIssueWithFix(t *testing.T) { 722 // default error check helper 723 neverHappend := func(err error) bool { return err != nil } 724 725 tests := []struct { 726 name string 727 src string 728 rng hcl.Range 729 fix func(tflint.Fixer) error 730 want Issues 731 fixed string 732 errCheck func(error) bool 733 }{ 734 { 735 name: "with fix", 736 src: ` 737 resource "aws_instance" "foo" { 738 instance_type = "t2.micro" 739 }`, 740 rng: hcl.Range{ 741 Filename: "main.tf", 742 Start: hcl.Pos{Line: 3, Column: 19, Byte: 51}, 743 End: hcl.Pos{Line: 3, Column: 29, Byte: 61}, 744 }, 745 fix: func(fixer tflint.Fixer) error { 746 return fixer.ReplaceText( 747 hcl.Range{ 748 Filename: "main.tf", 749 Start: hcl.Pos{Line: 3, Column: 19, Byte: 51}, 750 End: hcl.Pos{Line: 3, Column: 29, Byte: 61}, 751 }, 752 `"t3.micro"`, 753 ) 754 }, 755 want: Issues{ 756 { 757 Rule: &dummyRule{}, 758 Message: "issue found", 759 Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 3, Column: 19}, End: hcl.Pos{Line: 3, Column: 29}}, 760 }, 761 }, 762 fixed: ` 763 resource "aws_instance" "foo" { 764 instance_type = "t3.micro" 765 }`, 766 errCheck: neverHappend, 767 }, 768 { 769 name: "autofix is not supported", 770 src: ` 771 resource "aws_instance" "foo" { 772 instance_type = "t2.micro" 773 }`, 774 rng: hcl.Range{ 775 Filename: "main.tf", 776 Start: hcl.Pos{Line: 3, Column: 19, Byte: 51}, 777 End: hcl.Pos{Line: 3, Column: 29, Byte: 61}, 778 }, 779 fix: func(fixer tflint.Fixer) error { 780 if err := fixer.ReplaceText( 781 hcl.Range{ 782 Filename: "main.tf", 783 Start: hcl.Pos{Line: 3, Column: 19, Byte: 51}, 784 End: hcl.Pos{Line: 3, Column: 29, Byte: 61}, 785 }, 786 `"t3.micro"`, 787 ); err != nil { 788 return err 789 } 790 return tflint.ErrFixNotSupported 791 }, 792 want: Issues{ 793 { 794 Rule: &dummyRule{}, 795 Message: "issue found", 796 Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 3, Column: 19}, End: hcl.Pos{Line: 3, Column: 29}}, 797 }, 798 }, 799 errCheck: neverHappend, 800 }, 801 { 802 name: "other errors", 803 src: ` 804 resource "aws_instance" "foo" { 805 instance_type = "t2.micro" 806 }`, 807 rng: hcl.Range{ 808 Filename: "main.tf", 809 Start: hcl.Pos{Line: 3, Column: 19, Byte: 51}, 810 End: hcl.Pos{Line: 3, Column: 29, Byte: 61}, 811 }, 812 fix: func(fixer tflint.Fixer) error { 813 if err := fixer.ReplaceText( 814 hcl.Range{ 815 Filename: "main.tf", 816 Start: hcl.Pos{Line: 3, Column: 19, Byte: 51}, 817 End: hcl.Pos{Line: 3, Column: 29, Byte: 61}, 818 }, 819 `"t3.micro"`, 820 ); err != nil { 821 return err 822 } 823 return errors.New("unexpected error") 824 }, 825 want: Issues{}, 826 fixed: ` 827 resource "aws_instance" "foo" { 828 instance_type = "t3.micro" 829 }`, 830 errCheck: func(err error) bool { 831 return err == nil && err.Error() != "unexpected error" 832 }, 833 }, 834 } 835 836 for _, test := range tests { 837 t.Run(test.name, func(t *testing.T) { 838 runner := TestRunner(t, map[string]string{"main.tf": test.src}) 839 840 err := runner.EmitIssueWithFix(&dummyRule{}, "issue found", test.rng, test.fix) 841 if test.errCheck(err) { 842 t.Fatal(err) 843 } 844 845 opt := cmpopts.IgnoreFields(hcl.Pos{}, "Byte") 846 if diff := cmp.Diff(test.want, runner.Issues, opt); diff != "" { 847 t.Fatal(diff) 848 } 849 if diff := cmp.Diff(test.fixed, string(runner.Changes()["main.tf"]), opt); diff != "" { 850 t.Fatal(diff) 851 } 852 }) 853 } 854 } 855 856 func TestChanges(t *testing.T) { 857 tests := []struct { 858 name string 859 src string 860 fix func(tflint.Fixer) error 861 want string 862 }{ 863 { 864 name: "changes", 865 src: ` 866 locals { 867 foo = "bar" 868 }`, 869 fix: func(fixer tflint.Fixer) error { 870 return fixer.InsertTextBefore( 871 hcl.Range{ 872 Filename: "main.tf", 873 Start: hcl.Pos{Byte: 12}, 874 End: hcl.Pos{Byte: 15}, 875 }, 876 "bar = \"baz\"\n", 877 ) 878 }, 879 want: ` 880 locals { 881 bar = "baz" 882 foo = "bar" 883 }`, 884 }, 885 } 886 887 for _, test := range tests { 888 t.Run(test.name, func(t *testing.T) { 889 runner := TestRunner(t, map[string]string{"main.tf": test.src}) 890 891 if err := test.fix(runner.fixer); err != nil { 892 t.Fatal(err) 893 } 894 895 if diff := cmp.Diff(test.want, string(runner.Changes()["main.tf"])); diff != "" { 896 t.Fatal(diff) 897 } 898 }) 899 } 900 } 901 902 func Test_EnsureNoError(t *testing.T) { 903 runner := TestRunner(t, map[string]string{}) 904 905 var run bool 906 err := runner.EnsureNoError(nil, func() error { 907 run = true 908 return nil 909 }) 910 if err != nil { 911 t.Fatal(err) 912 } 913 914 if !run { 915 t.Fatal("Expected to exec the passed proc, but doesn't") 916 } 917 }