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