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