github.com/opentofu/opentofu@v1.7.1/internal/tofu/test_context_test.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package tofu 7 8 import ( 9 "testing" 10 11 "github.com/google/go-cmp/cmp" 12 "github.com/zclconf/go-cty/cty" 13 ctyjson "github.com/zclconf/go-cty/cty/json" 14 ctymsgpack "github.com/zclconf/go-cty/cty/msgpack" 15 16 "github.com/opentofu/opentofu/internal/addrs" 17 "github.com/opentofu/opentofu/internal/configs/configschema" 18 "github.com/opentofu/opentofu/internal/lang/marks" 19 "github.com/opentofu/opentofu/internal/moduletest" 20 "github.com/opentofu/opentofu/internal/plans" 21 "github.com/opentofu/opentofu/internal/providers" 22 "github.com/opentofu/opentofu/internal/states" 23 "github.com/opentofu/opentofu/internal/tfdiags" 24 ) 25 26 func TestTestContext_EvaluateAgainstState(t *testing.T) { 27 tcs := map[string]struct { 28 configs map[string]string 29 state *states.State 30 variables InputValues 31 provider *MockProvider 32 33 expectedDiags []tfdiags.Description 34 expectedStatus moduletest.Status 35 }{ 36 "basic_passing": { 37 configs: map[string]string{ 38 "main.tf": ` 39 resource "test_resource" "a" { 40 value = "Hello, world!" 41 } 42 `, 43 "main.tftest.hcl": ` 44 run "test_case" { 45 assert { 46 condition = test_resource.a.value == "Hello, world!" 47 error_message = "invalid value" 48 } 49 } 50 `, 51 }, 52 state: states.BuildState(func(state *states.SyncState) { 53 state.SetResourceInstanceCurrent( 54 addrs.Resource{ 55 Mode: addrs.ManagedResourceMode, 56 Type: "test_resource", 57 Name: "a", 58 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 59 &states.ResourceInstanceObjectSrc{ 60 Status: states.ObjectReady, 61 AttrsJSON: encodeCtyValue(t, cty.ObjectVal(map[string]cty.Value{ 62 "value": cty.StringVal("Hello, world!"), 63 })), 64 }, 65 addrs.AbsProviderConfig{ 66 Module: addrs.RootModule, 67 Provider: addrs.NewDefaultProvider("test"), 68 }) 69 }), 70 provider: &MockProvider{ 71 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 72 ResourceTypes: map[string]providers.Schema{ 73 "test_resource": { 74 Block: &configschema.Block{ 75 Attributes: map[string]*configschema.Attribute{ 76 "value": { 77 Type: cty.String, 78 Required: true, 79 }, 80 }, 81 }, 82 }, 83 }, 84 }, 85 }, 86 expectedStatus: moduletest.Pass, 87 }, 88 "basic_passing_with_sensitive_value": { 89 configs: map[string]string{ 90 "main.tf": ` 91 resource "test_resource" "a" { 92 sensitive_value = "Shhhhh!" 93 } 94 `, 95 "main.tftest.hcl": ` 96 run "test_case" { 97 assert { 98 condition = test_resource.a.sensitive_value == "Shhhhh!" 99 error_message = "invalid value" 100 } 101 } 102 `, 103 }, 104 state: states.BuildState(func(state *states.SyncState) { 105 state.SetResourceInstanceCurrent( 106 addrs.Resource{ 107 Mode: addrs.ManagedResourceMode, 108 Type: "test_resource", 109 Name: "a", 110 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 111 &states.ResourceInstanceObjectSrc{ 112 Status: states.ObjectReady, 113 AttrsJSON: encodeCtyValue(t, cty.ObjectVal(map[string]cty.Value{ 114 "sensitive_value": cty.StringVal("Shhhhh!"), 115 })), 116 AttrSensitivePaths: []cty.PathValueMarks{ 117 { 118 Path: cty.GetAttrPath("sensitive_value"), 119 Marks: cty.NewValueMarks(marks.Sensitive), 120 }, 121 }, 122 }, 123 addrs.AbsProviderConfig{ 124 Module: addrs.RootModule, 125 Provider: addrs.NewDefaultProvider("test"), 126 }) 127 }), 128 provider: &MockProvider{ 129 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 130 ResourceTypes: map[string]providers.Schema{ 131 "test_resource": { 132 Block: &configschema.Block{ 133 Attributes: map[string]*configschema.Attribute{ 134 "sensitive_value": { 135 Type: cty.String, 136 Required: true, 137 }, 138 }, 139 }, 140 }, 141 }, 142 }, 143 }, 144 expectedStatus: moduletest.Pass, 145 }, 146 "with_variables": { 147 configs: map[string]string{ 148 "main.tf": ` 149 variable "value" { 150 type = string 151 } 152 153 resource "test_resource" "a" { 154 value = var.value 155 } 156 `, 157 "main.tftest.hcl": ` 158 variables { 159 value = "Hello, world!" 160 } 161 162 run "test_case" { 163 assert { 164 condition = test_resource.a.value == var.value 165 error_message = "invalid value" 166 } 167 } 168 `, 169 }, 170 state: states.BuildState(func(state *states.SyncState) { 171 state.SetResourceInstanceCurrent( 172 addrs.Resource{ 173 Mode: addrs.ManagedResourceMode, 174 Type: "test_resource", 175 Name: "a", 176 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 177 &states.ResourceInstanceObjectSrc{ 178 Status: states.ObjectReady, 179 AttrsJSON: encodeCtyValue(t, cty.ObjectVal(map[string]cty.Value{ 180 "value": cty.StringVal("Hello, world!"), 181 })), 182 }, 183 addrs.AbsProviderConfig{ 184 Module: addrs.RootModule, 185 Provider: addrs.NewDefaultProvider("test"), 186 }) 187 }), 188 variables: InputValues{ 189 "value": { 190 Value: cty.StringVal("Hello, world!"), 191 }, 192 }, 193 provider: &MockProvider{ 194 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 195 ResourceTypes: map[string]providers.Schema{ 196 "test_resource": { 197 Block: &configschema.Block{ 198 Attributes: map[string]*configschema.Attribute{ 199 "value": { 200 Type: cty.String, 201 Required: true, 202 }, 203 }, 204 }, 205 }, 206 }, 207 }, 208 }, 209 expectedStatus: moduletest.Pass, 210 }, 211 "basic_failing": { 212 configs: map[string]string{ 213 "main.tf": ` 214 resource "test_resource" "a" { 215 value = "Hello, world!" 216 } 217 `, 218 "main.tftest.hcl": ` 219 run "test_case" { 220 assert { 221 condition = test_resource.a.value == "incorrect!" 222 error_message = "invalid value" 223 } 224 } 225 `, 226 }, 227 state: states.BuildState(func(state *states.SyncState) { 228 state.SetResourceInstanceCurrent( 229 addrs.Resource{ 230 Mode: addrs.ManagedResourceMode, 231 Type: "test_resource", 232 Name: "a", 233 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 234 &states.ResourceInstanceObjectSrc{ 235 Status: states.ObjectReady, 236 AttrsJSON: encodeCtyValue(t, cty.ObjectVal(map[string]cty.Value{ 237 "value": cty.StringVal("Hello, world!"), 238 })), 239 }, 240 addrs.AbsProviderConfig{ 241 Module: addrs.RootModule, 242 Provider: addrs.NewDefaultProvider("test"), 243 }) 244 }), 245 provider: &MockProvider{ 246 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 247 ResourceTypes: map[string]providers.Schema{ 248 "test_resource": { 249 Block: &configschema.Block{ 250 Attributes: map[string]*configschema.Attribute{ 251 "value": { 252 Type: cty.String, 253 Required: true, 254 }, 255 }, 256 }, 257 }, 258 }, 259 }, 260 }, 261 expectedStatus: moduletest.Fail, 262 expectedDiags: []tfdiags.Description{ 263 { 264 Summary: "Test assertion failed", 265 Detail: "invalid value", 266 }, 267 }, 268 }, 269 "two_failing_assertions": { 270 configs: map[string]string{ 271 "main.tf": ` 272 resource "test_resource" "a" { 273 value = "Hello, world!" 274 } 275 `, 276 "main.tftest.hcl": ` 277 run "test_case" { 278 assert { 279 condition = test_resource.a.value == "incorrect!" 280 error_message = "invalid value" 281 } 282 283 assert { 284 condition = test_resource.a.value == "also incorrect!" 285 error_message = "still invalid" 286 } 287 } 288 `, 289 }, 290 state: states.BuildState(func(state *states.SyncState) { 291 state.SetResourceInstanceCurrent( 292 addrs.Resource{ 293 Mode: addrs.ManagedResourceMode, 294 Type: "test_resource", 295 Name: "a", 296 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 297 &states.ResourceInstanceObjectSrc{ 298 Status: states.ObjectReady, 299 AttrsJSON: encodeCtyValue(t, cty.ObjectVal(map[string]cty.Value{ 300 "value": cty.StringVal("Hello, world!"), 301 })), 302 }, 303 addrs.AbsProviderConfig{ 304 Module: addrs.RootModule, 305 Provider: addrs.NewDefaultProvider("test"), 306 }) 307 }), 308 provider: &MockProvider{ 309 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 310 ResourceTypes: map[string]providers.Schema{ 311 "test_resource": { 312 Block: &configschema.Block{ 313 Attributes: map[string]*configschema.Attribute{ 314 "value": { 315 Type: cty.String, 316 Required: true, 317 }, 318 }, 319 }, 320 }, 321 }, 322 }, 323 }, 324 expectedStatus: moduletest.Fail, 325 expectedDiags: []tfdiags.Description{ 326 { 327 Summary: "Test assertion failed", 328 Detail: "invalid value", 329 }, 330 { 331 Summary: "Test assertion failed", 332 Detail: "still invalid", 333 }, 334 }, 335 }, 336 } 337 for name, tc := range tcs { 338 t.Run(name, func(t *testing.T) { 339 config := testModuleInline(t, tc.configs) 340 ctx := testContext2(t, &ContextOpts{ 341 Providers: map[addrs.Provider]providers.Factory{ 342 addrs.NewDefaultProvider("test"): testProviderFuncFixed(tc.provider), 343 }, 344 }) 345 346 run := moduletest.Run{ 347 Config: config.Module.Tests["main.tftest.hcl"].Runs[0], 348 Name: "test_case", 349 } 350 351 tctx := ctx.TestContext(config, tc.state, &plans.Plan{}, tc.variables) 352 tctx.EvaluateAgainstState(&run) 353 354 if expected, actual := tc.expectedStatus, run.Status; expected != actual { 355 t.Errorf("expected status \"%s\" but got \"%s\"", expected, actual) 356 } 357 358 compareDiagnosticsFromTestResult(t, tc.expectedDiags, run.Diagnostics) 359 }) 360 } 361 } 362 363 func TestTestContext_EvaluateAgainstPlan(t *testing.T) { 364 tcs := map[string]struct { 365 configs map[string]string 366 state *states.State 367 plan *plans.Plan 368 variables InputValues 369 provider *MockProvider 370 371 expectedDiags []tfdiags.Description 372 expectedStatus moduletest.Status 373 }{ 374 "basic_passing": { 375 configs: map[string]string{ 376 "main.tf": ` 377 resource "test_resource" "a" { 378 value = "Hello, world!" 379 } 380 `, 381 "main.tftest.hcl": ` 382 run "test_case" { 383 assert { 384 condition = test_resource.a.value == "Hello, world!" 385 error_message = "invalid value" 386 } 387 } 388 `, 389 }, 390 state: states.BuildState(func(state *states.SyncState) { 391 state.SetResourceInstanceCurrent( 392 addrs.Resource{ 393 Mode: addrs.ManagedResourceMode, 394 Type: "test_resource", 395 Name: "a", 396 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 397 &states.ResourceInstanceObjectSrc{ 398 Status: states.ObjectPlanned, 399 AttrsJSON: encodeCtyValue(t, cty.NullVal(cty.Object(map[string]cty.Type{ 400 "value": cty.String, 401 }))), 402 }, 403 addrs.AbsProviderConfig{ 404 Module: addrs.RootModule, 405 Provider: addrs.NewDefaultProvider("test"), 406 }) 407 }), 408 plan: &plans.Plan{ 409 Changes: &plans.Changes{ 410 Resources: []*plans.ResourceInstanceChangeSrc{ 411 { 412 Addr: addrs.Resource{ 413 Mode: addrs.ManagedResourceMode, 414 Type: "test_resource", 415 Name: "a", 416 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 417 ProviderAddr: addrs.AbsProviderConfig{ 418 Module: addrs.RootModule, 419 Provider: addrs.NewDefaultProvider("test"), 420 }, 421 ChangeSrc: plans.ChangeSrc{ 422 Action: plans.Create, 423 Before: nil, 424 After: encodeDynamicValue(t, cty.ObjectVal(map[string]cty.Value{ 425 "value": cty.StringVal("Hello, world!"), 426 })), 427 }, 428 }, 429 }, 430 }, 431 }, 432 provider: &MockProvider{ 433 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 434 ResourceTypes: map[string]providers.Schema{ 435 "test_resource": { 436 Block: &configschema.Block{ 437 Attributes: map[string]*configschema.Attribute{ 438 "value": { 439 Type: cty.String, 440 Required: true, 441 }, 442 }, 443 }, 444 }, 445 }, 446 }, 447 }, 448 expectedStatus: moduletest.Pass, 449 }, 450 "basic_failing": { 451 configs: map[string]string{ 452 "main.tf": ` 453 resource "test_resource" "a" { 454 value = "Hello, world!" 455 } 456 `, 457 "main.tftest.hcl": ` 458 run "test_case" { 459 assert { 460 condition = test_resource.a.value == "incorrect!" 461 error_message = "invalid value" 462 } 463 } 464 `, 465 }, 466 state: states.BuildState(func(state *states.SyncState) { 467 state.SetResourceInstanceCurrent( 468 addrs.Resource{ 469 Mode: addrs.ManagedResourceMode, 470 Type: "test_resource", 471 Name: "a", 472 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 473 &states.ResourceInstanceObjectSrc{ 474 Status: states.ObjectPlanned, 475 AttrsJSON: encodeCtyValue(t, cty.NullVal(cty.Object(map[string]cty.Type{ 476 "value": cty.String, 477 }))), 478 }, 479 addrs.AbsProviderConfig{ 480 Module: addrs.RootModule, 481 Provider: addrs.NewDefaultProvider("test"), 482 }) 483 }), 484 plan: &plans.Plan{ 485 Changes: &plans.Changes{ 486 Resources: []*plans.ResourceInstanceChangeSrc{ 487 { 488 Addr: addrs.Resource{ 489 Mode: addrs.ManagedResourceMode, 490 Type: "test_resource", 491 Name: "a", 492 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 493 ProviderAddr: addrs.AbsProviderConfig{ 494 Module: addrs.RootModule, 495 Provider: addrs.NewDefaultProvider("test"), 496 }, 497 ChangeSrc: plans.ChangeSrc{ 498 Action: plans.Create, 499 Before: nil, 500 After: encodeDynamicValue(t, cty.ObjectVal(map[string]cty.Value{ 501 "value": cty.StringVal("Hello, world!"), 502 })), 503 }, 504 }, 505 }, 506 }, 507 }, 508 provider: &MockProvider{ 509 GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ 510 ResourceTypes: map[string]providers.Schema{ 511 "test_resource": { 512 Block: &configschema.Block{ 513 Attributes: map[string]*configschema.Attribute{ 514 "value": { 515 Type: cty.String, 516 Required: true, 517 }, 518 }, 519 }, 520 }, 521 }, 522 }, 523 }, 524 expectedStatus: moduletest.Fail, 525 expectedDiags: []tfdiags.Description{ 526 { 527 Summary: "Test assertion failed", 528 Detail: "invalid value", 529 }, 530 }, 531 }, 532 } 533 for name, tc := range tcs { 534 t.Run(name, func(t *testing.T) { 535 config := testModuleInline(t, tc.configs) 536 ctx := testContext2(t, &ContextOpts{ 537 Providers: map[addrs.Provider]providers.Factory{ 538 addrs.NewDefaultProvider("test"): testProviderFuncFixed(tc.provider), 539 }, 540 }) 541 542 run := moduletest.Run{ 543 Config: config.Module.Tests["main.tftest.hcl"].Runs[0], 544 Name: "test_case", 545 } 546 547 tctx := ctx.TestContext(config, tc.state, tc.plan, tc.variables) 548 tctx.EvaluateAgainstPlan(&run) 549 550 if expected, actual := tc.expectedStatus, run.Status; expected != actual { 551 t.Errorf("expected status \"%s\" but got \"%s\"", expected, actual) 552 } 553 554 compareDiagnosticsFromTestResult(t, tc.expectedDiags, run.Diagnostics) 555 }) 556 } 557 } 558 559 func compareDiagnosticsFromTestResult(t *testing.T, expected []tfdiags.Description, actual tfdiags.Diagnostics) { 560 if len(expected) != len(actual) { 561 t.Errorf("found invalid number of diagnostics, expected %d but found %d", len(expected), len(actual)) 562 } 563 564 length := len(expected) 565 if len(actual) > length { 566 length = len(actual) 567 } 568 569 for ix := 0; ix < length; ix++ { 570 if ix >= len(expected) { 571 t.Errorf("found extra diagnostic at %d:\n%v", ix, actual[ix].Description()) 572 } else if ix >= len(actual) { 573 t.Errorf("missing diagnostic at %d:\n%v", ix, expected[ix]) 574 } else { 575 expected := expected[ix] 576 actual := actual[ix].Description() 577 if diff := cmp.Diff(expected, actual); len(diff) > 0 { 578 t.Errorf("found different diagnostics at %d:\nexpected:\n%s\nactual:\n%s\ndiff:%s", ix, expected, actual, diff) 579 } 580 } 581 } 582 } 583 584 func encodeDynamicValue(t *testing.T, value cty.Value) []byte { 585 data, err := ctymsgpack.Marshal(value, value.Type()) 586 if err != nil { 587 t.Fatalf("failed to marshal JSON: %s", err) 588 } 589 return data 590 } 591 592 func encodeCtyValue(t *testing.T, value cty.Value) []byte { 593 data, err := ctyjson.Marshal(value, value.Type()) 594 if err != nil { 595 t.Fatalf("failed to marshal JSON: %s", err) 596 } 597 return data 598 }