github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/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/eliastor/durgaform/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  		"missing nested block items": {
   365  			src: `
   366  container {
   367    foo {
   368      bar = "one"
   369    }
   370  }
   371  `,
   372  			schema: &configschema.Block{
   373  				BlockTypes: map[string]*configschema.NestedBlock{
   374  					"container": {
   375  						Nesting:  configschema.NestingList,
   376  						MinItems: 2,
   377  						Block: configschema.Block{
   378  							Attributes: map[string]*configschema.Attribute{
   379  								"foo": {
   380  									Type: cty.List(cty.Object(map[string]cty.Type{
   381  										"bar": cty.String,
   382  									})),
   383  									Optional: true,
   384  								},
   385  							},
   386  						},
   387  					},
   388  				},
   389  			},
   390  			want: cty.ObjectVal(map[string]cty.Value{
   391  				"container": cty.ListVal([]cty.Value{
   392  					cty.ObjectVal(map[string]cty.Value{
   393  						"foo": cty.ListVal([]cty.Value{
   394  							cty.ObjectVal(map[string]cty.Value{
   395  								"bar": cty.StringVal("baz"),
   396  							}),
   397  						}),
   398  					}),
   399  				}),
   400  			}),
   401  			wantErrs: true,
   402  		},
   403  		"no fixup allowed with NestedType": {
   404  			src: `
   405   container {
   406     foo = "one"
   407   }
   408   `,
   409  			schema: &configschema.Block{
   410  				Attributes: map[string]*configschema.Attribute{
   411  					"container": {
   412  						NestedType: &configschema.Object{
   413  							Nesting: configschema.NestingList,
   414  							Attributes: map[string]*configschema.Attribute{
   415  								"foo": {
   416  									Type: cty.String,
   417  								},
   418  							},
   419  						},
   420  					},
   421  				},
   422  			},
   423  			want: cty.ObjectVal(map[string]cty.Value{
   424  				"container": cty.NullVal(cty.List(
   425  					cty.Object(map[string]cty.Type{
   426  						"foo": cty.String,
   427  					}),
   428  				)),
   429  			}),
   430  			wantErrs: true,
   431  		},
   432  		"no fixup allowed new types": {
   433  			src: `
   434   container {
   435     foo = "one"
   436   }
   437   `,
   438  			schema: &configschema.Block{
   439  				Attributes: map[string]*configschema.Attribute{
   440  					// This could be a ConfigModeAttr fixup
   441  					"container": {
   442  						Type: cty.List(cty.Object(map[string]cty.Type{
   443  							"foo": cty.String,
   444  						})),
   445  					},
   446  					// But the presence of this type means it must have been
   447  					// declared by a new SDK
   448  					"new_type": {
   449  						Type: cty.Object(map[string]cty.Type{
   450  							"boo": cty.String,
   451  						}),
   452  					},
   453  				},
   454  			},
   455  			want: cty.ObjectVal(map[string]cty.Value{
   456  				"container": cty.NullVal(cty.List(
   457  					cty.Object(map[string]cty.Type{
   458  						"foo": cty.String,
   459  					}),
   460  				)),
   461  			}),
   462  			wantErrs: true,
   463  		},
   464  	}
   465  
   466  	ctx := &hcl.EvalContext{
   467  		Variables: map[string]cty.Value{
   468  			"bar":  cty.StringVal("bar value"),
   469  			"baz":  cty.StringVal("baz value"),
   470  			"beep": cty.StringVal("beep value"),
   471  		},
   472  	}
   473  
   474  	for name, test := range tests {
   475  		t.Run(name, func(t *testing.T) {
   476  			var f *hcl.File
   477  			var diags hcl.Diagnostics
   478  			if test.json {
   479  				f, diags = hcljson.Parse([]byte(test.src), "test.tf.json")
   480  			} else {
   481  				f, diags = hclsyntax.ParseConfig([]byte(test.src), "test.tf", hcl.Pos{Line: 1, Column: 1})
   482  			}
   483  			if diags.HasErrors() {
   484  				for _, diag := range diags {
   485  					t.Errorf("unexpected diagnostic: %s", diag)
   486  				}
   487  				t.FailNow()
   488  			}
   489  
   490  			// We'll expand dynamic blocks in the body first, to mimic how
   491  			// we process this fixup when using the main "lang" package API.
   492  			spec := test.schema.DecoderSpec()
   493  			body := dynblock.Expand(f.Body, ctx)
   494  
   495  			body = FixUpBlockAttrs(body, test.schema)
   496  			got, diags := hcldec.Decode(body, spec, ctx)
   497  
   498  			if test.wantErrs {
   499  				if !diags.HasErrors() {
   500  					t.Errorf("succeeded, but want error\ngot: %#v", got)
   501  				}
   502  
   503  				// check that our wrapped body returns the correct context by
   504  				// verifying the Subject is valid.
   505  				for _, d := range diags {
   506  					if d.Subject.Filename == "" {
   507  						t.Errorf("empty diagnostic subject: %#v", d.Subject)
   508  					}
   509  				}
   510  				return
   511  			}
   512  
   513  			if !test.want.RawEquals(got) {
   514  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.want)
   515  			}
   516  			for _, diag := range diags {
   517  				t.Errorf("unexpected diagnostic: %s", diag)
   518  			}
   519  		})
   520  	}
   521  }