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  }