github.com/opentofu/opentofu@v1.7.1/internal/command/jsonstate/state_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 jsonstate
     7  
     8  import (
     9  	"encoding/json"
    10  	"reflect"
    11  	"testing"
    12  
    13  	"github.com/google/go-cmp/cmp"
    14  	"github.com/zclconf/go-cty/cty"
    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/providers"
    20  	"github.com/opentofu/opentofu/internal/states"
    21  	"github.com/opentofu/opentofu/internal/tofu"
    22  )
    23  
    24  func TestMarshalOutputs(t *testing.T) {
    25  	tests := []struct {
    26  		Outputs map[string]*states.OutputValue
    27  		Want    map[string]Output
    28  		Err     bool
    29  	}{
    30  		{
    31  			nil,
    32  			nil,
    33  			false,
    34  		},
    35  		{
    36  			map[string]*states.OutputValue{
    37  				"test": {
    38  					Sensitive: true,
    39  					Value:     cty.StringVal("sekret"),
    40  				},
    41  			},
    42  			map[string]Output{
    43  				"test": {
    44  					Sensitive: true,
    45  					Value:     json.RawMessage(`"sekret"`),
    46  					Type:      json.RawMessage(`"string"`),
    47  				},
    48  			},
    49  			false,
    50  		},
    51  		{
    52  			map[string]*states.OutputValue{
    53  				"test": {
    54  					Sensitive: false,
    55  					Value:     cty.StringVal("not_so_sekret"),
    56  				},
    57  			},
    58  			map[string]Output{
    59  				"test": {
    60  					Sensitive: false,
    61  					Value:     json.RawMessage(`"not_so_sekret"`),
    62  					Type:      json.RawMessage(`"string"`),
    63  				},
    64  			},
    65  			false,
    66  		},
    67  		{
    68  			map[string]*states.OutputValue{
    69  				"mapstring": {
    70  					Sensitive: false,
    71  					Value: cty.MapVal(map[string]cty.Value{
    72  						"beep": cty.StringVal("boop"),
    73  					}),
    74  				},
    75  				"setnumber": {
    76  					Sensitive: false,
    77  					Value: cty.SetVal([]cty.Value{
    78  						cty.NumberIntVal(3),
    79  						cty.NumberIntVal(5),
    80  						cty.NumberIntVal(7),
    81  						cty.NumberIntVal(11),
    82  					}),
    83  				},
    84  			},
    85  			map[string]Output{
    86  				"mapstring": {
    87  					Sensitive: false,
    88  					Value:     json.RawMessage(`{"beep":"boop"}`),
    89  					Type:      json.RawMessage(`["map","string"]`),
    90  				},
    91  				"setnumber": {
    92  					Sensitive: false,
    93  					Value:     json.RawMessage(`[3,5,7,11]`),
    94  					Type:      json.RawMessage(`["set","number"]`),
    95  				},
    96  			},
    97  			false,
    98  		},
    99  	}
   100  
   101  	for _, test := range tests {
   102  		got, err := MarshalOutputs(test.Outputs)
   103  		if test.Err {
   104  			if err == nil {
   105  				t.Fatal("succeeded; want error")
   106  			}
   107  			return
   108  		} else if err != nil {
   109  			t.Fatalf("unexpected error: %s", err)
   110  		}
   111  		if !cmp.Equal(test.Want, got) {
   112  			t.Fatalf("wrong result:\n%s", cmp.Diff(test.Want, got))
   113  		}
   114  	}
   115  }
   116  
   117  func TestMarshalAttributeValues(t *testing.T) {
   118  	tests := []struct {
   119  		Attr cty.Value
   120  		Want AttributeValues
   121  	}{
   122  		{
   123  			cty.NilVal,
   124  			nil,
   125  		},
   126  		{
   127  			cty.NullVal(cty.String),
   128  			nil,
   129  		},
   130  		{
   131  			cty.ObjectVal(map[string]cty.Value{
   132  				"foo": cty.StringVal("bar"),
   133  			}),
   134  			AttributeValues{"foo": json.RawMessage(`"bar"`)},
   135  		},
   136  		{
   137  			cty.ObjectVal(map[string]cty.Value{
   138  				"foo": cty.NullVal(cty.String),
   139  			}),
   140  			AttributeValues{"foo": json.RawMessage(`null`)},
   141  		},
   142  		{
   143  			cty.ObjectVal(map[string]cty.Value{
   144  				"bar": cty.MapVal(map[string]cty.Value{
   145  					"hello": cty.StringVal("world"),
   146  				}),
   147  				"baz": cty.ListVal([]cty.Value{
   148  					cty.StringVal("goodnight"),
   149  					cty.StringVal("moon"),
   150  				}),
   151  			}),
   152  			AttributeValues{
   153  				"bar": json.RawMessage(`{"hello":"world"}`),
   154  				"baz": json.RawMessage(`["goodnight","moon"]`),
   155  			},
   156  		},
   157  		// Marked values
   158  		{
   159  			cty.ObjectVal(map[string]cty.Value{
   160  				"bar": cty.MapVal(map[string]cty.Value{
   161  					"hello": cty.StringVal("world"),
   162  				}),
   163  				"baz": cty.ListVal([]cty.Value{
   164  					cty.StringVal("goodnight"),
   165  					cty.StringVal("moon").Mark(marks.Sensitive),
   166  				}),
   167  			}),
   168  			AttributeValues{
   169  				"bar": json.RawMessage(`{"hello":"world"}`),
   170  				"baz": json.RawMessage(`["goodnight","moon"]`),
   171  			},
   172  		},
   173  	}
   174  
   175  	for _, test := range tests {
   176  		got := marshalAttributeValues(test.Attr)
   177  		eq := reflect.DeepEqual(got, test.Want)
   178  		if !eq {
   179  			t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want)
   180  		}
   181  	}
   182  }
   183  
   184  func TestMarshalResources(t *testing.T) {
   185  	deposedKey := states.NewDeposedKey()
   186  	tests := map[string]struct {
   187  		Resources map[string]*states.Resource
   188  		Schemas   *tofu.Schemas
   189  		Want      []Resource
   190  		Err       bool
   191  	}{
   192  		"nil": {
   193  			nil,
   194  			nil,
   195  			nil,
   196  			false,
   197  		},
   198  		"single resource": {
   199  			map[string]*states.Resource{
   200  				"test_thing.baz": {
   201  					Addr: addrs.AbsResource{
   202  						Resource: addrs.Resource{
   203  							Mode: addrs.ManagedResourceMode,
   204  							Type: "test_thing",
   205  							Name: "bar",
   206  						},
   207  					},
   208  					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
   209  						addrs.NoKey: {
   210  							Current: &states.ResourceInstanceObjectSrc{
   211  								Status:    states.ObjectReady,
   212  								AttrsJSON: []byte(`{"woozles":"confuzles"}`),
   213  							},
   214  						},
   215  					},
   216  					ProviderConfig: addrs.AbsProviderConfig{
   217  						Provider: addrs.NewDefaultProvider("test"),
   218  						Module:   addrs.RootModule,
   219  					},
   220  				},
   221  			},
   222  			testSchemas(),
   223  			[]Resource{
   224  				{
   225  					Address:      "test_thing.bar",
   226  					Mode:         "managed",
   227  					Type:         "test_thing",
   228  					Name:         "bar",
   229  					Index:        nil,
   230  					ProviderName: "registry.opentofu.org/hashicorp/test",
   231  					AttributeValues: AttributeValues{
   232  						"foozles": json.RawMessage(`null`),
   233  						"woozles": json.RawMessage(`"confuzles"`),
   234  					},
   235  					SensitiveValues: json.RawMessage("{\"foozles\":true}"),
   236  				},
   237  			},
   238  			false,
   239  		},
   240  		"single resource_with_sensitive": {
   241  			map[string]*states.Resource{
   242  				"test_thing.baz": {
   243  					Addr: addrs.AbsResource{
   244  						Resource: addrs.Resource{
   245  							Mode: addrs.ManagedResourceMode,
   246  							Type: "test_thing",
   247  							Name: "bar",
   248  						},
   249  					},
   250  					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
   251  						addrs.NoKey: {
   252  							Current: &states.ResourceInstanceObjectSrc{
   253  								Status:    states.ObjectReady,
   254  								AttrsJSON: []byte(`{"woozles":"confuzles","foozles":"sensuzles"}`),
   255  							},
   256  						},
   257  					},
   258  					ProviderConfig: addrs.AbsProviderConfig{
   259  						Provider: addrs.NewDefaultProvider("test"),
   260  						Module:   addrs.RootModule,
   261  					},
   262  				},
   263  			},
   264  			testSchemas(),
   265  			[]Resource{
   266  				{
   267  					Address:      "test_thing.bar",
   268  					Mode:         "managed",
   269  					Type:         "test_thing",
   270  					Name:         "bar",
   271  					Index:        nil,
   272  					ProviderName: "registry.opentofu.org/hashicorp/test",
   273  					AttributeValues: AttributeValues{
   274  						"foozles": json.RawMessage(`"sensuzles"`),
   275  						"woozles": json.RawMessage(`"confuzles"`),
   276  					},
   277  					SensitiveValues: json.RawMessage("{\"foozles\":true}"),
   278  				},
   279  			},
   280  			false,
   281  		},
   282  		"resource with marks": {
   283  			map[string]*states.Resource{
   284  				"test_thing.bar": {
   285  					Addr: addrs.AbsResource{
   286  						Resource: addrs.Resource{
   287  							Mode: addrs.ManagedResourceMode,
   288  							Type: "test_thing",
   289  							Name: "bar",
   290  						},
   291  					},
   292  					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
   293  						addrs.NoKey: {
   294  							Current: &states.ResourceInstanceObjectSrc{
   295  								Status:    states.ObjectReady,
   296  								AttrsJSON: []byte(`{"foozles":"confuzles"}`),
   297  								AttrSensitivePaths: []cty.PathValueMarks{{
   298  									Path:  cty.Path{cty.GetAttrStep{Name: "foozles"}},
   299  									Marks: cty.NewValueMarks(marks.Sensitive)},
   300  								},
   301  							},
   302  						},
   303  					},
   304  					ProviderConfig: addrs.AbsProviderConfig{
   305  						Provider: addrs.NewDefaultProvider("test"),
   306  						Module:   addrs.RootModule,
   307  					},
   308  				},
   309  			},
   310  			testSchemas(),
   311  			[]Resource{
   312  				{
   313  					Address:      "test_thing.bar",
   314  					Mode:         "managed",
   315  					Type:         "test_thing",
   316  					Name:         "bar",
   317  					Index:        nil,
   318  					ProviderName: "registry.opentofu.org/hashicorp/test",
   319  					AttributeValues: AttributeValues{
   320  						"foozles": json.RawMessage(`"confuzles"`),
   321  						"woozles": json.RawMessage(`null`),
   322  					},
   323  					SensitiveValues: json.RawMessage(`{"foozles":true}`),
   324  				},
   325  			},
   326  			false,
   327  		},
   328  		"single resource wrong schema": {
   329  			map[string]*states.Resource{
   330  				"test_thing.baz": {
   331  					Addr: addrs.AbsResource{
   332  						Resource: addrs.Resource{
   333  							Mode: addrs.ManagedResourceMode,
   334  							Type: "test_thing",
   335  							Name: "bar",
   336  						},
   337  					},
   338  					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
   339  						addrs.NoKey: {
   340  							Current: &states.ResourceInstanceObjectSrc{
   341  								SchemaVersion: 1,
   342  								Status:        states.ObjectReady,
   343  								AttrsJSON:     []byte(`{"woozles":["confuzles"]}`),
   344  							},
   345  						},
   346  					},
   347  					ProviderConfig: addrs.AbsProviderConfig{
   348  						Provider: addrs.NewDefaultProvider("test"),
   349  						Module:   addrs.RootModule,
   350  					},
   351  				},
   352  			},
   353  			testSchemas(),
   354  			nil,
   355  			true,
   356  		},
   357  		"resource with count": {
   358  			map[string]*states.Resource{
   359  				"test_thing.bar": {
   360  					Addr: addrs.AbsResource{
   361  						Resource: addrs.Resource{
   362  							Mode: addrs.ManagedResourceMode,
   363  							Type: "test_thing",
   364  							Name: "bar",
   365  						},
   366  					},
   367  					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
   368  						addrs.IntKey(0): {
   369  							Current: &states.ResourceInstanceObjectSrc{
   370  								Status:    states.ObjectReady,
   371  								AttrsJSON: []byte(`{"woozles":"confuzles"}`),
   372  							},
   373  						},
   374  					},
   375  					ProviderConfig: addrs.AbsProviderConfig{
   376  						Provider: addrs.NewDefaultProvider("test"),
   377  						Module:   addrs.RootModule,
   378  					},
   379  				},
   380  			},
   381  			testSchemas(),
   382  			[]Resource{
   383  				{
   384  					Address:      "test_thing.bar[0]",
   385  					Mode:         "managed",
   386  					Type:         "test_thing",
   387  					Name:         "bar",
   388  					Index:        json.RawMessage(`0`),
   389  					ProviderName: "registry.opentofu.org/hashicorp/test",
   390  					AttributeValues: AttributeValues{
   391  						"foozles": json.RawMessage(`null`),
   392  						"woozles": json.RawMessage(`"confuzles"`),
   393  					},
   394  					SensitiveValues: json.RawMessage("{\"foozles\":true}"),
   395  				},
   396  			},
   397  			false,
   398  		},
   399  		"resource with for_each": {
   400  			map[string]*states.Resource{
   401  				"test_thing.bar": {
   402  					Addr: addrs.AbsResource{
   403  						Resource: addrs.Resource{
   404  							Mode: addrs.ManagedResourceMode,
   405  							Type: "test_thing",
   406  							Name: "bar",
   407  						},
   408  					},
   409  					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
   410  						addrs.StringKey("rockhopper"): {
   411  							Current: &states.ResourceInstanceObjectSrc{
   412  								Status:    states.ObjectReady,
   413  								AttrsJSON: []byte(`{"woozles":"confuzles"}`),
   414  							},
   415  						},
   416  					},
   417  					ProviderConfig: addrs.AbsProviderConfig{
   418  						Provider: addrs.NewDefaultProvider("test"),
   419  						Module:   addrs.RootModule,
   420  					},
   421  				},
   422  			},
   423  			testSchemas(),
   424  			[]Resource{
   425  				{
   426  					Address:      "test_thing.bar[\"rockhopper\"]",
   427  					Mode:         "managed",
   428  					Type:         "test_thing",
   429  					Name:         "bar",
   430  					Index:        json.RawMessage(`"rockhopper"`),
   431  					ProviderName: "registry.opentofu.org/hashicorp/test",
   432  					AttributeValues: AttributeValues{
   433  						"foozles": json.RawMessage(`null`),
   434  						"woozles": json.RawMessage(`"confuzles"`),
   435  					},
   436  					SensitiveValues: json.RawMessage("{\"foozles\":true}"),
   437  				},
   438  			},
   439  			false,
   440  		},
   441  		"deposed resource": {
   442  			map[string]*states.Resource{
   443  				"test_thing.baz": {
   444  					Addr: addrs.AbsResource{
   445  						Resource: addrs.Resource{
   446  							Mode: addrs.ManagedResourceMode,
   447  							Type: "test_thing",
   448  							Name: "bar",
   449  						},
   450  					},
   451  					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
   452  						addrs.NoKey: {
   453  							Deposed: map[states.DeposedKey]*states.ResourceInstanceObjectSrc{
   454  								states.DeposedKey(deposedKey): {
   455  									Status:    states.ObjectReady,
   456  									AttrsJSON: []byte(`{"woozles":"confuzles"}`),
   457  								},
   458  							},
   459  						},
   460  					},
   461  					ProviderConfig: addrs.AbsProviderConfig{
   462  						Provider: addrs.NewDefaultProvider("test"),
   463  						Module:   addrs.RootModule,
   464  					},
   465  				},
   466  			},
   467  			testSchemas(),
   468  			[]Resource{
   469  				{
   470  					Address:      "test_thing.bar",
   471  					Mode:         "managed",
   472  					Type:         "test_thing",
   473  					Name:         "bar",
   474  					Index:        nil,
   475  					ProviderName: "registry.opentofu.org/hashicorp/test",
   476  					DeposedKey:   deposedKey.String(),
   477  					AttributeValues: AttributeValues{
   478  						"foozles": json.RawMessage(`null`),
   479  						"woozles": json.RawMessage(`"confuzles"`),
   480  					},
   481  					SensitiveValues: json.RawMessage("{\"foozles\":true}"),
   482  				},
   483  			},
   484  			false,
   485  		},
   486  		"deposed and current resource": {
   487  			map[string]*states.Resource{
   488  				"test_thing.baz": {
   489  					Addr: addrs.AbsResource{
   490  						Resource: addrs.Resource{
   491  							Mode: addrs.ManagedResourceMode,
   492  							Type: "test_thing",
   493  							Name: "bar",
   494  						},
   495  					},
   496  					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
   497  						addrs.NoKey: {
   498  							Deposed: map[states.DeposedKey]*states.ResourceInstanceObjectSrc{
   499  								states.DeposedKey(deposedKey): {
   500  									Status:    states.ObjectReady,
   501  									AttrsJSON: []byte(`{"woozles":"confuzles"}`),
   502  								},
   503  							},
   504  							Current: &states.ResourceInstanceObjectSrc{
   505  								Status:    states.ObjectReady,
   506  								AttrsJSON: []byte(`{"woozles":"confuzles"}`),
   507  							},
   508  						},
   509  					},
   510  					ProviderConfig: addrs.AbsProviderConfig{
   511  						Provider: addrs.NewDefaultProvider("test"),
   512  						Module:   addrs.RootModule,
   513  					},
   514  				},
   515  			},
   516  			testSchemas(),
   517  			[]Resource{
   518  				{
   519  					Address:      "test_thing.bar",
   520  					Mode:         "managed",
   521  					Type:         "test_thing",
   522  					Name:         "bar",
   523  					Index:        nil,
   524  					ProviderName: "registry.opentofu.org/hashicorp/test",
   525  					AttributeValues: AttributeValues{
   526  						"foozles": json.RawMessage(`null`),
   527  						"woozles": json.RawMessage(`"confuzles"`),
   528  					},
   529  					SensitiveValues: json.RawMessage("{\"foozles\":true}"),
   530  				},
   531  				{
   532  					Address:      "test_thing.bar",
   533  					Mode:         "managed",
   534  					Type:         "test_thing",
   535  					Name:         "bar",
   536  					Index:        nil,
   537  					ProviderName: "registry.opentofu.org/hashicorp/test",
   538  					DeposedKey:   deposedKey.String(),
   539  					AttributeValues: AttributeValues{
   540  						"foozles": json.RawMessage(`null`),
   541  						"woozles": json.RawMessage(`"confuzles"`),
   542  					},
   543  					SensitiveValues: json.RawMessage("{\"foozles\":true}"),
   544  				},
   545  			},
   546  			false,
   547  		},
   548  		"resource with marked map attr": {
   549  			map[string]*states.Resource{
   550  				"test_map_attr.bar": {
   551  					Addr: addrs.AbsResource{
   552  						Resource: addrs.Resource{
   553  							Mode: addrs.ManagedResourceMode,
   554  							Type: "test_map_attr",
   555  							Name: "bar",
   556  						},
   557  					},
   558  					Instances: map[addrs.InstanceKey]*states.ResourceInstance{
   559  						addrs.NoKey: {
   560  							Current: &states.ResourceInstanceObjectSrc{
   561  								Status:    states.ObjectReady,
   562  								AttrsJSON: []byte(`{"data":{"woozles":"confuzles"}}`),
   563  								AttrSensitivePaths: []cty.PathValueMarks{{
   564  									Path:  cty.Path{cty.GetAttrStep{Name: "data"}},
   565  									Marks: cty.NewValueMarks(marks.Sensitive)},
   566  								},
   567  							},
   568  						},
   569  					},
   570  					ProviderConfig: addrs.AbsProviderConfig{
   571  						Provider: addrs.NewDefaultProvider("test"),
   572  						Module:   addrs.RootModule,
   573  					},
   574  				},
   575  			},
   576  			testSchemas(),
   577  			[]Resource{
   578  				{
   579  					Address:      "test_map_attr.bar",
   580  					Mode:         "managed",
   581  					Type:         "test_map_attr",
   582  					Name:         "bar",
   583  					Index:        nil,
   584  					ProviderName: "registry.opentofu.org/hashicorp/test",
   585  					AttributeValues: AttributeValues{
   586  						"data": json.RawMessage(`{"woozles":"confuzles"}`),
   587  					},
   588  					SensitiveValues: json.RawMessage(`{"data":true}`),
   589  				},
   590  			},
   591  			false,
   592  		},
   593  	}
   594  
   595  	for name, test := range tests {
   596  		t.Run(name, func(t *testing.T) {
   597  			got, err := marshalResources(test.Resources, addrs.RootModuleInstance, test.Schemas)
   598  			if test.Err {
   599  				if err == nil {
   600  					t.Fatal("succeeded; want error")
   601  				}
   602  				return
   603  			} else if err != nil {
   604  				t.Fatalf("unexpected error: %s", err)
   605  			}
   606  
   607  			diff := cmp.Diff(got, test.Want)
   608  			if diff != "" {
   609  				t.Fatalf("wrong result: %s\n", diff)
   610  			}
   611  
   612  		})
   613  	}
   614  }
   615  
   616  func TestMarshalModules_basic(t *testing.T) {
   617  	childModule, _ := addrs.ParseModuleInstanceStr("module.child")
   618  	subModule, _ := addrs.ParseModuleInstanceStr("module.submodule")
   619  	testState := states.BuildState(func(s *states.SyncState) {
   620  		s.SetResourceInstanceCurrent(
   621  			addrs.Resource{
   622  				Mode: addrs.ManagedResourceMode,
   623  				Type: "test_instance",
   624  				Name: "foo",
   625  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   626  			&states.ResourceInstanceObjectSrc{
   627  				AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`),
   628  				Status:    states.ObjectReady,
   629  			},
   630  			addrs.AbsProviderConfig{
   631  				Provider: addrs.NewDefaultProvider("test"),
   632  				Module:   addrs.RootModule,
   633  			},
   634  		)
   635  		s.SetResourceInstanceCurrent(
   636  			addrs.Resource{
   637  				Mode: addrs.ManagedResourceMode,
   638  				Type: "test_instance",
   639  				Name: "foo",
   640  			}.Instance(addrs.NoKey).Absolute(childModule),
   641  			&states.ResourceInstanceObjectSrc{
   642  				AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`),
   643  				Status:    states.ObjectReady,
   644  			},
   645  			addrs.AbsProviderConfig{
   646  				Provider: addrs.NewDefaultProvider("test"),
   647  				Module:   childModule.Module(),
   648  			},
   649  		)
   650  		s.SetResourceInstanceCurrent(
   651  			addrs.Resource{
   652  				Mode: addrs.ManagedResourceMode,
   653  				Type: "test_instance",
   654  				Name: "foo",
   655  			}.Instance(addrs.NoKey).Absolute(subModule),
   656  			&states.ResourceInstanceObjectSrc{
   657  				AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`),
   658  				Status:    states.ObjectReady,
   659  			},
   660  			addrs.AbsProviderConfig{
   661  				Provider: addrs.NewDefaultProvider("test"),
   662  				Module:   subModule.Module(),
   663  			},
   664  		)
   665  	})
   666  	moduleMap := make(map[string][]addrs.ModuleInstance)
   667  	moduleMap[""] = []addrs.ModuleInstance{childModule, subModule}
   668  
   669  	got, err := marshalModules(testState, testSchemas(), moduleMap[""], moduleMap)
   670  
   671  	if err != nil {
   672  		t.Fatalf("unexpected error: %s", err.Error())
   673  	}
   674  
   675  	if len(got) != 2 {
   676  		t.Fatalf("wrong result! got %d modules, expected 2", len(got))
   677  	}
   678  
   679  	if got[0].Address != "module.child" || got[1].Address != "module.submodule" {
   680  		t.Fatalf("wrong result! got %#v\n", got)
   681  	}
   682  
   683  }
   684  
   685  func TestMarshalModules_nested(t *testing.T) {
   686  	childModule, _ := addrs.ParseModuleInstanceStr("module.child")
   687  	subModule, _ := addrs.ParseModuleInstanceStr("module.child.module.submodule")
   688  	testState := states.BuildState(func(s *states.SyncState) {
   689  		s.SetResourceInstanceCurrent(
   690  			addrs.Resource{
   691  				Mode: addrs.ManagedResourceMode,
   692  				Type: "test_instance",
   693  				Name: "foo",
   694  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   695  			&states.ResourceInstanceObjectSrc{
   696  				AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`),
   697  				Status:    states.ObjectReady,
   698  			},
   699  			addrs.AbsProviderConfig{
   700  				Provider: addrs.NewDefaultProvider("test"),
   701  				Module:   addrs.RootModule,
   702  			},
   703  		)
   704  		s.SetResourceInstanceCurrent(
   705  			addrs.Resource{
   706  				Mode: addrs.ManagedResourceMode,
   707  				Type: "test_instance",
   708  				Name: "foo",
   709  			}.Instance(addrs.NoKey).Absolute(childModule),
   710  			&states.ResourceInstanceObjectSrc{
   711  				AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`),
   712  				Status:    states.ObjectReady,
   713  			},
   714  			addrs.AbsProviderConfig{
   715  				Provider: addrs.NewDefaultProvider("test"),
   716  				Module:   childModule.Module(),
   717  			},
   718  		)
   719  		s.SetResourceInstanceCurrent(
   720  			addrs.Resource{
   721  				Mode: addrs.ManagedResourceMode,
   722  				Type: "test_instance",
   723  				Name: "foo",
   724  			}.Instance(addrs.NoKey).Absolute(subModule),
   725  			&states.ResourceInstanceObjectSrc{
   726  				AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`),
   727  				Status:    states.ObjectReady,
   728  			},
   729  			addrs.AbsProviderConfig{
   730  				Provider: addrs.NewDefaultProvider("test"),
   731  				Module:   subModule.Module(),
   732  			},
   733  		)
   734  	})
   735  	moduleMap := make(map[string][]addrs.ModuleInstance)
   736  	moduleMap[""] = []addrs.ModuleInstance{childModule}
   737  	moduleMap[childModule.String()] = []addrs.ModuleInstance{subModule}
   738  
   739  	got, err := marshalModules(testState, testSchemas(), moduleMap[""], moduleMap)
   740  
   741  	if err != nil {
   742  		t.Fatalf("unexpected error: %s", err.Error())
   743  	}
   744  
   745  	if len(got) != 1 {
   746  		t.Fatalf("wrong result! got %d modules, expected 1", len(got))
   747  	}
   748  
   749  	if got[0].Address != "module.child" {
   750  		t.Fatalf("wrong result! got %#v\n", got)
   751  	}
   752  
   753  	if got[0].ChildModules[0].Address != "module.child.module.submodule" {
   754  		t.Fatalf("wrong result! got %#v\n", got)
   755  	}
   756  }
   757  
   758  func TestMarshalModules_parent_no_resources(t *testing.T) {
   759  	subModule, _ := addrs.ParseModuleInstanceStr("module.child.module.submodule")
   760  	testState := states.BuildState(func(s *states.SyncState) {
   761  		s.SetResourceInstanceCurrent(
   762  			addrs.Resource{
   763  				Mode: addrs.ManagedResourceMode,
   764  				Type: "test_instance",
   765  				Name: "foo",
   766  			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   767  			&states.ResourceInstanceObjectSrc{
   768  				AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`),
   769  				Status:    states.ObjectReady,
   770  			},
   771  			addrs.AbsProviderConfig{
   772  				Provider: addrs.NewDefaultProvider("test"),
   773  				Module:   addrs.RootModule,
   774  			},
   775  		)
   776  		s.SetResourceInstanceCurrent(
   777  			addrs.Resource{
   778  				Mode: addrs.ManagedResourceMode,
   779  				Type: "test_instance",
   780  				Name: "foo",
   781  			}.Instance(addrs.NoKey).Absolute(subModule),
   782  			&states.ResourceInstanceObjectSrc{
   783  				AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`),
   784  				Status:    states.ObjectReady,
   785  			},
   786  			addrs.AbsProviderConfig{
   787  				Provider: addrs.NewDefaultProvider("test"),
   788  				Module:   subModule.Module(),
   789  			},
   790  		)
   791  	})
   792  	got, err := marshalRootModule(testState, testSchemas())
   793  
   794  	if err != nil {
   795  		t.Fatalf("unexpected error: %s", err.Error())
   796  	}
   797  
   798  	if len(got.ChildModules) != 1 {
   799  		t.Fatalf("wrong result! got %d modules, expected 1", len(got.ChildModules))
   800  	}
   801  
   802  	if got.ChildModules[0].Address != "module.child" {
   803  		t.Fatalf("wrong result! got %#v\n", got)
   804  	}
   805  
   806  	if got.ChildModules[0].ChildModules[0].Address != "module.child.module.submodule" {
   807  		t.Fatalf("wrong result! got %#v\n", got)
   808  	}
   809  }
   810  
   811  func testSchemas() *tofu.Schemas {
   812  	return &tofu.Schemas{
   813  		Providers: map[addrs.Provider]providers.ProviderSchema{
   814  			addrs.NewDefaultProvider("test"): {
   815  				ResourceTypes: map[string]providers.Schema{
   816  					"test_thing": {
   817  						Block: &configschema.Block{
   818  							Attributes: map[string]*configschema.Attribute{
   819  								"woozles": {Type: cty.String, Optional: true, Computed: true},
   820  								"foozles": {Type: cty.String, Optional: true, Sensitive: true},
   821  							},
   822  						},
   823  					},
   824  					"test_instance": {
   825  						Block: &configschema.Block{
   826  							Attributes: map[string]*configschema.Attribute{
   827  								"id":  {Type: cty.String, Optional: true, Computed: true},
   828  								"foo": {Type: cty.String, Optional: true},
   829  								"bar": {Type: cty.String, Optional: true},
   830  							},
   831  						},
   832  					},
   833  					"test_map_attr": {
   834  						Block: &configschema.Block{
   835  							Attributes: map[string]*configschema.Attribute{
   836  								"data": {Type: cty.Map(cty.String), Optional: true, Computed: true, Sensitive: true},
   837  							},
   838  						},
   839  					},
   840  				},
   841  			},
   842  		},
   843  	}
   844  }
   845  
   846  func TestSensitiveAsBool(t *testing.T) {
   847  	tests := []struct {
   848  		Input cty.Value
   849  		Want  cty.Value
   850  	}{
   851  		{
   852  			cty.StringVal("hello"),
   853  			cty.False,
   854  		},
   855  		{
   856  			cty.NullVal(cty.String),
   857  			cty.False,
   858  		},
   859  		{
   860  			cty.StringVal("hello").Mark(marks.Sensitive),
   861  			cty.True,
   862  		},
   863  		{
   864  			cty.NullVal(cty.String).Mark(marks.Sensitive),
   865  			cty.True,
   866  		},
   867  
   868  		{
   869  			cty.NullVal(cty.DynamicPseudoType).Mark(marks.Sensitive),
   870  			cty.True,
   871  		},
   872  		{
   873  			cty.NullVal(cty.Object(map[string]cty.Type{"test": cty.String})),
   874  			cty.False,
   875  		},
   876  		{
   877  			cty.NullVal(cty.Object(map[string]cty.Type{"test": cty.String})).Mark(marks.Sensitive),
   878  			cty.True,
   879  		},
   880  		{
   881  			cty.DynamicVal,
   882  			cty.False,
   883  		},
   884  		{
   885  			cty.DynamicVal.Mark(marks.Sensitive),
   886  			cty.True,
   887  		},
   888  
   889  		{
   890  			cty.ListValEmpty(cty.String),
   891  			cty.EmptyTupleVal,
   892  		},
   893  		{
   894  			cty.ListValEmpty(cty.String).Mark(marks.Sensitive),
   895  			cty.True,
   896  		},
   897  		{
   898  			cty.ListVal([]cty.Value{
   899  				cty.StringVal("hello"),
   900  				cty.StringVal("friend").Mark(marks.Sensitive),
   901  			}),
   902  			cty.TupleVal([]cty.Value{
   903  				cty.False,
   904  				cty.True,
   905  			}),
   906  		},
   907  		{
   908  			cty.SetValEmpty(cty.String),
   909  			cty.EmptyTupleVal,
   910  		},
   911  		{
   912  			cty.SetValEmpty(cty.String).Mark(marks.Sensitive),
   913  			cty.True,
   914  		},
   915  		{
   916  			cty.SetVal([]cty.Value{cty.StringVal("hello")}),
   917  			cty.TupleVal([]cty.Value{cty.False}),
   918  		},
   919  		{
   920  			cty.SetVal([]cty.Value{cty.StringVal("hello").Mark(marks.Sensitive)}),
   921  			cty.True,
   922  		},
   923  		{
   924  			cty.EmptyTupleVal.Mark(marks.Sensitive),
   925  			cty.True,
   926  		},
   927  		{
   928  			cty.TupleVal([]cty.Value{
   929  				cty.StringVal("hello"),
   930  				cty.StringVal("friend").Mark(marks.Sensitive),
   931  			}),
   932  			cty.TupleVal([]cty.Value{
   933  				cty.False,
   934  				cty.True,
   935  			}),
   936  		},
   937  		{
   938  			cty.MapValEmpty(cty.String),
   939  			cty.EmptyObjectVal,
   940  		},
   941  		{
   942  			cty.MapValEmpty(cty.String).Mark(marks.Sensitive),
   943  			cty.True,
   944  		},
   945  		{
   946  			cty.MapVal(map[string]cty.Value{
   947  				"greeting": cty.StringVal("hello"),
   948  				"animal":   cty.StringVal("horse"),
   949  			}),
   950  			cty.EmptyObjectVal,
   951  		},
   952  		{
   953  			cty.MapVal(map[string]cty.Value{
   954  				"greeting": cty.StringVal("hello"),
   955  				"animal":   cty.StringVal("horse").Mark(marks.Sensitive),
   956  			}),
   957  			cty.ObjectVal(map[string]cty.Value{
   958  				"animal": cty.True,
   959  			}),
   960  		},
   961  		{
   962  			cty.MapVal(map[string]cty.Value{
   963  				"greeting": cty.StringVal("hello"),
   964  				"animal":   cty.StringVal("horse").Mark(marks.Sensitive),
   965  			}).Mark(marks.Sensitive),
   966  			cty.True,
   967  		},
   968  		{
   969  			cty.EmptyObjectVal,
   970  			cty.EmptyObjectVal,
   971  		},
   972  		{
   973  			cty.ObjectVal(map[string]cty.Value{
   974  				"greeting": cty.StringVal("hello"),
   975  				"animal":   cty.StringVal("horse"),
   976  			}),
   977  			cty.EmptyObjectVal,
   978  		},
   979  		{
   980  			cty.ObjectVal(map[string]cty.Value{
   981  				"greeting": cty.StringVal("hello"),
   982  				"animal":   cty.StringVal("horse").Mark(marks.Sensitive),
   983  			}),
   984  			cty.ObjectVal(map[string]cty.Value{
   985  				"animal": cty.True,
   986  			}),
   987  		},
   988  		{
   989  			cty.ObjectVal(map[string]cty.Value{
   990  				"greeting": cty.StringVal("hello"),
   991  				"animal":   cty.StringVal("horse").Mark(marks.Sensitive),
   992  			}).Mark(marks.Sensitive),
   993  			cty.True,
   994  		},
   995  		{
   996  			cty.ListVal([]cty.Value{
   997  				cty.ObjectVal(map[string]cty.Value{
   998  					"a": cty.UnknownVal(cty.String),
   999  				}),
  1000  				cty.ObjectVal(map[string]cty.Value{
  1001  					"a": cty.StringVal("known").Mark(marks.Sensitive),
  1002  				}),
  1003  			}),
  1004  			cty.TupleVal([]cty.Value{
  1005  				cty.EmptyObjectVal,
  1006  				cty.ObjectVal(map[string]cty.Value{
  1007  					"a": cty.True,
  1008  				}),
  1009  			}),
  1010  		},
  1011  		{
  1012  			cty.ListVal([]cty.Value{
  1013  				cty.MapValEmpty(cty.String),
  1014  				cty.MapVal(map[string]cty.Value{
  1015  					"a": cty.StringVal("known").Mark(marks.Sensitive),
  1016  				}),
  1017  				cty.MapVal(map[string]cty.Value{
  1018  					"a": cty.UnknownVal(cty.String),
  1019  				}),
  1020  			}),
  1021  			cty.TupleVal([]cty.Value{
  1022  				cty.EmptyObjectVal,
  1023  				cty.ObjectVal(map[string]cty.Value{
  1024  					"a": cty.True,
  1025  				}),
  1026  				cty.EmptyObjectVal,
  1027  			}),
  1028  		},
  1029  		{
  1030  			cty.ObjectVal(map[string]cty.Value{
  1031  				"list":   cty.UnknownVal(cty.List(cty.String)),
  1032  				"set":    cty.UnknownVal(cty.Set(cty.Bool)),
  1033  				"tuple":  cty.UnknownVal(cty.Tuple([]cty.Type{cty.String, cty.Number})),
  1034  				"map":    cty.UnknownVal(cty.Map(cty.String)),
  1035  				"object": cty.UnknownVal(cty.Object(map[string]cty.Type{"a": cty.String})),
  1036  			}),
  1037  			cty.ObjectVal(map[string]cty.Value{
  1038  				"list":   cty.EmptyTupleVal,
  1039  				"set":    cty.EmptyTupleVal,
  1040  				"tuple":  cty.EmptyTupleVal,
  1041  				"map":    cty.EmptyObjectVal,
  1042  				"object": cty.EmptyObjectVal,
  1043  			}),
  1044  		},
  1045  	}
  1046  
  1047  	for _, test := range tests {
  1048  		got := SensitiveAsBool(test.Input)
  1049  		if !reflect.DeepEqual(got, test.Want) {
  1050  			t.Errorf(
  1051  				"wrong result\ninput: %#v\ngot:   %#v\nwant:  %#v",
  1052  				test.Input, got, test.Want,
  1053  			)
  1054  		}
  1055  	}
  1056  }