github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/command/views/plan_test.go (about) 1 package views 2 3 import ( 4 "testing" 5 6 "github.com/eliastor/durgaform/internal/addrs" 7 "github.com/eliastor/durgaform/internal/command/arguments" 8 "github.com/eliastor/durgaform/internal/configs/configschema" 9 "github.com/eliastor/durgaform/internal/lang/globalref" 10 "github.com/eliastor/durgaform/internal/plans" 11 "github.com/eliastor/durgaform/internal/providers" 12 "github.com/eliastor/durgaform/internal/terminal" 13 "github.com/eliastor/durgaform/internal/durgaform" 14 "github.com/zclconf/go-cty/cty" 15 ) 16 17 // Ensure that the correct view type and in-automation settings propagate to the 18 // Operation view. 19 func TestPlanHuman_operation(t *testing.T) { 20 streams, done := terminal.StreamsForTesting(t) 21 defer done(t) 22 v := NewPlan(arguments.ViewHuman, NewView(streams).SetRunningInAutomation(true)).Operation() 23 if hv, ok := v.(*OperationHuman); !ok { 24 t.Fatalf("unexpected return type %t", v) 25 } else if hv.inAutomation != true { 26 t.Fatalf("unexpected inAutomation value on Operation view") 27 } 28 } 29 30 // Verify that Hooks includes a UI hook 31 func TestPlanHuman_hooks(t *testing.T) { 32 streams, done := terminal.StreamsForTesting(t) 33 defer done(t) 34 v := NewPlan(arguments.ViewHuman, NewView(streams).SetRunningInAutomation((true))) 35 hooks := v.Hooks() 36 37 var uiHook *UiHook 38 for _, hook := range hooks { 39 if ch, ok := hook.(*UiHook); ok { 40 uiHook = ch 41 } 42 } 43 if uiHook == nil { 44 t.Fatalf("expected Hooks to include a UiHook: %#v", hooks) 45 } 46 } 47 48 // Helper functions to build a trivial test plan, to exercise the plan 49 // renderer. 50 func testPlan(t *testing.T) *plans.Plan { 51 t.Helper() 52 53 plannedVal := cty.ObjectVal(map[string]cty.Value{ 54 "id": cty.UnknownVal(cty.String), 55 "foo": cty.StringVal("bar"), 56 }) 57 priorValRaw, err := plans.NewDynamicValue(cty.NullVal(plannedVal.Type()), plannedVal.Type()) 58 if err != nil { 59 t.Fatal(err) 60 } 61 plannedValRaw, err := plans.NewDynamicValue(plannedVal, plannedVal.Type()) 62 if err != nil { 63 t.Fatal(err) 64 } 65 66 changes := plans.NewChanges() 67 addr := addrs.Resource{ 68 Mode: addrs.ManagedResourceMode, 69 Type: "test_resource", 70 Name: "foo", 71 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) 72 73 changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{ 74 Addr: addr, 75 PrevRunAddr: addr, 76 ProviderAddr: addrs.AbsProviderConfig{ 77 Provider: addrs.NewDefaultProvider("test"), 78 Module: addrs.RootModule, 79 }, 80 ChangeSrc: plans.ChangeSrc{ 81 Action: plans.Create, 82 Before: priorValRaw, 83 After: plannedValRaw, 84 }, 85 }) 86 87 return &plans.Plan{ 88 Changes: changes, 89 } 90 } 91 92 func testSchemas() *durgaform.Schemas { 93 provider := testProvider() 94 return &durgaform.Schemas{ 95 Providers: map[addrs.Provider]*durgaform.ProviderSchema{ 96 addrs.NewDefaultProvider("test"): provider.ProviderSchema(), 97 }, 98 } 99 } 100 101 func testProvider() *durgaform.MockProvider { 102 p := new(durgaform.MockProvider) 103 p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { 104 return providers.ReadResourceResponse{NewState: req.PriorState} 105 } 106 107 p.GetProviderSchemaResponse = testProviderSchema() 108 109 return p 110 } 111 112 func testProviderSchema() *providers.GetProviderSchemaResponse { 113 return &providers.GetProviderSchemaResponse{ 114 Provider: providers.Schema{ 115 Block: &configschema.Block{}, 116 }, 117 ResourceTypes: map[string]providers.Schema{ 118 "test_resource": { 119 Block: &configschema.Block{ 120 Attributes: map[string]*configschema.Attribute{ 121 "id": {Type: cty.String, Computed: true}, 122 "foo": {Type: cty.String, Optional: true}, 123 }, 124 }, 125 }, 126 }, 127 } 128 } 129 130 func TestFilterRefreshChange(t *testing.T) { 131 tests := map[string]struct { 132 paths []cty.Path 133 before, after, expected cty.Value 134 }{ 135 "attr was null": { 136 // nested attr was null 137 paths: []cty.Path{ 138 cty.GetAttrPath("attr").GetAttr("attr_null_before").GetAttr("b"), 139 }, 140 before: cty.ObjectVal(map[string]cty.Value{ 141 "attr": cty.ObjectVal(map[string]cty.Value{ 142 "attr_null_before": cty.ObjectVal(map[string]cty.Value{ 143 "a": cty.StringVal("old"), 144 "b": cty.NullVal(cty.String), 145 }), 146 }), 147 }), 148 after: cty.ObjectVal(map[string]cty.Value{ 149 "attr": cty.ObjectVal(map[string]cty.Value{ 150 "attr_null_before": cty.ObjectVal(map[string]cty.Value{ 151 "a": cty.StringVal("new"), 152 "b": cty.StringVal("new"), 153 }), 154 }), 155 }), 156 expected: cty.ObjectVal(map[string]cty.Value{ 157 "attr": cty.ObjectVal(map[string]cty.Value{ 158 "attr_null_before": cty.ObjectVal(map[string]cty.Value{ 159 // we old picked the change in b 160 "a": cty.StringVal("old"), 161 "b": cty.StringVal("new"), 162 }), 163 }), 164 }), 165 }, 166 "object was null": { 167 // nested object attrs were null 168 paths: []cty.Path{ 169 cty.GetAttrPath("attr").GetAttr("obj_null_before").GetAttr("b"), 170 }, 171 before: cty.ObjectVal(map[string]cty.Value{ 172 "attr": cty.ObjectVal(map[string]cty.Value{ 173 "obj_null_before": cty.NullVal(cty.Object(map[string]cty.Type{ 174 "a": cty.String, 175 "b": cty.String, 176 })), 177 "other": cty.ObjectVal(map[string]cty.Value{ 178 "o": cty.StringVal("old"), 179 }), 180 }), 181 }), 182 after: cty.ObjectVal(map[string]cty.Value{ 183 "attr": cty.ObjectVal(map[string]cty.Value{ 184 "obj_null_before": cty.ObjectVal(map[string]cty.Value{ 185 "a": cty.StringVal("new"), 186 "b": cty.StringVal("new"), 187 }), 188 "other": cty.ObjectVal(map[string]cty.Value{ 189 "o": cty.StringVal("new"), 190 }), 191 }), 192 }), 193 expected: cty.ObjectVal(map[string]cty.Value{ 194 "attr": cty.ObjectVal(map[string]cty.Value{ 195 "obj_null_before": cty.ObjectVal(map[string]cty.Value{ 196 // optimally "a" would be null, but we need to take the 197 // entire object since it was null before. 198 "a": cty.StringVal("new"), 199 "b": cty.StringVal("new"), 200 }), 201 "other": cty.ObjectVal(map[string]cty.Value{ 202 "o": cty.StringVal("old"), 203 }), 204 }), 205 }), 206 }, 207 "object becomes null": { 208 // nested object attr becoming null 209 paths: []cty.Path{ 210 cty.GetAttrPath("attr").GetAttr("obj_null_after").GetAttr("a"), 211 }, 212 before: cty.ObjectVal(map[string]cty.Value{ 213 "attr": cty.ObjectVal(map[string]cty.Value{ 214 "obj_null_after": cty.ObjectVal(map[string]cty.Value{ 215 "a": cty.StringVal("old"), 216 "b": cty.StringVal("old"), 217 }), 218 "other": cty.ObjectVal(map[string]cty.Value{ 219 "o": cty.StringVal("old"), 220 }), 221 }), 222 }), 223 after: cty.ObjectVal(map[string]cty.Value{ 224 "attr": cty.ObjectVal(map[string]cty.Value{ 225 "obj_null_after": cty.NullVal(cty.Object(map[string]cty.Type{ 226 "a": cty.String, 227 "b": cty.String, 228 })), 229 "other": cty.ObjectVal(map[string]cty.Value{ 230 "o": cty.StringVal("new"), 231 }), 232 }), 233 }), 234 expected: cty.ObjectVal(map[string]cty.Value{ 235 "attr": cty.ObjectVal(map[string]cty.Value{ 236 "obj_null_after": cty.ObjectVal(map[string]cty.Value{ 237 "a": cty.NullVal(cty.String), 238 "b": cty.StringVal("old"), 239 }), 240 "other": cty.ObjectVal(map[string]cty.Value{ 241 "o": cty.StringVal("old"), 242 }), 243 }), 244 }), 245 }, 246 "dynamic adding values": { 247 // dynamic gaining values 248 paths: []cty.Path{ 249 cty.GetAttrPath("attr").GetAttr("after").GetAttr("a"), 250 }, 251 before: cty.ObjectVal(map[string]cty.Value{ 252 "attr": cty.DynamicVal, 253 }), 254 after: cty.ObjectVal(map[string]cty.Value{ 255 "attr": cty.ObjectVal(map[string]cty.Value{ 256 // the entire attr object is taken here because there is 257 // nothing to compare within the before value 258 "after": cty.ObjectVal(map[string]cty.Value{ 259 "a": cty.StringVal("new"), 260 "b": cty.StringVal("new"), 261 }), 262 "other": cty.ObjectVal(map[string]cty.Value{ 263 "o": cty.StringVal("new"), 264 }), 265 }), 266 }), 267 expected: cty.ObjectVal(map[string]cty.Value{ 268 "attr": cty.ObjectVal(map[string]cty.Value{ 269 "after": cty.ObjectVal(map[string]cty.Value{ 270 "a": cty.StringVal("new"), 271 "b": cty.StringVal("new"), 272 }), 273 // "other" is picked up here too this time, because we need 274 // to take the entire dynamic "attr" value 275 "other": cty.ObjectVal(map[string]cty.Value{ 276 "o": cty.StringVal("new"), 277 }), 278 }), 279 }), 280 }, 281 "whole object becomes null": { 282 // whole object becomes null 283 paths: []cty.Path{ 284 cty.GetAttrPath("attr").GetAttr("after").GetAttr("a"), 285 }, 286 before: cty.ObjectVal(map[string]cty.Value{ 287 "attr": cty.ObjectVal(map[string]cty.Value{ 288 "after": cty.ObjectVal(map[string]cty.Value{ 289 "a": cty.StringVal("old"), 290 "b": cty.StringVal("old"), 291 }), 292 }), 293 }), 294 after: cty.NullVal(cty.Object(map[string]cty.Type{ 295 "attr": cty.DynamicPseudoType, 296 })), 297 // since we have a dynamic type we have to take the entire object 298 // because the paths may not apply between versions. 299 expected: cty.NullVal(cty.Object(map[string]cty.Type{ 300 "attr": cty.DynamicPseudoType, 301 })), 302 }, 303 "whole object was null": { 304 // whole object was null 305 paths: []cty.Path{ 306 cty.GetAttrPath("attr").GetAttr("after").GetAttr("a"), 307 }, 308 before: cty.NullVal(cty.Object(map[string]cty.Type{ 309 "attr": cty.DynamicPseudoType, 310 })), 311 after: cty.ObjectVal(map[string]cty.Value{ 312 "attr": cty.ObjectVal(map[string]cty.Value{ 313 "after": cty.ObjectVal(map[string]cty.Value{ 314 "a": cty.StringVal("new"), 315 "b": cty.StringVal("new"), 316 }), 317 }), 318 }), 319 expected: cty.ObjectVal(map[string]cty.Value{ 320 "attr": cty.ObjectVal(map[string]cty.Value{ 321 "after": cty.ObjectVal(map[string]cty.Value{ 322 "a": cty.StringVal("new"), 323 "b": cty.StringVal("new"), 324 }), 325 }), 326 }), 327 }, 328 "restructured dynamic": { 329 // dynamic value changing structure significantly 330 paths: []cty.Path{ 331 cty.GetAttrPath("attr").GetAttr("list").IndexInt(1).GetAttr("a"), 332 }, 333 before: cty.ObjectVal(map[string]cty.Value{ 334 "attr": cty.ObjectVal(map[string]cty.Value{ 335 "list": cty.ListVal([]cty.Value{ 336 cty.ObjectVal(map[string]cty.Value{ 337 "a": cty.StringVal("old"), 338 }), 339 }), 340 }), 341 }), 342 after: cty.ObjectVal(map[string]cty.Value{ 343 "attr": cty.ObjectVal(map[string]cty.Value{ 344 "after": cty.ObjectVal(map[string]cty.Value{ 345 "a": cty.StringVal("new"), 346 "b": cty.StringVal("new"), 347 }), 348 }), 349 }), 350 // the path does not apply at all to the new object, so we must 351 // take all the changes 352 expected: cty.ObjectVal(map[string]cty.Value{ 353 "attr": cty.ObjectVal(map[string]cty.Value{ 354 "after": cty.ObjectVal(map[string]cty.Value{ 355 "a": cty.StringVal("new"), 356 "b": cty.StringVal("new"), 357 }), 358 }), 359 }), 360 }, 361 } 362 363 for k, tc := range tests { 364 t.Run(k, func(t *testing.T) { 365 addr, diags := addrs.ParseAbsResourceInstanceStr("test_resource.a") 366 if diags != nil { 367 t.Fatal(diags.ErrWithWarnings()) 368 } 369 370 change := &plans.ResourceInstanceChange{ 371 Addr: addr, 372 Change: plans.Change{ 373 Before: tc.before, 374 After: tc.after, 375 Action: plans.Update, 376 }, 377 } 378 379 var contributing []globalref.ResourceAttr 380 for _, p := range tc.paths { 381 contributing = append(contributing, globalref.ResourceAttr{ 382 Resource: addr, 383 Attr: p, 384 }) 385 } 386 387 res := filterRefreshChange(change, contributing) 388 if !res.After.RawEquals(tc.expected) { 389 t.Errorf("\nexpected: %#v\ngot: %#v\n", tc.expected, res.After) 390 } 391 }) 392 } 393 }