github.com/terraform-linters/tflint-plugin-sdk@v0.22.0/plugin/internal/plugin2host/plugin2host_test.go (about) 1 package plugin2host 2 3 import ( 4 "errors" 5 "fmt" 6 "reflect" 7 "testing" 8 9 "github.com/google/go-cmp/cmp" 10 "github.com/google/go-cmp/cmp/cmpopts" 11 "github.com/hashicorp/go-plugin" 12 "github.com/hashicorp/hcl/v2" 13 "github.com/hashicorp/hcl/v2/hclsyntax" 14 "github.com/hashicorp/hcl/v2/json" 15 "github.com/terraform-linters/tflint-plugin-sdk/hclext" 16 "github.com/terraform-linters/tflint-plugin-sdk/internal" 17 "github.com/terraform-linters/tflint-plugin-sdk/plugin/internal/proto" 18 "github.com/terraform-linters/tflint-plugin-sdk/terraform/addrs" 19 "github.com/terraform-linters/tflint-plugin-sdk/terraform/lang/marks" 20 "github.com/terraform-linters/tflint-plugin-sdk/tflint" 21 "github.com/zclconf/go-cty/cty" 22 "google.golang.org/grpc" 23 ) 24 25 func startTestGRPCServer(t *testing.T, runner Server) *GRPCClient { 26 conn, _ := plugin.TestGRPCConn(t, func(server *grpc.Server) { 27 proto.RegisterRunnerServer(server, &GRPCServer{Impl: runner}) 28 }) 29 30 return &GRPCClient{ 31 Client: proto.NewRunnerClient(conn), 32 Fixer: internal.NewFixer(runner.GetFiles(tflint.RootModuleCtxType)), 33 FixEnabled: false, 34 } 35 } 36 37 var _ Server = &mockServer{} 38 39 type mockServer struct { 40 impl mockServerImpl 41 } 42 43 type mockServerImpl struct { 44 getOriginalwd func() string 45 getModulePath func() []string 46 getModuleContent func(*hclext.BodySchema, tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) 47 getFile func(string) (*hcl.File, error) 48 getFiles func() map[string][]byte 49 getRuleConfigContent func(string, *hclext.BodySchema) (*hclext.BodyContent, map[string][]byte, error) 50 evaluateExpr func(hcl.Expression, tflint.EvaluateExprOption) (cty.Value, error) 51 emitIssue func(tflint.Rule, string, hcl.Range, bool) (bool, error) 52 applyChanges func(map[string][]byte) error 53 } 54 55 func newMockServer(impl mockServerImpl) *mockServer { 56 return &mockServer{impl: impl} 57 } 58 59 func (s *mockServer) GetOriginalwd() string { 60 if s.impl.getOriginalwd != nil { 61 return s.impl.getOriginalwd() 62 } 63 return "" 64 } 65 66 func (s *mockServer) GetModulePath() []string { 67 if s.impl.getModulePath != nil { 68 return s.impl.getModulePath() 69 } 70 return []string{} 71 } 72 73 func (s *mockServer) GetModuleContent(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 74 if s.impl.getModuleContent != nil { 75 return s.impl.getModuleContent(schema, opts) 76 } 77 return &hclext.BodyContent{}, hcl.Diagnostics{} 78 } 79 80 func (s *mockServer) GetFile(filename string) (*hcl.File, error) { 81 if s.impl.getFile != nil { 82 return s.impl.getFile(filename) 83 } 84 return nil, nil 85 } 86 87 func (s *mockServer) GetFiles(tflint.ModuleCtxType) map[string][]byte { 88 if s.impl.getFiles != nil { 89 return s.impl.getFiles() 90 } 91 return map[string][]byte{} 92 } 93 94 func (s *mockServer) GetRuleConfigContent(name string, schema *hclext.BodySchema) (*hclext.BodyContent, map[string][]byte, error) { 95 if s.impl.getRuleConfigContent != nil { 96 return s.impl.getRuleConfigContent(name, schema) 97 } 98 return &hclext.BodyContent{}, map[string][]byte{}, nil 99 } 100 101 func (s *mockServer) EvaluateExpr(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 102 if s.impl.evaluateExpr != nil { 103 return s.impl.evaluateExpr(expr, opts) 104 } 105 return cty.Value{}, nil 106 } 107 108 func (s *mockServer) EmitIssue(rule tflint.Rule, message string, location hcl.Range, fixable bool) (bool, error) { 109 if s.impl.emitIssue != nil { 110 return s.impl.emitIssue(rule, message, location, fixable) 111 } 112 return true, nil 113 } 114 115 func (s *mockServer) ApplyChanges(sources map[string][]byte) error { 116 if s.impl.applyChanges != nil { 117 return s.impl.applyChanges(sources) 118 } 119 return nil 120 } 121 122 // @see https://github.com/google/go-cmp/issues/40 123 var allowAllUnexported = cmp.Exporter(func(reflect.Type) bool { return true }) 124 125 func TestGetOriginalwd(t *testing.T) { 126 tests := []struct { 127 Name string 128 ServerImpl func() string 129 Want string 130 }{ 131 { 132 Name: "get the original working directory", 133 ServerImpl: func() string { 134 return "/work" 135 }, 136 Want: "/work", 137 }, 138 } 139 140 for _, test := range tests { 141 t.Run(test.Name, func(t *testing.T) { 142 client := startTestGRPCServer(t, newMockServer(mockServerImpl{getOriginalwd: test.ServerImpl})) 143 144 got, err := client.GetOriginalwd() 145 if err != nil { 146 t.Fatalf("failed to call GetOriginalwd: %s", err) 147 } 148 if diff := cmp.Diff(got, test.Want); diff != "" { 149 t.Errorf("diff: %s", diff) 150 } 151 }) 152 } 153 } 154 155 func TestGetModulePath(t *testing.T) { 156 tests := []struct { 157 Name string 158 ServerImpl func() []string 159 Want addrs.Module 160 }{ 161 { 162 Name: "get root module path", 163 ServerImpl: func() []string { 164 return []string{} 165 }, 166 Want: nil, 167 }, 168 { 169 Name: "get child module path", 170 ServerImpl: func() []string { 171 return []string{"child1", "child2"} 172 }, 173 Want: []string{"child1", "child2"}, 174 }, 175 } 176 177 for _, test := range tests { 178 t.Run(test.Name, func(t *testing.T) { 179 client := startTestGRPCServer(t, newMockServer(mockServerImpl{getModulePath: test.ServerImpl})) 180 181 got, err := client.GetModulePath() 182 if err != nil { 183 t.Fatalf("failed to call GetModulePath: %s", err) 184 } 185 if diff := cmp.Diff(got, test.Want); diff != "" { 186 t.Errorf("diff: %s", diff) 187 } 188 }) 189 } 190 } 191 192 func TestGetResourceContent(t *testing.T) { 193 // default error check helper 194 neverHappend := func(err error) bool { return err != nil } 195 196 // default getFileImpl function 197 files := map[string][]byte{} 198 fileExists := func() map[string][]byte { 199 return files 200 } 201 202 // test util functions 203 hclFile := func(filename string, code string) *hcl.File { 204 file, diags := hclsyntax.ParseConfig([]byte(code), filename, hcl.InitialPos) 205 if diags.HasErrors() { 206 panic(diags) 207 } 208 files[filename] = file.Bytes 209 return file 210 } 211 jsonFile := func(filename string, code string) *hcl.File { 212 file, diags := json.Parse([]byte(code), filename) 213 if diags.HasErrors() { 214 panic(diags) 215 } 216 files[filename] = file.Bytes 217 return file 218 } 219 220 tests := []struct { 221 Name string 222 Args func() (string, *hclext.BodySchema, *tflint.GetModuleContentOption) 223 ServerImpl func(*hclext.BodySchema, tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) 224 Want func(string, *hclext.BodySchema, *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) 225 ErrCheck func(error) bool 226 }{ 227 { 228 Name: "get HCL content", 229 Args: func() (string, *hclext.BodySchema, *tflint.GetModuleContentOption) { 230 return "aws_instance", &hclext.BodySchema{ 231 Attributes: []hclext.AttributeSchema{{Name: "instance_type"}}, 232 }, nil 233 }, 234 ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 235 file := hclFile("test.tf", ` 236 resource "aws_instance" "foo" { 237 instance_type = "t2.micro" 238 } 239 240 resource "aws_s3_bucket" "bar" { 241 bucket = "test" 242 }`) 243 return hclext.PartialContent(file.Body, schema) 244 }, 245 Want: func(resource string, schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 246 // Removed "aws_s3_bucket" resource 247 file := hclFile("test.tf", ` 248 resource "aws_instance" "foo" { 249 instance_type = "t2.micro" 250 }`) 251 return hclext.Content(file.Body, &hclext.BodySchema{ 252 Blocks: []hclext.BlockSchema{ 253 { 254 Type: "resource", 255 LabelNames: []string{"type", "name"}, 256 Body: &hclext.BodySchema{ 257 Attributes: []hclext.AttributeSchema{{Name: "instance_type"}}, 258 }, 259 }, 260 }, 261 }) 262 }, 263 ErrCheck: neverHappend, 264 }, 265 { 266 Name: "get JSON content", 267 Args: func() (string, *hclext.BodySchema, *tflint.GetModuleContentOption) { 268 return "aws_instance", &hclext.BodySchema{ 269 Attributes: []hclext.AttributeSchema{{Name: "instance_type"}}, 270 }, nil 271 }, 272 ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 273 file := jsonFile("test.tf.json", ` 274 { 275 "resource": { 276 "aws_instance": { 277 "foo": { 278 "instance_type": "t2.micro" 279 } 280 }, 281 "aws_s3_bucket": { 282 "bar": { 283 "bucket": "test" 284 } 285 } 286 } 287 }`) 288 return hclext.PartialContent(file.Body, schema) 289 }, 290 Want: func(resource string, schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 291 // Removed "aws_s3_bucket" resource 292 file := jsonFile("test.tf.json", ` 293 { 294 "resource": { 295 "aws_instance": { 296 "foo": { 297 "instance_type": "t2.micro" 298 } 299 } 300 } 301 }`) 302 return hclext.Content(file.Body, &hclext.BodySchema{ 303 Blocks: []hclext.BlockSchema{ 304 { 305 Type: "resource", 306 LabelNames: []string{"type", "name"}, 307 Body: &hclext.BodySchema{ 308 Attributes: []hclext.AttributeSchema{{Name: "instance_type"}}, 309 }, 310 }, 311 }, 312 }) 313 }, 314 ErrCheck: neverHappend, 315 }, 316 { 317 Name: "get content with options", 318 Args: func() (string, *hclext.BodySchema, *tflint.GetModuleContentOption) { 319 return "aws_instance", &hclext.BodySchema{}, &tflint.GetModuleContentOption{ 320 ModuleCtx: tflint.RootModuleCtxType, 321 } 322 }, 323 ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 324 if opts.ModuleCtx != tflint.RootModuleCtxType { 325 return &hclext.BodyContent{}, hcl.Diagnostics{ 326 &hcl.Diagnostic{Severity: hcl.DiagError, Summary: "unexpected moduleCtx options"}, 327 } 328 } 329 if opts.Hint.ResourceType != "aws_instance" { 330 return &hclext.BodyContent{}, hcl.Diagnostics{ 331 &hcl.Diagnostic{Severity: hcl.DiagError, Summary: "unexpected hint options"}, 332 } 333 } 334 return &hclext.BodyContent{}, hcl.Diagnostics{} 335 }, 336 Want: func(resource string, schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 337 return &hclext.BodyContent{ 338 Attributes: hclext.Attributes{}, 339 Blocks: hclext.Blocks{}, 340 }, hcl.Diagnostics{} 341 }, 342 ErrCheck: neverHappend, 343 }, 344 } 345 346 for _, test := range tests { 347 t.Run(test.Name, func(t *testing.T) { 348 client := startTestGRPCServer(t, newMockServer(mockServerImpl{getModuleContent: test.ServerImpl, getFiles: fileExists})) 349 350 got, err := client.GetResourceContent(test.Args()) 351 if test.ErrCheck(err) { 352 t.Fatalf("failed to call GetResourceContent: %s", err) 353 } 354 want, diags := test.Want(test.Args()) 355 if diags.HasErrors() { 356 t.Fatalf("failed to get want: %d diagsnotics", len(diags)) 357 for _, diag := range diags { 358 t.Logf(" - %s", diag.Error()) 359 } 360 } 361 362 opts := cmp.Options{ 363 cmp.Comparer(func(x, y cty.Value) bool { 364 return x.GoString() == y.GoString() 365 }), 366 cmpopts.EquateEmpty(), 367 allowAllUnexported, 368 } 369 if diff := cmp.Diff(got, want, opts); diff != "" { 370 t.Errorf("diff: %s", diff) 371 } 372 }) 373 } 374 } 375 376 func TestGetProviderContent(t *testing.T) { 377 // default error check helper 378 neverHappend := func(err error) bool { return err != nil } 379 380 // default getFileImpl function 381 files := map[string][]byte{} 382 fileExists := func() map[string][]byte { 383 return files 384 } 385 386 // test util functions 387 hclFile := func(filename string, code string) *hcl.File { 388 file, diags := hclsyntax.ParseConfig([]byte(code), filename, hcl.InitialPos) 389 if diags.HasErrors() { 390 panic(diags) 391 } 392 files[filename] = file.Bytes 393 return file 394 } 395 396 tests := []struct { 397 Name string 398 Args func() (string, *hclext.BodySchema, *tflint.GetModuleContentOption) 399 ServerImpl func(*hclext.BodySchema, tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) 400 Want func(string, *hclext.BodySchema, *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) 401 ErrCheck func(error) bool 402 }{ 403 { 404 Name: "get HCL content", 405 Args: func() (string, *hclext.BodySchema, *tflint.GetModuleContentOption) { 406 return "aws", &hclext.BodySchema{ 407 Attributes: []hclext.AttributeSchema{{Name: "region"}}, 408 }, nil 409 }, 410 ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 411 file := hclFile("test.tf", ` 412 provider "aws" { 413 region = "us-east-1" 414 } 415 416 provider "google" { 417 region = "us-central1" 418 }`) 419 return hclext.PartialContent(file.Body, schema) 420 }, 421 Want: func(resource string, schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 422 // Removed "google" provider 423 file := hclFile("test.tf", ` 424 provider "aws" { 425 region = "us-east-1" 426 }`) 427 return hclext.Content(file.Body, &hclext.BodySchema{ 428 Blocks: []hclext.BlockSchema{ 429 { 430 Type: "provider", 431 LabelNames: []string{"name"}, 432 Body: &hclext.BodySchema{ 433 Attributes: []hclext.AttributeSchema{{Name: "region"}}, 434 }, 435 }, 436 }, 437 }) 438 }, 439 ErrCheck: neverHappend, 440 }, 441 } 442 443 for _, test := range tests { 444 t.Run(test.Name, func(t *testing.T) { 445 client := startTestGRPCServer(t, newMockServer(mockServerImpl{getModuleContent: test.ServerImpl, getFiles: fileExists})) 446 447 got, err := client.GetProviderContent(test.Args()) 448 if test.ErrCheck(err) { 449 t.Fatalf("failed to call GetProviderContent: %s", err) 450 } 451 want, diags := test.Want(test.Args()) 452 if diags.HasErrors() { 453 t.Fatalf("failed to get want: %d diagsnotics", len(diags)) 454 for _, diag := range diags { 455 t.Logf(" - %s", diag.Error()) 456 } 457 } 458 459 opts := cmp.Options{ 460 cmp.Comparer(func(x, y cty.Value) bool { 461 return x.GoString() == y.GoString() 462 }), 463 cmpopts.EquateEmpty(), 464 allowAllUnexported, 465 } 466 if diff := cmp.Diff(got, want, opts); diff != "" { 467 t.Errorf("diff: %s", diff) 468 } 469 }) 470 } 471 } 472 473 func TestGetModuleContent(t *testing.T) { 474 // default error check helper 475 neverHappend := func(err error) bool { return err != nil } 476 477 // default getFileImpl function 478 files := map[string][]byte{} 479 fileExists := func() map[string][]byte { 480 return files 481 } 482 483 // test util functions 484 hclFile := func(filename string, code string) *hcl.File { 485 file, diags := hclsyntax.ParseConfig([]byte(code), filename, hcl.InitialPos) 486 if diags.HasErrors() { 487 panic(diags) 488 } 489 files[filename] = file.Bytes 490 return file 491 } 492 jsonFile := func(filename string, code string) *hcl.File { 493 file, diags := json.Parse([]byte(code), filename) 494 if diags.HasErrors() { 495 panic(diags) 496 } 497 files[filename] = file.Bytes 498 return file 499 } 500 501 tests := []struct { 502 Name string 503 Args func() (*hclext.BodySchema, *tflint.GetModuleContentOption) 504 ServerImpl func(*hclext.BodySchema, tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) 505 Want func(*hclext.BodySchema, *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) 506 ErrCheck func(error) bool 507 }{ 508 { 509 Name: "get HCL content", 510 Args: func() (*hclext.BodySchema, *tflint.GetModuleContentOption) { 511 return &hclext.BodySchema{ 512 Blocks: []hclext.BlockSchema{ 513 { 514 Type: "resource", 515 LabelNames: []string{"type", "name"}, 516 Body: &hclext.BodySchema{ 517 Attributes: []hclext.AttributeSchema{{Name: "instance_type"}}, 518 }, 519 }, 520 }, 521 }, nil 522 }, 523 ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 524 file := hclFile("test.tf", ` 525 resource "aws_instance" "foo" { 526 instance_type = "t2.micro" 527 }`) 528 return hclext.Content(file.Body, schema) 529 }, 530 Want: func(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 531 file := hclFile("test.tf", ` 532 resource "aws_instance" "foo" { 533 instance_type = "t2.micro" 534 }`) 535 return hclext.Content(file.Body, schema) 536 }, 537 ErrCheck: neverHappend, 538 }, 539 { 540 Name: "get JSON content", 541 Args: func() (*hclext.BodySchema, *tflint.GetModuleContentOption) { 542 return &hclext.BodySchema{ 543 Blocks: []hclext.BlockSchema{ 544 { 545 Type: "resource", 546 LabelNames: []string{"type", "name"}, 547 Body: &hclext.BodySchema{ 548 Attributes: []hclext.AttributeSchema{{Name: "instance_type"}}, 549 }, 550 }, 551 }, 552 }, nil 553 }, 554 ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 555 file := jsonFile("test.tf.json", ` 556 { 557 "resource": { 558 "aws_instance": { 559 "foo": { 560 "instance_type": "t2.micro" 561 } 562 } 563 } 564 }`) 565 return hclext.Content(file.Body, schema) 566 }, 567 Want: func(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 568 file := jsonFile("test.tf.json", ` 569 { 570 "resource": { 571 "aws_instance": { 572 "foo": { 573 "instance_type": "t2.micro" 574 } 575 } 576 } 577 }`) 578 return hclext.Content(file.Body, schema) 579 }, 580 ErrCheck: neverHappend, 581 }, 582 { 583 Name: "get content as just attributes", 584 Args: func() (*hclext.BodySchema, *tflint.GetModuleContentOption) { 585 return &hclext.BodySchema{Mode: hclext.SchemaJustAttributesMode}, nil 586 }, 587 ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 588 file := hclFile("test.tf", ` 589 instance_type = "t2.micro" 590 volume_size = 10`) 591 return hclext.Content(file.Body, schema) 592 }, 593 Want: func(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 594 file := hclFile("test.tf", ` 595 instance_type = "t2.micro" 596 volume_size = 10`) 597 return hclext.Content(file.Body, schema) 598 }, 599 ErrCheck: neverHappend, 600 }, 601 { 602 Name: "get content with bound expr", 603 Args: func() (*hclext.BodySchema, *tflint.GetModuleContentOption) { 604 return &hclext.BodySchema{ 605 Attributes: []hclext.AttributeSchema{{Name: "value"}}, 606 }, nil 607 }, 608 ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 609 file := hclFile("test.tf", "value = each.key") 610 attrs, diags := file.Body.JustAttributes() 611 if diags.HasErrors() { 612 return nil, diags 613 } 614 attr := attrs["value"] 615 616 return &hclext.BodyContent{ 617 Attributes: hclext.Attributes{ 618 "value": { 619 Name: attr.Name, 620 Expr: hclext.BindValue(cty.StringVal("bound value"), attr.Expr), 621 Range: attr.Range, 622 NameRange: attr.NameRange, 623 }, 624 }, 625 Blocks: hclext.Blocks{}, 626 }, nil 627 }, 628 Want: func(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 629 file := hclFile("test.tf", "value = each.key") 630 attrs, diags := file.Body.JustAttributes() 631 if diags.HasErrors() { 632 return nil, diags 633 } 634 attr := attrs["value"] 635 636 return &hclext.BodyContent{ 637 Attributes: hclext.Attributes{ 638 "value": { 639 Name: attr.Name, 640 Expr: hclext.BindValue(cty.StringVal("bound value"), attr.Expr), 641 Range: attr.Range, 642 NameRange: attr.NameRange, 643 }, 644 }, 645 Blocks: hclext.Blocks{}, 646 }, nil 647 }, 648 ErrCheck: neverHappend, 649 }, 650 { 651 Name: "get content with options", 652 Args: func() (*hclext.BodySchema, *tflint.GetModuleContentOption) { 653 return &hclext.BodySchema{}, &tflint.GetModuleContentOption{ 654 ModuleCtx: tflint.RootModuleCtxType, 655 ExpandMode: tflint.ExpandModeNone, 656 Hint: tflint.GetModuleContentHint{ResourceType: "aws_instance"}, 657 } 658 }, 659 ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 660 if opts.ModuleCtx != tflint.RootModuleCtxType { 661 return &hclext.BodyContent{}, hcl.Diagnostics{ 662 &hcl.Diagnostic{Severity: hcl.DiagError, Summary: "unexpected moduleCtx options"}, 663 } 664 } 665 if opts.ExpandMode != tflint.ExpandModeNone { 666 return &hclext.BodyContent{}, hcl.Diagnostics{ 667 &hcl.Diagnostic{Severity: hcl.DiagError, Summary: "unexpected expand mode options"}, 668 } 669 } 670 if opts.Hint.ResourceType != "aws_instance" { 671 return &hclext.BodyContent{}, hcl.Diagnostics{ 672 &hcl.Diagnostic{Severity: hcl.DiagError, Summary: "unexpected hint options"}, 673 } 674 } 675 return &hclext.BodyContent{}, hcl.Diagnostics{} 676 }, 677 Want: func(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 678 return &hclext.BodyContent{ 679 Attributes: hclext.Attributes{}, 680 Blocks: hclext.Blocks{}, 681 }, hcl.Diagnostics{} 682 }, 683 ErrCheck: neverHappend, 684 }, 685 { 686 Name: "schema is null", 687 Args: func() (*hclext.BodySchema, *tflint.GetModuleContentOption) { 688 return nil, nil 689 }, 690 Want: func(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 691 return &hclext.BodyContent{ 692 Attributes: hclext.Attributes{}, 693 Blocks: hclext.Blocks{}, 694 }, hcl.Diagnostics{} 695 }, 696 ErrCheck: neverHappend, 697 }, 698 { 699 Name: "server returns an error", 700 Args: func() (*hclext.BodySchema, *tflint.GetModuleContentOption) { 701 return &hclext.BodySchema{}, nil 702 }, 703 ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 704 return &hclext.BodyContent{}, hcl.Diagnostics{ 705 &hcl.Diagnostic{ 706 Severity: hcl.DiagError, 707 Summary: "unexpected error", 708 Detail: "unexpected error occurred", 709 Subject: &hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 1, Column: 1}, End: hcl.Pos{Line: 1, Column: 5}}, 710 }, 711 } 712 }, 713 Want: func(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 714 return nil, hcl.Diagnostics{} 715 }, 716 ErrCheck: func(err error) bool { 717 return err == nil || err.Error() != "test.tf:1,1-5: unexpected error; unexpected error occurred" 718 }, 719 }, 720 { 721 Name: "response body is empty", 722 Args: func() (*hclext.BodySchema, *tflint.GetModuleContentOption) { 723 return &hclext.BodySchema{}, nil 724 }, 725 ServerImpl: func(schema *hclext.BodySchema, opts tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 726 return nil, hcl.Diagnostics{} 727 }, 728 Want: func(schema *hclext.BodySchema, opts *tflint.GetModuleContentOption) (*hclext.BodyContent, hcl.Diagnostics) { 729 return nil, hcl.Diagnostics{} 730 }, 731 ErrCheck: func(err error) bool { 732 return err == nil || err.Error() != "response body is empty" 733 }, 734 }, 735 } 736 737 for _, test := range tests { 738 t.Run(test.Name, func(t *testing.T) { 739 client := startTestGRPCServer(t, newMockServer(mockServerImpl{getModuleContent: test.ServerImpl, getFiles: fileExists})) 740 741 got, err := client.GetModuleContent(test.Args()) 742 if test.ErrCheck(err) { 743 t.Fatalf("failed to call GetModuleContent: %s", err) 744 } 745 want, diags := test.Want(test.Args()) 746 if diags.HasErrors() { 747 t.Fatalf("failed to get want: %d diagsnotics", len(diags)) 748 for _, diag := range diags { 749 t.Logf(" - %s", diag.Error()) 750 } 751 } 752 753 opts := cmp.Options{ 754 cmp.Comparer(func(x, y cty.Value) bool { 755 return x.GoString() == y.GoString() 756 }), 757 allowAllUnexported, 758 } 759 if diff := cmp.Diff(got, want, opts); diff != "" { 760 t.Errorf("diff: %s", diff) 761 } 762 }) 763 } 764 } 765 766 func TestGetFile(t *testing.T) { 767 // default error check helper 768 neverHappend := func(err error) bool { return err != nil } 769 770 // test util functions 771 hclFile := func(filename string, code string) (*hcl.File, error) { 772 file, diags := hclsyntax.ParseConfig([]byte(code), filename, hcl.InitialPos) 773 if diags.HasErrors() { 774 return nil, diags 775 } 776 return file, nil 777 } 778 jsonFile := func(filename string, code string) (*hcl.File, error) { 779 file, diags := json.Parse([]byte(code), filename) 780 if diags.HasErrors() { 781 return nil, diags 782 } 783 return file, nil 784 } 785 786 tests := []struct { 787 Name string 788 Arg string 789 ServerImpl func(string) (*hcl.File, error) 790 Want string 791 ErrCheck func(error) bool 792 }{ 793 { 794 Name: "HCL file exists", 795 Arg: "test.tf", 796 ServerImpl: func(filename string) (*hcl.File, error) { 797 if filename != "test.tf" { 798 return nil, nil 799 } 800 return hclFile(filename, ` 801 resource "aws_instance" "foo" { 802 instance_type = "t2.micro" 803 }`) 804 }, 805 Want: ` 806 resource "aws_instance" "foo" { 807 instance_type = "t2.micro" 808 }`, 809 ErrCheck: neverHappend, 810 }, 811 { 812 Name: "JSON file exists", 813 Arg: "test.tf.json", 814 ServerImpl: func(filename string) (*hcl.File, error) { 815 if filename != "test.tf.json" { 816 return nil, nil 817 } 818 return jsonFile(filename, ` 819 { 820 "resource": { 821 "aws_instance": { 822 "foo": { 823 "instance_type": "t2.micro" 824 } 825 } 826 } 827 }`) 828 }, 829 Want: ` 830 { 831 "resource": { 832 "aws_instance": { 833 "foo": { 834 "instance_type": "t2.micro" 835 } 836 } 837 } 838 }`, 839 ErrCheck: neverHappend, 840 }, 841 { 842 Name: "file not found", 843 Arg: "test.tf", 844 ServerImpl: func(filename string) (*hcl.File, error) { 845 return nil, nil 846 }, 847 ErrCheck: func(err error) bool { 848 return err == nil || err.Error() != "file not found" 849 }, 850 }, 851 { 852 Name: "server returns an error", 853 Arg: "test.tf", 854 ServerImpl: func(filename string) (*hcl.File, error) { 855 if filename != "test.tf" { 856 return nil, nil 857 } 858 return nil, errors.New("unexpected error") 859 }, 860 ErrCheck: func(err error) bool { 861 return err == nil || err.Error() != "unexpected error" 862 }, 863 }, 864 { 865 Name: "filename is empty", 866 Arg: "", 867 ErrCheck: func(err error) bool { 868 return err == nil || err.Error() != "name should not be empty" 869 }, 870 }, 871 } 872 873 for _, test := range tests { 874 t.Run(test.Name, func(t *testing.T) { 875 client := startTestGRPCServer(t, newMockServer(mockServerImpl{getFile: test.ServerImpl})) 876 877 file, err := client.GetFile(test.Arg) 878 if test.ErrCheck(err) { 879 t.Fatalf("failed to call GetFile: %s", err) 880 } 881 882 var got string 883 if file != nil { 884 got = string(file.Bytes) 885 } 886 887 if got != test.Want { 888 t.Errorf("got: %s", got) 889 } 890 }) 891 } 892 } 893 894 func TestGetFiles(t *testing.T) { 895 // default error check helper 896 neverHappend := func(err error) bool { return err != nil } 897 898 // test util functions 899 hclFile := func(filename string, code string) *hcl.File { 900 file, diags := hclsyntax.ParseConfig([]byte(code), filename, hcl.InitialPos) 901 if diags.HasErrors() { 902 panic(diags) 903 } 904 return file 905 } 906 jsonFile := func(filename string, code string) *hcl.File { 907 file, diags := json.Parse([]byte(code), filename) 908 if diags.HasErrors() { 909 panic(diags) 910 } 911 return file 912 } 913 914 tests := []struct { 915 Name string 916 ServerImpl func() map[string][]byte 917 Want map[string]*hcl.File 918 ErrCheck func(error) bool 919 }{ 920 { 921 Name: "HCL files", 922 ServerImpl: func() map[string][]byte { 923 return map[string][]byte{ 924 "test1.tf": []byte(` 925 resource "aws_instance" "foo" { 926 instance_type = "t2.micro" 927 }`), 928 "test2.tf": []byte(` 929 resource "aws_s3_bucket" "bar" { 930 bucket = "baz" 931 }`), 932 } 933 }, 934 Want: map[string]*hcl.File{ 935 "test1.tf": hclFile("test1.tf", ` 936 resource "aws_instance" "foo" { 937 instance_type = "t2.micro" 938 }`), 939 "test2.tf": hclFile("test2.tf", ` 940 resource "aws_s3_bucket" "bar" { 941 bucket = "baz" 942 }`), 943 }, 944 ErrCheck: neverHappend, 945 }, 946 { 947 Name: "JSON files", 948 ServerImpl: func() map[string][]byte { 949 return map[string][]byte{ 950 "test1.tf.json": []byte(` 951 { 952 "resource": { 953 "aws_instance": { 954 "foo": { 955 "instance_type": "t2.micro" 956 } 957 } 958 } 959 }`), 960 "test2.tf.json": []byte(` 961 { 962 "resource": { 963 "aws_s3_bucket": { 964 "bar": { 965 "bucket": "baz" 966 } 967 } 968 } 969 }`), 970 } 971 }, 972 Want: map[string]*hcl.File{ 973 "test1.tf.json": jsonFile("test1.tf.json", ` 974 { 975 "resource": { 976 "aws_instance": { 977 "foo": { 978 "instance_type": "t2.micro" 979 } 980 } 981 } 982 }`), 983 "test2.tf.json": jsonFile("test2.tf.json", ` 984 { 985 "resource": { 986 "aws_s3_bucket": { 987 "bar": { 988 "bucket": "baz" 989 } 990 } 991 } 992 }`), 993 }, 994 ErrCheck: neverHappend, 995 }, 996 } 997 998 for _, test := range tests { 999 t.Run(test.Name, func(t *testing.T) { 1000 client := startTestGRPCServer(t, newMockServer(mockServerImpl{getFiles: test.ServerImpl})) 1001 1002 files, err := client.GetFiles() 1003 if test.ErrCheck(err) { 1004 t.Fatalf("failed to call GetFiles: %s", err) 1005 } 1006 1007 opts := cmp.Options{ 1008 cmp.Comparer(func(x, y cty.Value) bool { 1009 return x.GoString() == y.GoString() 1010 }), 1011 cmp.AllowUnexported(hclsyntax.Body{}), 1012 cmpopts.IgnoreFields(hcl.File{}, "Nav"), 1013 allowAllUnexported, 1014 } 1015 if diff := cmp.Diff(files, test.Want, opts); diff != "" { 1016 t.Errorf("diff: %s", diff) 1017 } 1018 }) 1019 } 1020 } 1021 1022 func TestWalkExpressions(t *testing.T) { 1023 tests := []struct { 1024 name string 1025 files map[string][]byte 1026 walked []hcl.Range 1027 }{ 1028 { 1029 name: "resource", 1030 files: map[string][]byte{ 1031 "resource.tf": []byte(` 1032 resource "null_resource" "test" { 1033 key = "foo" 1034 }`), 1035 }, 1036 walked: []hcl.Range{ 1037 {Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}}, 1038 {Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}}, 1039 }, 1040 }, 1041 { 1042 name: "data source", 1043 files: map[string][]byte{ 1044 "data.tf": []byte(` 1045 data "null_dataresource" "test" { 1046 key = "foo" 1047 }`), 1048 }, 1049 walked: []hcl.Range{ 1050 {Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}}, 1051 {Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}}, 1052 }, 1053 }, 1054 { 1055 name: "module call", 1056 files: map[string][]byte{ 1057 "module.tf": []byte(` 1058 module "m" { 1059 source = "./module" 1060 key = "foo" 1061 }`), 1062 }, 1063 walked: []hcl.Range{ 1064 {Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 22}}, 1065 {Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 21}}, 1066 {Start: hcl.Pos{Line: 4, Column: 12}, End: hcl.Pos{Line: 4, Column: 17}}, 1067 {Start: hcl.Pos{Line: 4, Column: 13}, End: hcl.Pos{Line: 4, Column: 16}}, 1068 }, 1069 }, 1070 { 1071 name: "provider config", 1072 files: map[string][]byte{ 1073 "provider.tf": []byte(` 1074 provider "p" { 1075 key = "foo" 1076 }`), 1077 }, 1078 walked: []hcl.Range{ 1079 {Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}}, 1080 {Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}}, 1081 }, 1082 }, 1083 { 1084 name: "locals", 1085 files: map[string][]byte{ 1086 "locals.tf": []byte(` 1087 locals { 1088 key = "foo" 1089 }`), 1090 }, 1091 walked: []hcl.Range{ 1092 {Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}}, 1093 {Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}}, 1094 }, 1095 }, 1096 { 1097 name: "output", 1098 files: map[string][]byte{ 1099 "output.tf": []byte(` 1100 output "o" { 1101 value = "foo" 1102 }`), 1103 }, 1104 walked: []hcl.Range{ 1105 {Start: hcl.Pos{Line: 3, Column: 11}, End: hcl.Pos{Line: 3, Column: 16}}, 1106 {Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 15}}, 1107 }, 1108 }, 1109 { 1110 name: "resource with block", 1111 files: map[string][]byte{ 1112 "resource.tf": []byte(` 1113 resource "null_resource" "test" { 1114 key = "foo" 1115 1116 lifecycle { 1117 ignore_changes = [key] 1118 } 1119 }`), 1120 }, 1121 walked: []hcl.Range{ 1122 {Start: hcl.Pos{Line: 3, Column: 9}, End: hcl.Pos{Line: 3, Column: 14}}, 1123 {Start: hcl.Pos{Line: 3, Column: 10}, End: hcl.Pos{Line: 3, Column: 13}}, 1124 {Start: hcl.Pos{Line: 6, Column: 22}, End: hcl.Pos{Line: 6, Column: 27}}, 1125 {Start: hcl.Pos{Line: 6, Column: 23}, End: hcl.Pos{Line: 6, Column: 26}}, 1126 }, 1127 }, 1128 { 1129 name: "resource json", 1130 files: map[string][]byte{ 1131 "resource.tf.json": []byte(` 1132 { 1133 "resource": { 1134 "null_resource": { 1135 "test": { 1136 "key": "foo", 1137 "nested": { 1138 "key": "foo" 1139 }, 1140 "list": [{ 1141 "key": "foo" 1142 }] 1143 } 1144 } 1145 } 1146 }`), 1147 }, 1148 walked: []hcl.Range{ 1149 {Start: hcl.Pos{Line: 3, Column: 15}, End: hcl.Pos{Line: 15, Column: 4}}, 1150 }, 1151 }, 1152 { 1153 name: "multiple files", 1154 files: map[string][]byte{ 1155 "main.tf": []byte(` 1156 provider "aws" { 1157 region = "us-east-1" 1158 1159 assume_role { 1160 role_arn = "arn:aws:iam::123412341234:role/ExampleRole" 1161 } 1162 }`), 1163 "main_override.tf": []byte(` 1164 provider "aws" { 1165 region = "us-east-1" 1166 1167 assume_role { 1168 role_arn = null 1169 } 1170 }`), 1171 }, 1172 walked: []hcl.Range{ 1173 {Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 23}, Filename: "main.tf"}, 1174 {Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 22}, Filename: "main.tf"}, 1175 {Start: hcl.Pos{Line: 6, Column: 16}, End: hcl.Pos{Line: 6, Column: 60}, Filename: "main.tf"}, 1176 {Start: hcl.Pos{Line: 6, Column: 17}, End: hcl.Pos{Line: 6, Column: 59}, Filename: "main.tf"}, 1177 {Start: hcl.Pos{Line: 3, Column: 12}, End: hcl.Pos{Line: 3, Column: 23}, Filename: "main_override.tf"}, 1178 {Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 22}, Filename: "main_override.tf"}, 1179 {Start: hcl.Pos{Line: 6, Column: 16}, End: hcl.Pos{Line: 6, Column: 20}, Filename: "main_override.tf"}, 1180 }, 1181 }, 1182 { 1183 name: "nested attributes", 1184 files: map[string][]byte{ 1185 "data.tf": []byte(` 1186 data "terraform_remote_state" "remote_state" { 1187 backend = "remote" 1188 1189 config = { 1190 organization = "Organization" 1191 workspaces = { 1192 name = "${var.environment}" 1193 } 1194 } 1195 }`), 1196 }, 1197 walked: []hcl.Range{ 1198 {Start: hcl.Pos{Line: 3, Column: 13}, End: hcl.Pos{Line: 3, Column: 21}}, 1199 {Start: hcl.Pos{Line: 3, Column: 14}, End: hcl.Pos{Line: 3, Column: 20}}, 1200 {Start: hcl.Pos{Line: 5, Column: 12}, End: hcl.Pos{Line: 10, Column: 4}}, 1201 {Start: hcl.Pos{Line: 6, Column: 5}, End: hcl.Pos{Line: 6, Column: 17}}, 1202 {Start: hcl.Pos{Line: 6, Column: 20}, End: hcl.Pos{Line: 6, Column: 34}}, 1203 {Start: hcl.Pos{Line: 6, Column: 21}, End: hcl.Pos{Line: 6, Column: 33}}, 1204 {Start: hcl.Pos{Line: 7, Column: 5}, End: hcl.Pos{Line: 7, Column: 15}}, 1205 {Start: hcl.Pos{Line: 7, Column: 18}, End: hcl.Pos{Line: 9, Column: 6}}, 1206 {Start: hcl.Pos{Line: 8, Column: 7}, End: hcl.Pos{Line: 8, Column: 11}}, 1207 {Start: hcl.Pos{Line: 8, Column: 14}, End: hcl.Pos{Line: 8, Column: 34}}, 1208 {Start: hcl.Pos{Line: 8, Column: 17}, End: hcl.Pos{Line: 8, Column: 32}}, 1209 }, 1210 }, 1211 } 1212 1213 for _, test := range tests { 1214 t.Run(test.name, func(t *testing.T) { 1215 getFilesImpl := func() map[string][]byte { return test.files } 1216 client := startTestGRPCServer(t, newMockServer(mockServerImpl{getFiles: getFilesImpl})) 1217 1218 walked := []hcl.Range{} 1219 diags := client.WalkExpressions(tflint.ExprWalkFunc(func(expr hcl.Expression) hcl.Diagnostics { 1220 walked = append(walked, expr.Range()) 1221 return nil 1222 })) 1223 if diags.HasErrors() { 1224 t.Fatal(diags) 1225 } 1226 opts := cmp.Options{ 1227 cmpopts.IgnoreFields(hcl.Range{}, "Filename"), 1228 cmpopts.IgnoreFields(hcl.Pos{}, "Byte"), 1229 cmpopts.SortSlices(func(x, y hcl.Range) bool { return x.String() > y.String() }), 1230 } 1231 if diff := cmp.Diff(walked, test.walked, opts); diff != "" { 1232 t.Error(diff) 1233 } 1234 }) 1235 } 1236 } 1237 1238 func TestDecodeRuleConfig(t *testing.T) { 1239 // default error check helper 1240 neverHappend := func(err error) bool { return err != nil } 1241 1242 // test struct for decoding 1243 type ruleConfig struct { 1244 Name string `hclext:"name"` 1245 } 1246 1247 tests := []struct { 1248 Name string 1249 RuleName string 1250 Target interface{} 1251 ServerImpl func(string, *hclext.BodySchema) (*hclext.BodyContent, map[string][]byte, error) 1252 Want interface{} 1253 ErrCheck func(error) bool 1254 }{ 1255 { 1256 Name: "decode to struct", 1257 RuleName: "test_rule", 1258 Target: &ruleConfig{}, 1259 ServerImpl: func(name string, schema *hclext.BodySchema) (*hclext.BodyContent, map[string][]byte, error) { 1260 if name != "test_rule" { 1261 return &hclext.BodyContent{}, map[string][]byte{}, errors.New("unexpected file name") 1262 } 1263 1264 sources := map[string][]byte{ 1265 ".tflint.hcl": []byte(` 1266 rule "test_rule" { 1267 name = "foo" 1268 }`), 1269 } 1270 1271 file, diags := hclsyntax.ParseConfig(sources[".tflint.hcl"], ".tflint.hcl", hcl.InitialPos) 1272 if diags.HasErrors() { 1273 return &hclext.BodyContent{}, sources, diags 1274 } 1275 1276 content, diags := file.Body.Content(&hcl.BodySchema{ 1277 Blocks: []hcl.BlockHeaderSchema{{Type: "rule", LabelNames: []string{"name"}}}, 1278 }) 1279 if diags.HasErrors() { 1280 return &hclext.BodyContent{}, sources, diags 1281 } 1282 1283 body, diags := hclext.Content(content.Blocks[0].Body, schema) 1284 if diags.HasErrors() { 1285 return &hclext.BodyContent{}, sources, diags 1286 } 1287 return body, sources, nil 1288 }, 1289 Want: &ruleConfig{Name: "foo"}, 1290 ErrCheck: neverHappend, 1291 }, 1292 { 1293 Name: "server returns an error", 1294 RuleName: "test_rule", 1295 Target: &ruleConfig{}, 1296 ServerImpl: func(name string, schema *hclext.BodySchema) (*hclext.BodyContent, map[string][]byte, error) { 1297 return nil, map[string][]byte{}, errors.New("unexpected error") 1298 }, 1299 Want: &ruleConfig{}, 1300 ErrCheck: func(err error) bool { 1301 return err == nil || err.Error() != "unexpected error" 1302 }, 1303 }, 1304 { 1305 Name: "response body is empty", 1306 RuleName: "test_rule", 1307 Target: &ruleConfig{}, 1308 ServerImpl: func(name string, schema *hclext.BodySchema) (*hclext.BodyContent, map[string][]byte, error) { 1309 return nil, map[string][]byte{}, nil 1310 }, 1311 Want: &ruleConfig{}, 1312 ErrCheck: func(err error) bool { 1313 return err == nil || err.Error() != "response body is empty" 1314 }, 1315 }, 1316 { 1317 Name: "config not found", 1318 RuleName: "not_found", 1319 Target: &ruleConfig{}, 1320 ServerImpl: func(name string, schema *hclext.BodySchema) (*hclext.BodyContent, map[string][]byte, error) { 1321 return &hclext.BodyContent{}, nil, nil 1322 }, 1323 Want: &ruleConfig{}, 1324 ErrCheck: neverHappend, 1325 }, 1326 { 1327 Name: "config not found with non-empty config", 1328 RuleName: "not_found", 1329 Target: &ruleConfig{}, 1330 ServerImpl: func(name string, schema *hclext.BodySchema) (*hclext.BodyContent, map[string][]byte, error) { 1331 return &hclext.BodyContent{Attributes: hclext.Attributes{"foo": &hclext.Attribute{}}}, nil, nil 1332 }, 1333 Want: &ruleConfig{}, 1334 ErrCheck: func(err error) bool { 1335 return err == nil || err.Error() != "config file not found" 1336 }, 1337 }, 1338 { 1339 Name: "name is empty", 1340 RuleName: "", 1341 Target: &ruleConfig{}, 1342 Want: &ruleConfig{}, 1343 ErrCheck: func(err error) bool { 1344 return err == nil || err.Error() != "name should not be empty" 1345 }, 1346 }, 1347 } 1348 1349 for _, test := range tests { 1350 t.Run(test.Name, func(t *testing.T) { 1351 client := startTestGRPCServer(t, newMockServer(mockServerImpl{getRuleConfigContent: test.ServerImpl})) 1352 1353 err := client.DecodeRuleConfig(test.RuleName, test.Target) 1354 if test.ErrCheck(err) { 1355 t.Fatalf("failed to call DecodeRuleConfig: %s", err) 1356 } 1357 1358 if diff := cmp.Diff(test.Target, test.Want); diff != "" { 1359 t.Errorf("diff: %s", diff) 1360 } 1361 }) 1362 } 1363 } 1364 1365 func TestEvaluateExpr(t *testing.T) { 1366 // default error check helper 1367 neverHappend := func(err error) bool { return err != nil } 1368 1369 // default getFileImpl function 1370 fileIdx := 1 1371 files := map[string]*hcl.File{} 1372 fileExists := func(filename string) (*hcl.File, error) { 1373 return files[filename], nil 1374 } 1375 1376 // test util functions 1377 hclExpr := func(expr string) hcl.Expression { 1378 filename := fmt.Sprintf("test%d.tf", fileIdx) 1379 file, diags := hclsyntax.ParseConfig([]byte(fmt.Sprintf(`expr = %s`, expr)), filename, hcl.InitialPos) 1380 if diags.HasErrors() { 1381 panic(diags) 1382 } 1383 attributes, diags := file.Body.JustAttributes() 1384 if diags.HasErrors() { 1385 panic(diags) 1386 } 1387 files[filename] = file 1388 fileIdx = fileIdx + 1 1389 return attributes["expr"].Expr 1390 } 1391 jsonExpr := func(expr string) hcl.Expression { 1392 filename := fmt.Sprintf("test%d.tf.json", fileIdx) 1393 file, diags := json.Parse([]byte(fmt.Sprintf(`{"expr": %s}`, expr)), filename) 1394 if diags.HasErrors() { 1395 panic(diags) 1396 } 1397 attributes, diags := file.Body.JustAttributes() 1398 if diags.HasErrors() { 1399 panic(diags) 1400 } 1401 files[filename] = file 1402 fileIdx = fileIdx + 1 1403 return attributes["expr"].Expr 1404 } 1405 evalExpr := func(expr hcl.Expression, ctx *hcl.EvalContext) (cty.Value, error) { 1406 val, diags := expr.Value(ctx) 1407 if diags.HasErrors() { 1408 return cty.Value{}, diags 1409 } 1410 return val, nil 1411 } 1412 1413 // test struct for decoding from cty.Value 1414 type Object struct { 1415 Name string `cty:"name"` 1416 Enabled bool `cty:"enabled"` 1417 } 1418 objectTy := cty.Object(map[string]cty.Type{"name": cty.String, "enabled": cty.Bool}) 1419 1420 tests := []struct { 1421 Name string 1422 Expr hcl.Expression 1423 TargetType reflect.Type 1424 Option *tflint.EvaluateExprOption 1425 ServerImpl func(hcl.Expression, tflint.EvaluateExprOption) (cty.Value, error) 1426 GetFileImpl func(string) (*hcl.File, error) 1427 Want interface{} 1428 ErrCheck func(err error) bool 1429 }{ 1430 { 1431 Name: "literal", 1432 Expr: hclExpr(`"foo"`), 1433 TargetType: reflect.TypeOf(""), 1434 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1435 if *opts.WantType != cty.String { 1436 return cty.Value{}, errors.New("wantType should be string") 1437 } 1438 if opts.ModuleCtx != tflint.SelfModuleCtxType { 1439 return cty.Value{}, errors.New("moduleCtx should be self") 1440 } 1441 return evalExpr(expr, nil) 1442 }, 1443 Want: "foo", 1444 GetFileImpl: fileExists, 1445 ErrCheck: neverHappend, 1446 }, 1447 { 1448 Name: "string variable", 1449 Expr: hclExpr(`var.foo`), 1450 TargetType: reflect.TypeOf(""), 1451 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1452 return evalExpr(expr, &hcl.EvalContext{ 1453 Variables: map[string]cty.Value{ 1454 "var": cty.MapVal(map[string]cty.Value{ 1455 "foo": cty.StringVal("bar"), 1456 }), 1457 }, 1458 }) 1459 }, 1460 Want: "bar", 1461 GetFileImpl: fileExists, 1462 ErrCheck: neverHappend, 1463 }, 1464 { 1465 Name: "number variable", 1466 Expr: hclExpr(`var.foo`), 1467 TargetType: reflect.TypeOf(0), 1468 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1469 if *opts.WantType != cty.Number { 1470 return cty.Value{}, errors.New("wantType should be number") 1471 } 1472 return evalExpr(expr, &hcl.EvalContext{ 1473 Variables: map[string]cty.Value{ 1474 "var": cty.MapVal(map[string]cty.Value{ 1475 "foo": cty.NumberIntVal(7), 1476 }), 1477 }, 1478 }) 1479 }, 1480 Want: 7, 1481 GetFileImpl: fileExists, 1482 ErrCheck: neverHappend, 1483 }, 1484 { 1485 Name: "bool variable", 1486 Expr: hclExpr(`var.foo`), 1487 TargetType: reflect.TypeOf(true), 1488 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1489 if *opts.WantType != cty.Bool { 1490 return cty.Value{}, errors.New("wantType should be bool") 1491 } 1492 return evalExpr(expr, &hcl.EvalContext{ 1493 Variables: map[string]cty.Value{ 1494 "var": cty.MapVal(map[string]cty.Value{ 1495 "foo": cty.BoolVal(true), 1496 }), 1497 }, 1498 }) 1499 }, 1500 Want: true, 1501 GetFileImpl: fileExists, 1502 ErrCheck: neverHappend, 1503 }, 1504 { 1505 Name: "string list variable", 1506 Expr: hclExpr(`var.foo`), 1507 TargetType: reflect.TypeOf([]string{}), 1508 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1509 if *opts.WantType != cty.List(cty.String) { 1510 return cty.Value{}, errors.New("wantType should be string list") 1511 } 1512 return evalExpr(expr, &hcl.EvalContext{ 1513 Variables: map[string]cty.Value{ 1514 "var": cty.MapVal(map[string]cty.Value{ 1515 "foo": cty.ListVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}), 1516 }), 1517 }, 1518 }) 1519 }, 1520 Want: []string{"foo", "bar"}, 1521 GetFileImpl: fileExists, 1522 ErrCheck: neverHappend, 1523 }, 1524 { 1525 Name: "number list variable", 1526 Expr: hclExpr(`var.foo`), 1527 TargetType: reflect.TypeOf([]int{}), 1528 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1529 if *opts.WantType != cty.List(cty.Number) { 1530 return cty.Value{}, errors.New("wantType should be number list") 1531 } 1532 return evalExpr(expr, &hcl.EvalContext{ 1533 Variables: map[string]cty.Value{ 1534 "var": cty.MapVal(map[string]cty.Value{ 1535 "foo": cty.ListVal([]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(2)}), 1536 }), 1537 }, 1538 }) 1539 }, 1540 Want: []int{1, 2}, 1541 GetFileImpl: fileExists, 1542 ErrCheck: neverHappend, 1543 }, 1544 { 1545 Name: "bool list variable", 1546 Expr: hclExpr(`var.foo`), 1547 TargetType: reflect.TypeOf([]bool{}), 1548 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1549 if *opts.WantType != cty.List(cty.Bool) { 1550 return cty.Value{}, errors.New("wantType should be bool list") 1551 } 1552 return evalExpr(expr, &hcl.EvalContext{ 1553 Variables: map[string]cty.Value{ 1554 "var": cty.MapVal(map[string]cty.Value{ 1555 "foo": cty.ListVal([]cty.Value{cty.BoolVal(true), cty.BoolVal(false)}), 1556 }), 1557 }, 1558 }) 1559 }, 1560 Want: []bool{true, false}, 1561 GetFileImpl: fileExists, 1562 ErrCheck: neverHappend, 1563 }, 1564 { 1565 Name: "string map variable", 1566 Expr: hclExpr(`var.foo`), 1567 TargetType: reflect.TypeOf(map[string]string{}), 1568 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1569 if *opts.WantType != cty.Map(cty.String) { 1570 return cty.Value{}, errors.New("wantType should be string map") 1571 } 1572 return evalExpr(expr, &hcl.EvalContext{ 1573 Variables: map[string]cty.Value{ 1574 "var": cty.MapVal(map[string]cty.Value{ 1575 "foo": cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("bar"), "baz": cty.StringVal("qux")}), 1576 }), 1577 }, 1578 }) 1579 }, 1580 Want: map[string]string{"foo": "bar", "baz": "qux"}, 1581 GetFileImpl: fileExists, 1582 ErrCheck: neverHappend, 1583 }, 1584 { 1585 Name: "number map variable", 1586 Expr: hclExpr(`var.foo`), 1587 TargetType: reflect.TypeOf(map[string]int{}), 1588 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1589 if *opts.WantType != cty.Map(cty.Number) { 1590 return cty.Value{}, errors.New("wantType should be number map") 1591 } 1592 return evalExpr(expr, &hcl.EvalContext{ 1593 Variables: map[string]cty.Value{ 1594 "var": cty.MapVal(map[string]cty.Value{ 1595 "foo": cty.MapVal(map[string]cty.Value{"foo": cty.NumberIntVal(1), "bar": cty.NumberIntVal(2)}), 1596 }), 1597 }, 1598 }) 1599 }, 1600 Want: map[string]int{"foo": 1, "bar": 2}, 1601 GetFileImpl: fileExists, 1602 ErrCheck: neverHappend, 1603 }, 1604 { 1605 Name: "bool map variable", 1606 Expr: hclExpr(`var.foo`), 1607 TargetType: reflect.TypeOf(map[string]bool{}), 1608 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1609 if *opts.WantType != cty.Map(cty.Bool) { 1610 return cty.Value{}, errors.New("wantType should be bool map") 1611 } 1612 return evalExpr(expr, &hcl.EvalContext{ 1613 Variables: map[string]cty.Value{ 1614 "var": cty.MapVal(map[string]cty.Value{ 1615 "foo": cty.MapVal(map[string]cty.Value{"foo": cty.BoolVal(true), "bar": cty.BoolVal(false)}), 1616 }), 1617 }, 1618 }) 1619 }, 1620 Want: map[string]bool{"foo": true, "bar": false}, 1621 GetFileImpl: fileExists, 1622 ErrCheck: neverHappend, 1623 }, 1624 { 1625 Name: "object variable", 1626 Expr: hclExpr(`var.foo`), 1627 TargetType: reflect.TypeOf(cty.Value{}), 1628 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1629 if *opts.WantType != cty.DynamicPseudoType { 1630 return cty.Value{}, errors.New("wantType should be pseudo type") 1631 } 1632 return evalExpr(expr, &hcl.EvalContext{ 1633 Variables: map[string]cty.Value{ 1634 "var": cty.MapVal(map[string]cty.Value{ 1635 "foo": cty.ObjectVal(map[string]cty.Value{ 1636 "foo": cty.NumberIntVal(1), 1637 "bar": cty.StringVal("baz"), 1638 "qux": cty.UnknownVal(cty.String), 1639 }), 1640 }), 1641 }, 1642 }) 1643 }, 1644 Want: cty.ObjectVal(map[string]cty.Value{ 1645 "foo": cty.NumberIntVal(1), 1646 "bar": cty.StringVal("baz"), 1647 "qux": cty.UnknownVal(cty.String), 1648 }), 1649 GetFileImpl: fileExists, 1650 ErrCheck: neverHappend, 1651 }, 1652 { 1653 Name: "object variable to struct", 1654 Expr: hclExpr(`var.foo`), 1655 TargetType: reflect.TypeOf(Object{}), 1656 Option: &tflint.EvaluateExprOption{WantType: &objectTy}, 1657 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1658 return evalExpr(expr, &hcl.EvalContext{ 1659 Variables: map[string]cty.Value{ 1660 "var": cty.MapVal(map[string]cty.Value{ 1661 "foo": cty.ObjectVal(map[string]cty.Value{ 1662 "name": cty.StringVal("bar"), 1663 "enabled": cty.BoolVal(true), 1664 }), 1665 }), 1666 }, 1667 }) 1668 }, 1669 Want: Object{Name: "bar", Enabled: true}, 1670 GetFileImpl: fileExists, 1671 ErrCheck: neverHappend, 1672 }, 1673 { 1674 Name: "JSON expr", 1675 Expr: jsonExpr(`"${var.foo}"`), 1676 TargetType: reflect.TypeOf(""), 1677 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1678 return evalExpr(expr, &hcl.EvalContext{ 1679 Variables: map[string]cty.Value{ 1680 "var": cty.MapVal(map[string]cty.Value{ 1681 "foo": cty.StringVal("bar"), 1682 }), 1683 }, 1684 }) 1685 }, 1686 Want: "bar", 1687 GetFileImpl: fileExists, 1688 ErrCheck: neverHappend, 1689 }, 1690 { 1691 Name: "JSON object", 1692 Expr: jsonExpr(`{"foo": "bar"}`), 1693 TargetType: reflect.TypeOf(map[string]string{}), 1694 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1695 return evalExpr(expr, nil) 1696 }, 1697 Want: map[string]string{"foo": "bar"}, 1698 GetFileImpl: fileExists, 1699 ErrCheck: neverHappend, 1700 }, 1701 { 1702 Name: "bound expr", 1703 Expr: hclext.BindValue(cty.StringVal("bound value"), hclExpr(`var.foo`)), 1704 TargetType: reflect.TypeOf(""), 1705 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1706 return evalExpr(expr, &hcl.EvalContext{}) 1707 }, 1708 Want: "bound value", 1709 GetFileImpl: fileExists, 1710 ErrCheck: neverHappend, 1711 }, 1712 { 1713 Name: "eval with moduleCtx option", 1714 Expr: hclExpr(`1`), 1715 TargetType: reflect.TypeOf(0), 1716 Option: &tflint.EvaluateExprOption{ModuleCtx: tflint.RootModuleCtxType}, 1717 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1718 if opts.ModuleCtx != tflint.RootModuleCtxType { 1719 return cty.Value{}, errors.New("moduleCtx should be root") 1720 } 1721 return evalExpr(expr, nil) 1722 }, 1723 Want: 1, 1724 GetFileImpl: fileExists, 1725 ErrCheck: neverHappend, 1726 }, 1727 { 1728 Name: "getFile returns no file", 1729 Expr: hclExpr(`1`), 1730 TargetType: reflect.TypeOf(0), 1731 Want: 0, 1732 GetFileImpl: func(string) (*hcl.File, error) { 1733 return nil, nil 1734 }, 1735 ErrCheck: func(err error) bool { 1736 return err == nil || err.Error() != "file not found" 1737 }, 1738 }, 1739 { 1740 Name: "getFile returns an error", 1741 Expr: hclExpr(`1`), 1742 TargetType: reflect.TypeOf(0), 1743 Want: 0, 1744 GetFileImpl: func(string) (*hcl.File, error) { 1745 return nil, errors.New("unexpected error") 1746 }, 1747 ErrCheck: func(err error) bool { 1748 return err == nil || err.Error() != "unexpected error" 1749 }, 1750 }, 1751 { 1752 Name: "server returns an unexpected error", 1753 Expr: hclExpr(`1`), 1754 TargetType: reflect.TypeOf(0), 1755 ServerImpl: func(hcl.Expression, tflint.EvaluateExprOption) (cty.Value, error) { 1756 return cty.Value{}, errors.New("unexpected error") 1757 }, 1758 Want: 0, 1759 GetFileImpl: fileExists, 1760 ErrCheck: func(err error) bool { 1761 return err == nil || err.Error() != "unexpected error" 1762 }, 1763 }, 1764 { 1765 Name: "server returns an unknown error", 1766 Expr: hclExpr(`1`), 1767 TargetType: reflect.TypeOf(0), 1768 ServerImpl: func(hcl.Expression, tflint.EvaluateExprOption) (cty.Value, error) { 1769 return cty.Value{}, fmt.Errorf("unknown%w", tflint.ErrUnknownValue) 1770 }, 1771 Want: 0, 1772 GetFileImpl: fileExists, 1773 ErrCheck: func(err error) bool { 1774 return !errors.Is(err, tflint.ErrUnknownValue) 1775 }, 1776 }, 1777 { 1778 Name: "server returns a null value error", 1779 Expr: hclExpr(`1`), 1780 TargetType: reflect.TypeOf(0), 1781 ServerImpl: func(hcl.Expression, tflint.EvaluateExprOption) (cty.Value, error) { 1782 return cty.Value{}, fmt.Errorf("null value%w", tflint.ErrNullValue) 1783 }, 1784 Want: 0, 1785 GetFileImpl: fileExists, 1786 ErrCheck: func(err error) bool { 1787 return !errors.Is(err, tflint.ErrNullValue) 1788 }, 1789 }, 1790 { 1791 Name: "server returns a unevaluable error", 1792 Expr: hclExpr(`1`), 1793 TargetType: reflect.TypeOf(0), 1794 ServerImpl: func(hcl.Expression, tflint.EvaluateExprOption) (cty.Value, error) { 1795 return cty.Value{}, fmt.Errorf("unevaluable%w", tflint.ErrUnevaluable) 1796 }, 1797 Want: 0, 1798 GetFileImpl: fileExists, 1799 ErrCheck: func(err error) bool { 1800 return !errors.Is(err, tflint.ErrUnevaluable) 1801 }, 1802 }, 1803 { 1804 Name: "server returns a sensitive error", 1805 Expr: hclExpr(`1`), 1806 TargetType: reflect.TypeOf(0), 1807 ServerImpl: func(hcl.Expression, tflint.EvaluateExprOption) (cty.Value, error) { 1808 return cty.Value{}, fmt.Errorf("sensitive%w", tflint.ErrSensitive) 1809 }, 1810 Want: 0, 1811 GetFileImpl: fileExists, 1812 ErrCheck: func(err error) bool { 1813 return !errors.Is(err, tflint.ErrSensitive) 1814 }, 1815 }, 1816 { 1817 Name: "unknown value", 1818 Expr: hclExpr(`var.foo`), 1819 TargetType: reflect.TypeOf(""), 1820 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1821 return evalExpr(expr, &hcl.EvalContext{ 1822 Variables: map[string]cty.Value{ 1823 "var": cty.MapVal(map[string]cty.Value{ 1824 "foo": cty.UnknownVal(cty.String), 1825 }), 1826 }, 1827 }) 1828 }, 1829 Want: "", 1830 GetFileImpl: fileExists, 1831 ErrCheck: func(err error) bool { 1832 return !errors.Is(err, tflint.ErrUnknownValue) 1833 }, 1834 }, 1835 { 1836 Name: "unknown value as cty.Value", 1837 Expr: hclExpr(`var.foo`), 1838 TargetType: reflect.TypeOf(cty.Value{}), 1839 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1840 return evalExpr(expr, &hcl.EvalContext{ 1841 Variables: map[string]cty.Value{ 1842 "var": cty.MapVal(map[string]cty.Value{ 1843 "foo": cty.UnknownVal(cty.String), 1844 }), 1845 }, 1846 }) 1847 }, 1848 Want: cty.UnknownVal(cty.String), 1849 GetFileImpl: fileExists, 1850 ErrCheck: neverHappend, 1851 }, 1852 { 1853 Name: "unknown value in object", 1854 Expr: hclExpr(`{ value = var.foo }`), 1855 TargetType: reflect.TypeOf(map[string]string{}), 1856 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1857 return evalExpr(expr, &hcl.EvalContext{ 1858 Variables: map[string]cty.Value{ 1859 "var": cty.MapVal(map[string]cty.Value{ 1860 "foo": cty.UnknownVal(cty.String), 1861 }), 1862 }, 1863 }) 1864 }, 1865 Want: (map[string]string)(nil), 1866 GetFileImpl: fileExists, 1867 ErrCheck: func(err error) bool { 1868 return !errors.Is(err, tflint.ErrUnknownValue) 1869 }, 1870 }, 1871 { 1872 Name: "null", 1873 Expr: hclExpr(`var.foo`), 1874 TargetType: reflect.TypeOf(""), 1875 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1876 return evalExpr(expr, &hcl.EvalContext{ 1877 Variables: map[string]cty.Value{ 1878 "var": cty.MapVal(map[string]cty.Value{ 1879 "foo": cty.NullVal(cty.String), 1880 }), 1881 }, 1882 }) 1883 }, 1884 Want: "", 1885 GetFileImpl: fileExists, 1886 ErrCheck: func(err error) bool { 1887 return !errors.Is(err, tflint.ErrNullValue) 1888 }, 1889 }, 1890 { 1891 Name: "null as cty.Value", 1892 Expr: hclExpr(`var.foo`), 1893 TargetType: reflect.TypeOf(cty.Value{}), 1894 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1895 return evalExpr(expr, &hcl.EvalContext{ 1896 Variables: map[string]cty.Value{ 1897 "var": cty.MapVal(map[string]cty.Value{ 1898 "foo": cty.NullVal(cty.String), 1899 }), 1900 }, 1901 }) 1902 }, 1903 Want: cty.NullVal(cty.String), 1904 GetFileImpl: fileExists, 1905 ErrCheck: neverHappend, 1906 }, 1907 { 1908 Name: "null value in object", 1909 Expr: hclExpr(`{ value = var.foo }`), 1910 TargetType: reflect.TypeOf(map[string]string{}), 1911 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1912 return evalExpr(expr, &hcl.EvalContext{ 1913 Variables: map[string]cty.Value{ 1914 "var": cty.MapVal(map[string]cty.Value{ 1915 "foo": cty.NullVal(cty.String), 1916 }), 1917 }, 1918 }) 1919 }, 1920 Want: (map[string]string)(nil), 1921 GetFileImpl: fileExists, 1922 ErrCheck: func(err error) bool { 1923 return !errors.Is(err, tflint.ErrNullValue) 1924 }, 1925 }, 1926 { 1927 Name: "sensitive", 1928 Expr: hclExpr(`var.foo`), 1929 TargetType: reflect.TypeOf(""), 1930 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1931 return evalExpr(expr, &hcl.EvalContext{ 1932 Variables: map[string]cty.Value{ 1933 "var": cty.MapVal(map[string]cty.Value{ 1934 "foo": cty.StringVal("bar").Mark(marks.Sensitive), 1935 }), 1936 }, 1937 }) 1938 }, 1939 Want: "", 1940 GetFileImpl: fileExists, 1941 ErrCheck: func(err error) bool { 1942 return !errors.Is(err, tflint.ErrSensitive) 1943 }, 1944 }, 1945 { 1946 Name: "sensitive as cty.Value", 1947 Expr: hclExpr(`var.foo`), 1948 TargetType: reflect.TypeOf(cty.Value{}), 1949 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1950 return evalExpr(expr, &hcl.EvalContext{ 1951 Variables: map[string]cty.Value{ 1952 "var": cty.MapVal(map[string]cty.Value{ 1953 "foo": cty.StringVal("bar").Mark(marks.Sensitive), 1954 }), 1955 }, 1956 }) 1957 }, 1958 Want: cty.StringVal("bar").Mark(marks.Sensitive), 1959 GetFileImpl: fileExists, 1960 ErrCheck: neverHappend, 1961 }, 1962 { 1963 Name: "sensitive in object", 1964 Expr: hclExpr(`{ value = var.foo }`), 1965 TargetType: reflect.TypeOf(map[string]string{}), 1966 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1967 return evalExpr(expr, &hcl.EvalContext{ 1968 Variables: map[string]cty.Value{ 1969 "var": cty.MapVal(map[string]cty.Value{ 1970 "foo": cty.StringVal("bar").Mark(marks.Sensitive), 1971 }), 1972 }, 1973 }) 1974 }, 1975 Want: (map[string]string)(nil), 1976 GetFileImpl: fileExists, 1977 ErrCheck: func(err error) bool { 1978 return !errors.Is(err, tflint.ErrSensitive) 1979 }, 1980 }, 1981 { 1982 Name: "sensitive object as cty.Value", 1983 Expr: hclExpr(`var.foo`), 1984 TargetType: reflect.TypeOf(cty.Value{}), 1985 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 1986 return evalExpr(expr, &hcl.EvalContext{ 1987 Variables: map[string]cty.Value{ 1988 "var": cty.MapVal(map[string]cty.Value{ 1989 "foo": cty.ObjectVal(map[string]cty.Value{ 1990 "bar": cty.StringVal("barval").Mark(marks.Sensitive), 1991 "baz": cty.ListVal([]cty.Value{cty.NumberIntVal(1).Mark(marks.Sensitive)}), 1992 "qux": cty.TupleVal([]cty.Value{cty.StringVal("quxval").Mark(marks.Sensitive)}), 1993 "quux": cty.MapVal(map[string]cty.Value{ 1994 "foo": cty.StringVal("fooval").Mark(marks.Sensitive), 1995 }), 1996 "corge": cty.ObjectVal(map[string]cty.Value{ 1997 "bar": cty.NumberIntVal(2).Mark(marks.Sensitive), 1998 }), 1999 }), 2000 }), 2001 }, 2002 }) 2003 }, 2004 Want: cty.ObjectVal(map[string]cty.Value{ 2005 "bar": cty.StringVal("barval").Mark(marks.Sensitive), 2006 "baz": cty.ListVal([]cty.Value{cty.NumberIntVal(1).Mark(marks.Sensitive)}), 2007 "qux": cty.TupleVal([]cty.Value{cty.StringVal("quxval").Mark(marks.Sensitive)}), 2008 "quux": cty.MapVal(map[string]cty.Value{ 2009 "foo": cty.StringVal("fooval").Mark(marks.Sensitive), 2010 }), 2011 "corge": cty.ObjectVal(map[string]cty.Value{ 2012 "bar": cty.NumberIntVal(2).Mark(marks.Sensitive), 2013 }), 2014 }), 2015 GetFileImpl: fileExists, 2016 ErrCheck: neverHappend, 2017 }, 2018 { 2019 Name: "ephemeral", 2020 Expr: hclExpr(`var.foo`), 2021 TargetType: reflect.TypeOf(""), 2022 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2023 return evalExpr(expr, &hcl.EvalContext{ 2024 Variables: map[string]cty.Value{ 2025 "var": cty.MapVal(map[string]cty.Value{ 2026 "foo": cty.StringVal("bar").Mark(marks.Ephemeral), 2027 }), 2028 }, 2029 }) 2030 }, 2031 Want: "", 2032 GetFileImpl: fileExists, 2033 ErrCheck: func(err error) bool { 2034 return !errors.Is(err, tflint.ErrEphemeral) 2035 }, 2036 }, 2037 { 2038 Name: "ephemeral as cty.Value", 2039 Expr: hclExpr(`var.foo`), 2040 TargetType: reflect.TypeOf(cty.Value{}), 2041 ServerImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2042 return evalExpr(expr, &hcl.EvalContext{ 2043 Variables: map[string]cty.Value{ 2044 "var": cty.MapVal(map[string]cty.Value{ 2045 "foo": cty.StringVal("bar").Mark(marks.Ephemeral), 2046 }), 2047 }, 2048 }) 2049 }, 2050 Want: cty.StringVal("bar").Mark(marks.Ephemeral), 2051 GetFileImpl: fileExists, 2052 ErrCheck: neverHappend, 2053 }, 2054 } 2055 2056 for _, test := range tests { 2057 t.Run(test.Name, func(t *testing.T) { 2058 target := reflect.New(test.TargetType) 2059 2060 client := startTestGRPCServer(t, newMockServer(mockServerImpl{evaluateExpr: test.ServerImpl, getFile: test.GetFileImpl})) 2061 2062 err := client.EvaluateExpr(test.Expr, target.Interface(), test.Option) 2063 if test.ErrCheck(err) { 2064 t.Fatalf("failed to call EvaluateExpr: %s", err) 2065 } 2066 2067 got := target.Elem().Interface() 2068 2069 opts := cmp.Options{ 2070 cmp.Comparer(func(x, y cty.Value) bool { 2071 return x.GoString() == y.GoString() 2072 }), 2073 } 2074 if diff := cmp.Diff(got, test.Want, opts); diff != "" { 2075 t.Errorf("diff: %s", diff) 2076 } 2077 }) 2078 } 2079 } 2080 2081 func TestEvaluateExpr_callback(t *testing.T) { 2082 // default error check helper 2083 neverHappend := func(err error) bool { return err != nil } 2084 2085 // default getFileImpl function 2086 fileIdx := 1 2087 files := map[string]*hcl.File{} 2088 fileExists := func(filename string) (*hcl.File, error) { 2089 return files[filename], nil 2090 } 2091 2092 // test util functions 2093 hclExpr := func(expr string) hcl.Expression { 2094 filename := fmt.Sprintf("test%d.tf", fileIdx) 2095 file, diags := hclsyntax.ParseConfig([]byte(fmt.Sprintf(`expr = %s`, expr)), filename, hcl.InitialPos) 2096 if diags.HasErrors() { 2097 panic(diags) 2098 } 2099 attributes, diags := file.Body.JustAttributes() 2100 if diags.HasErrors() { 2101 panic(diags) 2102 } 2103 files[filename] = file 2104 fileIdx = fileIdx + 1 2105 return attributes["expr"].Expr 2106 } 2107 2108 // test struct for decoding from cty.Value 2109 type Object struct { 2110 Name string `cty:"name"` 2111 Enabled bool `cty:"enabled"` 2112 } 2113 objectTy := cty.Object(map[string]cty.Type{"name": cty.String, "enabled": cty.Bool}) 2114 2115 tests := []struct { 2116 name string 2117 expr hcl.Expression 2118 target any 2119 option *tflint.EvaluateExprOption 2120 serverImpl func(hcl.Expression, tflint.EvaluateExprOption) (cty.Value, error) 2121 getFileImpl func(string) (*hcl.File, error) 2122 errCheck func(err error) bool 2123 }{ 2124 { 2125 name: "callback with string", 2126 expr: hclExpr(`"foo"`), 2127 target: func(val string) error { 2128 return fmt.Errorf("value is %s", val) 2129 }, 2130 serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2131 return cty.StringVal("foo"), nil 2132 }, 2133 getFileImpl: fileExists, 2134 errCheck: func(err error) bool { 2135 return err == nil || err.Error() != "value is foo" 2136 }, 2137 }, 2138 { 2139 name: "callback with int", 2140 expr: hclExpr(`1`), 2141 target: func(val int) error { 2142 return fmt.Errorf("value is %d", val) 2143 }, 2144 serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2145 return cty.NumberIntVal(1), nil 2146 }, 2147 getFileImpl: fileExists, 2148 errCheck: func(err error) bool { 2149 return err == nil || err.Error() != "value is 1" 2150 }, 2151 }, 2152 { 2153 name: "callback with bool", 2154 expr: hclExpr(`true`), 2155 target: func(val bool) error { 2156 return fmt.Errorf("value is %t", val) 2157 }, 2158 serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2159 return cty.BoolVal(true), nil 2160 }, 2161 getFileImpl: fileExists, 2162 errCheck: func(err error) bool { 2163 return err == nil || err.Error() != "value is true" 2164 }, 2165 }, 2166 { 2167 name: "callback with []string", 2168 expr: hclExpr(`["foo", "bar"]`), 2169 target: func(val []string) error { 2170 return fmt.Errorf("value is %#v", val) 2171 }, 2172 serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2173 return cty.ListVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}), nil 2174 }, 2175 getFileImpl: fileExists, 2176 errCheck: func(err error) bool { 2177 return err == nil || err.Error() != `value is []string{"foo", "bar"}` 2178 }, 2179 }, 2180 { 2181 name: "callback with []int", 2182 expr: hclExpr(`[1, 2]`), 2183 target: func(val []int) error { 2184 return fmt.Errorf("value is %#v", val) 2185 }, 2186 serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2187 return cty.ListVal([]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(2)}), nil 2188 }, 2189 getFileImpl: fileExists, 2190 errCheck: func(err error) bool { 2191 return err == nil || err.Error() != `value is []int{1, 2}` 2192 }, 2193 }, 2194 { 2195 name: "callback with []bool", 2196 expr: hclExpr(`[true, false]`), 2197 target: func(val []bool) error { 2198 return fmt.Errorf("value is %#v", val) 2199 }, 2200 serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2201 return cty.ListVal([]cty.Value{cty.BoolVal(true), cty.BoolVal(false)}), nil 2202 }, 2203 getFileImpl: fileExists, 2204 errCheck: func(err error) bool { 2205 return err == nil || err.Error() != `value is []bool{true, false}` 2206 }, 2207 }, 2208 { 2209 name: "callback with map[string]string", 2210 expr: hclExpr(`{ "foo" = "bar", "baz" = "qux" }`), 2211 target: func(val map[string]string) error { 2212 return fmt.Errorf("foo is %s, baz is %s", val["foo"], val["baz"]) 2213 }, 2214 serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2215 return cty.MapVal(map[string]cty.Value{ 2216 "foo": cty.StringVal("bar"), 2217 "baz": cty.StringVal("qux"), 2218 }), nil 2219 }, 2220 getFileImpl: fileExists, 2221 errCheck: func(err error) bool { 2222 return err == nil || err.Error() != `foo is bar, baz is qux` 2223 }, 2224 }, 2225 { 2226 name: "callback with map[string]int", 2227 expr: hclExpr(`{ "foo" = "bar", "baz" = "qux" }`), 2228 target: func(val map[string]int) error { 2229 return fmt.Errorf("foo is %d, baz is %d", val["foo"], val["baz"]) 2230 }, 2231 serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2232 return cty.MapVal(map[string]cty.Value{ 2233 "foo": cty.NumberIntVal(1), 2234 "baz": cty.NumberIntVal(2), 2235 }), nil 2236 }, 2237 getFileImpl: fileExists, 2238 errCheck: func(err error) bool { 2239 return err == nil || err.Error() != `foo is 1, baz is 2` 2240 }, 2241 }, 2242 { 2243 name: "callback with map[string]bool", 2244 expr: hclExpr(`{ "foo" = true, "baz" = false }`), 2245 target: func(val map[string]bool) error { 2246 return fmt.Errorf("foo is %t, baz is %t", val["foo"], val["baz"]) 2247 }, 2248 serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2249 return cty.MapVal(map[string]cty.Value{ 2250 "foo": cty.BoolVal(true), 2251 "baz": cty.BoolVal(false), 2252 }), nil 2253 }, 2254 getFileImpl: fileExists, 2255 errCheck: func(err error) bool { 2256 return err == nil || err.Error() != `foo is true, baz is false` 2257 }, 2258 }, 2259 { 2260 name: "callback with cty.Value", 2261 expr: hclExpr(`var.foo`), 2262 target: func(val cty.Value) error { 2263 return fmt.Errorf("value is %s", val.GoString()) 2264 }, 2265 serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2266 return cty.ObjectVal(map[string]cty.Value{ 2267 "foo": cty.NumberIntVal(1), 2268 "bar": cty.StringVal("baz"), 2269 "qux": cty.UnknownVal(cty.String), 2270 }), nil 2271 }, 2272 getFileImpl: fileExists, 2273 errCheck: func(err error) bool { 2274 return err == nil || err.Error() != `value is cty.ObjectVal(map[string]cty.Value{"bar":cty.StringVal("baz"), "foo":cty.NumberIntVal(1), "qux":cty.UnknownVal(cty.String)})` 2275 }, 2276 }, 2277 { 2278 name: "callback with struct", 2279 expr: hclExpr(`var.foo`), 2280 target: func(val Object) error { 2281 return fmt.Errorf("value is %#v", val) 2282 }, 2283 option: &tflint.EvaluateExprOption{WantType: &objectTy}, 2284 serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2285 return cty.ObjectVal(map[string]cty.Value{ 2286 "name": cty.StringVal("bar"), 2287 "enabled": cty.BoolVal(true), 2288 }), nil 2289 }, 2290 getFileImpl: fileExists, 2291 errCheck: func(err error) bool { 2292 return err == nil || err.Error() != `value is plugin2host.Object{Name:"bar", Enabled:true}` 2293 }, 2294 }, 2295 { 2296 name: "callback with unknown value as Go value", 2297 expr: hclExpr(`var.foo`), 2298 target: func(val string) error { 2299 return fmt.Errorf("value is %s", val) 2300 }, 2301 serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2302 return cty.UnknownVal(cty.String), nil 2303 }, 2304 getFileImpl: fileExists, 2305 errCheck: neverHappend, 2306 }, 2307 { 2308 name: "callback with unknown value as cty.Value", 2309 expr: hclExpr(`var.foo`), 2310 target: func(val cty.Value) error { 2311 return fmt.Errorf("value is %s", val.GoString()) 2312 }, 2313 serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2314 return cty.UnknownVal(cty.String), nil 2315 }, 2316 getFileImpl: fileExists, 2317 errCheck: func(err error) bool { 2318 return err == nil || err.Error() != `value is cty.UnknownVal(cty.String)` 2319 }, 2320 }, 2321 { 2322 name: "callback with null as Go value", 2323 expr: hclExpr(`var.foo`), 2324 target: func(val string) error { 2325 return fmt.Errorf("value is %s", val) 2326 }, 2327 serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2328 return cty.NullVal(cty.String), nil 2329 }, 2330 getFileImpl: fileExists, 2331 errCheck: neverHappend, 2332 }, 2333 { 2334 name: "callback with null as cty.Value", 2335 expr: hclExpr(`var.foo`), 2336 target: func(val cty.Value) error { 2337 return fmt.Errorf("value is %s", val.GoString()) 2338 }, 2339 serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2340 return cty.NullVal(cty.String), nil 2341 }, 2342 getFileImpl: fileExists, 2343 errCheck: func(err error) bool { 2344 return err == nil || err.Error() != `value is cty.NullVal(cty.String)` 2345 }, 2346 }, 2347 { 2348 name: "callback with sensitive value as Go value", 2349 expr: hclExpr(`var.foo`), 2350 target: func(val string) error { 2351 return fmt.Errorf("value is %s", val) 2352 }, 2353 serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2354 return cty.StringVal("foo").Mark(marks.Sensitive), nil 2355 }, 2356 getFileImpl: fileExists, 2357 errCheck: neverHappend, 2358 }, 2359 { 2360 name: "callback with sensitive value as cty.Value", 2361 expr: hclExpr(`var.foo`), 2362 target: func(val cty.Value) error { 2363 return fmt.Errorf("value is %s", val.GoString()) 2364 }, 2365 serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2366 return cty.StringVal("foo").Mark(marks.Sensitive), nil 2367 }, 2368 getFileImpl: fileExists, 2369 errCheck: func(err error) bool { 2370 return err == nil || err.Error() != `value is cty.StringVal("foo").Mark(marks.Sensitive)` 2371 }, 2372 }, 2373 { 2374 name: "callback with ephemeral value as Go value", 2375 expr: hclExpr(`var.foo`), 2376 target: func(val string) error { 2377 return fmt.Errorf("value is %s", val) 2378 }, 2379 serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2380 return cty.StringVal("foo").Mark(marks.Ephemeral), nil 2381 }, 2382 getFileImpl: fileExists, 2383 errCheck: neverHappend, 2384 }, 2385 { 2386 name: "callback with ephemeral value as cty.Value", 2387 expr: hclExpr(`var.foo`), 2388 target: func(val cty.Value) error { 2389 return fmt.Errorf("value is %s", val.GoString()) 2390 }, 2391 serverImpl: func(expr hcl.Expression, opts tflint.EvaluateExprOption) (cty.Value, error) { 2392 return cty.StringVal("foo").Mark(marks.Ephemeral), nil 2393 }, 2394 getFileImpl: fileExists, 2395 errCheck: func(err error) bool { 2396 return err == nil || err.Error() != `value is cty.StringVal("foo").Mark(marks.Ephemeral)` 2397 }, 2398 }, 2399 } 2400 2401 for _, test := range tests { 2402 t.Run(test.name, func(t *testing.T) { 2403 client := startTestGRPCServer(t, newMockServer(mockServerImpl{evaluateExpr: test.serverImpl, getFile: test.getFileImpl})) 2404 2405 err := client.EvaluateExpr(test.expr, test.target, test.option) 2406 if test.errCheck(err) { 2407 t.Fatalf("failed to call EvaluateExpr: %s", err) 2408 } 2409 }) 2410 } 2411 } 2412 2413 // test rule for TestEmitIssue 2414 type Rule struct { 2415 tflint.DefaultRule 2416 } 2417 2418 func (*Rule) Name() string { return "test_rule" } 2419 func (*Rule) Enabled() bool { return true } 2420 func (*Rule) Severity() tflint.Severity { return tflint.ERROR } 2421 func (*Rule) Link() string { return "https://example.com" } 2422 func (*Rule) Check(runner tflint.Runner) error { return nil } 2423 2424 func TestEmitIssue(t *testing.T) { 2425 // default error check helper 2426 neverHappend := func(err error) bool { return err != nil } 2427 2428 tests := []struct { 2429 Name string 2430 Args func() (tflint.Rule, string, hcl.Range) 2431 ServerImpl func(tflint.Rule, string, hcl.Range, bool) (bool, error) 2432 ErrCheck func(error) bool 2433 }{ 2434 { 2435 Name: "emit issue", 2436 Args: func() (tflint.Rule, string, hcl.Range) { 2437 return &Rule{}, "this is test", hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 2}, End: hcl.Pos{Line: 2, Column: 10}} 2438 }, 2439 ServerImpl: func(rule tflint.Rule, message string, location hcl.Range, fixable bool) (bool, error) { 2440 if rule.Name() != "test_rule" { 2441 return false, fmt.Errorf("rule.Name() should be test_rule, but %s", rule.Name()) 2442 } 2443 if rule.Enabled() != true { 2444 return false, fmt.Errorf("rule.Enabled() should be true, but %t", rule.Enabled()) 2445 } 2446 if rule.Severity() != tflint.ERROR { 2447 return false, fmt.Errorf("rule.Severity() should be ERROR, but %s", rule.Severity()) 2448 } 2449 if rule.Link() != "https://example.com" { 2450 return false, fmt.Errorf("rule.Link() should be https://example.com, but %s", rule.Link()) 2451 } 2452 if message != "this is test" { 2453 return false, fmt.Errorf("message should be `this is test`, but %s", message) 2454 } 2455 want := hcl.Range{ 2456 Filename: "test.tf", 2457 Start: hcl.Pos{Line: 2, Column: 2}, 2458 End: hcl.Pos{Line: 2, Column: 10}, 2459 } 2460 if diff := cmp.Diff(location, want); diff != "" { 2461 return false, fmt.Errorf("diff: %s", diff) 2462 } 2463 return true, nil 2464 }, 2465 ErrCheck: neverHappend, 2466 }, 2467 { 2468 Name: "server returns an error", 2469 Args: func() (tflint.Rule, string, hcl.Range) { 2470 return &Rule{}, "this is test", hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 2}, End: hcl.Pos{Line: 2, Column: 10}} 2471 }, 2472 ServerImpl: func(tflint.Rule, string, hcl.Range, bool) (bool, error) { 2473 return false, errors.New("unexpected error") 2474 }, 2475 ErrCheck: func(err error) bool { 2476 return err == nil || err.Error() != "unexpected error" 2477 }, 2478 }, 2479 } 2480 2481 for _, test := range tests { 2482 t.Run(test.Name, func(t *testing.T) { 2483 client := startTestGRPCServer(t, newMockServer(mockServerImpl{emitIssue: test.ServerImpl})) 2484 2485 err := client.EmitIssue(test.Args()) 2486 if test.ErrCheck(err) { 2487 t.Fatalf("failed to call EmitIssue: %s", err) 2488 } 2489 }) 2490 } 2491 } 2492 2493 func TestEmitIssueWithFix(t *testing.T) { 2494 // default error check helper 2495 neverHappend := func(err error) bool { return err != nil } 2496 getFiles := func() map[string][]byte { 2497 return map[string][]byte{ 2498 "test.tf": []byte(`foo = "bar"`), 2499 } 2500 } 2501 2502 tests := []struct { 2503 Name string 2504 Args func() (tflint.Rule, string, hcl.Range, func(tflint.Fixer) error) 2505 ServerImpl func(tflint.Rule, string, hcl.Range, bool) (bool, error) 2506 ModulePath []string 2507 DisableFix bool 2508 ErrCheck func(error) bool 2509 Changes map[string]string 2510 }{ 2511 { 2512 Name: "emit issue", 2513 Args: func() (tflint.Rule, string, hcl.Range, func(tflint.Fixer) error) { 2514 return &Rule{}, 2515 "this is test", 2516 hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 2}, End: hcl.Pos{Line: 2, Column: 10}}, 2517 func(f tflint.Fixer) error { 2518 return f.ReplaceText( 2519 hcl.Range{Filename: "test.tf", Start: hcl.Pos{Byte: 7}, End: hcl.Pos{Byte: 10}}, 2520 "baz", 2521 ) 2522 } 2523 }, 2524 ServerImpl: func(rule tflint.Rule, message string, location hcl.Range, fixable bool) (bool, error) { 2525 if rule.Name() != "test_rule" { 2526 return false, fmt.Errorf("rule.Name() should be test_rule, but %s", rule.Name()) 2527 } 2528 if rule.Enabled() != true { 2529 return false, fmt.Errorf("rule.Enabled() should be true, but %t", rule.Enabled()) 2530 } 2531 if rule.Severity() != tflint.ERROR { 2532 return false, fmt.Errorf("rule.Severity() should be ERROR, but %s", rule.Severity()) 2533 } 2534 if rule.Link() != "https://example.com" { 2535 return false, fmt.Errorf("rule.Link() should be https://example.com, but %s", rule.Link()) 2536 } 2537 if message != "this is test" { 2538 return false, fmt.Errorf("message should be `this is test`, but %s", message) 2539 } 2540 want := hcl.Range{ 2541 Filename: "test.tf", 2542 Start: hcl.Pos{Line: 2, Column: 2}, 2543 End: hcl.Pos{Line: 2, Column: 10}, 2544 } 2545 if diff := cmp.Diff(location, want); diff != "" { 2546 return false, fmt.Errorf("diff: %s", diff) 2547 } 2548 if fixable != true { 2549 return false, fmt.Errorf("fixable should be true, but %t", fixable) 2550 } 2551 return true, nil 2552 }, 2553 ErrCheck: neverHappend, 2554 Changes: map[string]string{ 2555 "test.tf": `foo = "baz"`, 2556 }, 2557 }, 2558 { 2559 Name: "child modules", 2560 Args: func() (tflint.Rule, string, hcl.Range, func(tflint.Fixer) error) { 2561 return &Rule{}, 2562 "this is test", 2563 hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 2}, End: hcl.Pos{Line: 2, Column: 10}}, 2564 func(f tflint.Fixer) error { 2565 return f.ReplaceText( 2566 hcl.Range{Filename: "test.tf", Start: hcl.Pos{Byte: 7}, End: hcl.Pos{Byte: 10}}, 2567 "baz", 2568 ) 2569 } 2570 }, 2571 ServerImpl: func(rule tflint.Rule, message string, location hcl.Range, fixable bool) (bool, error) { 2572 if fixable != false { 2573 return false, fmt.Errorf("fixable should be false, but %t", fixable) 2574 } 2575 return true, nil 2576 }, 2577 ModulePath: []string{"module", "child"}, 2578 ErrCheck: neverHappend, 2579 Changes: map[string]string{}, 2580 }, 2581 { 2582 Name: "autofix is not supported", 2583 Args: func() (tflint.Rule, string, hcl.Range, func(tflint.Fixer) error) { 2584 return &Rule{}, 2585 "this is test", 2586 hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 2}, End: hcl.Pos{Line: 2, Column: 10}}, 2587 func(f tflint.Fixer) error { 2588 if err := f.ReplaceText( 2589 hcl.Range{Filename: "test.tf", Start: hcl.Pos{Byte: 7}, End: hcl.Pos{Byte: 10}}, 2590 "baz", 2591 ); err != nil { 2592 return err 2593 } 2594 return tflint.ErrFixNotSupported 2595 } 2596 }, 2597 ServerImpl: func(rule tflint.Rule, message string, location hcl.Range, fixable bool) (bool, error) { 2598 if fixable != false { 2599 return false, fmt.Errorf("fixable should be false, but %t", fixable) 2600 } 2601 return true, nil 2602 }, 2603 ErrCheck: neverHappend, 2604 Changes: map[string]string{}, 2605 }, 2606 { 2607 Name: "fix is disbaled", 2608 Args: func() (tflint.Rule, string, hcl.Range, func(tflint.Fixer) error) { 2609 return &Rule{}, 2610 "this is test", 2611 hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 2}, End: hcl.Pos{Line: 2, Column: 10}}, 2612 func(f tflint.Fixer) error { 2613 return f.ReplaceText( 2614 hcl.Range{Filename: "test.tf", Start: hcl.Pos{Byte: 7}, End: hcl.Pos{Byte: 10}}, 2615 "baz", 2616 ) 2617 } 2618 }, 2619 ServerImpl: func(rule tflint.Rule, message string, location hcl.Range, fixable bool) (bool, error) { 2620 if fixable != true { 2621 return false, fmt.Errorf("fixable should be true, but %t", fixable) 2622 } 2623 return true, nil 2624 }, 2625 DisableFix: true, 2626 ErrCheck: neverHappend, 2627 Changes: map[string]string{}, 2628 }, 2629 { 2630 Name: "fix is not applied", 2631 Args: func() (tflint.Rule, string, hcl.Range, func(tflint.Fixer) error) { 2632 return &Rule{}, 2633 "this is test", 2634 hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 2}, End: hcl.Pos{Line: 2, Column: 10}}, 2635 func(f tflint.Fixer) error { 2636 return f.ReplaceText( 2637 hcl.Range{Filename: "test.tf", Start: hcl.Pos{Byte: 7}, End: hcl.Pos{Byte: 10}}, 2638 "baz", 2639 ) 2640 } 2641 }, 2642 ServerImpl: func(rule tflint.Rule, message string, location hcl.Range, fixable bool) (bool, error) { 2643 if fixable != true { 2644 return false, fmt.Errorf("fixable should be true, but %t", fixable) 2645 } 2646 return false, nil 2647 }, 2648 ErrCheck: neverHappend, 2649 Changes: map[string]string{}, 2650 }, 2651 { 2652 Name: "fix raises an error", 2653 Args: func() (tflint.Rule, string, hcl.Range, func(tflint.Fixer) error) { 2654 return &Rule{}, 2655 "this is test", 2656 hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 2}, End: hcl.Pos{Line: 2, Column: 10}}, 2657 func(f tflint.Fixer) error { 2658 if err := f.ReplaceText( 2659 hcl.Range{Filename: "test.tf", Start: hcl.Pos{Byte: 7}, End: hcl.Pos{Byte: 10}}, 2660 "baz", 2661 ); err != nil { 2662 return err 2663 } 2664 return errors.New("unexpected error") 2665 } 2666 }, 2667 ServerImpl: func(rule tflint.Rule, message string, location hcl.Range, fixable bool) (bool, error) { 2668 if fixable != true { 2669 return false, fmt.Errorf("fixable should be true, but %t", fixable) 2670 } 2671 return true, nil 2672 }, 2673 ErrCheck: func(err error) bool { 2674 return err == nil || err.Error() != "unexpected error" 2675 }, 2676 Changes: map[string]string{ 2677 "test.tf": `foo = "baz"`, 2678 }, 2679 }, 2680 { 2681 Name: "server returns an error", 2682 Args: func() (tflint.Rule, string, hcl.Range, func(tflint.Fixer) error) { 2683 return &Rule{}, 2684 "this is test", 2685 hcl.Range{Filename: "test.tf", Start: hcl.Pos{Line: 2, Column: 2}, End: hcl.Pos{Line: 2, Column: 10}}, 2686 func(f tflint.Fixer) error { 2687 return f.ReplaceText( 2688 hcl.Range{Filename: "test.tf", Start: hcl.Pos{Byte: 7}, End: hcl.Pos{Byte: 10}}, 2689 "baz", 2690 ) 2691 } 2692 }, 2693 ServerImpl: func(tflint.Rule, string, hcl.Range, bool) (bool, error) { 2694 return false, errors.New("unexpected error") 2695 }, 2696 ErrCheck: func(err error) bool { 2697 return err == nil || err.Error() != "unexpected error" 2698 }, 2699 Changes: map[string]string{ 2700 "test.tf": `foo = "baz"`, 2701 }, 2702 }, 2703 } 2704 2705 for _, test := range tests { 2706 t.Run(test.Name, func(t *testing.T) { 2707 client := startTestGRPCServer( 2708 t, 2709 newMockServer(mockServerImpl{ 2710 getFiles: getFiles, 2711 getModulePath: func() []string { return test.ModulePath }, 2712 emitIssue: test.ServerImpl, 2713 }), 2714 ) 2715 client.FixEnabled = !test.DisableFix 2716 2717 err := client.EmitIssueWithFix(test.Args()) 2718 if test.ErrCheck(err) { 2719 t.Fatalf("failed to call EmitIssueWithFix: %s", err) 2720 } 2721 2722 got := map[string]string{} 2723 for name, content := range client.Fixer.Changes() { 2724 got[name] = string(content) 2725 } 2726 if diff := cmp.Diff(got, test.Changes); diff != "" { 2727 t.Fatalf("diff: %s", diff) 2728 } 2729 }) 2730 } 2731 } 2732 2733 func TestApplyChanges(t *testing.T) { 2734 // default error check helper 2735 neverHappend := func(err error) bool { return err != nil } 2736 getFiles := func() map[string][]byte { 2737 return map[string][]byte{ 2738 "test.tf": []byte(`foo = "bar"`), 2739 } 2740 } 2741 2742 tests := []struct { 2743 name string 2744 fix func(tflint.Fixer) error 2745 serverImpl func(map[string][]byte) error 2746 errCheck func(error) bool 2747 }{ 2748 { 2749 name: "apply changes", 2750 fix: func(f tflint.Fixer) error { 2751 return f.ReplaceText( 2752 hcl.Range{Filename: "test.tf", Start: hcl.Pos{Byte: 7}, End: hcl.Pos{Byte: 10}}, 2753 "baz", 2754 ) 2755 }, 2756 serverImpl: func(files map[string][]byte) error { 2757 if diff := cmp.Diff(files, map[string][]byte{ 2758 "test.tf": []byte(`foo = "baz"`), 2759 }); diff != "" { 2760 return fmt.Errorf("diff: %s", diff) 2761 } 2762 return nil 2763 }, 2764 errCheck: neverHappend, 2765 }, 2766 { 2767 name: "server returns an error", 2768 fix: func(f tflint.Fixer) error { 2769 return f.ReplaceText( 2770 hcl.Range{Filename: "test.tf", Start: hcl.Pos{Byte: 7}, End: hcl.Pos{Byte: 10}}, 2771 "baz", 2772 ) 2773 }, 2774 serverImpl: func(files map[string][]byte) error { 2775 return errors.New("unexpected error") 2776 }, 2777 errCheck: func(err error) bool { 2778 return err == nil || err.Error() != "unexpected error" 2779 }, 2780 }, 2781 } 2782 2783 for _, test := range tests { 2784 t.Run(test.name, func(t *testing.T) { 2785 client := startTestGRPCServer(t, newMockServer(mockServerImpl{getFiles: getFiles, applyChanges: test.serverImpl})) 2786 client.FixEnabled = true 2787 2788 if err := test.fix(client.Fixer); err != nil { 2789 t.Fatalf("failed to call fix: %s", err) 2790 } 2791 2792 err := client.ApplyChanges() 2793 if test.errCheck(err) { 2794 t.Fatalf("failed to call ApplyChanges: %s", err) 2795 } 2796 2797 if err == nil && client.Fixer.HasChanges() { 2798 t.Fatal("fixer should have no changes") 2799 } 2800 }) 2801 } 2802 } 2803 2804 func TestEnsureNoError(t *testing.T) { 2805 // default error check helper 2806 neverHappend := func(err error) bool { return err != nil } 2807 2808 tests := []struct { 2809 Name string 2810 Err error 2811 Proc func() error 2812 ErrCheck func(error) bool 2813 }{ 2814 { 2815 Name: "no errors", 2816 Err: nil, 2817 Proc: func() error { 2818 return errors.New("should be called") 2819 }, 2820 ErrCheck: func(err error) bool { 2821 // should be passed result of proc() 2822 return err == nil || err.Error() != "should be called" 2823 }, 2824 }, 2825 { 2826 Name: "ErrUnevaluable", 2827 Err: fmt.Errorf("unevaluable%w", tflint.ErrUnevaluable), 2828 Proc: func() error { 2829 return errors.New("should not be called") 2830 }, 2831 ErrCheck: neverHappend, 2832 }, 2833 { 2834 Name: "ErrNullValue", 2835 Err: fmt.Errorf("null value%w", tflint.ErrNullValue), 2836 Proc: func() error { 2837 return errors.New("should not be called") 2838 }, 2839 ErrCheck: neverHappend, 2840 }, 2841 { 2842 Name: "ErrUnknownValue", 2843 Err: fmt.Errorf("unknown value%w", tflint.ErrUnknownValue), 2844 Proc: func() error { 2845 return errors.New("should not be called") 2846 }, 2847 ErrCheck: neverHappend, 2848 }, 2849 { 2850 Name: "unexpected error", 2851 Err: errors.New("unexpected error"), 2852 Proc: func() error { 2853 return errors.New("should not be called") 2854 }, 2855 ErrCheck: func(err error) bool { 2856 return err == nil || err.Error() != "unexpected error" 2857 }, 2858 }, 2859 } 2860 2861 for _, test := range tests { 2862 t.Run(test.Name, func(t *testing.T) { 2863 client := startTestGRPCServer(t, newMockServer(mockServerImpl{})) 2864 2865 err := client.EnsureNoError(test.Err, test.Proc) 2866 if test.ErrCheck(err) { 2867 t.Fatalf("failed to call EnsureNoError: %s", err) 2868 } 2869 }) 2870 } 2871 }