github.com/kevinklinger/open_terraform@v1.3.6/noninternal/configs/configschema/coerce_value_test.go (about)

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