github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/plans/objchange/objchange_test.go (about)

     1  package objchange
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/apparentlymart/go-dump/dump"
     7  	"github.com/zclconf/go-cty/cty"
     8  
     9  	"github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema"
    10  )
    11  
    12  func TestProposedNewObject(t *testing.T) {
    13  	tests := map[string]struct {
    14  		Schema *configschema.Block
    15  		Prior  cty.Value
    16  		Config cty.Value
    17  		Want   cty.Value
    18  	}{
    19  		"empty": {
    20  			&configschema.Block{},
    21  			cty.EmptyObjectVal,
    22  			cty.EmptyObjectVal,
    23  			cty.EmptyObjectVal,
    24  		},
    25  		"no prior": {
    26  			&configschema.Block{
    27  				Attributes: map[string]*configschema.Attribute{
    28  					"foo": {
    29  						Type:     cty.String,
    30  						Optional: true,
    31  					},
    32  					"bar": {
    33  						Type:     cty.String,
    34  						Computed: true,
    35  					},
    36  				},
    37  				BlockTypes: map[string]*configschema.NestedBlock{
    38  					"baz": {
    39  						Nesting: configschema.NestingSingle,
    40  						Block: configschema.Block{
    41  							Attributes: map[string]*configschema.Attribute{
    42  								"boz": {
    43  									Type:     cty.String,
    44  									Optional: true,
    45  									Computed: true,
    46  								},
    47  								"biz": {
    48  									Type:     cty.String,
    49  									Optional: true,
    50  									Computed: true,
    51  								},
    52  							},
    53  						},
    54  					},
    55  				},
    56  			},
    57  			cty.NullVal(cty.DynamicPseudoType),
    58  			cty.ObjectVal(map[string]cty.Value{
    59  				"foo": cty.StringVal("hello"),
    60  				"bar": cty.NullVal(cty.String),
    61  				"baz": cty.ObjectVal(map[string]cty.Value{
    62  					"boz": cty.StringVal("world"),
    63  
    64  					// An unknown in the config represents a situation where
    65  					// an argument is explicitly set to an expression result
    66  					// that is derived from an unknown value. This is distinct
    67  					// from leaving it null, which allows the provider itself
    68  					// to decide the value during PlanResourceChange.
    69  					"biz": cty.UnknownVal(cty.String),
    70  				}),
    71  			}),
    72  			cty.ObjectVal(map[string]cty.Value{
    73  				"foo": cty.StringVal("hello"),
    74  
    75  				// unset computed attributes are null in the proposal; provider
    76  				// usually changes them to "unknown" during PlanResourceChange,
    77  				// to indicate that the value will be decided during apply.
    78  				"bar": cty.NullVal(cty.String),
    79  
    80  				"baz": cty.ObjectVal(map[string]cty.Value{
    81  					"boz": cty.StringVal("world"),
    82  					"biz": cty.UnknownVal(cty.String), // explicit unknown preserved from config
    83  				}),
    84  			}),
    85  		},
    86  		"null block remains null": {
    87  			&configschema.Block{
    88  				Attributes: map[string]*configschema.Attribute{
    89  					"foo": {
    90  						Type:     cty.String,
    91  						Optional: true,
    92  					},
    93  				},
    94  				BlockTypes: map[string]*configschema.NestedBlock{
    95  					"baz": {
    96  						Nesting: configschema.NestingSingle,
    97  						Block: configschema.Block{
    98  							Attributes: map[string]*configschema.Attribute{
    99  								"boz": {
   100  									Type:     cty.String,
   101  									Optional: true,
   102  									Computed: true,
   103  								},
   104  							},
   105  						},
   106  					},
   107  				},
   108  			},
   109  			cty.NullVal(cty.DynamicPseudoType),
   110  			cty.ObjectVal(map[string]cty.Value{
   111  				"foo": cty.StringVal("bar"),
   112  				"baz": cty.NullVal(cty.Object(map[string]cty.Type{
   113  					"boz": cty.String,
   114  				})),
   115  			}),
   116  			// The baz block does not exist in the config, and therefore
   117  			// shouldn't be planned.
   118  			cty.ObjectVal(map[string]cty.Value{
   119  				"foo": cty.StringVal("bar"),
   120  				"baz": cty.NullVal(cty.Object(map[string]cty.Type{
   121  					"boz": cty.String,
   122  				})),
   123  			}),
   124  		},
   125  		"no prior with set": {
   126  			// This one is here because our handling of sets is more complex
   127  			// than others (due to the fuzzy correlation heuristic) and
   128  			// historically that caused us some panic-related grief.
   129  			&configschema.Block{
   130  				BlockTypes: map[string]*configschema.NestedBlock{
   131  					"baz": {
   132  						Nesting: configschema.NestingSet,
   133  						Block: configschema.Block{
   134  							Attributes: map[string]*configschema.Attribute{
   135  								"boz": {
   136  									Type:     cty.String,
   137  									Optional: true,
   138  									Computed: true,
   139  								},
   140  							},
   141  						},
   142  					},
   143  				},
   144  			},
   145  			cty.NullVal(cty.DynamicPseudoType),
   146  			cty.ObjectVal(map[string]cty.Value{
   147  				"baz": cty.SetVal([]cty.Value{
   148  					cty.ObjectVal(map[string]cty.Value{
   149  						"boz": cty.StringVal("world"),
   150  					}),
   151  				}),
   152  			}),
   153  			cty.ObjectVal(map[string]cty.Value{
   154  				"baz": cty.SetVal([]cty.Value{
   155  					cty.ObjectVal(map[string]cty.Value{
   156  						"boz": cty.StringVal("world"),
   157  					}),
   158  				}),
   159  			}),
   160  		},
   161  		"prior attributes": {
   162  			&configschema.Block{
   163  				Attributes: map[string]*configschema.Attribute{
   164  					"foo": {
   165  						Type:     cty.String,
   166  						Optional: true,
   167  					},
   168  					"bar": {
   169  						Type:     cty.String,
   170  						Computed: true,
   171  					},
   172  					"baz": {
   173  						Type:     cty.String,
   174  						Optional: true,
   175  						Computed: true,
   176  					},
   177  					"boz": {
   178  						Type:     cty.String,
   179  						Optional: true,
   180  						Computed: true,
   181  					},
   182  				},
   183  			},
   184  			cty.ObjectVal(map[string]cty.Value{
   185  				"foo": cty.StringVal("bonjour"),
   186  				"bar": cty.StringVal("petit dejeuner"),
   187  				"baz": cty.StringVal("grande dejeuner"),
   188  				"boz": cty.StringVal("a la monde"),
   189  			}),
   190  			cty.ObjectVal(map[string]cty.Value{
   191  				"foo": cty.StringVal("hello"),
   192  				"bar": cty.NullVal(cty.String),
   193  				"baz": cty.NullVal(cty.String),
   194  				"boz": cty.StringVal("world"),
   195  			}),
   196  			cty.ObjectVal(map[string]cty.Value{
   197  				"foo": cty.StringVal("hello"),
   198  				"bar": cty.StringVal("petit dejeuner"),
   199  				"baz": cty.StringVal("grande dejeuner"),
   200  				"boz": cty.StringVal("world"),
   201  			}),
   202  		},
   203  		"prior nested single": {
   204  			&configschema.Block{
   205  				BlockTypes: map[string]*configschema.NestedBlock{
   206  					"foo": {
   207  						Nesting: configschema.NestingSingle,
   208  						Block: configschema.Block{
   209  							Attributes: map[string]*configschema.Attribute{
   210  								"bar": {
   211  									Type:     cty.String,
   212  									Optional: true,
   213  									Computed: true,
   214  								},
   215  								"baz": {
   216  									Type:     cty.String,
   217  									Optional: true,
   218  									Computed: true,
   219  								},
   220  							},
   221  						},
   222  					},
   223  				},
   224  			},
   225  			cty.ObjectVal(map[string]cty.Value{
   226  				"foo": cty.ObjectVal(map[string]cty.Value{
   227  					"bar": cty.StringVal("beep"),
   228  					"baz": cty.StringVal("boop"),
   229  				}),
   230  			}),
   231  			cty.ObjectVal(map[string]cty.Value{
   232  				"foo": cty.ObjectVal(map[string]cty.Value{
   233  					"bar": cty.StringVal("bap"),
   234  					"baz": cty.NullVal(cty.String),
   235  				}),
   236  			}),
   237  			cty.ObjectVal(map[string]cty.Value{
   238  				"foo": cty.ObjectVal(map[string]cty.Value{
   239  					"bar": cty.StringVal("bap"),
   240  					"baz": cty.StringVal("boop"),
   241  				}),
   242  			}),
   243  		},
   244  		"prior nested list": {
   245  			&configschema.Block{
   246  				BlockTypes: map[string]*configschema.NestedBlock{
   247  					"foo": {
   248  						Nesting: configschema.NestingList,
   249  						Block: configschema.Block{
   250  							Attributes: map[string]*configschema.Attribute{
   251  								"bar": {
   252  									Type:     cty.String,
   253  									Optional: true,
   254  									Computed: true,
   255  								},
   256  								"baz": {
   257  									Type:     cty.String,
   258  									Optional: true,
   259  									Computed: true,
   260  								},
   261  							},
   262  						},
   263  					},
   264  				},
   265  			},
   266  			cty.ObjectVal(map[string]cty.Value{
   267  				"foo": cty.ListVal([]cty.Value{
   268  					cty.ObjectVal(map[string]cty.Value{
   269  						"bar": cty.StringVal("beep"),
   270  						"baz": cty.StringVal("boop"),
   271  					}),
   272  				}),
   273  			}),
   274  			cty.ObjectVal(map[string]cty.Value{
   275  				"foo": cty.ListVal([]cty.Value{
   276  					cty.ObjectVal(map[string]cty.Value{
   277  						"bar": cty.StringVal("bap"),
   278  						"baz": cty.NullVal(cty.String),
   279  					}),
   280  					cty.ObjectVal(map[string]cty.Value{
   281  						"bar": cty.StringVal("blep"),
   282  						"baz": cty.NullVal(cty.String),
   283  					}),
   284  				}),
   285  			}),
   286  			cty.ObjectVal(map[string]cty.Value{
   287  				"foo": cty.ListVal([]cty.Value{
   288  					cty.ObjectVal(map[string]cty.Value{
   289  						"bar": cty.StringVal("bap"),
   290  						"baz": cty.StringVal("boop"),
   291  					}),
   292  					cty.ObjectVal(map[string]cty.Value{
   293  						"bar": cty.StringVal("blep"),
   294  						"baz": cty.NullVal(cty.String),
   295  					}),
   296  				}),
   297  			}),
   298  		},
   299  		"prior nested list with dynamic": {
   300  			&configschema.Block{
   301  				BlockTypes: map[string]*configschema.NestedBlock{
   302  					"foo": {
   303  						Nesting: configschema.NestingList,
   304  						Block: configschema.Block{
   305  							Attributes: map[string]*configschema.Attribute{
   306  								"bar": {
   307  									Type:     cty.String,
   308  									Optional: true,
   309  									Computed: true,
   310  								},
   311  								"baz": {
   312  									Type:     cty.DynamicPseudoType,
   313  									Optional: true,
   314  									Computed: true,
   315  								},
   316  							},
   317  						},
   318  					},
   319  				},
   320  			},
   321  			cty.ObjectVal(map[string]cty.Value{
   322  				"foo": cty.TupleVal([]cty.Value{
   323  					cty.ObjectVal(map[string]cty.Value{
   324  						"bar": cty.StringVal("beep"),
   325  						"baz": cty.StringVal("boop"),
   326  					}),
   327  				}),
   328  			}),
   329  			cty.ObjectVal(map[string]cty.Value{
   330  				"foo": cty.TupleVal([]cty.Value{
   331  					cty.ObjectVal(map[string]cty.Value{
   332  						"bar": cty.StringVal("bap"),
   333  						"baz": cty.NullVal(cty.String),
   334  					}),
   335  					cty.ObjectVal(map[string]cty.Value{
   336  						"bar": cty.StringVal("blep"),
   337  						"baz": cty.NullVal(cty.String),
   338  					}),
   339  				}),
   340  			}),
   341  			cty.ObjectVal(map[string]cty.Value{
   342  				"foo": cty.TupleVal([]cty.Value{
   343  					cty.ObjectVal(map[string]cty.Value{
   344  						"bar": cty.StringVal("bap"),
   345  						"baz": cty.StringVal("boop"),
   346  					}),
   347  					cty.ObjectVal(map[string]cty.Value{
   348  						"bar": cty.StringVal("blep"),
   349  						"baz": cty.NullVal(cty.String),
   350  					}),
   351  				}),
   352  			}),
   353  		},
   354  		"prior nested map": {
   355  			&configschema.Block{
   356  				BlockTypes: map[string]*configschema.NestedBlock{
   357  					"foo": {
   358  						Nesting: configschema.NestingMap,
   359  						Block: configschema.Block{
   360  							Attributes: map[string]*configschema.Attribute{
   361  								"bar": {
   362  									Type:     cty.String,
   363  									Optional: true,
   364  									Computed: true,
   365  								},
   366  								"baz": {
   367  									Type:     cty.String,
   368  									Optional: true,
   369  									Computed: true,
   370  								},
   371  							},
   372  						},
   373  					},
   374  				},
   375  			},
   376  			cty.ObjectVal(map[string]cty.Value{
   377  				"foo": cty.MapVal(map[string]cty.Value{
   378  					"a": cty.ObjectVal(map[string]cty.Value{
   379  						"bar": cty.StringVal("beep"),
   380  						"baz": cty.StringVal("boop"),
   381  					}),
   382  					"b": cty.ObjectVal(map[string]cty.Value{
   383  						"bar": cty.StringVal("blep"),
   384  						"baz": cty.StringVal("boot"),
   385  					}),
   386  				}),
   387  			}),
   388  			cty.ObjectVal(map[string]cty.Value{
   389  				"foo": cty.MapVal(map[string]cty.Value{
   390  					"a": cty.ObjectVal(map[string]cty.Value{
   391  						"bar": cty.StringVal("bap"),
   392  						"baz": cty.NullVal(cty.String),
   393  					}),
   394  					"c": cty.ObjectVal(map[string]cty.Value{
   395  						"bar": cty.StringVal("bosh"),
   396  						"baz": cty.NullVal(cty.String),
   397  					}),
   398  				}),
   399  			}),
   400  			cty.ObjectVal(map[string]cty.Value{
   401  				"foo": cty.MapVal(map[string]cty.Value{
   402  					"a": cty.ObjectVal(map[string]cty.Value{
   403  						"bar": cty.StringVal("bap"),
   404  						"baz": cty.StringVal("boop"),
   405  					}),
   406  					"c": cty.ObjectVal(map[string]cty.Value{
   407  						"bar": cty.StringVal("bosh"),
   408  						"baz": cty.NullVal(cty.String),
   409  					}),
   410  				}),
   411  			}),
   412  		},
   413  		"prior nested map with dynamic": {
   414  			&configschema.Block{
   415  				BlockTypes: map[string]*configschema.NestedBlock{
   416  					"foo": {
   417  						Nesting: configschema.NestingMap,
   418  						Block: configschema.Block{
   419  							Attributes: map[string]*configschema.Attribute{
   420  								"bar": {
   421  									Type:     cty.String,
   422  									Optional: true,
   423  									Computed: true,
   424  								},
   425  								"baz": {
   426  									Type:     cty.DynamicPseudoType,
   427  									Optional: true,
   428  									Computed: true,
   429  								},
   430  							},
   431  						},
   432  					},
   433  				},
   434  			},
   435  			cty.ObjectVal(map[string]cty.Value{
   436  				"foo": cty.ObjectVal(map[string]cty.Value{
   437  					"a": cty.ObjectVal(map[string]cty.Value{
   438  						"bar": cty.StringVal("beep"),
   439  						"baz": cty.StringVal("boop"),
   440  					}),
   441  					"b": cty.ObjectVal(map[string]cty.Value{
   442  						"bar": cty.StringVal("blep"),
   443  						"baz": cty.StringVal("boot"),
   444  					}),
   445  				}),
   446  			}),
   447  			cty.ObjectVal(map[string]cty.Value{
   448  				"foo": cty.ObjectVal(map[string]cty.Value{
   449  					"a": cty.ObjectVal(map[string]cty.Value{
   450  						"bar": cty.StringVal("bap"),
   451  						"baz": cty.NullVal(cty.String),
   452  					}),
   453  					"c": cty.ObjectVal(map[string]cty.Value{
   454  						"bar": cty.StringVal("bosh"),
   455  						"baz": cty.NullVal(cty.String),
   456  					}),
   457  				}),
   458  			}),
   459  			cty.ObjectVal(map[string]cty.Value{
   460  				"foo": cty.ObjectVal(map[string]cty.Value{
   461  					"a": cty.ObjectVal(map[string]cty.Value{
   462  						"bar": cty.StringVal("bap"),
   463  						"baz": cty.StringVal("boop"),
   464  					}),
   465  					"c": cty.ObjectVal(map[string]cty.Value{
   466  						"bar": cty.StringVal("bosh"),
   467  						"baz": cty.NullVal(cty.String),
   468  					}),
   469  				}),
   470  			}),
   471  		},
   472  		"prior nested set": {
   473  			&configschema.Block{
   474  				BlockTypes: map[string]*configschema.NestedBlock{
   475  					"foo": {
   476  						Nesting: configschema.NestingSet,
   477  						Block: configschema.Block{
   478  							Attributes: map[string]*configschema.Attribute{
   479  								"bar": {
   480  									// This non-computed attribute will serve
   481  									// as our matching key for propagating
   482  									// "baz" from elements in the prior value.
   483  									Type:     cty.String,
   484  									Optional: true,
   485  								},
   486  								"baz": {
   487  									Type:     cty.String,
   488  									Optional: true,
   489  									Computed: true,
   490  								},
   491  							},
   492  						},
   493  					},
   494  				},
   495  			},
   496  			cty.ObjectVal(map[string]cty.Value{
   497  				"foo": cty.SetVal([]cty.Value{
   498  					cty.ObjectVal(map[string]cty.Value{
   499  						"bar": cty.StringVal("beep"),
   500  						"baz": cty.StringVal("boop"),
   501  					}),
   502  					cty.ObjectVal(map[string]cty.Value{
   503  						"bar": cty.StringVal("blep"),
   504  						"baz": cty.StringVal("boot"),
   505  					}),
   506  				}),
   507  			}),
   508  			cty.ObjectVal(map[string]cty.Value{
   509  				"foo": cty.SetVal([]cty.Value{
   510  					cty.ObjectVal(map[string]cty.Value{
   511  						"bar": cty.StringVal("beep"),
   512  						"baz": cty.NullVal(cty.String),
   513  					}),
   514  					cty.ObjectVal(map[string]cty.Value{
   515  						"bar": cty.StringVal("bosh"),
   516  						"baz": cty.NullVal(cty.String),
   517  					}),
   518  				}),
   519  			}),
   520  			cty.ObjectVal(map[string]cty.Value{
   521  				"foo": cty.SetVal([]cty.Value{
   522  					cty.ObjectVal(map[string]cty.Value{
   523  						"bar": cty.StringVal("beep"),
   524  						"baz": cty.StringVal("boop"),
   525  					}),
   526  					cty.ObjectVal(map[string]cty.Value{
   527  						"bar": cty.StringVal("bosh"),
   528  						"baz": cty.NullVal(cty.String),
   529  					}),
   530  				}),
   531  			}),
   532  		},
   533  		"sets differing only by unknown": {
   534  			&configschema.Block{
   535  				BlockTypes: map[string]*configschema.NestedBlock{
   536  					"multi": {
   537  						Nesting: configschema.NestingSet,
   538  						Block: configschema.Block{
   539  							Attributes: map[string]*configschema.Attribute{
   540  								"optional": {
   541  									Type:     cty.String,
   542  									Optional: true,
   543  									Computed: true,
   544  								},
   545  							},
   546  						},
   547  					},
   548  				},
   549  			},
   550  			cty.NullVal(cty.DynamicPseudoType),
   551  			cty.ObjectVal(map[string]cty.Value{
   552  				"multi": cty.SetVal([]cty.Value{
   553  					cty.ObjectVal(map[string]cty.Value{
   554  						"optional": cty.UnknownVal(cty.String),
   555  					}),
   556  					cty.ObjectVal(map[string]cty.Value{
   557  						"optional": cty.UnknownVal(cty.String),
   558  					}),
   559  				}),
   560  			}),
   561  			cty.ObjectVal(map[string]cty.Value{
   562  				"multi": cty.SetVal([]cty.Value{
   563  					// These remain distinct because unknown values never
   564  					// compare equal. They may be consolidated together once
   565  					// the values become known, though.
   566  					cty.ObjectVal(map[string]cty.Value{
   567  						"optional": cty.UnknownVal(cty.String),
   568  					}),
   569  					cty.ObjectVal(map[string]cty.Value{
   570  						"optional": cty.UnknownVal(cty.String),
   571  					}),
   572  				}),
   573  			}),
   574  		},
   575  	}
   576  
   577  	for name, test := range tests {
   578  		t.Run(name, func(t *testing.T) {
   579  			got := ProposedNewObject(test.Schema, test.Prior, test.Config)
   580  			if !got.RawEquals(test.Want) {
   581  				t.Errorf("wrong result\ngot:  %swant: %s", dump.Value(got), dump.Value(test.Want))
   582  			}
   583  		})
   584  	}
   585  }