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 }