github.com/opentofu/opentofu@v1.7.1/internal/configs/configschema/internal_validate_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 configschema
     7  
     8  import (
     9  	"testing"
    10  
    11  	"github.com/zclconf/go-cty/cty"
    12  
    13  	multierror "github.com/hashicorp/go-multierror"
    14  )
    15  
    16  func TestBlockInternalValidate(t *testing.T) {
    17  	tests := map[string]struct {
    18  		Block *Block
    19  		Errs  []string
    20  	}{
    21  		"empty": {
    22  			&Block{},
    23  			[]string{},
    24  		},
    25  		"valid": {
    26  			&Block{
    27  				Attributes: map[string]*Attribute{
    28  					"foo": {
    29  						Type:     cty.String,
    30  						Required: true,
    31  					},
    32  					"bar": {
    33  						Type:     cty.String,
    34  						Optional: true,
    35  					},
    36  					"baz": {
    37  						Type:     cty.String,
    38  						Computed: true,
    39  					},
    40  					"baz_maybe": {
    41  						Type:     cty.String,
    42  						Optional: true,
    43  						Computed: true,
    44  					},
    45  				},
    46  				BlockTypes: map[string]*NestedBlock{
    47  					"single": {
    48  						Nesting: NestingSingle,
    49  						Block:   Block{},
    50  					},
    51  					"single_required": {
    52  						Nesting:  NestingSingle,
    53  						Block:    Block{},
    54  						MinItems: 1,
    55  						MaxItems: 1,
    56  					},
    57  					"list": {
    58  						Nesting: NestingList,
    59  						Block:   Block{},
    60  					},
    61  					"list_required": {
    62  						Nesting:  NestingList,
    63  						Block:    Block{},
    64  						MinItems: 1,
    65  					},
    66  					"set": {
    67  						Nesting: NestingSet,
    68  						Block:   Block{},
    69  					},
    70  					"set_required": {
    71  						Nesting:  NestingSet,
    72  						Block:    Block{},
    73  						MinItems: 1,
    74  					},
    75  					"map": {
    76  						Nesting: NestingMap,
    77  						Block:   Block{},
    78  					},
    79  				},
    80  			},
    81  			[]string{},
    82  		},
    83  		"attribute with no flags set": {
    84  			&Block{
    85  				Attributes: map[string]*Attribute{
    86  					"foo": {
    87  						Type: cty.String,
    88  					},
    89  				},
    90  			},
    91  			[]string{"foo: must set Optional, Required or Computed"},
    92  		},
    93  		"attribute required and optional": {
    94  			&Block{
    95  				Attributes: map[string]*Attribute{
    96  					"foo": {
    97  						Type:     cty.String,
    98  						Required: true,
    99  						Optional: true,
   100  					},
   101  				},
   102  			},
   103  			[]string{"foo: cannot set both Optional and Required"},
   104  		},
   105  		"attribute required and computed": {
   106  			&Block{
   107  				Attributes: map[string]*Attribute{
   108  					"foo": {
   109  						Type:     cty.String,
   110  						Required: true,
   111  						Computed: true,
   112  					},
   113  				},
   114  			},
   115  			[]string{"foo: cannot set both Computed and Required"},
   116  		},
   117  		"attribute optional and computed": {
   118  			&Block{
   119  				Attributes: map[string]*Attribute{
   120  					"foo": {
   121  						Type:     cty.String,
   122  						Optional: true,
   123  						Computed: true,
   124  					},
   125  				},
   126  			},
   127  			[]string{},
   128  		},
   129  		"attribute with missing type": {
   130  			&Block{
   131  				Attributes: map[string]*Attribute{
   132  					"foo": {
   133  						Optional: true,
   134  					},
   135  				},
   136  			},
   137  			[]string{"foo: either Type or NestedType must be defined"},
   138  		},
   139  		"attribute with both type and nestedtype should not suppress other validation messages": {
   140  			&Block{
   141  				Attributes: map[string]*Attribute{
   142  					"foo": {
   143  						// These properties are here to make sure other errors are also reported.
   144  						Optional: true,
   145  						Required: true,
   146  						// Here's what we actually want to validate:
   147  						Type: cty.String,
   148  						NestedType: &Object{
   149  							Nesting: NestingSingle,
   150  							Attributes: map[string]*Attribute{
   151  								"foo": {
   152  									Type:     cty.String,
   153  									Required: true,
   154  								},
   155  							},
   156  						},
   157  					},
   158  				},
   159  			},
   160  			[]string{
   161  				"foo: cannot set both Optional and Required",
   162  				"foo: Type and NestedType cannot both be set",
   163  			},
   164  		},
   165  		/* FIXME: This caused errors when applied to existing providers (oci)
   166  		and cannot be enforced without coordination.
   167  
   168  		"attribute with invalid name": {&Block{Attributes:
   169  		    map[string]*Attribute{"fooBar": {Type:     cty.String, Optional:
   170  		    true,
   171  		            },
   172  		        },
   173  		    },
   174  		    []string{"fooBar: name may contain only lowercase letters, digits and underscores"},
   175  		},
   176  		*/
   177  		"attribute with invalid NestedType attribute": {
   178  			&Block{
   179  				Attributes: map[string]*Attribute{
   180  					"foo": {
   181  						NestedType: &Object{
   182  							Nesting: NestingSingle,
   183  							Attributes: map[string]*Attribute{
   184  								"foo": {
   185  									Type:     cty.String,
   186  									Required: true,
   187  									Optional: true,
   188  								},
   189  							},
   190  						},
   191  						Optional: true,
   192  					},
   193  				},
   194  			},
   195  			[]string{"foo: cannot set both Optional and Required"},
   196  		},
   197  		"block type with invalid name": {
   198  			&Block{
   199  				BlockTypes: map[string]*NestedBlock{
   200  					"fooBar": {
   201  						Nesting: NestingSingle,
   202  					},
   203  				},
   204  			},
   205  			[]string{"fooBar: name may contain only lowercase letters, digits and underscores"},
   206  		},
   207  		"colliding names": {
   208  			&Block{
   209  				Attributes: map[string]*Attribute{
   210  					"foo": {
   211  						Type:     cty.String,
   212  						Optional: true,
   213  					},
   214  				},
   215  				BlockTypes: map[string]*NestedBlock{
   216  					"foo": {
   217  						Nesting: NestingSingle,
   218  					},
   219  				},
   220  			},
   221  			[]string{"foo: name defined as both attribute and child block type"},
   222  		},
   223  		"nested block with badness": {
   224  			&Block{
   225  				BlockTypes: map[string]*NestedBlock{
   226  					"bad": {
   227  						Nesting: NestingSingle,
   228  						Block: Block{
   229  							Attributes: map[string]*Attribute{
   230  								"nested_bad": {
   231  									Type:     cty.String,
   232  									Required: true,
   233  									Optional: true,
   234  								},
   235  							},
   236  						},
   237  					},
   238  				},
   239  			},
   240  			[]string{"bad.nested_bad: cannot set both Optional and Required"},
   241  		},
   242  		"nested list block with dynamically-typed attribute": {
   243  			&Block{
   244  				BlockTypes: map[string]*NestedBlock{
   245  					"bad": {
   246  						Nesting: NestingList,
   247  						Block: Block{
   248  							Attributes: map[string]*Attribute{
   249  								"nested_bad": {
   250  									Type:     cty.DynamicPseudoType,
   251  									Optional: true,
   252  								},
   253  							},
   254  						},
   255  					},
   256  				},
   257  			},
   258  			[]string{},
   259  		},
   260  		"nested set block with dynamically-typed attribute": {
   261  			&Block{
   262  				BlockTypes: map[string]*NestedBlock{
   263  					"bad": {
   264  						Nesting: NestingSet,
   265  						Block: Block{
   266  							Attributes: map[string]*Attribute{
   267  								"nested_bad": {
   268  									Type:     cty.DynamicPseudoType,
   269  									Optional: true,
   270  								},
   271  							},
   272  						},
   273  					},
   274  				},
   275  			},
   276  			[]string{"bad: NestingSet blocks may not contain attributes of cty.DynamicPseudoType"},
   277  		},
   278  		"nil": {
   279  			nil,
   280  			[]string{"top-level block schema is nil"},
   281  		},
   282  		"nil attr": {
   283  			&Block{
   284  				Attributes: map[string]*Attribute{
   285  					"bad": nil,
   286  				},
   287  			},
   288  			[]string{"bad: attribute schema is nil"},
   289  		},
   290  		"nil block type": {
   291  			&Block{
   292  				BlockTypes: map[string]*NestedBlock{
   293  					"bad": nil,
   294  				},
   295  			},
   296  			[]string{"bad: block schema is nil"},
   297  		},
   298  	}
   299  
   300  	for name, test := range tests {
   301  		t.Run(name, func(t *testing.T) {
   302  			errs := multierrorErrors(test.Block.InternalValidate())
   303  			if got, want := len(errs), len(test.Errs); got != want {
   304  				t.Errorf("wrong number of errors %d; want %d", got, want)
   305  				for _, err := range errs {
   306  					t.Logf("- %s", err.Error())
   307  				}
   308  			} else {
   309  				if len(errs) > 0 {
   310  					for i := range errs {
   311  						if errs[i].Error() != test.Errs[i] {
   312  							t.Errorf("wrong error: got %s, want %s", errs[i].Error(), test.Errs[i])
   313  						}
   314  					}
   315  				}
   316  			}
   317  		})
   318  	}
   319  }
   320  
   321  func multierrorErrors(err error) []error {
   322  	// A function like this should really be part of the multierror package...
   323  
   324  	if err == nil {
   325  		return nil
   326  	}
   327  
   328  	switch terr := err.(type) {
   329  	case *multierror.Error:
   330  		return terr.Errors
   331  	default:
   332  		return []error{err}
   333  	}
   334  }