github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/lang/blocktoattr/fixup_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package blocktoattr
     5  
     6  import (
     7  	"testing"
     8  
     9  	"github.com/hashicorp/hcl/v2"
    10  	"github.com/hashicorp/hcl/v2/ext/dynblock"
    11  	"github.com/hashicorp/hcl/v2/hcldec"
    12  	"github.com/hashicorp/hcl/v2/hclsyntax"
    13  	hcljson "github.com/hashicorp/hcl/v2/json"
    14  	"github.com/terramate-io/tf/configs/configschema"
    15  	"github.com/zclconf/go-cty/cty"
    16  )
    17  
    18  func TestFixUpBlockAttrs(t *testing.T) {
    19  	fooSchema := &configschema.Block{
    20  		Attributes: map[string]*configschema.Attribute{
    21  			"foo": {
    22  				Type: cty.List(cty.Object(map[string]cty.Type{
    23  					"bar": cty.String,
    24  				})),
    25  				Optional: true,
    26  			},
    27  		},
    28  	}
    29  
    30  	tests := map[string]struct {
    31  		src      string
    32  		json     bool
    33  		schema   *configschema.Block
    34  		want     cty.Value
    35  		wantErrs bool
    36  	}{
    37  		"empty": {
    38  			src:    ``,
    39  			schema: &configschema.Block{},
    40  			want:   cty.EmptyObjectVal,
    41  		},
    42  		"empty JSON": {
    43  			src:    `{}`,
    44  			json:   true,
    45  			schema: &configschema.Block{},
    46  			want:   cty.EmptyObjectVal,
    47  		},
    48  		"unset": {
    49  			src:    ``,
    50  			schema: fooSchema,
    51  			want: cty.ObjectVal(map[string]cty.Value{
    52  				"foo": cty.NullVal(fooSchema.Attributes["foo"].Type),
    53  			}),
    54  		},
    55  		"unset JSON": {
    56  			src:    `{}`,
    57  			json:   true,
    58  			schema: fooSchema,
    59  			want: cty.ObjectVal(map[string]cty.Value{
    60  				"foo": cty.NullVal(fooSchema.Attributes["foo"].Type),
    61  			}),
    62  		},
    63  		"no fixup required, with one value": {
    64  			src: `
    65  foo = [
    66    {
    67      bar = "baz"
    68    },
    69  ]
    70  `,
    71  			schema: fooSchema,
    72  			want: cty.ObjectVal(map[string]cty.Value{
    73  				"foo": cty.ListVal([]cty.Value{
    74  					cty.ObjectVal(map[string]cty.Value{
    75  						"bar": cty.StringVal("baz"),
    76  					}),
    77  				}),
    78  			}),
    79  		},
    80  		"no fixup required, with two values": {
    81  			src: `
    82  foo = [
    83    {
    84      bar = "baz"
    85    },
    86    {
    87      bar = "boop"
    88    },
    89  ]
    90  `,
    91  			schema: fooSchema,
    92  			want: cty.ObjectVal(map[string]cty.Value{
    93  				"foo": cty.ListVal([]cty.Value{
    94  					cty.ObjectVal(map[string]cty.Value{
    95  						"bar": cty.StringVal("baz"),
    96  					}),
    97  					cty.ObjectVal(map[string]cty.Value{
    98  						"bar": cty.StringVal("boop"),
    99  					}),
   100  				}),
   101  			}),
   102  		},
   103  		"no fixup required, with values, JSON": {
   104  			src:    `{"foo": [{"bar": "baz"}]}`,
   105  			json:   true,
   106  			schema: fooSchema,
   107  			want: cty.ObjectVal(map[string]cty.Value{
   108  				"foo": cty.ListVal([]cty.Value{
   109  					cty.ObjectVal(map[string]cty.Value{
   110  						"bar": cty.StringVal("baz"),
   111  					}),
   112  				}),
   113  			}),
   114  		},
   115  		"no fixup required, empty": {
   116  			src: `
   117  foo = []
   118  `,
   119  			schema: fooSchema,
   120  			want: cty.ObjectVal(map[string]cty.Value{
   121  				"foo": cty.ListValEmpty(fooSchema.Attributes["foo"].Type.ElementType()),
   122  			}),
   123  		},
   124  		"no fixup required, empty, JSON": {
   125  			src:    `{"foo":[]}`,
   126  			json:   true,
   127  			schema: fooSchema,
   128  			want: cty.ObjectVal(map[string]cty.Value{
   129  				"foo": cty.ListValEmpty(fooSchema.Attributes["foo"].Type.ElementType()),
   130  			}),
   131  		},
   132  		"fixup one block": {
   133  			src: `
   134  foo {
   135    bar = "baz"
   136  }
   137  `,
   138  			schema: fooSchema,
   139  			want: cty.ObjectVal(map[string]cty.Value{
   140  				"foo": cty.ListVal([]cty.Value{
   141  					cty.ObjectVal(map[string]cty.Value{
   142  						"bar": cty.StringVal("baz"),
   143  					}),
   144  				}),
   145  			}),
   146  		},
   147  		"fixup one block omitting attribute": {
   148  			src: `
   149  foo {}
   150  `,
   151  			schema: fooSchema,
   152  			want: cty.ObjectVal(map[string]cty.Value{
   153  				"foo": cty.ListVal([]cty.Value{
   154  					cty.ObjectVal(map[string]cty.Value{
   155  						"bar": cty.NullVal(cty.String),
   156  					}),
   157  				}),
   158  			}),
   159  		},
   160  		"fixup two blocks": {
   161  			src: `
   162  foo {
   163    bar = baz
   164  }
   165  foo {
   166    bar = "boop"
   167  }
   168  `,
   169  			schema: fooSchema,
   170  			want: cty.ObjectVal(map[string]cty.Value{
   171  				"foo": cty.ListVal([]cty.Value{
   172  					cty.ObjectVal(map[string]cty.Value{
   173  						"bar": cty.StringVal("baz value"),
   174  					}),
   175  					cty.ObjectVal(map[string]cty.Value{
   176  						"bar": cty.StringVal("boop"),
   177  					}),
   178  				}),
   179  			}),
   180  		},
   181  		"interaction with dynamic block generation": {
   182  			src: `
   183  dynamic "foo" {
   184    for_each = ["baz", beep]
   185    content {
   186      bar = foo.value
   187    }
   188  }
   189  `,
   190  			schema: fooSchema,
   191  			want: cty.ObjectVal(map[string]cty.Value{
   192  				"foo": cty.ListVal([]cty.Value{
   193  					cty.ObjectVal(map[string]cty.Value{
   194  						"bar": cty.StringVal("baz"),
   195  					}),
   196  					cty.ObjectVal(map[string]cty.Value{
   197  						"bar": cty.StringVal("beep value"),
   198  					}),
   199  				}),
   200  			}),
   201  		},
   202  		"dynamic block with empty iterator": {
   203  			src: `
   204  dynamic "foo" {
   205    for_each = []
   206    content {
   207      bar = foo.value
   208    }
   209  }
   210  `,
   211  			schema: fooSchema,
   212  			want: cty.ObjectVal(map[string]cty.Value{
   213  				"foo": cty.NullVal(fooSchema.Attributes["foo"].Type),
   214  			}),
   215  		},
   216  		"both attribute and block syntax": {
   217  			src: `
   218  foo = []
   219  foo {
   220    bar = "baz"
   221  }
   222  `,
   223  			schema:   fooSchema,
   224  			wantErrs: true, // Unsupported block type (user must be consistent about whether they consider foo to be a block type or an attribute)
   225  			want: cty.ObjectVal(map[string]cty.Value{
   226  				"foo": cty.ListVal([]cty.Value{
   227  					cty.ObjectVal(map[string]cty.Value{
   228  						"bar": cty.StringVal("baz"),
   229  					}),
   230  					cty.ObjectVal(map[string]cty.Value{
   231  						"bar": cty.StringVal("boop"),
   232  					}),
   233  				}),
   234  			}),
   235  		},
   236  		"fixup inside block": {
   237  			src: `
   238  container {
   239    foo {
   240      bar = "baz"
   241    }
   242    foo {
   243      bar = "boop"
   244    }
   245  }
   246  container {
   247    foo {
   248      bar = beep
   249    }
   250  }
   251  `,
   252  			schema: &configschema.Block{
   253  				BlockTypes: map[string]*configschema.NestedBlock{
   254  					"container": {
   255  						Nesting: configschema.NestingList,
   256  						Block:   *fooSchema,
   257  					},
   258  				},
   259  			},
   260  			want: cty.ObjectVal(map[string]cty.Value{
   261  				"container": cty.ListVal([]cty.Value{
   262  					cty.ObjectVal(map[string]cty.Value{
   263  						"foo": cty.ListVal([]cty.Value{
   264  							cty.ObjectVal(map[string]cty.Value{
   265  								"bar": cty.StringVal("baz"),
   266  							}),
   267  							cty.ObjectVal(map[string]cty.Value{
   268  								"bar": cty.StringVal("boop"),
   269  							}),
   270  						}),
   271  					}),
   272  					cty.ObjectVal(map[string]cty.Value{
   273  						"foo": cty.ListVal([]cty.Value{
   274  							cty.ObjectVal(map[string]cty.Value{
   275  								"bar": cty.StringVal("beep value"),
   276  							}),
   277  						}),
   278  					}),
   279  				}),
   280  			}),
   281  		},
   282  		"fixup inside attribute-as-block": {
   283  			src: `
   284  container {
   285    foo {
   286      bar = "baz"
   287    }
   288    foo {
   289      bar = "boop"
   290    }
   291  }
   292  container {
   293    foo {
   294      bar = beep
   295    }
   296  }
   297  `,
   298  			schema: &configschema.Block{
   299  				Attributes: map[string]*configschema.Attribute{
   300  					"container": {
   301  						Type: cty.List(cty.Object(map[string]cty.Type{
   302  							"foo": cty.List(cty.Object(map[string]cty.Type{
   303  								"bar": cty.String,
   304  							})),
   305  						})),
   306  						Optional: true,
   307  					},
   308  				},
   309  			},
   310  			want: cty.ObjectVal(map[string]cty.Value{
   311  				"container": cty.ListVal([]cty.Value{
   312  					cty.ObjectVal(map[string]cty.Value{
   313  						"foo": cty.ListVal([]cty.Value{
   314  							cty.ObjectVal(map[string]cty.Value{
   315  								"bar": cty.StringVal("baz"),
   316  							}),
   317  							cty.ObjectVal(map[string]cty.Value{
   318  								"bar": cty.StringVal("boop"),
   319  							}),
   320  						}),
   321  					}),
   322  					cty.ObjectVal(map[string]cty.Value{
   323  						"foo": cty.ListVal([]cty.Value{
   324  							cty.ObjectVal(map[string]cty.Value{
   325  								"bar": cty.StringVal("beep value"),
   326  							}),
   327  						}),
   328  					}),
   329  				}),
   330  			}),
   331  		},
   332  		"nested fixup with dynamic block generation": {
   333  			src: `
   334  container {
   335    dynamic "foo" {
   336      for_each = ["baz", beep]
   337      content {
   338        bar = foo.value
   339      }
   340    }
   341  }
   342  `,
   343  			schema: &configschema.Block{
   344  				BlockTypes: map[string]*configschema.NestedBlock{
   345  					"container": {
   346  						Nesting: configschema.NestingList,
   347  						Block:   *fooSchema,
   348  					},
   349  				},
   350  			},
   351  			want: cty.ObjectVal(map[string]cty.Value{
   352  				"container": cty.ListVal([]cty.Value{
   353  					cty.ObjectVal(map[string]cty.Value{
   354  						"foo": cty.ListVal([]cty.Value{
   355  							cty.ObjectVal(map[string]cty.Value{
   356  								"bar": cty.StringVal("baz"),
   357  							}),
   358  							cty.ObjectVal(map[string]cty.Value{
   359  								"bar": cty.StringVal("beep value"),
   360  							}),
   361  						}),
   362  					}),
   363  				}),
   364  			}),
   365  		},
   366  
   367  		"missing nested block items": {
   368  			src: `
   369  container {
   370    foo {
   371      bar = "one"
   372    }
   373  }
   374  `,
   375  			schema: &configschema.Block{
   376  				BlockTypes: map[string]*configschema.NestedBlock{
   377  					"container": {
   378  						Nesting:  configschema.NestingList,
   379  						MinItems: 2,
   380  						Block: configschema.Block{
   381  							Attributes: map[string]*configschema.Attribute{
   382  								"foo": {
   383  									Type: cty.List(cty.Object(map[string]cty.Type{
   384  										"bar": cty.String,
   385  									})),
   386  									Optional: true,
   387  								},
   388  							},
   389  						},
   390  					},
   391  				},
   392  			},
   393  			want: cty.ObjectVal(map[string]cty.Value{
   394  				"container": cty.ListVal([]cty.Value{
   395  					cty.ObjectVal(map[string]cty.Value{
   396  						"foo": cty.ListVal([]cty.Value{
   397  							cty.ObjectVal(map[string]cty.Value{
   398  								"bar": cty.StringVal("baz"),
   399  							}),
   400  						}),
   401  					}),
   402  				}),
   403  			}),
   404  			wantErrs: true,
   405  		},
   406  		"no fixup allowed with NestedType": {
   407  			src: `
   408   container {
   409     foo = "one"
   410   }
   411   `,
   412  			schema: &configschema.Block{
   413  				Attributes: map[string]*configschema.Attribute{
   414  					"container": {
   415  						NestedType: &configschema.Object{
   416  							Nesting: configschema.NestingList,
   417  							Attributes: map[string]*configschema.Attribute{
   418  								"foo": {
   419  									Type: cty.String,
   420  								},
   421  							},
   422  						},
   423  					},
   424  				},
   425  			},
   426  			want: cty.ObjectVal(map[string]cty.Value{
   427  				"container": cty.NullVal(cty.List(
   428  					cty.Object(map[string]cty.Type{
   429  						"foo": cty.String,
   430  					}),
   431  				)),
   432  			}),
   433  			wantErrs: true,
   434  		},
   435  		"no fixup allowed new types": {
   436  			src: `
   437   container {
   438     foo = "one"
   439   }
   440   `,
   441  			schema: &configschema.Block{
   442  				Attributes: map[string]*configschema.Attribute{
   443  					// This could be a ConfigModeAttr fixup
   444  					"container": {
   445  						Type: cty.List(cty.Object(map[string]cty.Type{
   446  							"foo": cty.String,
   447  						})),
   448  					},
   449  					// But the presence of this type means it must have been
   450  					// declared by a new SDK
   451  					"new_type": {
   452  						Type: cty.Object(map[string]cty.Type{
   453  							"boo": cty.String,
   454  						}),
   455  					},
   456  				},
   457  			},
   458  			want: cty.ObjectVal(map[string]cty.Value{
   459  				"container": cty.NullVal(cty.List(
   460  					cty.Object(map[string]cty.Type{
   461  						"foo": cty.String,
   462  					}),
   463  				)),
   464  			}),
   465  			wantErrs: true,
   466  		},
   467  	}
   468  
   469  	ctx := &hcl.EvalContext{
   470  		Variables: map[string]cty.Value{
   471  			"bar":  cty.StringVal("bar value"),
   472  			"baz":  cty.StringVal("baz value"),
   473  			"beep": cty.StringVal("beep value"),
   474  		},
   475  	}
   476  
   477  	for name, test := range tests {
   478  		t.Run(name, func(t *testing.T) {
   479  			var f *hcl.File
   480  			var diags hcl.Diagnostics
   481  			if test.json {
   482  				f, diags = hcljson.Parse([]byte(test.src), "test.tf.json")
   483  			} else {
   484  				f, diags = hclsyntax.ParseConfig([]byte(test.src), "test.tf", hcl.Pos{Line: 1, Column: 1})
   485  			}
   486  			if diags.HasErrors() {
   487  				for _, diag := range diags {
   488  					t.Errorf("unexpected diagnostic: %s", diag)
   489  				}
   490  				t.FailNow()
   491  			}
   492  
   493  			// We'll expand dynamic blocks in the body first, to mimic how
   494  			// we process this fixup when using the main "lang" package API.
   495  			spec := test.schema.DecoderSpec()
   496  			body := dynblock.Expand(f.Body, ctx)
   497  
   498  			body = FixUpBlockAttrs(body, test.schema)
   499  			got, diags := hcldec.Decode(body, spec, ctx)
   500  
   501  			if test.wantErrs {
   502  				if !diags.HasErrors() {
   503  					t.Errorf("succeeded, but want error\ngot: %#v", got)
   504  				}
   505  
   506  				// check that our wrapped body returns the correct context by
   507  				// verifying the Subject is valid.
   508  				for _, d := range diags {
   509  					if d.Subject.Filename == "" {
   510  						t.Errorf("empty diagnostic subject: %#v", d.Subject)
   511  					}
   512  				}
   513  				return
   514  			}
   515  
   516  			if !test.want.RawEquals(got) {
   517  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.want)
   518  			}
   519  			for _, diag := range diags {
   520  				t.Errorf("unexpected diagnostic: %s", diag)
   521  			}
   522  		})
   523  	}
   524  }