github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/lang/blocktoattr/fixup_test.go (about) 1 package blocktoattr 2 3 import ( 4 "testing" 5 6 "github.com/hashicorp/hcl/v2" 7 "github.com/hashicorp/hcl/v2/ext/dynblock" 8 "github.com/hashicorp/hcl/v2/hcldec" 9 "github.com/hashicorp/hcl/v2/hclsyntax" 10 hcljson "github.com/hashicorp/hcl/v2/json" 11 "github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema" 12 "github.com/zclconf/go-cty/cty" 13 ) 14 15 func TestFixUpBlockAttrs(t *testing.T) { 16 fooSchema := &configschema.Block{ 17 Attributes: map[string]*configschema.Attribute{ 18 "foo": { 19 Type: cty.List(cty.Object(map[string]cty.Type{ 20 "bar": cty.String, 21 })), 22 Optional: true, 23 }, 24 }, 25 } 26 27 tests := map[string]struct { 28 src string 29 json bool 30 schema *configschema.Block 31 want cty.Value 32 wantErrs bool 33 }{ 34 "empty": { 35 src: ``, 36 schema: &configschema.Block{}, 37 want: cty.EmptyObjectVal, 38 }, 39 "empty JSON": { 40 src: `{}`, 41 json: true, 42 schema: &configschema.Block{}, 43 want: cty.EmptyObjectVal, 44 }, 45 "unset": { 46 src: ``, 47 schema: fooSchema, 48 want: cty.ObjectVal(map[string]cty.Value{ 49 "foo": cty.NullVal(fooSchema.Attributes["foo"].Type), 50 }), 51 }, 52 "unset JSON": { 53 src: `{}`, 54 json: true, 55 schema: fooSchema, 56 want: cty.ObjectVal(map[string]cty.Value{ 57 "foo": cty.NullVal(fooSchema.Attributes["foo"].Type), 58 }), 59 }, 60 "no fixup required, with one value": { 61 src: ` 62 foo = [ 63 { 64 bar = "baz" 65 }, 66 ] 67 `, 68 schema: fooSchema, 69 want: cty.ObjectVal(map[string]cty.Value{ 70 "foo": cty.ListVal([]cty.Value{ 71 cty.ObjectVal(map[string]cty.Value{ 72 "bar": cty.StringVal("baz"), 73 }), 74 }), 75 }), 76 }, 77 "no fixup required, with two values": { 78 src: ` 79 foo = [ 80 { 81 bar = "baz" 82 }, 83 { 84 bar = "boop" 85 }, 86 ] 87 `, 88 schema: fooSchema, 89 want: cty.ObjectVal(map[string]cty.Value{ 90 "foo": cty.ListVal([]cty.Value{ 91 cty.ObjectVal(map[string]cty.Value{ 92 "bar": cty.StringVal("baz"), 93 }), 94 cty.ObjectVal(map[string]cty.Value{ 95 "bar": cty.StringVal("boop"), 96 }), 97 }), 98 }), 99 }, 100 "no fixup required, with values, JSON": { 101 src: `{"foo": [{"bar": "baz"}]}`, 102 json: true, 103 schema: fooSchema, 104 want: cty.ObjectVal(map[string]cty.Value{ 105 "foo": cty.ListVal([]cty.Value{ 106 cty.ObjectVal(map[string]cty.Value{ 107 "bar": cty.StringVal("baz"), 108 }), 109 }), 110 }), 111 }, 112 "no fixup required, empty": { 113 src: ` 114 foo = [] 115 `, 116 schema: fooSchema, 117 want: cty.ObjectVal(map[string]cty.Value{ 118 "foo": cty.ListValEmpty(fooSchema.Attributes["foo"].Type.ElementType()), 119 }), 120 }, 121 "no fixup required, empty, JSON": { 122 src: `{"foo":[]}`, 123 json: true, 124 schema: fooSchema, 125 want: cty.ObjectVal(map[string]cty.Value{ 126 "foo": cty.ListValEmpty(fooSchema.Attributes["foo"].Type.ElementType()), 127 }), 128 }, 129 "fixup one block": { 130 src: ` 131 foo { 132 bar = "baz" 133 } 134 `, 135 schema: fooSchema, 136 want: cty.ObjectVal(map[string]cty.Value{ 137 "foo": cty.ListVal([]cty.Value{ 138 cty.ObjectVal(map[string]cty.Value{ 139 "bar": cty.StringVal("baz"), 140 }), 141 }), 142 }), 143 }, 144 "fixup one block omitting attribute": { 145 src: ` 146 foo {} 147 `, 148 schema: fooSchema, 149 want: cty.ObjectVal(map[string]cty.Value{ 150 "foo": cty.ListVal([]cty.Value{ 151 cty.ObjectVal(map[string]cty.Value{ 152 "bar": cty.NullVal(cty.String), 153 }), 154 }), 155 }), 156 }, 157 "fixup two blocks": { 158 src: ` 159 foo { 160 bar = baz 161 } 162 foo { 163 bar = "boop" 164 } 165 `, 166 schema: fooSchema, 167 want: cty.ObjectVal(map[string]cty.Value{ 168 "foo": cty.ListVal([]cty.Value{ 169 cty.ObjectVal(map[string]cty.Value{ 170 "bar": cty.StringVal("baz value"), 171 }), 172 cty.ObjectVal(map[string]cty.Value{ 173 "bar": cty.StringVal("boop"), 174 }), 175 }), 176 }), 177 }, 178 "interaction with dynamic block generation": { 179 src: ` 180 dynamic "foo" { 181 for_each = ["baz", beep] 182 content { 183 bar = foo.value 184 } 185 } 186 `, 187 schema: fooSchema, 188 want: cty.ObjectVal(map[string]cty.Value{ 189 "foo": cty.ListVal([]cty.Value{ 190 cty.ObjectVal(map[string]cty.Value{ 191 "bar": cty.StringVal("baz"), 192 }), 193 cty.ObjectVal(map[string]cty.Value{ 194 "bar": cty.StringVal("beep value"), 195 }), 196 }), 197 }), 198 }, 199 "dynamic block with empty iterator": { 200 src: ` 201 dynamic "foo" { 202 for_each = [] 203 content { 204 bar = foo.value 205 } 206 } 207 `, 208 schema: fooSchema, 209 want: cty.ObjectVal(map[string]cty.Value{ 210 "foo": cty.NullVal(fooSchema.Attributes["foo"].Type), 211 }), 212 }, 213 "both attribute and block syntax": { 214 src: ` 215 foo = [] 216 foo { 217 bar = "baz" 218 } 219 `, 220 schema: fooSchema, 221 wantErrs: true, // Unsupported block type (user must be consistent about whether they consider foo to be a block type or an attribute) 222 want: cty.ObjectVal(map[string]cty.Value{ 223 "foo": cty.ListVal([]cty.Value{ 224 cty.ObjectVal(map[string]cty.Value{ 225 "bar": cty.StringVal("baz"), 226 }), 227 cty.ObjectVal(map[string]cty.Value{ 228 "bar": cty.StringVal("boop"), 229 }), 230 }), 231 }), 232 }, 233 "fixup inside block": { 234 src: ` 235 container { 236 foo { 237 bar = "baz" 238 } 239 foo { 240 bar = "boop" 241 } 242 } 243 container { 244 foo { 245 bar = beep 246 } 247 } 248 `, 249 schema: &configschema.Block{ 250 BlockTypes: map[string]*configschema.NestedBlock{ 251 "container": { 252 Nesting: configschema.NestingList, 253 Block: *fooSchema, 254 }, 255 }, 256 }, 257 want: cty.ObjectVal(map[string]cty.Value{ 258 "container": cty.ListVal([]cty.Value{ 259 cty.ObjectVal(map[string]cty.Value{ 260 "foo": cty.ListVal([]cty.Value{ 261 cty.ObjectVal(map[string]cty.Value{ 262 "bar": cty.StringVal("baz"), 263 }), 264 cty.ObjectVal(map[string]cty.Value{ 265 "bar": cty.StringVal("boop"), 266 }), 267 }), 268 }), 269 cty.ObjectVal(map[string]cty.Value{ 270 "foo": cty.ListVal([]cty.Value{ 271 cty.ObjectVal(map[string]cty.Value{ 272 "bar": cty.StringVal("beep value"), 273 }), 274 }), 275 }), 276 }), 277 }), 278 }, 279 "fixup inside attribute-as-block": { 280 src: ` 281 container { 282 foo { 283 bar = "baz" 284 } 285 foo { 286 bar = "boop" 287 } 288 } 289 container { 290 foo { 291 bar = beep 292 } 293 } 294 `, 295 schema: &configschema.Block{ 296 Attributes: map[string]*configschema.Attribute{ 297 "container": { 298 Type: cty.List(cty.Object(map[string]cty.Type{ 299 "foo": cty.List(cty.Object(map[string]cty.Type{ 300 "bar": cty.String, 301 })), 302 })), 303 Optional: true, 304 }, 305 }, 306 }, 307 want: cty.ObjectVal(map[string]cty.Value{ 308 "container": cty.ListVal([]cty.Value{ 309 cty.ObjectVal(map[string]cty.Value{ 310 "foo": cty.ListVal([]cty.Value{ 311 cty.ObjectVal(map[string]cty.Value{ 312 "bar": cty.StringVal("baz"), 313 }), 314 cty.ObjectVal(map[string]cty.Value{ 315 "bar": cty.StringVal("boop"), 316 }), 317 }), 318 }), 319 cty.ObjectVal(map[string]cty.Value{ 320 "foo": cty.ListVal([]cty.Value{ 321 cty.ObjectVal(map[string]cty.Value{ 322 "bar": cty.StringVal("beep value"), 323 }), 324 }), 325 }), 326 }), 327 }), 328 }, 329 "nested fixup with dynamic block generation": { 330 src: ` 331 container { 332 dynamic "foo" { 333 for_each = ["baz", beep] 334 content { 335 bar = foo.value 336 } 337 } 338 } 339 `, 340 schema: &configschema.Block{ 341 BlockTypes: map[string]*configschema.NestedBlock{ 342 "container": { 343 Nesting: configschema.NestingList, 344 Block: *fooSchema, 345 }, 346 }, 347 }, 348 want: cty.ObjectVal(map[string]cty.Value{ 349 "container": cty.ListVal([]cty.Value{ 350 cty.ObjectVal(map[string]cty.Value{ 351 "foo": cty.ListVal([]cty.Value{ 352 cty.ObjectVal(map[string]cty.Value{ 353 "bar": cty.StringVal("baz"), 354 }), 355 cty.ObjectVal(map[string]cty.Value{ 356 "bar": cty.StringVal("beep value"), 357 }), 358 }), 359 }), 360 }), 361 }), 362 }, 363 } 364 365 ctx := &hcl.EvalContext{ 366 Variables: map[string]cty.Value{ 367 "bar": cty.StringVal("bar value"), 368 "baz": cty.StringVal("baz value"), 369 "beep": cty.StringVal("beep value"), 370 }, 371 } 372 373 for name, test := range tests { 374 t.Run(name, func(t *testing.T) { 375 var f *hcl.File 376 var diags hcl.Diagnostics 377 if test.json { 378 f, diags = hcljson.Parse([]byte(test.src), "test.tf.json") 379 } else { 380 f, diags = hclsyntax.ParseConfig([]byte(test.src), "test.tf", hcl.Pos{Line: 1, Column: 1}) 381 } 382 if diags.HasErrors() { 383 for _, diag := range diags { 384 t.Errorf("unexpected diagnostic: %s", diag) 385 } 386 t.FailNow() 387 } 388 389 // We'll expand dynamic blocks in the body first, to mimic how 390 // we process this fixup when using the main "lang" package API. 391 spec := test.schema.DecoderSpec() 392 body := dynblock.Expand(f.Body, ctx) 393 394 body = FixUpBlockAttrs(body, test.schema) 395 got, diags := hcldec.Decode(body, spec, ctx) 396 397 if test.wantErrs { 398 if !diags.HasErrors() { 399 t.Errorf("succeeded, but want error\ngot: %#v", got) 400 } 401 return 402 } 403 404 if !test.want.RawEquals(got) { 405 t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want) 406 } 407 for _, diag := range diags { 408 t.Errorf("unexpected diagnostic: %s", diag) 409 } 410 }) 411 } 412 }