github.com/hashicorp/hcl/v2@v2.20.0/hcldec/spec_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package hcldec
     5  
     6  import (
     7  	"fmt"
     8  	"reflect"
     9  	"testing"
    10  
    11  	"github.com/apparentlymart/go-dump/dump"
    12  	"github.com/google/go-cmp/cmp"
    13  	"github.com/zclconf/go-cty-debug/ctydebug"
    14  	"github.com/zclconf/go-cty/cty"
    15  	"github.com/zclconf/go-cty/cty/function"
    16  
    17  	"github.com/hashicorp/hcl/v2"
    18  	"github.com/hashicorp/hcl/v2/hclsyntax"
    19  )
    20  
    21  // Verify that all of our spec types implement the necessary interfaces
    22  var _ Spec = ObjectSpec(nil)
    23  var _ Spec = TupleSpec(nil)
    24  var _ Spec = (*AttrSpec)(nil)
    25  var _ Spec = (*LiteralSpec)(nil)
    26  var _ Spec = (*ExprSpec)(nil)
    27  var _ Spec = (*BlockSpec)(nil)
    28  var _ Spec = (*BlockListSpec)(nil)
    29  var _ Spec = (*BlockSetSpec)(nil)
    30  var _ Spec = (*BlockMapSpec)(nil)
    31  var _ Spec = (*BlockAttrsSpec)(nil)
    32  var _ Spec = (*BlockLabelSpec)(nil)
    33  var _ Spec = (*DefaultSpec)(nil)
    34  var _ Spec = (*TransformExprSpec)(nil)
    35  var _ Spec = (*TransformFuncSpec)(nil)
    36  var _ Spec = (*ValidateSpec)(nil)
    37  
    38  var _ attrSpec = (*AttrSpec)(nil)
    39  var _ attrSpec = (*DefaultSpec)(nil)
    40  
    41  var _ blockSpec = (*BlockSpec)(nil)
    42  var _ blockSpec = (*BlockListSpec)(nil)
    43  var _ blockSpec = (*BlockSetSpec)(nil)
    44  var _ blockSpec = (*BlockMapSpec)(nil)
    45  var _ blockSpec = (*BlockAttrsSpec)(nil)
    46  var _ blockSpec = (*DefaultSpec)(nil)
    47  
    48  var _ specNeedingVariables = (*AttrSpec)(nil)
    49  var _ specNeedingVariables = (*BlockSpec)(nil)
    50  var _ specNeedingVariables = (*BlockListSpec)(nil)
    51  var _ specNeedingVariables = (*BlockSetSpec)(nil)
    52  var _ specNeedingVariables = (*BlockMapSpec)(nil)
    53  var _ specNeedingVariables = (*BlockAttrsSpec)(nil)
    54  
    55  func TestDefaultSpec(t *testing.T) {
    56  	config := `
    57  foo = fooval
    58  bar = barval
    59  `
    60  	f, diags := hclsyntax.ParseConfig([]byte(config), "", hcl.Pos{Line: 1, Column: 1})
    61  	if diags.HasErrors() {
    62  		t.Fatal(diags.Error())
    63  	}
    64  
    65  	t.Run("primary set", func(t *testing.T) {
    66  		spec := &DefaultSpec{
    67  			Primary: &AttrSpec{
    68  				Name: "foo",
    69  				Type: cty.String,
    70  			},
    71  			Default: &AttrSpec{
    72  				Name: "bar",
    73  				Type: cty.String,
    74  			},
    75  		}
    76  
    77  		gotVars := Variables(f.Body, spec)
    78  		wantVars := []hcl.Traversal{
    79  			{
    80  				hcl.TraverseRoot{
    81  					Name: "fooval",
    82  					SrcRange: hcl.Range{
    83  						Filename: "",
    84  						Start:    hcl.Pos{Line: 2, Column: 7, Byte: 7},
    85  						End:      hcl.Pos{Line: 2, Column: 13, Byte: 13},
    86  					},
    87  				},
    88  			},
    89  			{
    90  				hcl.TraverseRoot{
    91  					Name: "barval",
    92  					SrcRange: hcl.Range{
    93  						Filename: "",
    94  						Start:    hcl.Pos{Line: 3, Column: 7, Byte: 20},
    95  						End:      hcl.Pos{Line: 3, Column: 13, Byte: 26},
    96  					},
    97  				},
    98  			},
    99  		}
   100  		if !reflect.DeepEqual(gotVars, wantVars) {
   101  			t.Errorf("wrong Variables result\ngot: %s\nwant: %s", dump.Value(gotVars), dump.Value(wantVars))
   102  		}
   103  
   104  		ctx := &hcl.EvalContext{
   105  			Variables: map[string]cty.Value{
   106  				"fooval": cty.StringVal("foo value"),
   107  				"barval": cty.StringVal("bar value"),
   108  			},
   109  		}
   110  
   111  		got, err := Decode(f.Body, spec, ctx)
   112  		if err != nil {
   113  			t.Fatal(err)
   114  		}
   115  		want := cty.StringVal("foo value")
   116  		if !got.RawEquals(want) {
   117  			t.Errorf("wrong Decode result\ngot:  %#v\nwant: %#v", got, want)
   118  		}
   119  	})
   120  
   121  	t.Run("primary not set", func(t *testing.T) {
   122  		spec := &DefaultSpec{
   123  			Primary: &AttrSpec{
   124  				Name: "foo",
   125  				Type: cty.String,
   126  			},
   127  			Default: &AttrSpec{
   128  				Name: "bar",
   129  				Type: cty.String,
   130  			},
   131  		}
   132  
   133  		ctx := &hcl.EvalContext{
   134  			Variables: map[string]cty.Value{
   135  				"fooval": cty.NullVal(cty.String),
   136  				"barval": cty.StringVal("bar value"),
   137  			},
   138  		}
   139  
   140  		got, err := Decode(f.Body, spec, ctx)
   141  		if err != nil {
   142  			t.Fatal(err)
   143  		}
   144  		want := cty.StringVal("bar value")
   145  		if !got.RawEquals(want) {
   146  			t.Errorf("wrong Decode result\ngot:  %#v\nwant: %#v", got, want)
   147  		}
   148  	})
   149  }
   150  
   151  func TestValidateFuncSpec(t *testing.T) {
   152  	config := `
   153  foo = "invalid"
   154  `
   155  	f, diags := hclsyntax.ParseConfig([]byte(config), "", hcl.Pos{Line: 1, Column: 1})
   156  	if diags.HasErrors() {
   157  		t.Fatal(diags.Error())
   158  	}
   159  
   160  	expectRange := map[string]*hcl.Range{
   161  		"without_range": nil,
   162  		"with_range": &hcl.Range{
   163  			Filename: "foobar",
   164  			Start:    hcl.Pos{Line: 99, Column: 99},
   165  			End:      hcl.Pos{Line: 999, Column: 999},
   166  		},
   167  	}
   168  
   169  	for name := range expectRange {
   170  		t.Run(name, func(t *testing.T) {
   171  			spec := &ValidateSpec{
   172  				Wrapped: &AttrSpec{
   173  					Name: "foo",
   174  					Type: cty.String,
   175  				},
   176  				Func: func(value cty.Value) hcl.Diagnostics {
   177  					if value.AsString() != "invalid" {
   178  						return hcl.Diagnostics{
   179  							&hcl.Diagnostic{
   180  								Severity: hcl.DiagError,
   181  								Summary:  "incorrect value",
   182  								Detail:   fmt.Sprintf("invalid value passed in: %s", value.GoString()),
   183  							},
   184  						}
   185  					}
   186  
   187  					return hcl.Diagnostics{
   188  						&hcl.Diagnostic{
   189  							Severity: hcl.DiagWarning,
   190  							Summary:  "OK",
   191  							Detail:   "validation called correctly",
   192  							Subject:  expectRange[name],
   193  						},
   194  					}
   195  				},
   196  			}
   197  
   198  			_, diags = Decode(f.Body, spec, nil)
   199  			if len(diags) != 1 ||
   200  				diags[0].Severity != hcl.DiagWarning ||
   201  				diags[0].Summary != "OK" ||
   202  				diags[0].Detail != "validation called correctly" {
   203  				t.Fatalf("unexpected diagnostics: %s", diags.Error())
   204  			}
   205  
   206  			if expectRange[name] == nil && diags[0].Subject == nil {
   207  				t.Fatal("returned diagnostic subject missing")
   208  			}
   209  
   210  			if expectRange[name] != nil && !reflect.DeepEqual(expectRange[name], diags[0].Subject) {
   211  				t.Fatalf("expected range %s, got range %s", expectRange[name], diags[0].Subject)
   212  			}
   213  		})
   214  	}
   215  }
   216  
   217  func TestRefineValueSpec(t *testing.T) {
   218  	config := `
   219  foo = "hello"
   220  bar = unk
   221  dyn = dyn
   222  marked = mark(unk)
   223  `
   224  
   225  	f, diags := hclsyntax.ParseConfig([]byte(config), "", hcl.InitialPos)
   226  	if diags.HasErrors() {
   227  		t.Fatal(diags.Error())
   228  	}
   229  
   230  	attrSpec := func(name string) Spec {
   231  		return &RefineValueSpec{
   232  			// RefineValueSpec should typically have a ValidateSpec wrapped
   233  			// inside it to catch any values that are outside of the required
   234  			// range and return a helpful error message about it. In this
   235  			// case our refinement is .NotNull so the validation function
   236  			// must reject null values.
   237  			Wrapped: &ValidateSpec{
   238  				Wrapped: &AttrSpec{
   239  					Name:     name,
   240  					Required: true,
   241  					Type:     cty.String,
   242  				},
   243  				Func: func(value cty.Value) hcl.Diagnostics {
   244  					var diags hcl.Diagnostics
   245  					if value.IsNull() {
   246  						diags = diags.Append(&hcl.Diagnostic{
   247  							Severity: hcl.DiagError,
   248  							Summary:  "Cannot be null",
   249  							Detail:   "Argument is required.",
   250  						})
   251  					}
   252  					return diags
   253  				},
   254  			},
   255  			Refine: func(rb *cty.RefinementBuilder) *cty.RefinementBuilder {
   256  				return rb.NotNull()
   257  			},
   258  		}
   259  	}
   260  	spec := &ObjectSpec{
   261  		"foo":    attrSpec("foo"),
   262  		"bar":    attrSpec("bar"),
   263  		"dyn":    attrSpec("dyn"),
   264  		"marked": attrSpec("marked"),
   265  	}
   266  
   267  	got, diags := Decode(f.Body, spec, &hcl.EvalContext{
   268  		Variables: map[string]cty.Value{
   269  			"unk": cty.UnknownVal(cty.String),
   270  			"dyn": cty.DynamicVal,
   271  		},
   272  		Functions: map[string]function.Function{
   273  			"mark": function.New(&function.Spec{
   274  				Params: []function.Parameter{
   275  					{
   276  						Name:             "v",
   277  						Type:             cty.DynamicPseudoType,
   278  						AllowMarked:      true,
   279  						AllowNull:        true,
   280  						AllowUnknown:     true,
   281  						AllowDynamicType: true,
   282  					},
   283  				},
   284  				Type: func(args []cty.Value) (cty.Type, error) {
   285  					return args[0].Type(), nil
   286  				},
   287  				Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
   288  					return args[0].Mark("boop"), nil
   289  				},
   290  			}),
   291  		},
   292  	})
   293  	if diags.HasErrors() {
   294  		t.Fatal(diags.Error())
   295  	}
   296  
   297  	want := cty.ObjectVal(map[string]cty.Value{
   298  		// This argument had a known value, so it's unchanged but the
   299  		// RefineValueSpec still checks that it isn't null to catch
   300  		// bugs in the application's validation function.
   301  		"foo": cty.StringVal("hello"),
   302  
   303  		// The final value of bar is unknown but refined as non-null.
   304  		"bar": cty.UnknownVal(cty.String).RefineNotNull(),
   305  
   306  		// The final value of dyn is unknown but refined as non-null.
   307  		// Correct behavior here requires that we convert the DynamicVal
   308  		// to an unknown string first and then refine it.
   309  		"dyn": cty.UnknownVal(cty.String).RefineNotNull(),
   310  
   311  		// This argument had a mark applied, which should be preserved
   312  		// despite the refinement.
   313  		"marked": cty.UnknownVal(cty.String).RefineNotNull().Mark("boop"),
   314  	})
   315  	if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" {
   316  		t.Errorf("wrong result\n%s", diff)
   317  	}
   318  }