github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/legacy/helper/schema/core_schema_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package schema
     5  
     6  import (
     7  	"fmt"
     8  	"testing"
     9  
    10  	"github.com/google/go-cmp/cmp"
    11  	"github.com/zclconf/go-cty/cty"
    12  
    13  	"github.com/terramate-io/tf/configs/configschema"
    14  )
    15  
    16  // add the implicit "id" attribute for test resources
    17  func testResource(block *configschema.Block) *configschema.Block {
    18  	if block.Attributes == nil {
    19  		block.Attributes = make(map[string]*configschema.Attribute)
    20  	}
    21  
    22  	if block.BlockTypes == nil {
    23  		block.BlockTypes = make(map[string]*configschema.NestedBlock)
    24  	}
    25  
    26  	if block.Attributes["id"] == nil {
    27  		block.Attributes["id"] = &configschema.Attribute{
    28  			Type:     cty.String,
    29  			Optional: true,
    30  			Computed: true,
    31  		}
    32  	}
    33  	return block
    34  }
    35  
    36  func TestSchemaMapCoreConfigSchema(t *testing.T) {
    37  	tests := map[string]struct {
    38  		Schema map[string]*Schema
    39  		Want   *configschema.Block
    40  	}{
    41  		"empty": {
    42  			map[string]*Schema{},
    43  			testResource(&configschema.Block{}),
    44  		},
    45  		"primitives": {
    46  			map[string]*Schema{
    47  				"int": {
    48  					Type:        TypeInt,
    49  					Required:    true,
    50  					Description: "foo bar baz",
    51  				},
    52  				"float": {
    53  					Type:     TypeFloat,
    54  					Optional: true,
    55  				},
    56  				"bool": {
    57  					Type:     TypeBool,
    58  					Computed: true,
    59  				},
    60  				"string": {
    61  					Type:     TypeString,
    62  					Optional: true,
    63  					Computed: true,
    64  				},
    65  			},
    66  			testResource(&configschema.Block{
    67  				Attributes: map[string]*configschema.Attribute{
    68  					"int": {
    69  						Type:        cty.Number,
    70  						Required:    true,
    71  						Description: "foo bar baz",
    72  					},
    73  					"float": {
    74  						Type:     cty.Number,
    75  						Optional: true,
    76  					},
    77  					"bool": {
    78  						Type:     cty.Bool,
    79  						Computed: true,
    80  					},
    81  					"string": {
    82  						Type:     cty.String,
    83  						Optional: true,
    84  						Computed: true,
    85  					},
    86  				},
    87  				BlockTypes: map[string]*configschema.NestedBlock{},
    88  			}),
    89  		},
    90  		"simple collections": {
    91  			map[string]*Schema{
    92  				"list": {
    93  					Type:     TypeList,
    94  					Required: true,
    95  					Elem: &Schema{
    96  						Type: TypeInt,
    97  					},
    98  				},
    99  				"set": {
   100  					Type:     TypeSet,
   101  					Optional: true,
   102  					Elem: &Schema{
   103  						Type: TypeString,
   104  					},
   105  				},
   106  				"map": {
   107  					Type:     TypeMap,
   108  					Optional: true,
   109  					Elem: &Schema{
   110  						Type: TypeBool,
   111  					},
   112  				},
   113  				"map_default_type": {
   114  					Type:     TypeMap,
   115  					Optional: true,
   116  					// Maps historically don't have elements because we
   117  					// assumed they would be strings, so this needs to work
   118  					// for pre-existing schemas.
   119  				},
   120  			},
   121  			testResource(&configschema.Block{
   122  				Attributes: map[string]*configschema.Attribute{
   123  					"list": {
   124  						Type:     cty.List(cty.Number),
   125  						Required: true,
   126  					},
   127  					"set": {
   128  						Type:     cty.Set(cty.String),
   129  						Optional: true,
   130  					},
   131  					"map": {
   132  						Type:     cty.Map(cty.Bool),
   133  						Optional: true,
   134  					},
   135  					"map_default_type": {
   136  						Type:     cty.Map(cty.String),
   137  						Optional: true,
   138  					},
   139  				},
   140  				BlockTypes: map[string]*configschema.NestedBlock{},
   141  			}),
   142  		},
   143  		"incorrectly-specified collections": {
   144  			// Historically we tolerated setting a type directly as the Elem
   145  			// attribute, rather than a Schema object. This is common enough
   146  			// in existing provider code that we must support it as an alias
   147  			// for a schema object with the given type.
   148  			map[string]*Schema{
   149  				"list": {
   150  					Type:     TypeList,
   151  					Required: true,
   152  					Elem:     TypeInt,
   153  				},
   154  				"set": {
   155  					Type:     TypeSet,
   156  					Optional: true,
   157  					Elem:     TypeString,
   158  				},
   159  				"map": {
   160  					Type:     TypeMap,
   161  					Optional: true,
   162  					Elem:     TypeBool,
   163  				},
   164  			},
   165  			testResource(&configschema.Block{
   166  				Attributes: map[string]*configschema.Attribute{
   167  					"list": {
   168  						Type:     cty.List(cty.Number),
   169  						Required: true,
   170  					},
   171  					"set": {
   172  						Type:     cty.Set(cty.String),
   173  						Optional: true,
   174  					},
   175  					"map": {
   176  						Type:     cty.Map(cty.Bool),
   177  						Optional: true,
   178  					},
   179  				},
   180  				BlockTypes: map[string]*configschema.NestedBlock{},
   181  			}),
   182  		},
   183  		"sub-resource collections": {
   184  			map[string]*Schema{
   185  				"list": {
   186  					Type:     TypeList,
   187  					Required: true,
   188  					Elem: &Resource{
   189  						Schema: map[string]*Schema{},
   190  					},
   191  					MinItems: 1,
   192  					MaxItems: 2,
   193  				},
   194  				"set": {
   195  					Type:     TypeSet,
   196  					Required: true,
   197  					Elem: &Resource{
   198  						Schema: map[string]*Schema{},
   199  					},
   200  				},
   201  				"map": {
   202  					Type:     TypeMap,
   203  					Optional: true,
   204  					Elem: &Resource{
   205  						Schema: map[string]*Schema{},
   206  					},
   207  				},
   208  			},
   209  			testResource(&configschema.Block{
   210  				Attributes: map[string]*configschema.Attribute{
   211  					// This one becomes a string attribute because helper/schema
   212  					// doesn't actually support maps of resource. The given
   213  					// "Elem" is just ignored entirely here, which is important
   214  					// because that is also true of the helper/schema logic and
   215  					// existing providers rely on this being ignored for
   216  					// correct operation.
   217  					"map": {
   218  						Type:     cty.Map(cty.String),
   219  						Optional: true,
   220  					},
   221  				},
   222  				BlockTypes: map[string]*configschema.NestedBlock{
   223  					"list": {
   224  						Nesting:  configschema.NestingList,
   225  						Block:    configschema.Block{},
   226  						MinItems: 1,
   227  						MaxItems: 2,
   228  					},
   229  					"set": {
   230  						Nesting:  configschema.NestingSet,
   231  						Block:    configschema.Block{},
   232  						MinItems: 1, // because schema is Required
   233  					},
   234  				},
   235  			}),
   236  		},
   237  		"sub-resource collections minitems+optional": {
   238  			// This particular case is an odd one where the provider gives
   239  			// conflicting information about whether a sub-resource is required,
   240  			// by marking it as optional but also requiring one item.
   241  			// Historically the optional-ness "won" here, and so we must
   242  			// honor that for compatibility with providers that relied on this
   243  			// undocumented interaction.
   244  			map[string]*Schema{
   245  				"list": {
   246  					Type:     TypeList,
   247  					Optional: true,
   248  					Elem: &Resource{
   249  						Schema: map[string]*Schema{},
   250  					},
   251  					MinItems: 1,
   252  					MaxItems: 1,
   253  				},
   254  				"set": {
   255  					Type:     TypeSet,
   256  					Optional: true,
   257  					Elem: &Resource{
   258  						Schema: map[string]*Schema{},
   259  					},
   260  					MinItems: 1,
   261  					MaxItems: 1,
   262  				},
   263  			},
   264  			testResource(&configschema.Block{
   265  				Attributes: map[string]*configschema.Attribute{},
   266  				BlockTypes: map[string]*configschema.NestedBlock{
   267  					"list": {
   268  						Nesting:  configschema.NestingList,
   269  						Block:    configschema.Block{},
   270  						MinItems: 0,
   271  						MaxItems: 1,
   272  					},
   273  					"set": {
   274  						Nesting:  configschema.NestingSet,
   275  						Block:    configschema.Block{},
   276  						MinItems: 0,
   277  						MaxItems: 1,
   278  					},
   279  				},
   280  			}),
   281  		},
   282  		"sub-resource collections minitems+computed": {
   283  			map[string]*Schema{
   284  				"list": {
   285  					Type:     TypeList,
   286  					Computed: true,
   287  					Elem: &Resource{
   288  						Schema: map[string]*Schema{},
   289  					},
   290  					MinItems: 1,
   291  					MaxItems: 1,
   292  				},
   293  				"set": {
   294  					Type:     TypeSet,
   295  					Computed: true,
   296  					Elem: &Resource{
   297  						Schema: map[string]*Schema{},
   298  					},
   299  					MinItems: 1,
   300  					MaxItems: 1,
   301  				},
   302  			},
   303  			testResource(&configschema.Block{
   304  				Attributes: map[string]*configschema.Attribute{
   305  					"list": {
   306  						Type:     cty.List(cty.EmptyObject),
   307  						Computed: true,
   308  					},
   309  					"set": {
   310  						Type:     cty.Set(cty.EmptyObject),
   311  						Computed: true,
   312  					},
   313  				},
   314  			}),
   315  		},
   316  		"nested attributes and blocks": {
   317  			map[string]*Schema{
   318  				"foo": {
   319  					Type:     TypeList,
   320  					Required: true,
   321  					Elem: &Resource{
   322  						Schema: map[string]*Schema{
   323  							"bar": {
   324  								Type:     TypeList,
   325  								Required: true,
   326  								Elem: &Schema{
   327  									Type: TypeList,
   328  									Elem: &Schema{
   329  										Type: TypeString,
   330  									},
   331  								},
   332  							},
   333  							"baz": {
   334  								Type:     TypeSet,
   335  								Optional: true,
   336  								Elem: &Resource{
   337  									Schema: map[string]*Schema{},
   338  								},
   339  							},
   340  						},
   341  					},
   342  				},
   343  			},
   344  			testResource(&configschema.Block{
   345  				Attributes: map[string]*configschema.Attribute{},
   346  				BlockTypes: map[string]*configschema.NestedBlock{
   347  					"foo": &configschema.NestedBlock{
   348  						Nesting: configschema.NestingList,
   349  						Block: configschema.Block{
   350  							Attributes: map[string]*configschema.Attribute{
   351  								"bar": {
   352  									Type:     cty.List(cty.List(cty.String)),
   353  									Required: true,
   354  								},
   355  							},
   356  							BlockTypes: map[string]*configschema.NestedBlock{
   357  								"baz": {
   358  									Nesting: configschema.NestingSet,
   359  									Block:   configschema.Block{},
   360  								},
   361  							},
   362  						},
   363  						MinItems: 1, // because schema is Required
   364  					},
   365  				},
   366  			}),
   367  		},
   368  		"sensitive": {
   369  			map[string]*Schema{
   370  				"string": {
   371  					Type:      TypeString,
   372  					Optional:  true,
   373  					Sensitive: true,
   374  				},
   375  			},
   376  			testResource(&configschema.Block{
   377  				Attributes: map[string]*configschema.Attribute{
   378  					"string": {
   379  						Type:      cty.String,
   380  						Optional:  true,
   381  						Sensitive: true,
   382  					},
   383  				},
   384  				BlockTypes: map[string]*configschema.NestedBlock{},
   385  			}),
   386  		},
   387  		"conditionally required on": {
   388  			map[string]*Schema{
   389  				"string": {
   390  					Type:     TypeString,
   391  					Required: true,
   392  					DefaultFunc: func() (interface{}, error) {
   393  						return nil, nil
   394  					},
   395  				},
   396  			},
   397  			testResource(&configschema.Block{
   398  				Attributes: map[string]*configschema.Attribute{
   399  					"string": {
   400  						Type:     cty.String,
   401  						Required: true,
   402  					},
   403  				},
   404  				BlockTypes: map[string]*configschema.NestedBlock{},
   405  			}),
   406  		},
   407  		"conditionally required off": {
   408  			map[string]*Schema{
   409  				"string": {
   410  					Type:     TypeString,
   411  					Required: true,
   412  					DefaultFunc: func() (interface{}, error) {
   413  						// If we return a non-nil default then this overrides
   414  						// the "Required: true" for the purpose of building
   415  						// the core schema, so that core will ignore it not
   416  						// being set and let the provider handle it.
   417  						return "boop", nil
   418  					},
   419  				},
   420  			},
   421  			testResource(&configschema.Block{
   422  				Attributes: map[string]*configschema.Attribute{
   423  					"string": {
   424  						Type:     cty.String,
   425  						Optional: true,
   426  					},
   427  				},
   428  				BlockTypes: map[string]*configschema.NestedBlock{},
   429  			}),
   430  		},
   431  		"conditionally required error": {
   432  			map[string]*Schema{
   433  				"string": {
   434  					Type:     TypeString,
   435  					Required: true,
   436  					DefaultFunc: func() (interface{}, error) {
   437  						return nil, fmt.Errorf("placeholder error")
   438  					},
   439  				},
   440  			},
   441  			testResource(&configschema.Block{
   442  				Attributes: map[string]*configschema.Attribute{
   443  					"string": {
   444  						Type:     cty.String,
   445  						Optional: true, // Just so we can progress to provider-driven validation and return the error there
   446  					},
   447  				},
   448  				BlockTypes: map[string]*configschema.NestedBlock{},
   449  			}),
   450  		},
   451  	}
   452  
   453  	for name, test := range tests {
   454  		t.Run(name, func(t *testing.T) {
   455  			got := (&Resource{Schema: test.Schema}).CoreConfigSchema()
   456  			if !cmp.Equal(got, test.Want, equateEmpty, typeComparer) {
   457  				t.Error(cmp.Diff(got, test.Want, equateEmpty, typeComparer))
   458  			}
   459  		})
   460  	}
   461  }