github.com/hashicorp/hcl/v2@v2.20.0/ext/tryfunc/tryfunc_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package tryfunc
     5  
     6  import (
     7  	"testing"
     8  
     9  	"github.com/hashicorp/hcl/v2"
    10  	"github.com/hashicorp/hcl/v2/hclsyntax"
    11  	"github.com/zclconf/go-cty/cty"
    12  	"github.com/zclconf/go-cty/cty/function"
    13  )
    14  
    15  func TestTryFunc(t *testing.T) {
    16  	tests := map[string]struct {
    17  		expr    string
    18  		vars    map[string]cty.Value
    19  		want    cty.Value
    20  		wantErr string
    21  	}{
    22  		"one argument succeeds": {
    23  			`try(1)`,
    24  			nil,
    25  			cty.NumberIntVal(1),
    26  			``,
    27  		},
    28  		"one marked argument succeeds": {
    29  			`try(sensitive)`,
    30  			map[string]cty.Value{
    31  				"sensitive": cty.StringVal("secret").Mark("porpoise"),
    32  			},
    33  			cty.StringVal("secret").Mark("porpoise"),
    34  			``,
    35  		},
    36  		"two arguments, first succeeds": {
    37  			`try(1, 2)`,
    38  			nil,
    39  			cty.NumberIntVal(1),
    40  			``,
    41  		},
    42  		"two arguments, first fails": {
    43  			`try(nope, 2)`,
    44  			nil,
    45  			cty.NumberIntVal(2),
    46  			``,
    47  		},
    48  		"two arguments, first depends on unknowns": {
    49  			`try(unknown, 2)`,
    50  			map[string]cty.Value{
    51  				"unknown": cty.UnknownVal(cty.Number),
    52  			},
    53  			cty.DynamicVal, // can't proceed until first argument is known
    54  			``,
    55  		},
    56  		"two arguments, first succeeds and second depends on unknowns": {
    57  			`try(1, unknown)`,
    58  			map[string]cty.Value{
    59  				"unknown": cty.UnknownVal(cty.Number),
    60  			},
    61  			cty.NumberIntVal(1), // we know 1st succeeds, so it doesn't matter that 2nd is unknown
    62  			``,
    63  		},
    64  		"two arguments, first depends on unknowns deeply": {
    65  			`try(has_unknowns, 2)`,
    66  			map[string]cty.Value{
    67  				"has_unknowns": cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}),
    68  			},
    69  			cty.DynamicVal, // can't proceed until first argument is wholly known
    70  			``,
    71  		},
    72  		"two arguments, first traverses through an unkown": {
    73  			`try(unknown.baz, 2)`,
    74  			map[string]cty.Value{
    75  				"unknown": cty.UnknownVal(cty.Map(cty.String)),
    76  			},
    77  			cty.DynamicVal, // can't proceed until first argument is wholly known
    78  			``,
    79  		},
    80  		"two arguments, both marked, first succeeds": {
    81  			`try(sensitive, other)`,
    82  			map[string]cty.Value{
    83  				"sensitive": cty.StringVal("secret").Mark("porpoise"),
    84  				"other":     cty.StringVal("that").Mark("a"),
    85  			},
    86  			cty.StringVal("secret").Mark("porpoise"),
    87  			``,
    88  		},
    89  		"two arguments, both marked, second succeeds": {
    90  			`try(sensitive, other)`,
    91  			map[string]cty.Value{
    92  				"other": cty.StringVal("that").Mark("a"),
    93  			},
    94  			cty.StringVal("that").Mark("a"),
    95  			``,
    96  		},
    97  		"two arguments, result is element of marked list ": {
    98  			`try(sensitive[0], other)`,
    99  			map[string]cty.Value{
   100  				"sensitive": cty.ListVal([]cty.Value{
   101  					cty.StringVal("list"),
   102  					cty.StringVal("of "),
   103  					cty.StringVal("secrets"),
   104  				}).Mark("secret"),
   105  				"other": cty.StringVal("not"),
   106  			},
   107  			cty.StringVal("list").Mark("secret"),
   108  			``,
   109  		},
   110  		"nested known expression from unknown": {
   111  			// this expression contains an unknown, but will always return in
   112  			// "bar"
   113  			`try({u: false ? unknown : "bar"}, other)`,
   114  			map[string]cty.Value{
   115  				"unknown": cty.UnknownVal(cty.String),
   116  				"other": cty.MapVal(map[string]cty.Value{
   117  					"v": cty.StringVal("oops"),
   118  				}),
   119  			},
   120  			cty.ObjectVal(map[string]cty.Value{
   121  				"u": cty.StringVal("bar"),
   122  			}),
   123  			``,
   124  		},
   125  		"nested index op on unknown": {
   126  			// unknown and other have identical types, but we must return a
   127  			// dynamic value since v could change within the final result value
   128  			// after the first argument becomes known.
   129  			`try({u: unknown["foo"], v: "orig"}, other)`,
   130  			map[string]cty.Value{
   131  				"unknown": cty.UnknownVal(cty.Map(cty.String)),
   132  				"other": cty.MapVal(map[string]cty.Value{
   133  					"u": cty.StringVal("oops"),
   134  					"v": cty.StringVal("oops"),
   135  				}),
   136  			},
   137  			cty.DynamicVal,
   138  			``,
   139  		},
   140  		"three arguments, all fail": {
   141  			`try(this, that, this_thing_in_particular)`,
   142  			nil,
   143  			cty.NumberIntVal(2),
   144  			// The grammar of this stringification of the message is unfortunate,
   145  			// but caller can type-assert our result to get the original
   146  			// diagnostics directly in order to produce a better result.
   147  			`test.hcl:1,1-5: Error in function call; Call to function "try" failed: no expression succeeded:
   148  - Variables not allowed (at test.hcl:1,5-9)
   149    Variables may not be used here.
   150  - Variables not allowed (at test.hcl:1,11-15)
   151    Variables may not be used here.
   152  - Variables not allowed (at test.hcl:1,17-41)
   153    Variables may not be used here.
   154  
   155  At least one expression must produce a successful result.`,
   156  		},
   157  		"no arguments": {
   158  			`try()`,
   159  			nil,
   160  			cty.NilVal,
   161  			`test.hcl:1,1-5: Error in function call; Call to function "try" failed: at least one argument is required.`,
   162  		},
   163  	}
   164  
   165  	for k, test := range tests {
   166  		t.Run(k, func(t *testing.T) {
   167  			expr, diags := hclsyntax.ParseExpression([]byte(test.expr), "test.hcl", hcl.Pos{Line: 1, Column: 1})
   168  			if diags.HasErrors() {
   169  				t.Fatalf("unexpected problems: %s", diags.Error())
   170  			}
   171  
   172  			ctx := &hcl.EvalContext{
   173  				Variables: test.vars,
   174  				Functions: map[string]function.Function{
   175  					"try": TryFunc,
   176  				},
   177  			}
   178  
   179  			got, err := expr.Value(ctx)
   180  
   181  			if err != nil {
   182  				if test.wantErr != "" {
   183  					if got, want := err.Error(), test.wantErr; got != want {
   184  						t.Errorf("wrong error\ngot:  %s\nwant: %s", got, want)
   185  					}
   186  				} else {
   187  					t.Errorf("unexpected error\ngot:  %s\nwant: <nil>", err)
   188  				}
   189  				return
   190  			}
   191  			if test.wantErr != "" {
   192  				t.Errorf("wrong error\ngot:  <nil>\nwant: %s", test.wantErr)
   193  			}
   194  
   195  			if !test.want.RawEquals(got) {
   196  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.want)
   197  			}
   198  		})
   199  	}
   200  }
   201  
   202  func TestCanFunc(t *testing.T) {
   203  	tests := map[string]struct {
   204  		expr string
   205  		vars map[string]cty.Value
   206  		want cty.Value
   207  	}{
   208  		"succeeds": {
   209  			`can(1)`,
   210  			nil,
   211  			cty.True,
   212  		},
   213  		"fails": {
   214  			`can(nope)`,
   215  			nil,
   216  			cty.False,
   217  		},
   218  		"simple unknown": {
   219  			`can(unknown)`,
   220  			map[string]cty.Value{
   221  				"unknown": cty.UnknownVal(cty.Number),
   222  			},
   223  			cty.UnknownVal(cty.Bool),
   224  		},
   225  		"traversal through unknown": {
   226  			`can(unknown.foo)`,
   227  			map[string]cty.Value{
   228  				"unknown": cty.UnknownVal(cty.Map(cty.Number)),
   229  			},
   230  			cty.UnknownVal(cty.Bool),
   231  		},
   232  		"deep unknown": {
   233  			`can(has_unknown)`,
   234  			map[string]cty.Value{
   235  				"has_unknown": cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}),
   236  			},
   237  			cty.UnknownVal(cty.Bool),
   238  		},
   239  	}
   240  
   241  	for k, test := range tests {
   242  		t.Run(k, func(t *testing.T) {
   243  			expr, diags := hclsyntax.ParseExpression([]byte(test.expr), "test.hcl", hcl.Pos{Line: 1, Column: 1})
   244  			if diags.HasErrors() {
   245  				t.Fatalf("unexpected problems: %s", diags.Error())
   246  			}
   247  
   248  			ctx := &hcl.EvalContext{
   249  				Variables: test.vars,
   250  				Functions: map[string]function.Function{
   251  					"can": CanFunc,
   252  				},
   253  			}
   254  
   255  			got, err := expr.Value(ctx)
   256  			if err != nil {
   257  				t.Errorf("unexpected error\ngot:  %s\nwant: <nil>", err)
   258  			}
   259  			if !test.want.RawEquals(got) {
   260  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.want)
   261  			}
   262  		})
   263  	}
   264  }