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