github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/configs/configschema/decoder_spec_test.go (about)

     1  package configschema
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/apparentlymart/go-dump/dump"
     7  	"github.com/davecgh/go-spew/spew"
     8  
     9  	"github.com/hashicorp/hcl/v2"
    10  	"github.com/hashicorp/hcl/v2/hcldec"
    11  	"github.com/hashicorp/hcl/v2/hcltest"
    12  	"github.com/zclconf/go-cty/cty"
    13  )
    14  
    15  func TestBlockDecoderSpec(t *testing.T) {
    16  	tests := map[string]struct {
    17  		Schema    *Block
    18  		TestBody  hcl.Body
    19  		Want      cty.Value
    20  		DiagCount int
    21  	}{
    22  		"empty": {
    23  			&Block{},
    24  			hcl.EmptyBody(),
    25  			cty.EmptyObjectVal,
    26  			0,
    27  		},
    28  		"nil": {
    29  			nil,
    30  			hcl.EmptyBody(),
    31  			cty.EmptyObjectVal,
    32  			0,
    33  		},
    34  		"attributes": {
    35  			&Block{
    36  				Attributes: map[string]*Attribute{
    37  					"optional": {
    38  						Type:     cty.Number,
    39  						Optional: true,
    40  					},
    41  					"required": {
    42  						Type:     cty.String,
    43  						Required: true,
    44  					},
    45  					"computed": {
    46  						Type:     cty.List(cty.Bool),
    47  						Computed: true,
    48  					},
    49  					"optional_computed": {
    50  						Type:     cty.Map(cty.Bool),
    51  						Optional: true,
    52  						Computed: true,
    53  					},
    54  					"optional_computed_overridden": {
    55  						Type:     cty.Bool,
    56  						Optional: true,
    57  						Computed: true,
    58  					},
    59  					"optional_computed_unknown": {
    60  						Type:     cty.String,
    61  						Optional: true,
    62  						Computed: true,
    63  					},
    64  				},
    65  			},
    66  			hcltest.MockBody(&hcl.BodyContent{
    67  				Attributes: hcl.Attributes{
    68  					"required": {
    69  						Name: "required",
    70  						Expr: hcltest.MockExprLiteral(cty.NumberIntVal(5)),
    71  					},
    72  					"optional_computed_overridden": {
    73  						Name: "optional_computed_overridden",
    74  						Expr: hcltest.MockExprLiteral(cty.True),
    75  					},
    76  					"optional_computed_unknown": {
    77  						Name: "optional_computed_overridden",
    78  						Expr: hcltest.MockExprLiteral(cty.UnknownVal(cty.String)),
    79  					},
    80  				},
    81  			}),
    82  			cty.ObjectVal(map[string]cty.Value{
    83  				"optional":                     cty.NullVal(cty.Number),
    84  				"required":                     cty.StringVal("5"), // converted from number to string
    85  				"computed":                     cty.NullVal(cty.List(cty.Bool)),
    86  				"optional_computed":            cty.NullVal(cty.Map(cty.Bool)),
    87  				"optional_computed_overridden": cty.True,
    88  				"optional_computed_unknown":    cty.UnknownVal(cty.String),
    89  			}),
    90  			0,
    91  		},
    92  		"dynamically-typed attribute": {
    93  			&Block{
    94  				Attributes: map[string]*Attribute{
    95  					"foo": {
    96  						Type:     cty.DynamicPseudoType, // any type is permitted
    97  						Required: true,
    98  					},
    99  				},
   100  			},
   101  			hcltest.MockBody(&hcl.BodyContent{
   102  				Attributes: hcl.Attributes{
   103  					"foo": {
   104  						Name: "foo",
   105  						Expr: hcltest.MockExprLiteral(cty.True),
   106  					},
   107  				},
   108  			}),
   109  			cty.ObjectVal(map[string]cty.Value{
   110  				"foo": cty.True,
   111  			}),
   112  			0,
   113  		},
   114  		"dynamically-typed attribute omitted": {
   115  			&Block{
   116  				Attributes: map[string]*Attribute{
   117  					"foo": {
   118  						Type:     cty.DynamicPseudoType, // any type is permitted
   119  						Optional: true,
   120  					},
   121  				},
   122  			},
   123  			hcltest.MockBody(&hcl.BodyContent{}),
   124  			cty.ObjectVal(map[string]cty.Value{
   125  				"foo": cty.NullVal(cty.DynamicPseudoType),
   126  			}),
   127  			0,
   128  		},
   129  		"required attribute omitted": {
   130  			&Block{
   131  				Attributes: map[string]*Attribute{
   132  					"foo": {
   133  						Type:     cty.Bool,
   134  						Required: true,
   135  					},
   136  				},
   137  			},
   138  			hcltest.MockBody(&hcl.BodyContent{}),
   139  			cty.ObjectVal(map[string]cty.Value{
   140  				"foo": cty.NullVal(cty.Bool),
   141  			}),
   142  			1, // missing required attribute
   143  		},
   144  		"wrong attribute type": {
   145  			&Block{
   146  				Attributes: map[string]*Attribute{
   147  					"optional": {
   148  						Type:     cty.Number,
   149  						Optional: true,
   150  					},
   151  				},
   152  			},
   153  			hcltest.MockBody(&hcl.BodyContent{
   154  				Attributes: hcl.Attributes{
   155  					"optional": {
   156  						Name: "optional",
   157  						Expr: hcltest.MockExprLiteral(cty.True),
   158  					},
   159  				},
   160  			}),
   161  			cty.ObjectVal(map[string]cty.Value{
   162  				"optional": cty.UnknownVal(cty.Number),
   163  			}),
   164  			1, // incorrect type; number required
   165  		},
   166  		"blocks": {
   167  			&Block{
   168  				BlockTypes: map[string]*NestedBlock{
   169  					"single": {
   170  						Nesting: NestingSingle,
   171  						Block:   Block{},
   172  					},
   173  					"list": {
   174  						Nesting: NestingList,
   175  						Block:   Block{},
   176  					},
   177  					"set": {
   178  						Nesting: NestingSet,
   179  						Block:   Block{},
   180  					},
   181  					"map": {
   182  						Nesting: NestingMap,
   183  						Block:   Block{},
   184  					},
   185  				},
   186  			},
   187  			hcltest.MockBody(&hcl.BodyContent{
   188  				Blocks: hcl.Blocks{
   189  					&hcl.Block{
   190  						Type: "list",
   191  						Body: hcl.EmptyBody(),
   192  					},
   193  					&hcl.Block{
   194  						Type: "single",
   195  						Body: hcl.EmptyBody(),
   196  					},
   197  					&hcl.Block{
   198  						Type: "list",
   199  						Body: hcl.EmptyBody(),
   200  					},
   201  					&hcl.Block{
   202  						Type: "set",
   203  						Body: hcl.EmptyBody(),
   204  					},
   205  					&hcl.Block{
   206  						Type:        "map",
   207  						Labels:      []string{"foo"},
   208  						LabelRanges: []hcl.Range{{}},
   209  						Body:        hcl.EmptyBody(),
   210  					},
   211  					&hcl.Block{
   212  						Type:        "map",
   213  						Labels:      []string{"bar"},
   214  						LabelRanges: []hcl.Range{{}},
   215  						Body:        hcl.EmptyBody(),
   216  					},
   217  					&hcl.Block{
   218  						Type: "set",
   219  						Body: hcl.EmptyBody(),
   220  					},
   221  				},
   222  			}),
   223  			cty.ObjectVal(map[string]cty.Value{
   224  				"single": cty.EmptyObjectVal,
   225  				"list": cty.ListVal([]cty.Value{
   226  					cty.EmptyObjectVal,
   227  					cty.EmptyObjectVal,
   228  				}),
   229  				"set": cty.SetVal([]cty.Value{
   230  					cty.EmptyObjectVal,
   231  					cty.EmptyObjectVal,
   232  				}),
   233  				"map": cty.MapVal(map[string]cty.Value{
   234  					"foo": cty.EmptyObjectVal,
   235  					"bar": cty.EmptyObjectVal,
   236  				}),
   237  			}),
   238  			0,
   239  		},
   240  		"blocks with dynamically-typed attributes": {
   241  			&Block{
   242  				BlockTypes: map[string]*NestedBlock{
   243  					"single": {
   244  						Nesting: NestingSingle,
   245  						Block: Block{
   246  							Attributes: map[string]*Attribute{
   247  								"a": {
   248  									Type:     cty.DynamicPseudoType,
   249  									Optional: true,
   250  								},
   251  							},
   252  						},
   253  					},
   254  					"list": {
   255  						Nesting: NestingList,
   256  						Block: Block{
   257  							Attributes: map[string]*Attribute{
   258  								"a": {
   259  									Type:     cty.DynamicPseudoType,
   260  									Optional: true,
   261  								},
   262  							},
   263  						},
   264  					},
   265  					"map": {
   266  						Nesting: NestingMap,
   267  						Block: Block{
   268  							Attributes: map[string]*Attribute{
   269  								"a": {
   270  									Type:     cty.DynamicPseudoType,
   271  									Optional: true,
   272  								},
   273  							},
   274  						},
   275  					},
   276  				},
   277  			},
   278  			hcltest.MockBody(&hcl.BodyContent{
   279  				Blocks: hcl.Blocks{
   280  					&hcl.Block{
   281  						Type: "list",
   282  						Body: hcl.EmptyBody(),
   283  					},
   284  					&hcl.Block{
   285  						Type: "single",
   286  						Body: hcl.EmptyBody(),
   287  					},
   288  					&hcl.Block{
   289  						Type: "list",
   290  						Body: hcl.EmptyBody(),
   291  					},
   292  					&hcl.Block{
   293  						Type:        "map",
   294  						Labels:      []string{"foo"},
   295  						LabelRanges: []hcl.Range{{}},
   296  						Body:        hcl.EmptyBody(),
   297  					},
   298  					&hcl.Block{
   299  						Type:        "map",
   300  						Labels:      []string{"bar"},
   301  						LabelRanges: []hcl.Range{{}},
   302  						Body:        hcl.EmptyBody(),
   303  					},
   304  				},
   305  			}),
   306  			cty.ObjectVal(map[string]cty.Value{
   307  				"single": cty.ObjectVal(map[string]cty.Value{
   308  					"a": cty.NullVal(cty.DynamicPseudoType),
   309  				}),
   310  				"list": cty.TupleVal([]cty.Value{
   311  					cty.ObjectVal(map[string]cty.Value{
   312  						"a": cty.NullVal(cty.DynamicPseudoType),
   313  					}),
   314  					cty.ObjectVal(map[string]cty.Value{
   315  						"a": cty.NullVal(cty.DynamicPseudoType),
   316  					}),
   317  				}),
   318  				"map": cty.ObjectVal(map[string]cty.Value{
   319  					"foo": cty.ObjectVal(map[string]cty.Value{
   320  						"a": cty.NullVal(cty.DynamicPseudoType),
   321  					}),
   322  					"bar": cty.ObjectVal(map[string]cty.Value{
   323  						"a": cty.NullVal(cty.DynamicPseudoType),
   324  					}),
   325  				}),
   326  			}),
   327  			0,
   328  		},
   329  		"too many list items": {
   330  			&Block{
   331  				BlockTypes: map[string]*NestedBlock{
   332  					"foo": {
   333  						Nesting:  NestingList,
   334  						Block:    Block{},
   335  						MaxItems: 1,
   336  					},
   337  				},
   338  			},
   339  			hcltest.MockBody(&hcl.BodyContent{
   340  				Blocks: hcl.Blocks{
   341  					&hcl.Block{
   342  						Type: "foo",
   343  						Body: hcl.EmptyBody(),
   344  					},
   345  					&hcl.Block{
   346  						Type: "foo",
   347  						Body: hcl.EmptyBody(),
   348  					},
   349  				},
   350  			}),
   351  			cty.ObjectVal(map[string]cty.Value{
   352  				"foo": cty.ListVal([]cty.Value{
   353  					cty.EmptyObjectVal,
   354  					cty.EmptyObjectVal,
   355  				}),
   356  			}),
   357  			0, // max items cannot be validated during decode
   358  		},
   359  		// dynamic blocks may fulfill MinItems, but there is only one block to
   360  		// decode.
   361  		"required MinItems": {
   362  			&Block{
   363  				BlockTypes: map[string]*NestedBlock{
   364  					"foo": {
   365  						Nesting:  NestingList,
   366  						Block:    Block{},
   367  						MinItems: 2,
   368  					},
   369  				},
   370  			},
   371  			hcltest.MockBody(&hcl.BodyContent{
   372  				Blocks: hcl.Blocks{
   373  					&hcl.Block{
   374  						Type: "foo",
   375  						Body: hcl.EmptyBody(),
   376  					},
   377  				},
   378  			}),
   379  			cty.ObjectVal(map[string]cty.Value{
   380  				"foo": cty.ListVal([]cty.Value{
   381  					cty.EmptyObjectVal,
   382  				}),
   383  			}),
   384  			0,
   385  		},
   386  		"extraneous attribute": {
   387  			&Block{},
   388  			hcltest.MockBody(&hcl.BodyContent{
   389  				Attributes: hcl.Attributes{
   390  					"extra": {
   391  						Name: "extra",
   392  						Expr: hcltest.MockExprLiteral(cty.StringVal("hello")),
   393  					},
   394  				},
   395  			}),
   396  			cty.EmptyObjectVal,
   397  			1, // extraneous attribute
   398  		},
   399  	}
   400  
   401  	for name, test := range tests {
   402  		t.Run(name, func(t *testing.T) {
   403  			spec := test.Schema.DecoderSpec()
   404  			got, diags := hcldec.Decode(test.TestBody, spec, nil)
   405  			if len(diags) != test.DiagCount {
   406  				t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
   407  				for _, diag := range diags {
   408  					t.Logf("- %s", diag.Error())
   409  				}
   410  			}
   411  
   412  			if !got.RawEquals(test.Want) {
   413  				t.Logf("[INFO] implied schema is %s", spew.Sdump(hcldec.ImpliedSchema(spec)))
   414  				t.Errorf("wrong result\ngot:  %s\nwant: %s", dump.Value(got), dump.Value(test.Want))
   415  			}
   416  
   417  			// Double-check that we're producing consistent results for DecoderSpec
   418  			// and ImpliedType.
   419  			impliedType := test.Schema.ImpliedType()
   420  			if errs := got.Type().TestConformance(impliedType); len(errs) != 0 {
   421  				t.Errorf("result does not conform to the schema's implied type")
   422  				for _, err := range errs {
   423  					t.Logf("- %s", err.Error())
   424  				}
   425  			}
   426  		})
   427  	}
   428  }