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 }