github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/plans/objchange/plan_valid_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  	"github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags"
    11  )
    12  
    13  func TestAssertPlanValid(t *testing.T) {
    14  	tests := map[string]struct {
    15  		Schema   *configschema.Block
    16  		Prior    cty.Value
    17  		Config   cty.Value
    18  		Planned  cty.Value
    19  		WantErrs []string
    20  	}{
    21  		"all empty": {
    22  			&configschema.Block{},
    23  			cty.EmptyObjectVal,
    24  			cty.EmptyObjectVal,
    25  			cty.EmptyObjectVal,
    26  			nil,
    27  		},
    28  		"no computed, all match": {
    29  			&configschema.Block{
    30  				Attributes: map[string]*configschema.Attribute{
    31  					"a": {
    32  						Type:     cty.String,
    33  						Optional: true,
    34  					},
    35  				},
    36  				BlockTypes: map[string]*configschema.NestedBlock{
    37  					"b": {
    38  						Nesting: configschema.NestingList,
    39  						Block: configschema.Block{
    40  							Attributes: map[string]*configschema.Attribute{
    41  								"c": {
    42  									Type:     cty.String,
    43  									Optional: true,
    44  								},
    45  							},
    46  						},
    47  					},
    48  				},
    49  			},
    50  			cty.ObjectVal(map[string]cty.Value{
    51  				"a": cty.StringVal("a value"),
    52  				"b": cty.ListVal([]cty.Value{
    53  					cty.ObjectVal(map[string]cty.Value{
    54  						"c": cty.StringVal("c value"),
    55  					}),
    56  				}),
    57  			}),
    58  			cty.ObjectVal(map[string]cty.Value{
    59  				"a": cty.StringVal("a value"),
    60  				"b": cty.ListVal([]cty.Value{
    61  					cty.ObjectVal(map[string]cty.Value{
    62  						"c": cty.StringVal("c value"),
    63  					}),
    64  				}),
    65  			}),
    66  			cty.ObjectVal(map[string]cty.Value{
    67  				"a": cty.StringVal("a value"),
    68  				"b": cty.ListVal([]cty.Value{
    69  					cty.ObjectVal(map[string]cty.Value{
    70  						"c": cty.StringVal("c value"),
    71  					}),
    72  				}),
    73  			}),
    74  			nil,
    75  		},
    76  		"no computed, plan matches, no prior": {
    77  			&configschema.Block{
    78  				Attributes: map[string]*configschema.Attribute{
    79  					"a": {
    80  						Type:     cty.String,
    81  						Optional: true,
    82  					},
    83  				},
    84  				BlockTypes: map[string]*configschema.NestedBlock{
    85  					"b": {
    86  						Nesting: configschema.NestingList,
    87  						Block: configschema.Block{
    88  							Attributes: map[string]*configschema.Attribute{
    89  								"c": {
    90  									Type:     cty.String,
    91  									Optional: true,
    92  								},
    93  							},
    94  						},
    95  					},
    96  				},
    97  			},
    98  			cty.NullVal(cty.Object(map[string]cty.Type{
    99  				"a": cty.String,
   100  				"b": cty.List(cty.Object(map[string]cty.Type{
   101  					"c": cty.String,
   102  				})),
   103  			})),
   104  			cty.ObjectVal(map[string]cty.Value{
   105  				"a": cty.StringVal("a value"),
   106  				"b": cty.ListVal([]cty.Value{
   107  					cty.ObjectVal(map[string]cty.Value{
   108  						"c": cty.StringVal("c value"),
   109  					}),
   110  				}),
   111  			}),
   112  			cty.ObjectVal(map[string]cty.Value{
   113  				"a": cty.StringVal("a value"),
   114  				"b": cty.ListVal([]cty.Value{
   115  					cty.ObjectVal(map[string]cty.Value{
   116  						"c": cty.StringVal("c value"),
   117  					}),
   118  				}),
   119  			}),
   120  			nil,
   121  		},
   122  		"no computed, invalid change in plan": {
   123  			&configschema.Block{
   124  				Attributes: map[string]*configschema.Attribute{
   125  					"a": {
   126  						Type:     cty.String,
   127  						Optional: true,
   128  					},
   129  				},
   130  				BlockTypes: map[string]*configschema.NestedBlock{
   131  					"b": {
   132  						Nesting: configschema.NestingList,
   133  						Block: configschema.Block{
   134  							Attributes: map[string]*configschema.Attribute{
   135  								"c": {
   136  									Type:     cty.String,
   137  									Optional: true,
   138  								},
   139  							},
   140  						},
   141  					},
   142  				},
   143  			},
   144  			cty.NullVal(cty.Object(map[string]cty.Type{
   145  				"a": cty.String,
   146  				"b": cty.List(cty.Object(map[string]cty.Type{
   147  					"c": cty.String,
   148  				})),
   149  			})),
   150  			cty.ObjectVal(map[string]cty.Value{
   151  				"a": cty.StringVal("a value"),
   152  				"b": cty.ListVal([]cty.Value{
   153  					cty.ObjectVal(map[string]cty.Value{
   154  						"c": cty.StringVal("c value"),
   155  					}),
   156  				}),
   157  			}),
   158  			cty.ObjectVal(map[string]cty.Value{
   159  				"a": cty.StringVal("a value"),
   160  				"b": cty.ListVal([]cty.Value{
   161  					cty.ObjectVal(map[string]cty.Value{
   162  						"c": cty.StringVal("new c value"),
   163  					}),
   164  				}),
   165  			}),
   166  			[]string{
   167  				`.b[0].c: planned value cty.StringVal("new c value") does not match config value cty.StringVal("c value")`,
   168  			},
   169  		},
   170  		"no computed, invalid change in plan sensitive": {
   171  			&configschema.Block{
   172  				Attributes: map[string]*configschema.Attribute{
   173  					"a": {
   174  						Type:     cty.String,
   175  						Optional: true,
   176  					},
   177  				},
   178  				BlockTypes: map[string]*configschema.NestedBlock{
   179  					"b": {
   180  						Nesting: configschema.NestingList,
   181  						Block: configschema.Block{
   182  							Attributes: map[string]*configschema.Attribute{
   183  								"c": {
   184  									Type:      cty.String,
   185  									Optional:  true,
   186  									Sensitive: true,
   187  								},
   188  							},
   189  						},
   190  					},
   191  				},
   192  			},
   193  			cty.NullVal(cty.Object(map[string]cty.Type{
   194  				"a": cty.String,
   195  				"b": cty.List(cty.Object(map[string]cty.Type{
   196  					"c": cty.String,
   197  				})),
   198  			})),
   199  			cty.ObjectVal(map[string]cty.Value{
   200  				"a": cty.StringVal("a value"),
   201  				"b": cty.ListVal([]cty.Value{
   202  					cty.ObjectVal(map[string]cty.Value{
   203  						"c": cty.StringVal("c value"),
   204  					}),
   205  				}),
   206  			}),
   207  			cty.ObjectVal(map[string]cty.Value{
   208  				"a": cty.StringVal("a value"),
   209  				"b": cty.ListVal([]cty.Value{
   210  					cty.ObjectVal(map[string]cty.Value{
   211  						"c": cty.StringVal("new c value"),
   212  					}),
   213  				}),
   214  			}),
   215  			[]string{
   216  				`.b[0].c: sensitive planned value does not match config value`,
   217  			},
   218  		},
   219  		"no computed, diff suppression in plan": {
   220  			&configschema.Block{
   221  				Attributes: map[string]*configschema.Attribute{
   222  					"a": {
   223  						Type:     cty.String,
   224  						Optional: true,
   225  					},
   226  				},
   227  				BlockTypes: map[string]*configschema.NestedBlock{
   228  					"b": {
   229  						Nesting: configschema.NestingList,
   230  						Block: configschema.Block{
   231  							Attributes: map[string]*configschema.Attribute{
   232  								"c": {
   233  									Type:     cty.String,
   234  									Optional: true,
   235  								},
   236  							},
   237  						},
   238  					},
   239  				},
   240  			},
   241  			cty.ObjectVal(map[string]cty.Value{
   242  				"a": cty.StringVal("a value"),
   243  				"b": cty.ListVal([]cty.Value{
   244  					cty.ObjectVal(map[string]cty.Value{
   245  						"c": cty.StringVal("c value"),
   246  					}),
   247  				}),
   248  			}),
   249  			cty.ObjectVal(map[string]cty.Value{
   250  				"a": cty.StringVal("a value"),
   251  				"b": cty.ListVal([]cty.Value{
   252  					cty.ObjectVal(map[string]cty.Value{
   253  						"c": cty.StringVal("new c value"),
   254  					}),
   255  				}),
   256  			}),
   257  			cty.ObjectVal(map[string]cty.Value{
   258  				"a": cty.StringVal("a value"),
   259  				"b": cty.ListVal([]cty.Value{
   260  					cty.ObjectVal(map[string]cty.Value{
   261  						"c": cty.StringVal("c value"), // plan uses value from prior object
   262  					}),
   263  				}),
   264  			}),
   265  			nil,
   266  		},
   267  		"no computed, all null": {
   268  			&configschema.Block{
   269  				Attributes: map[string]*configschema.Attribute{
   270  					"a": {
   271  						Type:     cty.String,
   272  						Optional: true,
   273  					},
   274  				},
   275  				BlockTypes: map[string]*configschema.NestedBlock{
   276  					"b": {
   277  						Nesting: configschema.NestingList,
   278  						Block: configschema.Block{
   279  							Attributes: map[string]*configschema.Attribute{
   280  								"c": {
   281  									Type:     cty.String,
   282  									Optional: true,
   283  								},
   284  							},
   285  						},
   286  					},
   287  				},
   288  			},
   289  			cty.ObjectVal(map[string]cty.Value{
   290  				"a": cty.NullVal(cty.String),
   291  				"b": cty.ListVal([]cty.Value{
   292  					cty.ObjectVal(map[string]cty.Value{
   293  						"c": cty.NullVal(cty.String),
   294  					}),
   295  				}),
   296  			}),
   297  			cty.ObjectVal(map[string]cty.Value{
   298  				"a": cty.NullVal(cty.String),
   299  				"b": cty.ListVal([]cty.Value{
   300  					cty.ObjectVal(map[string]cty.Value{
   301  						"c": cty.NullVal(cty.String),
   302  					}),
   303  				}),
   304  			}),
   305  			cty.ObjectVal(map[string]cty.Value{
   306  				"a": cty.NullVal(cty.String),
   307  				"b": cty.ListVal([]cty.Value{
   308  					cty.ObjectVal(map[string]cty.Value{
   309  						"c": cty.NullVal(cty.String),
   310  					}),
   311  				}),
   312  			}),
   313  			nil,
   314  		},
   315  		"nested map, normal update": {
   316  			&configschema.Block{
   317  				BlockTypes: map[string]*configschema.NestedBlock{
   318  					"b": {
   319  						Nesting: configschema.NestingMap,
   320  						Block: configschema.Block{
   321  							Attributes: map[string]*configschema.Attribute{
   322  								"c": {
   323  									Type:     cty.String,
   324  									Optional: true,
   325  								},
   326  							},
   327  						},
   328  					},
   329  				},
   330  			},
   331  			cty.ObjectVal(map[string]cty.Value{
   332  				"b": cty.MapVal(map[string]cty.Value{
   333  					"boop": cty.ObjectVal(map[string]cty.Value{
   334  						"c": cty.StringVal("hello"),
   335  					}),
   336  				}),
   337  			}),
   338  			cty.ObjectVal(map[string]cty.Value{
   339  				"b": cty.MapVal(map[string]cty.Value{
   340  					"boop": cty.ObjectVal(map[string]cty.Value{
   341  						"c": cty.StringVal("howdy"),
   342  					}),
   343  				}),
   344  			}),
   345  			cty.ObjectVal(map[string]cty.Value{
   346  				"b": cty.MapVal(map[string]cty.Value{
   347  					"boop": cty.ObjectVal(map[string]cty.Value{
   348  						"c": cty.StringVal("howdy"),
   349  					}),
   350  				}),
   351  			}),
   352  			nil,
   353  		},
   354  
   355  		// Nested block collections are never null
   356  		"nested list, null in plan": {
   357  			&configschema.Block{
   358  				BlockTypes: map[string]*configschema.NestedBlock{
   359  					"b": {
   360  						Nesting: configschema.NestingList,
   361  						Block: configschema.Block{
   362  							Attributes: map[string]*configschema.Attribute{
   363  								"c": {
   364  									Type:     cty.String,
   365  									Optional: true,
   366  								},
   367  							},
   368  						},
   369  					},
   370  				},
   371  			},
   372  			cty.NullVal(cty.Object(map[string]cty.Type{
   373  				"b": cty.List(cty.Object(map[string]cty.Type{
   374  					"c": cty.String,
   375  				})),
   376  			})),
   377  			cty.ObjectVal(map[string]cty.Value{
   378  				"b": cty.ListValEmpty(cty.Object(map[string]cty.Type{
   379  					"c": cty.String,
   380  				})),
   381  			}),
   382  			cty.ObjectVal(map[string]cty.Value{
   383  				"b": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
   384  					"c": cty.String,
   385  				}))),
   386  			}),
   387  			[]string{
   388  				`.b: attribute representing a list of nested blocks must be empty to indicate no blocks, not null`,
   389  			},
   390  		},
   391  		"nested set, null in plan": {
   392  			&configschema.Block{
   393  				BlockTypes: map[string]*configschema.NestedBlock{
   394  					"b": {
   395  						Nesting: configschema.NestingSet,
   396  						Block: configschema.Block{
   397  							Attributes: map[string]*configschema.Attribute{
   398  								"c": {
   399  									Type:     cty.String,
   400  									Optional: true,
   401  								},
   402  							},
   403  						},
   404  					},
   405  				},
   406  			},
   407  			cty.NullVal(cty.Object(map[string]cty.Type{
   408  				"b": cty.Set(cty.Object(map[string]cty.Type{
   409  					"c": cty.String,
   410  				})),
   411  			})),
   412  			cty.ObjectVal(map[string]cty.Value{
   413  				"b": cty.SetValEmpty(cty.Object(map[string]cty.Type{
   414  					"c": cty.String,
   415  				})),
   416  			}),
   417  			cty.ObjectVal(map[string]cty.Value{
   418  				"b": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
   419  					"c": cty.String,
   420  				}))),
   421  			}),
   422  			[]string{
   423  				`.b: attribute representing a set of nested blocks must be empty to indicate no blocks, not null`,
   424  			},
   425  		},
   426  		"nested map, null in plan": {
   427  			&configschema.Block{
   428  				BlockTypes: map[string]*configschema.NestedBlock{
   429  					"b": {
   430  						Nesting: configschema.NestingMap,
   431  						Block: configschema.Block{
   432  							Attributes: map[string]*configschema.Attribute{
   433  								"c": {
   434  									Type:     cty.String,
   435  									Optional: true,
   436  								},
   437  							},
   438  						},
   439  					},
   440  				},
   441  			},
   442  			cty.NullVal(cty.Object(map[string]cty.Type{
   443  				"b": cty.Map(cty.Object(map[string]cty.Type{
   444  					"c": cty.String,
   445  				})),
   446  			})),
   447  			cty.ObjectVal(map[string]cty.Value{
   448  				"b": cty.MapValEmpty(cty.Object(map[string]cty.Type{
   449  					"c": cty.String,
   450  				})),
   451  			}),
   452  			cty.ObjectVal(map[string]cty.Value{
   453  				"b": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{
   454  					"c": cty.String,
   455  				}))),
   456  			}),
   457  			[]string{
   458  				`.b: attribute representing a map of nested blocks must be empty to indicate no blocks, not null`,
   459  			},
   460  		},
   461  
   462  		// We don't actually do any validation for nested set blocks, and so
   463  		// the remaining cases here are just intending to ensure we don't
   464  		// inadvertently start generating errors incorrectly in future.
   465  		"nested set, no computed, no changes": {
   466  			&configschema.Block{
   467  				BlockTypes: map[string]*configschema.NestedBlock{
   468  					"b": {
   469  						Nesting: configschema.NestingSet,
   470  						Block: configschema.Block{
   471  							Attributes: map[string]*configschema.Attribute{
   472  								"c": {
   473  									Type:     cty.String,
   474  									Optional: true,
   475  								},
   476  							},
   477  						},
   478  					},
   479  				},
   480  			},
   481  			cty.ObjectVal(map[string]cty.Value{
   482  				"b": cty.SetVal([]cty.Value{
   483  					cty.ObjectVal(map[string]cty.Value{
   484  						"c": cty.StringVal("c value"),
   485  					}),
   486  				}),
   487  			}),
   488  			cty.ObjectVal(map[string]cty.Value{
   489  				"b": cty.SetVal([]cty.Value{
   490  					cty.ObjectVal(map[string]cty.Value{
   491  						"c": cty.StringVal("c value"),
   492  					}),
   493  				}),
   494  			}),
   495  			cty.ObjectVal(map[string]cty.Value{
   496  				"b": cty.SetVal([]cty.Value{
   497  					cty.ObjectVal(map[string]cty.Value{
   498  						"c": cty.StringVal("c value"),
   499  					}),
   500  				}),
   501  			}),
   502  			nil,
   503  		},
   504  		"nested set, no computed, invalid change in plan": {
   505  			&configschema.Block{
   506  				BlockTypes: map[string]*configschema.NestedBlock{
   507  					"b": {
   508  						Nesting: configschema.NestingSet,
   509  						Block: configschema.Block{
   510  							Attributes: map[string]*configschema.Attribute{
   511  								"c": {
   512  									Type:     cty.String,
   513  									Optional: true,
   514  								},
   515  							},
   516  						},
   517  					},
   518  				},
   519  			},
   520  			cty.ObjectVal(map[string]cty.Value{
   521  				"b": cty.SetVal([]cty.Value{
   522  					cty.ObjectVal(map[string]cty.Value{
   523  						"c": cty.StringVal("c value"),
   524  					}),
   525  				}),
   526  			}),
   527  			cty.ObjectVal(map[string]cty.Value{
   528  				"b": cty.SetVal([]cty.Value{
   529  					cty.ObjectVal(map[string]cty.Value{
   530  						"c": cty.StringVal("c value"),
   531  					}),
   532  				}),
   533  			}),
   534  			cty.ObjectVal(map[string]cty.Value{
   535  				"b": cty.SetVal([]cty.Value{
   536  					cty.ObjectVal(map[string]cty.Value{
   537  						"c": cty.StringVal("new c value"), // matches neither prior nor config
   538  					}),
   539  				}),
   540  			}),
   541  			nil,
   542  		},
   543  		"nested set, no computed, diff suppressed": {
   544  			&configschema.Block{
   545  				BlockTypes: map[string]*configschema.NestedBlock{
   546  					"b": {
   547  						Nesting: configschema.NestingSet,
   548  						Block: configschema.Block{
   549  							Attributes: map[string]*configschema.Attribute{
   550  								"c": {
   551  									Type:     cty.String,
   552  									Optional: true,
   553  								},
   554  							},
   555  						},
   556  					},
   557  				},
   558  			},
   559  			cty.ObjectVal(map[string]cty.Value{
   560  				"b": cty.SetVal([]cty.Value{
   561  					cty.ObjectVal(map[string]cty.Value{
   562  						"c": cty.StringVal("c value"),
   563  					}),
   564  				}),
   565  			}),
   566  			cty.ObjectVal(map[string]cty.Value{
   567  				"b": cty.SetVal([]cty.Value{
   568  					cty.ObjectVal(map[string]cty.Value{
   569  						"c": cty.StringVal("new c value"),
   570  					}),
   571  				}),
   572  			}),
   573  			cty.ObjectVal(map[string]cty.Value{
   574  				"b": cty.SetVal([]cty.Value{
   575  					cty.ObjectVal(map[string]cty.Value{
   576  						"c": cty.StringVal("c value"), // plan uses value from prior object
   577  					}),
   578  				}),
   579  			}),
   580  			nil,
   581  		},
   582  	}
   583  
   584  	for name, test := range tests {
   585  		t.Run(name, func(t *testing.T) {
   586  			errs := AssertPlanValid(test.Schema, test.Prior, test.Config, test.Planned)
   587  
   588  			wantErrs := make(map[string]struct{})
   589  			gotErrs := make(map[string]struct{})
   590  			for _, err := range errs {
   591  				gotErrs[tfdiags.FormatError(err)] = struct{}{}
   592  			}
   593  			for _, msg := range test.WantErrs {
   594  				wantErrs[msg] = struct{}{}
   595  			}
   596  
   597  			t.Logf(
   598  				"\nprior:  %sconfig:  %splanned: %s",
   599  				dump.Value(test.Planned),
   600  				dump.Value(test.Config),
   601  				dump.Value(test.Planned),
   602  			)
   603  			for msg := range wantErrs {
   604  				if _, ok := gotErrs[msg]; !ok {
   605  					t.Errorf("missing expected error: %s", msg)
   606  				}
   607  			}
   608  			for msg := range gotErrs {
   609  				if _, ok := wantErrs[msg]; !ok {
   610  					t.Errorf("unexpected extra error: %s", msg)
   611  				}
   612  			}
   613  		})
   614  	}
   615  }