github.com/opentofu/opentofu@v1.7.1/internal/configs/configschema/coerce_value_test.go (about)

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