github.com/opentofu/opentofu@v1.7.1/internal/tofu/context_functions_test.go (about)

     1  package tofu
     2  
     3  import (
     4  	"strings"
     5  	"testing"
     6  
     7  	"github.com/hashicorp/hcl/v2"
     8  	"github.com/hashicorp/hcl/v2/hclsyntax"
     9  	"github.com/opentofu/opentofu/internal/addrs"
    10  	"github.com/opentofu/opentofu/internal/configs"
    11  	"github.com/opentofu/opentofu/internal/lang/marks"
    12  	"github.com/opentofu/opentofu/internal/providers"
    13  	"github.com/opentofu/opentofu/internal/tfdiags"
    14  	"github.com/zclconf/go-cty/cty"
    15  	"github.com/zclconf/go-cty/cty/function"
    16  )
    17  
    18  func TestFunctions(t *testing.T) {
    19  	mockProvider := &MockProvider{
    20  		GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
    21  			Provider: providers.Schema{},
    22  			Functions: map[string]providers.FunctionSpec{
    23  				"echo": providers.FunctionSpec{
    24  					Parameters: []providers.FunctionParameterSpec{providers.FunctionParameterSpec{
    25  						Name:               "input",
    26  						Type:               cty.String,
    27  						AllowNullValue:     false,
    28  						AllowUnknownValues: false,
    29  					}},
    30  					Return: cty.String,
    31  				},
    32  				"concat": providers.FunctionSpec{
    33  					Parameters: []providers.FunctionParameterSpec{providers.FunctionParameterSpec{
    34  						Name:               "input",
    35  						Type:               cty.String,
    36  						AllowNullValue:     false,
    37  						AllowUnknownValues: false,
    38  					}},
    39  					VariadicParameter: &providers.FunctionParameterSpec{
    40  						Name:           "vary",
    41  						Type:           cty.String,
    42  						AllowNullValue: false,
    43  					},
    44  					Return: cty.String,
    45  				},
    46  				"coalesce": providers.FunctionSpec{
    47  					Parameters: []providers.FunctionParameterSpec{providers.FunctionParameterSpec{
    48  						Name:               "input1",
    49  						Type:               cty.String,
    50  						AllowNullValue:     true,
    51  						AllowUnknownValues: false,
    52  					}, providers.FunctionParameterSpec{
    53  						Name:               "input2",
    54  						Type:               cty.String,
    55  						AllowNullValue:     false,
    56  						AllowUnknownValues: false,
    57  					}},
    58  					Return: cty.String,
    59  				},
    60  				"unknown_param": providers.FunctionSpec{
    61  					Parameters: []providers.FunctionParameterSpec{providers.FunctionParameterSpec{
    62  						Name:               "input",
    63  						Type:               cty.String,
    64  						AllowNullValue:     false,
    65  						AllowUnknownValues: true,
    66  					}},
    67  					Return: cty.String,
    68  				},
    69  				"error_param": providers.FunctionSpec{
    70  					Parameters: []providers.FunctionParameterSpec{providers.FunctionParameterSpec{
    71  						Name:               "input",
    72  						Type:               cty.String,
    73  						AllowNullValue:     false,
    74  						AllowUnknownValues: false,
    75  					}},
    76  					Return: cty.String,
    77  				},
    78  			},
    79  		},
    80  	}
    81  
    82  	mockProvider.CallFunctionFn = func(req providers.CallFunctionRequest) (resp providers.CallFunctionResponse) {
    83  		switch req.Name {
    84  		case "echo":
    85  			resp.Result = req.Arguments[0]
    86  		case "concat":
    87  			str := ""
    88  			for _, arg := range req.Arguments {
    89  				str += arg.AsString()
    90  			}
    91  			resp.Result = cty.StringVal(str)
    92  		case "coalesce":
    93  			resp.Result = req.Arguments[0]
    94  			if resp.Result.IsNull() {
    95  				resp.Result = req.Arguments[1]
    96  			}
    97  		case "unknown_param":
    98  			resp.Result = cty.StringVal("knownvalue")
    99  		case "error_param":
   100  			resp.Error = &providers.CallFunctionArgumentError{
   101  				Text:             "my error text",
   102  				FunctionArgument: 0,
   103  			}
   104  		default:
   105  			panic("Invalid function")
   106  		}
   107  		return resp
   108  	}
   109  
   110  	mockProvider.GetFunctionsFn = func() (resp providers.GetFunctionsResponse) {
   111  		resp.Functions = mockProvider.GetProviderSchemaResponse.Functions
   112  		return resp
   113  	}
   114  
   115  	addr := addrs.NewDefaultProvider("mock")
   116  	rng := tfdiags.SourceRange{}
   117  	providerFunc := func(fn string) addrs.ProviderFunction {
   118  		pf, _ := addrs.ParseFunction(fn).AsProviderFunction()
   119  		return pf
   120  	}
   121  
   122  	mockCtx := new(MockEvalContext)
   123  	cfg := &configs.Config{
   124  		Module: &configs.Module{
   125  			ProviderRequirements: &configs.RequiredProviders{
   126  				RequiredProviders: map[string]*configs.RequiredProvider{
   127  					"mockname": &configs.RequiredProvider{
   128  						Name: "mock",
   129  						Type: addr,
   130  					},
   131  				},
   132  			},
   133  		},
   134  	}
   135  
   136  	// Provider missing
   137  	_, diags := evalContextProviderFunction(mockCtx.Provider, cfg, walkValidate, providerFunc("provider::invalid::unknown"), rng)
   138  	if !diags.HasErrors() {
   139  		t.Fatal("expected unknown function provider")
   140  	}
   141  	if diags.Err().Error() != `Unknown function provider: Provider "invalid" does not exist within the required_providers of this module` {
   142  		t.Fatal(diags.Err())
   143  	}
   144  
   145  	// Provider not initialized
   146  	_, diags = evalContextProviderFunction(mockCtx.Provider, cfg, walkValidate, providerFunc("provider::mockname::missing"), rng)
   147  	if !diags.HasErrors() {
   148  		t.Fatal("expected unknown function provider")
   149  	}
   150  	if diags.Err().Error() != `BUG: Uninitialized function provider: Provider "provider[\"registry.opentofu.org/hashicorp/mock\"]" has not yet been initialized` {
   151  		t.Fatal(diags.Err())
   152  	}
   153  
   154  	// "initialize" provider
   155  	mockCtx.ProviderProvider = mockProvider
   156  
   157  	// Function missing (validate)
   158  	mockProvider.GetFunctionsCalled = false
   159  	_, diags = evalContextProviderFunction(mockCtx.Provider, cfg, walkValidate, providerFunc("provider::mockname::missing"), rng)
   160  	if diags.HasErrors() {
   161  		t.Fatal(diags.Err())
   162  	}
   163  	if mockProvider.GetFunctionsCalled {
   164  		t.Fatal("expected GetFunctions NOT to be called since it's not initialized")
   165  	}
   166  
   167  	// Function missing (Non-validate)
   168  	mockProvider.GetFunctionsCalled = false
   169  	_, diags = evalContextProviderFunction(mockCtx.Provider, cfg, walkPlan, providerFunc("provider::mockname::missing"), rng)
   170  	if !diags.HasErrors() {
   171  		t.Fatal("expected unknown function")
   172  	}
   173  	if diags.Err().Error() != `Function not found in provider: Function "missing" was not registered by provider "provider[\"registry.opentofu.org/hashicorp/mock\"]"` {
   174  		t.Fatal(diags.Err())
   175  	}
   176  	if !mockProvider.GetFunctionsCalled {
   177  		t.Fatal("expected GetFunctions to be called")
   178  	}
   179  
   180  	ctx := &hcl.EvalContext{
   181  		Functions: map[string]function.Function{},
   182  		Variables: map[string]cty.Value{
   183  			"unknown_value":   cty.UnknownVal(cty.String),
   184  			"sensitive_value": cty.StringVal("sensitive!").Mark(marks.Sensitive),
   185  		},
   186  	}
   187  
   188  	// Load functions into ctx
   189  	for _, fn := range []string{"echo", "concat", "coalesce", "unknown_param", "error_param"} {
   190  		pf := providerFunc("provider::mockname::" + fn)
   191  		impl, diags := evalContextProviderFunction(mockCtx.Provider, cfg, walkPlan, pf, rng)
   192  		if diags.HasErrors() {
   193  			t.Fatal(diags.Err())
   194  		}
   195  		ctx.Functions[pf.String()] = *impl
   196  	}
   197  	evaluate := func(exprStr string) (cty.Value, hcl.Diagnostics) {
   198  		expr, diags := hclsyntax.ParseExpression([]byte(exprStr), "exprtest", hcl.InitialPos)
   199  		if diags.HasErrors() {
   200  			t.Fatal(diags)
   201  		}
   202  		return expr.Value(ctx)
   203  	}
   204  
   205  	t.Run("echo function", func(t *testing.T) {
   206  		// These are all assumptions that the provider implementation should not have to worry about:
   207  
   208  		t.Log("Checking not enough arguments")
   209  		_, diags := evaluate("provider::mockname::echo()")
   210  		if !strings.Contains(diags.Error(), `Not enough function arguments; Function "provider::mockname::echo" expects 1 argument(s). Missing value for "input"`) {
   211  			t.Error(diags.Error())
   212  		}
   213  
   214  		t.Log("Checking too many arguments")
   215  		_, diags = evaluate(`provider::mockname::echo("1", "2", "3")`)
   216  		if !strings.Contains(diags.Error(), `Too many function arguments; Function "provider::mockname::echo" expects only 1 argument(s)`) {
   217  			t.Error(diags.Error())
   218  		}
   219  
   220  		t.Log("Checking null argument")
   221  		_, diags = evaluate(`provider::mockname::echo(null)`)
   222  		if !strings.Contains(diags.Error(), `Invalid function argument; Invalid value for "input" parameter: argument must not be null`) {
   223  			t.Error(diags.Error())
   224  		}
   225  
   226  		t.Log("Checking unknown argument")
   227  		val, diags := evaluate(`provider::mockname::echo(unknown_value)`)
   228  		if diags.HasErrors() {
   229  			t.Error(diags.Error())
   230  		}
   231  		if !val.RawEquals(cty.UnknownVal(cty.String)) {
   232  			t.Error(val.AsString())
   233  		}
   234  
   235  		// Actually test the function implementation
   236  
   237  		t.Log("Checking valid argument")
   238  
   239  		val, diags = evaluate(`provider::mockname::echo("hello functions!")`)
   240  		if diags.HasErrors() {
   241  			t.Error(diags.Error())
   242  		}
   243  		if !val.RawEquals(cty.StringVal("hello functions!")) {
   244  			t.Error(val.AsString())
   245  		}
   246  
   247  		t.Log("Checking sensitive argument")
   248  
   249  		val, diags = evaluate(`provider::mockname::echo(sensitive_value)`)
   250  		if diags.HasErrors() {
   251  			t.Error(diags.Error())
   252  		}
   253  		if !val.RawEquals(cty.StringVal("sensitive!").Mark(marks.Sensitive)) {
   254  			t.Error(val.AsString())
   255  		}
   256  	})
   257  
   258  	t.Run("concat function", func(t *testing.T) {
   259  		// Make sure varargs are handled properly
   260  
   261  		// Single
   262  		val, diags := evaluate(`provider::mockname::concat("foo")`)
   263  		if diags.HasErrors() {
   264  			t.Error(diags.Error())
   265  		}
   266  		if !val.RawEquals(cty.StringVal("foo")) {
   267  			t.Error(val.AsString())
   268  		}
   269  
   270  		// Multi
   271  		val, diags = evaluate(`provider::mockname::concat("foo", "bar", "baz")`)
   272  		if diags.HasErrors() {
   273  			t.Error(diags.Error())
   274  		}
   275  		if !val.RawEquals(cty.StringVal("foobarbaz")) {
   276  			t.Error(val.AsString())
   277  		}
   278  	})
   279  
   280  	t.Run("coalesce function", func(t *testing.T) {
   281  		val, diags := evaluate(`provider::mockname::coalesce("first", "second")`)
   282  		if diags.HasErrors() {
   283  			t.Error(diags.Error())
   284  		}
   285  		if !val.RawEquals(cty.StringVal("first")) {
   286  			t.Error(val.AsString())
   287  		}
   288  
   289  		val, diags = evaluate(`provider::mockname::coalesce(null, "second")`)
   290  		if diags.HasErrors() {
   291  			t.Error(diags.Error())
   292  		}
   293  		if !val.RawEquals(cty.StringVal("second")) {
   294  			t.Error(val.AsString())
   295  		}
   296  	})
   297  
   298  	t.Run("unknown_param function", func(t *testing.T) {
   299  		val, diags := evaluate(`provider::mockname::unknown_param(unknown_value)`)
   300  		if diags.HasErrors() {
   301  			t.Error(diags.Error())
   302  		}
   303  		if !val.RawEquals(cty.StringVal("knownvalue")) {
   304  			t.Error(val.AsString())
   305  		}
   306  	})
   307  	t.Run("error_param function", func(t *testing.T) {
   308  		_, diags := evaluate(`provider::mockname::error_param("foo")`)
   309  		if !strings.Contains(diags.Error(), `Invalid function argument; Invalid value for "input" parameter: my error text.`) {
   310  			t.Error(diags.Error())
   311  		}
   312  	})
   313  }