github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/configs/configschema/coerce_value_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package configschema
     5  
     6  import (
     7  	"testing"
     8  
     9  	"github.com/zclconf/go-cty/cty"
    10  
    11  	"github.com/terramate-io/tf/tfdiags"
    12  )
    13  
    14  func TestCoerceValue(t *testing.T) {
    15  	tests := map[string]struct {
    16  		Schema    *Block
    17  		Input     cty.Value
    18  		WantValue cty.Value
    19  		WantErr   string
    20  	}{
    21  		"empty schema and value": {
    22  			&Block{},
    23  			cty.EmptyObjectVal,
    24  			cty.EmptyObjectVal,
    25  			``,
    26  		},
    27  		"attribute present": {
    28  			&Block{
    29  				Attributes: map[string]*Attribute{
    30  					"foo": {
    31  						Type:     cty.String,
    32  						Optional: true,
    33  					},
    34  				},
    35  			},
    36  			cty.ObjectVal(map[string]cty.Value{
    37  				"foo": cty.True,
    38  			}),
    39  			cty.ObjectVal(map[string]cty.Value{
    40  				"foo": cty.StringVal("true"),
    41  			}),
    42  			``,
    43  		},
    44  		"single block present": {
    45  			&Block{
    46  				BlockTypes: map[string]*NestedBlock{
    47  					"foo": {
    48  						Block:   Block{},
    49  						Nesting: NestingSingle,
    50  					},
    51  				},
    52  			},
    53  			cty.ObjectVal(map[string]cty.Value{
    54  				"foo": cty.EmptyObjectVal,
    55  			}),
    56  			cty.ObjectVal(map[string]cty.Value{
    57  				"foo": cty.EmptyObjectVal,
    58  			}),
    59  			``,
    60  		},
    61  		"single block wrong type": {
    62  			&Block{
    63  				BlockTypes: map[string]*NestedBlock{
    64  					"foo": {
    65  						Block:   Block{},
    66  						Nesting: NestingSingle,
    67  					},
    68  				},
    69  			},
    70  			cty.ObjectVal(map[string]cty.Value{
    71  				"foo": cty.True,
    72  			}),
    73  			cty.DynamicVal,
    74  			`.foo: an object is required`,
    75  		},
    76  		"list block with one item": {
    77  			&Block{
    78  				BlockTypes: map[string]*NestedBlock{
    79  					"foo": {
    80  						Block:   Block{},
    81  						Nesting: NestingList,
    82  					},
    83  				},
    84  			},
    85  			cty.ObjectVal(map[string]cty.Value{
    86  				"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}),
    87  			}),
    88  			cty.ObjectVal(map[string]cty.Value{
    89  				"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}),
    90  			}),
    91  			``,
    92  		},
    93  		"set block with one item": {
    94  			&Block{
    95  				BlockTypes: map[string]*NestedBlock{
    96  					"foo": {
    97  						Block:   Block{},
    98  						Nesting: NestingSet,
    99  					},
   100  				},
   101  			},
   102  			cty.ObjectVal(map[string]cty.Value{
   103  				"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}), // can implicitly convert to set
   104  			}),
   105  			cty.ObjectVal(map[string]cty.Value{
   106  				"foo": cty.SetVal([]cty.Value{cty.EmptyObjectVal}),
   107  			}),
   108  			``,
   109  		},
   110  		"map block with one item": {
   111  			&Block{
   112  				BlockTypes: map[string]*NestedBlock{
   113  					"foo": {
   114  						Block:   Block{},
   115  						Nesting: NestingMap,
   116  					},
   117  				},
   118  			},
   119  			cty.ObjectVal(map[string]cty.Value{
   120  				"foo": cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}),
   121  			}),
   122  			cty.ObjectVal(map[string]cty.Value{
   123  				"foo": cty.MapVal(map[string]cty.Value{"foo": cty.EmptyObjectVal}),
   124  			}),
   125  			``,
   126  		},
   127  		"list block with one item having an attribute": {
   128  			&Block{
   129  				BlockTypes: map[string]*NestedBlock{
   130  					"foo": {
   131  						Block: Block{
   132  							Attributes: map[string]*Attribute{
   133  								"bar": {
   134  									Type:     cty.String,
   135  									Required: true,
   136  								},
   137  							},
   138  						},
   139  						Nesting: NestingList,
   140  					},
   141  				},
   142  			},
   143  			cty.ObjectVal(map[string]cty.Value{
   144  				"foo": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
   145  					"bar": cty.StringVal("hello"),
   146  				})}),
   147  			}),
   148  			cty.ObjectVal(map[string]cty.Value{
   149  				"foo": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
   150  					"bar": cty.StringVal("hello"),
   151  				})}),
   152  			}),
   153  			``,
   154  		},
   155  		"list block with one item having a missing attribute": {
   156  			&Block{
   157  				BlockTypes: map[string]*NestedBlock{
   158  					"foo": {
   159  						Block: Block{
   160  							Attributes: map[string]*Attribute{
   161  								"bar": {
   162  									Type:     cty.String,
   163  									Required: true,
   164  								},
   165  							},
   166  						},
   167  						Nesting: NestingList,
   168  					},
   169  				},
   170  			},
   171  			cty.ObjectVal(map[string]cty.Value{
   172  				"foo": cty.ListVal([]cty.Value{cty.EmptyObjectVal}),
   173  			}),
   174  			cty.DynamicVal,
   175  			`.foo[0]: attribute "bar" is required`,
   176  		},
   177  		"list block with one item having an extraneous attribute": {
   178  			&Block{
   179  				BlockTypes: map[string]*NestedBlock{
   180  					"foo": {
   181  						Block:   Block{},
   182  						Nesting: NestingList,
   183  					},
   184  				},
   185  			},
   186  			cty.ObjectVal(map[string]cty.Value{
   187  				"foo": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
   188  					"bar": cty.StringVal("hello"),
   189  				})}),
   190  			}),
   191  			cty.DynamicVal,
   192  			`.foo[0]: unexpected attribute "bar"`,
   193  		},
   194  		"missing optional attribute": {
   195  			&Block{
   196  				Attributes: map[string]*Attribute{
   197  					"foo": {
   198  						Type:     cty.String,
   199  						Optional: true,
   200  					},
   201  				},
   202  			},
   203  			cty.EmptyObjectVal,
   204  			cty.ObjectVal(map[string]cty.Value{
   205  				"foo": cty.NullVal(cty.String),
   206  			}),
   207  			``,
   208  		},
   209  		"missing optional single block": {
   210  			&Block{
   211  				BlockTypes: map[string]*NestedBlock{
   212  					"foo": {
   213  						Block:   Block{},
   214  						Nesting: NestingSingle,
   215  					},
   216  				},
   217  			},
   218  			cty.EmptyObjectVal,
   219  			cty.ObjectVal(map[string]cty.Value{
   220  				"foo": cty.NullVal(cty.EmptyObject),
   221  			}),
   222  			``,
   223  		},
   224  		"missing optional list block": {
   225  			&Block{
   226  				BlockTypes: map[string]*NestedBlock{
   227  					"foo": {
   228  						Block:   Block{},
   229  						Nesting: NestingList,
   230  					},
   231  				},
   232  			},
   233  			cty.EmptyObjectVal,
   234  			cty.ObjectVal(map[string]cty.Value{
   235  				"foo": cty.ListValEmpty(cty.EmptyObject),
   236  			}),
   237  			``,
   238  		},
   239  		"missing optional set block": {
   240  			&Block{
   241  				BlockTypes: map[string]*NestedBlock{
   242  					"foo": {
   243  						Block:   Block{},
   244  						Nesting: NestingSet,
   245  					},
   246  				},
   247  			},
   248  			cty.EmptyObjectVal,
   249  			cty.ObjectVal(map[string]cty.Value{
   250  				"foo": cty.SetValEmpty(cty.EmptyObject),
   251  			}),
   252  			``,
   253  		},
   254  		"missing optional map block": {
   255  			&Block{
   256  				BlockTypes: map[string]*NestedBlock{
   257  					"foo": {
   258  						Block:   Block{},
   259  						Nesting: NestingMap,
   260  					},
   261  				},
   262  			},
   263  			cty.EmptyObjectVal,
   264  			cty.ObjectVal(map[string]cty.Value{
   265  				"foo": cty.MapValEmpty(cty.EmptyObject),
   266  			}),
   267  			``,
   268  		},
   269  		"missing required attribute": {
   270  			&Block{
   271  				Attributes: map[string]*Attribute{
   272  					"foo": {
   273  						Type:     cty.String,
   274  						Required: true,
   275  					},
   276  				},
   277  			},
   278  			cty.EmptyObjectVal,
   279  			cty.DynamicVal,
   280  			`attribute "foo" is required`,
   281  		},
   282  		"missing required single block": {
   283  			&Block{
   284  				BlockTypes: map[string]*NestedBlock{
   285  					"foo": {
   286  						Block:    Block{},
   287  						Nesting:  NestingSingle,
   288  						MinItems: 1,
   289  						MaxItems: 1,
   290  					},
   291  				},
   292  			},
   293  			cty.EmptyObjectVal,
   294  			cty.ObjectVal(map[string]cty.Value{
   295  				"foo": cty.NullVal(cty.EmptyObject),
   296  			}),
   297  			``,
   298  		},
   299  		"unknown nested list": {
   300  			&Block{
   301  				Attributes: map[string]*Attribute{
   302  					"attr": {
   303  						Type:     cty.String,
   304  						Required: true,
   305  					},
   306  				},
   307  				BlockTypes: map[string]*NestedBlock{
   308  					"foo": {
   309  						Block:    Block{},
   310  						Nesting:  NestingList,
   311  						MinItems: 2,
   312  					},
   313  				},
   314  			},
   315  			cty.ObjectVal(map[string]cty.Value{
   316  				"attr": cty.StringVal("test"),
   317  				"foo":  cty.UnknownVal(cty.EmptyObject),
   318  			}),
   319  			cty.ObjectVal(map[string]cty.Value{
   320  				"attr": cty.StringVal("test"),
   321  				"foo":  cty.UnknownVal(cty.List(cty.EmptyObject)),
   322  			}),
   323  			"",
   324  		},
   325  		"unknowns in nested list": {
   326  			&Block{
   327  				BlockTypes: map[string]*NestedBlock{
   328  					"foo": {
   329  						Block: Block{
   330  							Attributes: map[string]*Attribute{
   331  								"attr": {
   332  									Type:     cty.String,
   333  									Required: true,
   334  								},
   335  							},
   336  						},
   337  						Nesting:  NestingList,
   338  						MinItems: 2,
   339  					},
   340  				},
   341  			},
   342  			cty.ObjectVal(map[string]cty.Value{
   343  				"foo": cty.ListVal([]cty.Value{
   344  					cty.ObjectVal(map[string]cty.Value{
   345  						"attr": cty.UnknownVal(cty.String),
   346  					}),
   347  				}),
   348  			}),
   349  			cty.ObjectVal(map[string]cty.Value{
   350  				"foo": cty.ListVal([]cty.Value{
   351  					cty.ObjectVal(map[string]cty.Value{
   352  						"attr": cty.UnknownVal(cty.String),
   353  					}),
   354  				}),
   355  			}),
   356  			"",
   357  		},
   358  		"unknown nested set": {
   359  			&Block{
   360  				Attributes: map[string]*Attribute{
   361  					"attr": {
   362  						Type:     cty.String,
   363  						Required: true,
   364  					},
   365  				},
   366  				BlockTypes: map[string]*NestedBlock{
   367  					"foo": {
   368  						Block:    Block{},
   369  						Nesting:  NestingSet,
   370  						MinItems: 1,
   371  					},
   372  				},
   373  			},
   374  			cty.ObjectVal(map[string]cty.Value{
   375  				"attr": cty.StringVal("test"),
   376  				"foo":  cty.UnknownVal(cty.EmptyObject),
   377  			}),
   378  			cty.ObjectVal(map[string]cty.Value{
   379  				"attr": cty.StringVal("test"),
   380  				"foo":  cty.UnknownVal(cty.Set(cty.EmptyObject)),
   381  			}),
   382  			"",
   383  		},
   384  		"unknown nested map": {
   385  			&Block{
   386  				Attributes: map[string]*Attribute{
   387  					"attr": {
   388  						Type:     cty.String,
   389  						Required: true,
   390  					},
   391  				},
   392  				BlockTypes: map[string]*NestedBlock{
   393  					"foo": {
   394  						Block:    Block{},
   395  						Nesting:  NestingMap,
   396  						MinItems: 1,
   397  					},
   398  				},
   399  			},
   400  			cty.ObjectVal(map[string]cty.Value{
   401  				"attr": cty.StringVal("test"),
   402  				"foo":  cty.UnknownVal(cty.Map(cty.String)),
   403  			}),
   404  			cty.ObjectVal(map[string]cty.Value{
   405  				"attr": cty.StringVal("test"),
   406  				"foo":  cty.UnknownVal(cty.Map(cty.EmptyObject)),
   407  			}),
   408  			"",
   409  		},
   410  		"extraneous attribute": {
   411  			&Block{},
   412  			cty.ObjectVal(map[string]cty.Value{
   413  				"foo": cty.StringVal("bar"),
   414  			}),
   415  			cty.DynamicVal,
   416  			`unexpected attribute "foo"`,
   417  		},
   418  		"wrong attribute type": {
   419  			&Block{
   420  				Attributes: map[string]*Attribute{
   421  					"foo": {
   422  						Type:     cty.Number,
   423  						Required: true,
   424  					},
   425  				},
   426  			},
   427  			cty.ObjectVal(map[string]cty.Value{
   428  				"foo": cty.False,
   429  			}),
   430  			cty.DynamicVal,
   431  			`.foo: number required`,
   432  		},
   433  		"unset computed value": {
   434  			&Block{
   435  				Attributes: map[string]*Attribute{
   436  					"foo": {
   437  						Type:     cty.String,
   438  						Optional: true,
   439  						Computed: true,
   440  					},
   441  				},
   442  			},
   443  			cty.ObjectVal(map[string]cty.Value{}),
   444  			cty.ObjectVal(map[string]cty.Value{
   445  				"foo": cty.NullVal(cty.String),
   446  			}),
   447  			``,
   448  		},
   449  		"dynamic value attributes": {
   450  			&Block{
   451  				BlockTypes: map[string]*NestedBlock{
   452  					"foo": {
   453  						Nesting: NestingMap,
   454  						Block: Block{
   455  							Attributes: map[string]*Attribute{
   456  								"bar": {
   457  									Type:     cty.String,
   458  									Optional: true,
   459  									Computed: true,
   460  								},
   461  								"baz": {
   462  									Type:     cty.DynamicPseudoType,
   463  									Optional: true,
   464  									Computed: true,
   465  								},
   466  							},
   467  						},
   468  					},
   469  				},
   470  			},
   471  			cty.ObjectVal(map[string]cty.Value{
   472  				"foo": cty.ObjectVal(map[string]cty.Value{
   473  					"a": cty.ObjectVal(map[string]cty.Value{
   474  						"bar": cty.StringVal("beep"),
   475  					}),
   476  					"b": cty.ObjectVal(map[string]cty.Value{
   477  						"bar": cty.StringVal("boop"),
   478  						"baz": cty.NumberIntVal(8),
   479  					}),
   480  				}),
   481  			}),
   482  			cty.ObjectVal(map[string]cty.Value{
   483  				"foo": cty.ObjectVal(map[string]cty.Value{
   484  					"a": cty.ObjectVal(map[string]cty.Value{
   485  						"bar": cty.StringVal("beep"),
   486  						"baz": cty.NullVal(cty.DynamicPseudoType),
   487  					}),
   488  					"b": cty.ObjectVal(map[string]cty.Value{
   489  						"bar": cty.StringVal("boop"),
   490  						"baz": cty.NumberIntVal(8),
   491  					}),
   492  				}),
   493  			}),
   494  			``,
   495  		},
   496  		"dynamic attributes in map": {
   497  			// Convert a block represented as a map to an object if a
   498  			// DynamicPseudoType causes the element types to mismatch.
   499  			&Block{
   500  				BlockTypes: map[string]*NestedBlock{
   501  					"foo": {
   502  						Nesting: NestingMap,
   503  						Block: Block{
   504  							Attributes: map[string]*Attribute{
   505  								"bar": {
   506  									Type:     cty.String,
   507  									Optional: true,
   508  									Computed: true,
   509  								},
   510  								"baz": {
   511  									Type:     cty.DynamicPseudoType,
   512  									Optional: true,
   513  									Computed: true,
   514  								},
   515  							},
   516  						},
   517  					},
   518  				},
   519  			},
   520  			cty.ObjectVal(map[string]cty.Value{
   521  				"foo": cty.MapVal(map[string]cty.Value{
   522  					"a": cty.ObjectVal(map[string]cty.Value{
   523  						"bar": cty.StringVal("beep"),
   524  					}),
   525  					"b": cty.ObjectVal(map[string]cty.Value{
   526  						"bar": cty.StringVal("boop"),
   527  					}),
   528  				}),
   529  			}),
   530  			cty.ObjectVal(map[string]cty.Value{
   531  				"foo": cty.ObjectVal(map[string]cty.Value{
   532  					"a": cty.ObjectVal(map[string]cty.Value{
   533  						"bar": cty.StringVal("beep"),
   534  						"baz": cty.NullVal(cty.DynamicPseudoType),
   535  					}),
   536  					"b": cty.ObjectVal(map[string]cty.Value{
   537  						"bar": cty.StringVal("boop"),
   538  						"baz": cty.NullVal(cty.DynamicPseudoType),
   539  					}),
   540  				}),
   541  			}),
   542  			``,
   543  		},
   544  		"nested types": {
   545  			// handle NestedTypes
   546  			&Block{
   547  				Attributes: map[string]*Attribute{
   548  					"foo": {
   549  						NestedType: &Object{
   550  							Nesting: NestingList,
   551  							Attributes: map[string]*Attribute{
   552  								"bar": {
   553  									Type:     cty.String,
   554  									Required: true,
   555  								},
   556  								"baz": {
   557  									Type:     cty.Map(cty.String),
   558  									Optional: true,
   559  								},
   560  							},
   561  						},
   562  						Optional: true,
   563  					},
   564  					"fob": {
   565  						NestedType: &Object{
   566  							Nesting: NestingSet,
   567  							Attributes: map[string]*Attribute{
   568  								"bar": {
   569  									Type:     cty.String,
   570  									Optional: true,
   571  								},
   572  							},
   573  						},
   574  						Optional: true,
   575  					},
   576  				},
   577  			},
   578  			cty.ObjectVal(map[string]cty.Value{
   579  				"foo": cty.ListVal([]cty.Value{
   580  					cty.ObjectVal(map[string]cty.Value{
   581  						"bar": cty.StringVal("beep"),
   582  					}),
   583  					cty.ObjectVal(map[string]cty.Value{
   584  						"bar": cty.StringVal("boop"),
   585  					}),
   586  				}),
   587  			}),
   588  			cty.ObjectVal(map[string]cty.Value{
   589  				"foo": cty.ListVal([]cty.Value{
   590  					cty.ObjectVal(map[string]cty.Value{
   591  						"bar": cty.StringVal("beep"),
   592  						"baz": cty.NullVal(cty.Map(cty.String)),
   593  					}),
   594  					cty.ObjectVal(map[string]cty.Value{
   595  						"bar": cty.StringVal("boop"),
   596  						"baz": cty.NullVal(cty.Map(cty.String)),
   597  					}),
   598  				}),
   599  				"fob": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
   600  					"bar": cty.String,
   601  				}))),
   602  			}),
   603  			``,
   604  		},
   605  	}
   606  
   607  	for name, test := range tests {
   608  		t.Run(name, func(t *testing.T) {
   609  			gotValue, gotErrObj := test.Schema.CoerceValue(test.Input)
   610  
   611  			if gotErrObj == nil {
   612  				if test.WantErr != "" {
   613  					t.Fatalf("coersion succeeded; want error: %q", test.WantErr)
   614  				}
   615  			} else {
   616  				gotErr := tfdiags.FormatError(gotErrObj)
   617  				if gotErr != test.WantErr {
   618  					t.Fatalf("wrong error\ngot:  %s\nwant: %s", gotErr, test.WantErr)
   619  				}
   620  				return
   621  			}
   622  
   623  			if !gotValue.RawEquals(test.WantValue) {
   624  				t.Errorf("wrong result\ninput: %#v\ngot:   %#v\nwant:  %#v", test.Input, gotValue, test.WantValue)
   625  			}
   626  		})
   627  	}
   628  }