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  }