github.com/opentofu/opentofu@v1.7.1/internal/command/jsonplan/values_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 jsonplan
     7  
     8  import (
     9  	"encoding/json"
    10  	"reflect"
    11  	"testing"
    12  
    13  	"github.com/zclconf/go-cty/cty"
    14  
    15  	"github.com/opentofu/opentofu/internal/addrs"
    16  	"github.com/opentofu/opentofu/internal/configs/configschema"
    17  	"github.com/opentofu/opentofu/internal/plans"
    18  	"github.com/opentofu/opentofu/internal/providers"
    19  	"github.com/opentofu/opentofu/internal/tofu"
    20  )
    21  
    22  func TestMarshalAttributeValues(t *testing.T) {
    23  	tests := []struct {
    24  		Attr   cty.Value
    25  		Schema *configschema.Block
    26  		Want   AttributeValues
    27  	}{
    28  		{
    29  			cty.NilVal,
    30  			&configschema.Block{
    31  				Attributes: map[string]*configschema.Attribute{
    32  					"foo": {
    33  						Type:     cty.String,
    34  						Optional: true,
    35  					},
    36  				},
    37  			},
    38  			nil,
    39  		},
    40  		{
    41  			cty.NullVal(cty.String),
    42  			&configschema.Block{
    43  				Attributes: map[string]*configschema.Attribute{
    44  					"foo": {
    45  						Type:     cty.String,
    46  						Optional: true,
    47  					},
    48  				},
    49  			},
    50  			nil,
    51  		},
    52  		{
    53  			cty.ObjectVal(map[string]cty.Value{
    54  				"foo": cty.StringVal("bar"),
    55  			}),
    56  			&configschema.Block{
    57  				Attributes: map[string]*configschema.Attribute{
    58  					"foo": {
    59  						Type:     cty.String,
    60  						Optional: true,
    61  					},
    62  				},
    63  			},
    64  			AttributeValues{"foo": json.RawMessage(`"bar"`)},
    65  		},
    66  		{
    67  			cty.ObjectVal(map[string]cty.Value{
    68  				"foo": cty.NullVal(cty.String),
    69  			}),
    70  			&configschema.Block{
    71  				Attributes: map[string]*configschema.Attribute{
    72  					"foo": {
    73  						Type:     cty.String,
    74  						Optional: true,
    75  					},
    76  				},
    77  			},
    78  			AttributeValues{"foo": json.RawMessage(`null`)},
    79  		},
    80  		{
    81  			cty.ObjectVal(map[string]cty.Value{
    82  				"bar": cty.MapVal(map[string]cty.Value{
    83  					"hello": cty.StringVal("world"),
    84  				}),
    85  				"baz": cty.ListVal([]cty.Value{
    86  					cty.StringVal("goodnight"),
    87  					cty.StringVal("moon"),
    88  				}),
    89  			}),
    90  			&configschema.Block{
    91  				Attributes: map[string]*configschema.Attribute{
    92  					"bar": {
    93  						Type:     cty.Map(cty.String),
    94  						Required: true,
    95  					},
    96  					"baz": {
    97  						Type:     cty.List(cty.String),
    98  						Optional: true,
    99  					},
   100  				},
   101  			},
   102  			AttributeValues{
   103  				"bar": json.RawMessage(`{"hello":"world"}`),
   104  				"baz": json.RawMessage(`["goodnight","moon"]`),
   105  			},
   106  		},
   107  	}
   108  
   109  	for _, test := range tests {
   110  		got := marshalAttributeValues(test.Attr, test.Schema)
   111  		eq := reflect.DeepEqual(got, test.Want)
   112  		if !eq {
   113  			t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want)
   114  		}
   115  	}
   116  }
   117  
   118  func TestMarshalPlannedOutputs(t *testing.T) {
   119  	after, _ := plans.NewDynamicValue(cty.StringVal("after"), cty.DynamicPseudoType)
   120  
   121  	tests := []struct {
   122  		Changes *plans.Changes
   123  		Want    map[string]Output
   124  		Err     bool
   125  	}{
   126  		{
   127  			&plans.Changes{},
   128  			nil,
   129  			false,
   130  		},
   131  		{
   132  			&plans.Changes{
   133  				Outputs: []*plans.OutputChangeSrc{
   134  					{
   135  						Addr: addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance),
   136  						ChangeSrc: plans.ChangeSrc{
   137  							Action: plans.Create,
   138  							After:  after,
   139  						},
   140  						Sensitive: false,
   141  					},
   142  				},
   143  			},
   144  			map[string]Output{
   145  				"bar": {
   146  					Sensitive: false,
   147  					Type:      json.RawMessage(`"string"`),
   148  					Value:     json.RawMessage(`"after"`),
   149  				},
   150  			},
   151  			false,
   152  		},
   153  		{ // Delete action
   154  			&plans.Changes{
   155  				Outputs: []*plans.OutputChangeSrc{
   156  					{
   157  						Addr: addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance),
   158  						ChangeSrc: plans.ChangeSrc{
   159  							Action: plans.Delete,
   160  						},
   161  						Sensitive: false,
   162  					},
   163  				},
   164  			},
   165  			map[string]Output{},
   166  			false,
   167  		},
   168  	}
   169  
   170  	for _, test := range tests {
   171  		got, err := marshalPlannedOutputs(test.Changes)
   172  		if test.Err {
   173  			if err == nil {
   174  				t.Fatal("succeeded; want error")
   175  			}
   176  			return
   177  		} else if err != nil {
   178  			t.Fatalf("unexpected error: %s", err)
   179  		}
   180  
   181  		eq := reflect.DeepEqual(got, test.Want)
   182  		if !eq {
   183  			t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want)
   184  		}
   185  	}
   186  }
   187  
   188  func TestMarshalPlanResources(t *testing.T) {
   189  	tests := map[string]struct {
   190  		Action plans.Action
   191  		Before cty.Value
   192  		After  cty.Value
   193  		Want   []Resource
   194  		Err    bool
   195  	}{
   196  		"create with unknowns": {
   197  			Action: plans.Create,
   198  			Before: cty.NullVal(cty.EmptyObject),
   199  			After: cty.ObjectVal(map[string]cty.Value{
   200  				"woozles": cty.UnknownVal(cty.String),
   201  				"foozles": cty.UnknownVal(cty.String),
   202  			}),
   203  			Want: []Resource{{
   204  				Address:         "test_thing.example",
   205  				Mode:            "managed",
   206  				Type:            "test_thing",
   207  				Name:            "example",
   208  				Index:           addrs.InstanceKey(nil),
   209  				ProviderName:    "registry.opentofu.org/hashicorp/test",
   210  				SchemaVersion:   1,
   211  				AttributeValues: AttributeValues{},
   212  				SensitiveValues: json.RawMessage("{}"),
   213  			}},
   214  			Err: false,
   215  		},
   216  		"delete with null and nil": {
   217  			Action: plans.Delete,
   218  			Before: cty.NullVal(cty.EmptyObject),
   219  			After:  cty.NilVal,
   220  			Want:   nil,
   221  			Err:    false,
   222  		},
   223  		"delete": {
   224  			Action: plans.Delete,
   225  			Before: cty.ObjectVal(map[string]cty.Value{
   226  				"woozles": cty.StringVal("foo"),
   227  				"foozles": cty.StringVal("bar"),
   228  			}),
   229  			After: cty.NullVal(cty.Object(map[string]cty.Type{
   230  				"woozles": cty.String,
   231  				"foozles": cty.String,
   232  			})),
   233  			Want: nil,
   234  			Err:  false,
   235  		},
   236  		"update without unknowns": {
   237  			Action: plans.Update,
   238  			Before: cty.ObjectVal(map[string]cty.Value{
   239  				"woozles": cty.StringVal("foo"),
   240  				"foozles": cty.StringVal("bar"),
   241  			}),
   242  			After: cty.ObjectVal(map[string]cty.Value{
   243  				"woozles": cty.StringVal("baz"),
   244  				"foozles": cty.StringVal("bat"),
   245  			}),
   246  			Want: []Resource{{
   247  				Address:       "test_thing.example",
   248  				Mode:          "managed",
   249  				Type:          "test_thing",
   250  				Name:          "example",
   251  				Index:         addrs.InstanceKey(nil),
   252  				ProviderName:  "registry.opentofu.org/hashicorp/test",
   253  				SchemaVersion: 1,
   254  				AttributeValues: AttributeValues{
   255  					"woozles": json.RawMessage(`"baz"`),
   256  					"foozles": json.RawMessage(`"bat"`),
   257  				},
   258  				SensitiveValues: json.RawMessage("{}"),
   259  			}},
   260  			Err: false,
   261  		},
   262  	}
   263  
   264  	for name, test := range tests {
   265  		t.Run(name, func(t *testing.T) {
   266  			before, err := plans.NewDynamicValue(test.Before, test.Before.Type())
   267  			if err != nil {
   268  				t.Fatal(err)
   269  			}
   270  
   271  			after, err := plans.NewDynamicValue(test.After, test.After.Type())
   272  			if err != nil {
   273  				t.Fatal(err)
   274  			}
   275  			testChange := &plans.Changes{
   276  				Resources: []*plans.ResourceInstanceChangeSrc{
   277  					{
   278  						Addr: addrs.Resource{
   279  							Mode: addrs.ManagedResourceMode,
   280  							Type: "test_thing",
   281  							Name: "example",
   282  						}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   283  						ProviderAddr: addrs.AbsProviderConfig{
   284  							Provider: addrs.NewDefaultProvider("test"),
   285  							Module:   addrs.RootModule,
   286  						},
   287  						ChangeSrc: plans.ChangeSrc{
   288  							Action: test.Action,
   289  							Before: before,
   290  							After:  after,
   291  						},
   292  					},
   293  				},
   294  			}
   295  
   296  			ris := testResourceAddrs()
   297  
   298  			got, err := marshalPlanResources(testChange, ris, testSchemas())
   299  			if test.Err {
   300  				if err == nil {
   301  					t.Fatal("succeeded; want error")
   302  				}
   303  				return
   304  			} else if err != nil {
   305  				t.Fatalf("unexpected error: %s", err)
   306  			}
   307  
   308  			eq := reflect.DeepEqual(got, test.Want)
   309  			if !eq {
   310  				t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want)
   311  			}
   312  		})
   313  	}
   314  }
   315  
   316  func TestMarshalPlanValuesNoopDeposed(t *testing.T) {
   317  	dynamicNull, err := plans.NewDynamicValue(cty.NullVal(cty.DynamicPseudoType), cty.DynamicPseudoType)
   318  	if err != nil {
   319  		t.Fatal(err)
   320  	}
   321  	testChange := &plans.Changes{
   322  		Resources: []*plans.ResourceInstanceChangeSrc{
   323  			{
   324  				Addr: addrs.Resource{
   325  					Mode: addrs.ManagedResourceMode,
   326  					Type: "test_thing",
   327  					Name: "example",
   328  				}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
   329  				DeposedKey: "12345678",
   330  				ProviderAddr: addrs.AbsProviderConfig{
   331  					Provider: addrs.NewDefaultProvider("test"),
   332  					Module:   addrs.RootModule,
   333  				},
   334  				ChangeSrc: plans.ChangeSrc{
   335  					Action: plans.NoOp,
   336  					Before: dynamicNull,
   337  					After:  dynamicNull,
   338  				},
   339  			},
   340  		},
   341  	}
   342  
   343  	_, err = marshalPlannedValues(testChange, testSchemas())
   344  	if err != nil {
   345  		t.Fatal(err)
   346  	}
   347  }
   348  
   349  func testSchemas() *tofu.Schemas {
   350  	return &tofu.Schemas{
   351  		Providers: map[addrs.Provider]providers.ProviderSchema{
   352  			addrs.NewDefaultProvider("test"): providers.ProviderSchema{
   353  				ResourceTypes: map[string]providers.Schema{
   354  					"test_thing": {
   355  						Version: 1,
   356  						Block: &configschema.Block{
   357  							Attributes: map[string]*configschema.Attribute{
   358  								"woozles": {Type: cty.String, Optional: true, Computed: true},
   359  								"foozles": {Type: cty.String, Optional: true},
   360  							},
   361  						},
   362  					},
   363  				},
   364  			},
   365  		},
   366  	}
   367  }
   368  
   369  func testResourceAddrs() []addrs.AbsResourceInstance {
   370  	return []addrs.AbsResourceInstance{
   371  		mustAddr("test_thing.example"),
   372  	}
   373  }
   374  
   375  func mustAddr(str string) addrs.AbsResourceInstance {
   376  	addr, diags := addrs.ParseAbsResourceInstanceStr(str)
   377  	if diags.HasErrors() {
   378  		panic(diags.Err())
   379  	}
   380  	return addr
   381  }