github.com/opentofu/opentofu@v1.7.1/internal/legacy/helper/schema/core_schema_test.go (about)

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