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 }