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