github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/legacy/helper/schema/core_schema_test.go (about)

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