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  }