github.com/opentofu/opentofu@v1.7.1/internal/tofu/node_resource_validate_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  	"errors"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/hashicorp/hcl/v2"
    14  	"github.com/hashicorp/hcl/v2/hcltest"
    15  	"github.com/opentofu/opentofu/internal/addrs"
    16  	"github.com/opentofu/opentofu/internal/configs"
    17  	"github.com/opentofu/opentofu/internal/configs/configschema"
    18  	"github.com/opentofu/opentofu/internal/lang/marks"
    19  	"github.com/opentofu/opentofu/internal/providers"
    20  	"github.com/opentofu/opentofu/internal/provisioners"
    21  	"github.com/opentofu/opentofu/internal/tfdiags"
    22  	"github.com/zclconf/go-cty/cty"
    23  )
    24  
    25  func TestNodeValidatableResource_ValidateProvisioner_valid(t *testing.T) {
    26  	ctx := &MockEvalContext{}
    27  	ctx.installSimpleEval()
    28  	mp := &MockProvisioner{}
    29  	ps := &configschema.Block{}
    30  	ctx.ProvisionerSchemaSchema = ps
    31  	ctx.ProvisionerProvisioner = mp
    32  
    33  	pc := &configs.Provisioner{
    34  		Type:   "baz",
    35  		Config: hcl.EmptyBody(),
    36  		Connection: &configs.Connection{
    37  			Config: configs.SynthBody("", map[string]cty.Value{
    38  				"host": cty.StringVal("localhost"),
    39  				"type": cty.StringVal("ssh"),
    40  				"port": cty.NumberIntVal(10022),
    41  			}),
    42  		},
    43  	}
    44  
    45  	rc := &configs.Resource{
    46  		Mode:   addrs.ManagedResourceMode,
    47  		Type:   "test_foo",
    48  		Name:   "bar",
    49  		Config: configs.SynthBody("", map[string]cty.Value{}),
    50  	}
    51  
    52  	node := NodeValidatableResource{
    53  		NodeAbstractResource: &NodeAbstractResource{
    54  			Addr:   mustConfigResourceAddr("test_foo.bar"),
    55  			Config: rc,
    56  		},
    57  	}
    58  
    59  	diags := node.validateProvisioner(ctx, pc)
    60  	if diags.HasErrors() {
    61  		t.Fatalf("node.Eval failed: %s", diags.Err())
    62  	}
    63  	if !mp.ValidateProvisionerConfigCalled {
    64  		t.Fatalf("p.ValidateProvisionerConfig not called")
    65  	}
    66  }
    67  
    68  func TestNodeValidatableResource_ValidateProvisioner__warning(t *testing.T) {
    69  	ctx := &MockEvalContext{}
    70  	ctx.installSimpleEval()
    71  	mp := &MockProvisioner{}
    72  	ps := &configschema.Block{}
    73  	ctx.ProvisionerSchemaSchema = ps
    74  	ctx.ProvisionerProvisioner = mp
    75  
    76  	pc := &configs.Provisioner{
    77  		Type:   "baz",
    78  		Config: hcl.EmptyBody(),
    79  	}
    80  
    81  	rc := &configs.Resource{
    82  		Mode:    addrs.ManagedResourceMode,
    83  		Type:    "test_foo",
    84  		Name:    "bar",
    85  		Config:  configs.SynthBody("", map[string]cty.Value{}),
    86  		Managed: &configs.ManagedResource{},
    87  	}
    88  
    89  	node := NodeValidatableResource{
    90  		NodeAbstractResource: &NodeAbstractResource{
    91  			Addr:   mustConfigResourceAddr("test_foo.bar"),
    92  			Config: rc,
    93  		},
    94  	}
    95  
    96  	{
    97  		var diags tfdiags.Diagnostics
    98  		diags = diags.Append(tfdiags.SimpleWarning("foo is deprecated"))
    99  		mp.ValidateProvisionerConfigResponse = provisioners.ValidateProvisionerConfigResponse{
   100  			Diagnostics: diags,
   101  		}
   102  	}
   103  
   104  	diags := node.validateProvisioner(ctx, pc)
   105  	if len(diags) != 1 {
   106  		t.Fatalf("wrong number of diagnostics in %s; want one warning", diags.ErrWithWarnings())
   107  	}
   108  
   109  	if got, want := diags[0].Description().Summary, mp.ValidateProvisionerConfigResponse.Diagnostics[0].Description().Summary; got != want {
   110  		t.Fatalf("wrong warning %q; want %q", got, want)
   111  	}
   112  }
   113  
   114  func TestNodeValidatableResource_ValidateProvisioner__connectionInvalid(t *testing.T) {
   115  	ctx := &MockEvalContext{}
   116  	ctx.installSimpleEval()
   117  	mp := &MockProvisioner{}
   118  	ps := &configschema.Block{}
   119  	ctx.ProvisionerSchemaSchema = ps
   120  	ctx.ProvisionerProvisioner = mp
   121  
   122  	pc := &configs.Provisioner{
   123  		Type:   "baz",
   124  		Config: hcl.EmptyBody(),
   125  		Connection: &configs.Connection{
   126  			Config: configs.SynthBody("", map[string]cty.Value{
   127  				"type":             cty.StringVal("ssh"),
   128  				"bananananananana": cty.StringVal("foo"),
   129  				"bazaz":            cty.StringVal("bar"),
   130  			}),
   131  		},
   132  	}
   133  
   134  	rc := &configs.Resource{
   135  		Mode:    addrs.ManagedResourceMode,
   136  		Type:    "test_foo",
   137  		Name:    "bar",
   138  		Config:  configs.SynthBody("", map[string]cty.Value{}),
   139  		Managed: &configs.ManagedResource{},
   140  	}
   141  
   142  	node := NodeValidatableResource{
   143  		NodeAbstractResource: &NodeAbstractResource{
   144  			Addr:   mustConfigResourceAddr("test_foo.bar"),
   145  			Config: rc,
   146  		},
   147  	}
   148  
   149  	diags := node.validateProvisioner(ctx, pc)
   150  	if !diags.HasErrors() {
   151  		t.Fatalf("node.Eval succeeded; want error")
   152  	}
   153  	if len(diags) != 3 {
   154  		t.Fatalf("wrong number of diagnostics; want two errors\n\n%s", diags.Err())
   155  	}
   156  
   157  	errStr := diags.Err().Error()
   158  	if !(strings.Contains(errStr, "bananananananana") && strings.Contains(errStr, "bazaz")) {
   159  		t.Fatalf("wrong errors %q; want something about each of our invalid connInfo keys", errStr)
   160  	}
   161  }
   162  
   163  func TestNodeValidatableResource_ValidateResource_managedResource(t *testing.T) {
   164  	mp := simpleMockProvider()
   165  	mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse {
   166  		if got, want := req.TypeName, "test_object"; got != want {
   167  			t.Fatalf("wrong resource type\ngot:  %#v\nwant: %#v", got, want)
   168  		}
   169  		if got, want := req.Config.GetAttr("test_string"), cty.StringVal("bar"); !got.RawEquals(want) {
   170  			t.Fatalf("wrong value for test_string\ngot:  %#v\nwant: %#v", got, want)
   171  		}
   172  		if got, want := req.Config.GetAttr("test_number"), cty.NumberIntVal(2); !got.RawEquals(want) {
   173  			t.Fatalf("wrong value for test_number\ngot:  %#v\nwant: %#v", got, want)
   174  		}
   175  		return providers.ValidateResourceConfigResponse{}
   176  	}
   177  
   178  	p := providers.Interface(mp)
   179  	rc := &configs.Resource{
   180  		Mode: addrs.ManagedResourceMode,
   181  		Type: "test_object",
   182  		Name: "foo",
   183  		Config: configs.SynthBody("", map[string]cty.Value{
   184  			"test_string": cty.StringVal("bar"),
   185  			"test_number": cty.NumberIntVal(2).Mark(marks.Sensitive),
   186  		}),
   187  	}
   188  	node := NodeValidatableResource{
   189  		NodeAbstractResource: &NodeAbstractResource{
   190  			Addr:             mustConfigResourceAddr("test_foo.bar"),
   191  			Config:           rc,
   192  			ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
   193  		},
   194  	}
   195  
   196  	ctx := &MockEvalContext{}
   197  	ctx.installSimpleEval()
   198  	ctx.ProviderSchemaSchema = mp.GetProviderSchema()
   199  	ctx.ProviderProvider = p
   200  
   201  	err := node.validateResource(ctx)
   202  	if err != nil {
   203  		t.Fatalf("err: %s", err)
   204  	}
   205  
   206  	if !mp.ValidateResourceConfigCalled {
   207  		t.Fatal("Expected ValidateResourceConfig to be called, but it was not!")
   208  	}
   209  }
   210  
   211  func TestNodeValidatableResource_ValidateResource_managedResourceCount(t *testing.T) {
   212  	// Setup
   213  	mp := simpleMockProvider()
   214  	mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse {
   215  		if got, want := req.TypeName, "test_object"; got != want {
   216  			t.Fatalf("wrong resource type\ngot:  %#v\nwant: %#v", got, want)
   217  		}
   218  		if got, want := req.Config.GetAttr("test_string"), cty.StringVal("bar"); !got.RawEquals(want) {
   219  			t.Fatalf("wrong value for test_string\ngot:  %#v\nwant: %#v", got, want)
   220  		}
   221  		return providers.ValidateResourceConfigResponse{}
   222  	}
   223  
   224  	p := providers.Interface(mp)
   225  
   226  	ctx := &MockEvalContext{}
   227  	ctx.installSimpleEval()
   228  	ctx.ProviderSchemaSchema = mp.GetProviderSchema()
   229  	ctx.ProviderProvider = p
   230  
   231  	tests := []struct {
   232  		name  string
   233  		count hcl.Expression
   234  	}{
   235  		{
   236  			"simple count",
   237  			hcltest.MockExprLiteral(cty.NumberIntVal(2)),
   238  		},
   239  		{
   240  			"marked count value",
   241  			hcltest.MockExprLiteral(cty.NumberIntVal(3).Mark("marked")),
   242  		},
   243  	}
   244  
   245  	for _, test := range tests {
   246  		t.Run(test.name, func(t *testing.T) {
   247  			rc := &configs.Resource{
   248  				Mode:  addrs.ManagedResourceMode,
   249  				Type:  "test_object",
   250  				Name:  "foo",
   251  				Count: test.count,
   252  				Config: configs.SynthBody("", map[string]cty.Value{
   253  					"test_string": cty.StringVal("bar"),
   254  				}),
   255  			}
   256  			node := NodeValidatableResource{
   257  				NodeAbstractResource: &NodeAbstractResource{
   258  					Addr:             mustConfigResourceAddr("test_foo.bar"),
   259  					Config:           rc,
   260  					ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
   261  				},
   262  			}
   263  
   264  			diags := node.validateResource(ctx)
   265  			if diags.HasErrors() {
   266  				t.Fatalf("err: %s", diags.Err())
   267  			}
   268  
   269  			if !mp.ValidateResourceConfigCalled {
   270  				t.Fatal("Expected ValidateResourceConfig to be called, but it was not!")
   271  			}
   272  		})
   273  	}
   274  }
   275  
   276  func TestNodeValidatableResource_ValidateResource_dataSource(t *testing.T) {
   277  	mp := simpleMockProvider()
   278  	mp.ValidateDataResourceConfigFn = func(req providers.ValidateDataResourceConfigRequest) providers.ValidateDataResourceConfigResponse {
   279  		if got, want := req.TypeName, "test_object"; got != want {
   280  			t.Fatalf("wrong resource type\ngot:  %#v\nwant: %#v", got, want)
   281  		}
   282  		if got, want := req.Config.GetAttr("test_string"), cty.StringVal("bar"); !got.RawEquals(want) {
   283  			t.Fatalf("wrong value for test_string\ngot:  %#v\nwant: %#v", got, want)
   284  		}
   285  		if got, want := req.Config.GetAttr("test_number"), cty.NumberIntVal(2); !got.RawEquals(want) {
   286  			t.Fatalf("wrong value for test_number\ngot:  %#v\nwant: %#v", got, want)
   287  		}
   288  		return providers.ValidateDataResourceConfigResponse{}
   289  	}
   290  
   291  	p := providers.Interface(mp)
   292  	rc := &configs.Resource{
   293  		Mode: addrs.DataResourceMode,
   294  		Type: "test_object",
   295  		Name: "foo",
   296  		Config: configs.SynthBody("", map[string]cty.Value{
   297  			"test_string": cty.StringVal("bar"),
   298  			"test_number": cty.NumberIntVal(2).Mark(marks.Sensitive),
   299  		}),
   300  	}
   301  
   302  	node := NodeValidatableResource{
   303  		NodeAbstractResource: &NodeAbstractResource{
   304  			Addr:             mustConfigResourceAddr("test_foo.bar"),
   305  			Config:           rc,
   306  			ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
   307  		},
   308  	}
   309  
   310  	ctx := &MockEvalContext{}
   311  	ctx.installSimpleEval()
   312  	ctx.ProviderSchemaSchema = mp.GetProviderSchema()
   313  	ctx.ProviderProvider = p
   314  
   315  	diags := node.validateResource(ctx)
   316  	if diags.HasErrors() {
   317  		t.Fatalf("err: %s", diags.Err())
   318  	}
   319  
   320  	if !mp.ValidateDataResourceConfigCalled {
   321  		t.Fatal("Expected ValidateDataSourceConfig to be called, but it was not!")
   322  	}
   323  }
   324  
   325  func TestNodeValidatableResource_ValidateResource_valid(t *testing.T) {
   326  	mp := simpleMockProvider()
   327  	mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse {
   328  		return providers.ValidateResourceConfigResponse{}
   329  	}
   330  
   331  	p := providers.Interface(mp)
   332  	rc := &configs.Resource{
   333  		Mode:   addrs.ManagedResourceMode,
   334  		Type:   "test_object",
   335  		Name:   "foo",
   336  		Config: configs.SynthBody("", map[string]cty.Value{}),
   337  	}
   338  	node := NodeValidatableResource{
   339  		NodeAbstractResource: &NodeAbstractResource{
   340  			Addr:             mustConfigResourceAddr("test_object.foo"),
   341  			Config:           rc,
   342  			ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
   343  		},
   344  	}
   345  
   346  	ctx := &MockEvalContext{}
   347  	ctx.installSimpleEval()
   348  	ctx.ProviderSchemaSchema = mp.GetProviderSchema()
   349  	ctx.ProviderProvider = p
   350  
   351  	diags := node.validateResource(ctx)
   352  	if diags.HasErrors() {
   353  		t.Fatalf("err: %s", diags.Err())
   354  	}
   355  }
   356  
   357  func TestNodeValidatableResource_ValidateResource_warningsAndErrorsPassedThrough(t *testing.T) {
   358  	mp := simpleMockProvider()
   359  	mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse {
   360  		var diags tfdiags.Diagnostics
   361  		diags = diags.Append(tfdiags.SimpleWarning("warn"))
   362  		diags = diags.Append(errors.New("err"))
   363  		return providers.ValidateResourceConfigResponse{
   364  			Diagnostics: diags,
   365  		}
   366  	}
   367  
   368  	p := providers.Interface(mp)
   369  	rc := &configs.Resource{
   370  		Mode:   addrs.ManagedResourceMode,
   371  		Type:   "test_object",
   372  		Name:   "foo",
   373  		Config: configs.SynthBody("", map[string]cty.Value{}),
   374  	}
   375  	node := NodeValidatableResource{
   376  		NodeAbstractResource: &NodeAbstractResource{
   377  			Addr:             mustConfigResourceAddr("test_foo.bar"),
   378  			Config:           rc,
   379  			ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
   380  		},
   381  	}
   382  
   383  	ctx := &MockEvalContext{}
   384  	ctx.installSimpleEval()
   385  	ctx.ProviderSchemaSchema = mp.GetProviderSchema()
   386  	ctx.ProviderProvider = p
   387  
   388  	diags := node.validateResource(ctx)
   389  	if !diags.HasErrors() {
   390  		t.Fatal("unexpected success; want error")
   391  	}
   392  
   393  	bySeverity := map[tfdiags.Severity]tfdiags.Diagnostics{}
   394  	for _, diag := range diags {
   395  		bySeverity[diag.Severity()] = append(bySeverity[diag.Severity()], diag)
   396  	}
   397  	if len(bySeverity[tfdiags.Warning]) != 1 || bySeverity[tfdiags.Warning][0].Description().Summary != "warn" {
   398  		t.Errorf("Expected 1 warning 'warn', got: %s", bySeverity[tfdiags.Warning].ErrWithWarnings())
   399  	}
   400  	if len(bySeverity[tfdiags.Error]) != 1 || bySeverity[tfdiags.Error][0].Description().Summary != "err" {
   401  		t.Errorf("Expected 1 error 'err', got: %s", bySeverity[tfdiags.Error].Err())
   402  	}
   403  }
   404  
   405  func TestNodeValidatableResource_ValidateResource_invalidDependsOn(t *testing.T) {
   406  	mp := simpleMockProvider()
   407  	mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse {
   408  		return providers.ValidateResourceConfigResponse{}
   409  	}
   410  
   411  	// We'll check a _valid_ config first, to make sure we're not failing
   412  	// for some other reason, and then make it invalid.
   413  	p := providers.Interface(mp)
   414  	rc := &configs.Resource{
   415  		Mode:   addrs.ManagedResourceMode,
   416  		Type:   "test_object",
   417  		Name:   "foo",
   418  		Config: configs.SynthBody("", map[string]cty.Value{}),
   419  		DependsOn: []hcl.Traversal{
   420  			// Depending on path.module is pointless, since it is immediately
   421  			// available, but we allow all of the referencable addrs here
   422  			// for consistency: referencing them is harmless, and avoids the
   423  			// need for us to document a different subset of addresses that
   424  			// are valid in depends_on.
   425  			// For the sake of this test, it's a valid address we can use that
   426  			// doesn't require something else to exist in the configuration.
   427  			{
   428  				hcl.TraverseRoot{
   429  					Name: "path",
   430  				},
   431  				hcl.TraverseAttr{
   432  					Name: "module",
   433  				},
   434  			},
   435  		},
   436  	}
   437  	node := NodeValidatableResource{
   438  		NodeAbstractResource: &NodeAbstractResource{
   439  			Addr:             mustConfigResourceAddr("test_foo.bar"),
   440  			Config:           rc,
   441  			ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
   442  		},
   443  	}
   444  
   445  	ctx := &MockEvalContext{}
   446  	ctx.installSimpleEval()
   447  
   448  	ctx.ProviderSchemaSchema = mp.GetProviderSchema()
   449  	ctx.ProviderProvider = p
   450  
   451  	diags := node.validateResource(ctx)
   452  	if diags.HasErrors() {
   453  		t.Fatalf("error for supposedly-valid config: %s", diags.ErrWithWarnings())
   454  	}
   455  
   456  	// Now we'll make it invalid by adding additional traversal steps at
   457  	// the end of what we're referencing. This is intended to catch the
   458  	// situation where the user tries to depend on e.g. a specific resource
   459  	// attribute, rather than the whole resource, like aws_instance.foo.id.
   460  	rc.DependsOn = append(rc.DependsOn, hcl.Traversal{
   461  		hcl.TraverseRoot{
   462  			Name: "path",
   463  		},
   464  		hcl.TraverseAttr{
   465  			Name: "module",
   466  		},
   467  		hcl.TraverseAttr{
   468  			Name: "extra",
   469  		},
   470  	})
   471  
   472  	diags = node.validateResource(ctx)
   473  	if !diags.HasErrors() {
   474  		t.Fatal("no error for invalid depends_on")
   475  	}
   476  	if got, want := diags.Err().Error(), "Invalid depends_on reference"; !strings.Contains(got, want) {
   477  		t.Fatalf("wrong error\ngot:  %s\nwant: Message containing %q", got, want)
   478  	}
   479  
   480  	// Test for handling an unknown root without attribute, like a
   481  	// typo that omits the dot inbetween "path.module".
   482  	rc.DependsOn = append(rc.DependsOn, hcl.Traversal{
   483  		hcl.TraverseRoot{
   484  			Name: "pathmodule",
   485  		},
   486  	})
   487  
   488  	diags = node.validateResource(ctx)
   489  	if !diags.HasErrors() {
   490  		t.Fatal("no error for invalid depends_on")
   491  	}
   492  	if got, want := diags.Err().Error(), "Invalid depends_on reference"; !strings.Contains(got, want) {
   493  		t.Fatalf("wrong error\ngot:  %s\nwant: Message containing %q", got, want)
   494  	}
   495  }
   496  
   497  func TestNodeValidatableResource_ValidateResource_invalidIgnoreChangesNonexistent(t *testing.T) {
   498  	mp := simpleMockProvider()
   499  	mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse {
   500  		return providers.ValidateResourceConfigResponse{}
   501  	}
   502  
   503  	// We'll check a _valid_ config first, to make sure we're not failing
   504  	// for some other reason, and then make it invalid.
   505  	p := providers.Interface(mp)
   506  	rc := &configs.Resource{
   507  		Mode:   addrs.ManagedResourceMode,
   508  		Type:   "test_object",
   509  		Name:   "foo",
   510  		Config: configs.SynthBody("", map[string]cty.Value{}),
   511  		Managed: &configs.ManagedResource{
   512  			IgnoreChanges: []hcl.Traversal{
   513  				{
   514  					hcl.TraverseAttr{
   515  						Name: "test_string",
   516  					},
   517  				},
   518  			},
   519  		},
   520  	}
   521  	node := NodeValidatableResource{
   522  		NodeAbstractResource: &NodeAbstractResource{
   523  			Addr:             mustConfigResourceAddr("test_foo.bar"),
   524  			Config:           rc,
   525  			ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
   526  		},
   527  	}
   528  
   529  	ctx := &MockEvalContext{}
   530  	ctx.installSimpleEval()
   531  
   532  	ctx.ProviderSchemaSchema = mp.GetProviderSchema()
   533  	ctx.ProviderProvider = p
   534  
   535  	diags := node.validateResource(ctx)
   536  	if diags.HasErrors() {
   537  		t.Fatalf("error for supposedly-valid config: %s", diags.ErrWithWarnings())
   538  	}
   539  
   540  	// Now we'll make it invalid by attempting to ignore a nonexistent
   541  	// attribute.
   542  	rc.Managed.IgnoreChanges = append(rc.Managed.IgnoreChanges, hcl.Traversal{
   543  		hcl.TraverseAttr{
   544  			Name: "nonexistent",
   545  		},
   546  	})
   547  
   548  	diags = node.validateResource(ctx)
   549  	if !diags.HasErrors() {
   550  		t.Fatal("no error for invalid ignore_changes")
   551  	}
   552  	if got, want := diags.Err().Error(), "Unsupported attribute: This object has no argument, nested block, or exported attribute named \"nonexistent\""; !strings.Contains(got, want) {
   553  		t.Fatalf("wrong error\ngot:  %s\nwant: Message containing %q", got, want)
   554  	}
   555  }
   556  
   557  func TestNodeValidatableResource_ValidateResource_invalidIgnoreChangesComputed(t *testing.T) {
   558  	// construct a schema with a computed attribute
   559  	ms := &configschema.Block{
   560  		Attributes: map[string]*configschema.Attribute{
   561  			"test_string": {
   562  				Type:     cty.String,
   563  				Optional: true,
   564  			},
   565  			"computed_string": {
   566  				Type:     cty.String,
   567  				Computed: true,
   568  				Optional: false,
   569  			},
   570  		},
   571  	}
   572  
   573  	mp := &MockProvider{
   574  		GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
   575  			Provider: providers.Schema{Block: ms},
   576  			ResourceTypes: map[string]providers.Schema{
   577  				"test_object": providers.Schema{Block: ms},
   578  			},
   579  		},
   580  	}
   581  
   582  	mp.ValidateResourceConfigFn = func(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse {
   583  		return providers.ValidateResourceConfigResponse{}
   584  	}
   585  
   586  	// We'll check a _valid_ config first, to make sure we're not failing
   587  	// for some other reason, and then make it invalid.
   588  	p := providers.Interface(mp)
   589  	rc := &configs.Resource{
   590  		Mode:   addrs.ManagedResourceMode,
   591  		Type:   "test_object",
   592  		Name:   "foo",
   593  		Config: configs.SynthBody("", map[string]cty.Value{}),
   594  		Managed: &configs.ManagedResource{
   595  			IgnoreChanges: []hcl.Traversal{
   596  				{
   597  					hcl.TraverseAttr{
   598  						Name: "test_string",
   599  					},
   600  				},
   601  			},
   602  		},
   603  	}
   604  	node := NodeValidatableResource{
   605  		NodeAbstractResource: &NodeAbstractResource{
   606  			Addr:             mustConfigResourceAddr("test_foo.bar"),
   607  			Config:           rc,
   608  			ResolvedProvider: mustProviderConfig(`provider["registry.opentofu.org/hashicorp/aws"]`),
   609  		},
   610  	}
   611  
   612  	ctx := &MockEvalContext{}
   613  	ctx.installSimpleEval()
   614  
   615  	ctx.ProviderSchemaSchema = mp.GetProviderSchema()
   616  	ctx.ProviderProvider = p
   617  
   618  	diags := node.validateResource(ctx)
   619  	if diags.HasErrors() {
   620  		t.Fatalf("error for supposedly-valid config: %s", diags.ErrWithWarnings())
   621  	}
   622  
   623  	// Now we'll make it invalid by attempting to ignore a computed
   624  	// attribute.
   625  	rc.Managed.IgnoreChanges = append(rc.Managed.IgnoreChanges, hcl.Traversal{
   626  		hcl.TraverseAttr{
   627  			Name: "computed_string",
   628  		},
   629  	})
   630  
   631  	diags = node.validateResource(ctx)
   632  	if diags.HasErrors() {
   633  		t.Fatalf("got unexpected error: %s", diags.ErrWithWarnings())
   634  	}
   635  	if got, want := diags.ErrWithWarnings().Error(), `Redundant ignore_changes element: Adding an attribute name to ignore_changes tells OpenTofu to ignore future changes to the argument in configuration after the object has been created, retaining the value originally configured.
   636  
   637  The attribute computed_string is decided by the provider alone and therefore there can be no configured value to compare with. Including this attribute in ignore_changes has no effect. Remove the attribute from ignore_changes to quiet this warning.`; !strings.Contains(got, want) {
   638  		t.Fatalf("wrong error\ngot:  %s\nwant: Message containing %q", got, want)
   639  	}
   640  }