github.com/terraform-linters/tflint@v0.51.2-0.20240520175844-3750771571b6/terraform/lang/functions_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package lang
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"testing"
    12  
    13  	"github.com/hashicorp/hcl/v2"
    14  	"github.com/hashicorp/hcl/v2/hclsyntax"
    15  	homedir "github.com/mitchellh/go-homedir"
    16  	"github.com/terraform-linters/tflint-plugin-sdk/terraform/lang/marks"
    17  	"github.com/zclconf/go-cty/cty"
    18  )
    19  
    20  // TestFunctions tests that functions are callable through the functionality
    21  // in the langs package, via HCL.
    22  //
    23  // These tests are primarily here to assert that the functions are properly
    24  // registered in the functions table, rather than to test all of the details
    25  // of the functions. Each function should only have one or two tests here,
    26  // since the main set of unit tests for a function should live alongside that
    27  // function either in the "funcs" subdirectory here or over in the cty
    28  // function/stdlib package.
    29  //
    30  // One exception to that is we can use this test mechanism to assert common
    31  // patterns that are used in real-world configurations which rely on behaviors
    32  // implemented either in this lang package or in HCL itself, such as automatic
    33  // type conversions. The function unit tests don't cover those things because
    34  // they call directly into the functions.
    35  //
    36  // With that said then, this test function should contain at least one simple
    37  // test case per function registered in the functions table (just to prove
    38  // it really is registered correctly) and possibly a small set of additional
    39  // functions showing real-world use-cases that rely on type conversion
    40  // behaviors.
    41  func TestFunctions(t *testing.T) {
    42  	// used in `pathexpand()` test
    43  	homePath, err := homedir.Dir()
    44  	if err != nil {
    45  		t.Fatalf("Error getting home directory: %v", err)
    46  	}
    47  
    48  	tests := map[string][]struct {
    49  		src  string
    50  		want cty.Value
    51  	}{
    52  		// Please maintain this list in alphabetical order by function, with
    53  		// a blank line between the group of tests for each function.
    54  
    55  		"abs": {
    56  			{
    57  				`abs(-1)`,
    58  				cty.NumberIntVal(1),
    59  			},
    60  		},
    61  
    62  		"abspath": {
    63  			{
    64  				`abspath(".")`,
    65  				cty.StringVal((func() string {
    66  					cwd, err := os.Getwd()
    67  					if err != nil {
    68  						panic(err)
    69  					}
    70  					return filepath.ToSlash(cwd)
    71  				})()),
    72  			},
    73  		},
    74  
    75  		"alltrue": {
    76  			{
    77  				`alltrue(["true", true])`,
    78  				cty.True,
    79  			},
    80  		},
    81  
    82  		"anytrue": {
    83  			{
    84  				`anytrue([])`,
    85  				cty.False,
    86  			},
    87  		},
    88  
    89  		"base64decode": {
    90  			{
    91  				`base64decode("YWJjMTIzIT8kKiYoKSctPUB+")`,
    92  				cty.StringVal("abc123!?$*&()'-=@~"),
    93  			},
    94  		},
    95  
    96  		"base64encode": {
    97  			{
    98  				`base64encode("abc123!?$*&()'-=@~")`,
    99  				cty.StringVal("YWJjMTIzIT8kKiYoKSctPUB+"),
   100  			},
   101  		},
   102  
   103  		"base64gzip": {
   104  			{
   105  				`base64gzip("test")`,
   106  				cty.StringVal("H4sIAAAAAAAA/ypJLS4BAAAA//8BAAD//wx+f9gEAAAA"),
   107  			},
   108  		},
   109  
   110  		"base64sha256": {
   111  			{
   112  				`base64sha256("test")`,
   113  				cty.StringVal("n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="),
   114  			},
   115  		},
   116  
   117  		"base64sha512": {
   118  			{
   119  				`base64sha512("test")`,
   120  				cty.StringVal("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="),
   121  			},
   122  		},
   123  
   124  		"basename": {
   125  			{
   126  				`basename("testdata/hello.txt")`,
   127  				cty.StringVal("hello.txt"),
   128  			},
   129  		},
   130  
   131  		"can": {
   132  			{
   133  				`can(true)`,
   134  				cty.True,
   135  			},
   136  			{
   137  				// Note: "can" only works with expressions that pass static
   138  				// validation, because it only gets an opportunity to run in
   139  				// that case. The following "works" (captures the error) because
   140  				// Terraform understands it as a reference to an attribute
   141  				// that does not exist during dynamic evaluation.
   142  				//
   143  				// "can" doesn't work with references that could never possibly
   144  				// be valid and are thus caught during static validation, such
   145  				// as an expression like "foo" alone which would be understood
   146  				// as an invalid resource reference.
   147  				`can({}.baz)`,
   148  				cty.False,
   149  			},
   150  		},
   151  
   152  		"ceil": {
   153  			{
   154  				`ceil(1.2)`,
   155  				cty.NumberIntVal(2),
   156  			},
   157  		},
   158  
   159  		"chomp": {
   160  			{
   161  				`chomp("goodbye\ncruel\nworld\n")`,
   162  				cty.StringVal("goodbye\ncruel\nworld"),
   163  			},
   164  		},
   165  
   166  		"chunklist": {
   167  			{
   168  				`chunklist(["a", "b", "c"], 1)`,
   169  				cty.ListVal([]cty.Value{
   170  					cty.ListVal([]cty.Value{
   171  						cty.StringVal("a"),
   172  					}),
   173  					cty.ListVal([]cty.Value{
   174  						cty.StringVal("b"),
   175  					}),
   176  					cty.ListVal([]cty.Value{
   177  						cty.StringVal("c"),
   178  					}),
   179  				}),
   180  			},
   181  		},
   182  
   183  		"cidrhost": {
   184  			{
   185  				`cidrhost("192.168.1.0/24", 5)`,
   186  				cty.StringVal("192.168.1.5"),
   187  			},
   188  		},
   189  
   190  		"cidrnetmask": {
   191  			{
   192  				`cidrnetmask("192.168.1.0/24")`,
   193  				cty.StringVal("255.255.255.0"),
   194  			},
   195  		},
   196  
   197  		"cidrsubnet": {
   198  			{
   199  				`cidrsubnet("192.168.2.0/20", 4, 6)`,
   200  				cty.StringVal("192.168.6.0/24"),
   201  			},
   202  		},
   203  
   204  		"cidrsubnets": {
   205  			{
   206  				`cidrsubnets("10.0.0.0/8", 8, 8, 16, 8)`,
   207  				cty.ListVal([]cty.Value{
   208  					cty.StringVal("10.0.0.0/16"),
   209  					cty.StringVal("10.1.0.0/16"),
   210  					cty.StringVal("10.2.0.0/24"),
   211  					cty.StringVal("10.3.0.0/16"),
   212  				}),
   213  			},
   214  		},
   215  
   216  		"coalesce": {
   217  			{
   218  				`coalesce("first", "second", "third")`,
   219  				cty.StringVal("first"),
   220  			},
   221  
   222  			{
   223  				`coalescelist(["first", "second"], ["third", "fourth"])`,
   224  				cty.TupleVal([]cty.Value{
   225  					cty.StringVal("first"), cty.StringVal("second"),
   226  				}),
   227  			},
   228  		},
   229  
   230  		"coalescelist": {
   231  			{
   232  				`coalescelist(tolist(["a", "b"]), tolist(["c", "d"]))`,
   233  				cty.ListVal([]cty.Value{
   234  					cty.StringVal("a"),
   235  					cty.StringVal("b"),
   236  				}),
   237  			},
   238  			{
   239  				`coalescelist(["a", "b"], ["c", "d"])`,
   240  				cty.TupleVal([]cty.Value{
   241  					cty.StringVal("a"),
   242  					cty.StringVal("b"),
   243  				}),
   244  			},
   245  		},
   246  
   247  		"compact": {
   248  			{
   249  				`compact(["test", "", "test"])`,
   250  				cty.ListVal([]cty.Value{
   251  					cty.StringVal("test"), cty.StringVal("test"),
   252  				}),
   253  			},
   254  		},
   255  
   256  		"concat": {
   257  			{
   258  				`concat(["a", ""], ["b", "c"])`,
   259  				cty.TupleVal([]cty.Value{
   260  					cty.StringVal("a"),
   261  					cty.StringVal(""),
   262  					cty.StringVal("b"),
   263  					cty.StringVal("c"),
   264  				}),
   265  			},
   266  		},
   267  
   268  		"contains": {
   269  			{
   270  				`contains(["a", "b"], "a")`,
   271  				cty.True,
   272  			},
   273  			{ // Should also work with sets, due to automatic conversion
   274  				`contains(toset(["a", "b"]), "a")`,
   275  				cty.True,
   276  			},
   277  		},
   278  
   279  		"csvdecode": {
   280  			{
   281  				`csvdecode("a,b,c\n1,2,3\n4,5,6")`,
   282  				cty.ListVal([]cty.Value{
   283  					cty.ObjectVal(map[string]cty.Value{
   284  						"a": cty.StringVal("1"),
   285  						"b": cty.StringVal("2"),
   286  						"c": cty.StringVal("3"),
   287  					}),
   288  					cty.ObjectVal(map[string]cty.Value{
   289  						"a": cty.StringVal("4"),
   290  						"b": cty.StringVal("5"),
   291  						"c": cty.StringVal("6"),
   292  					}),
   293  				}),
   294  			},
   295  		},
   296  
   297  		"dirname": {
   298  			{
   299  				`dirname("testdata/hello.txt")`,
   300  				cty.StringVal("testdata"),
   301  			},
   302  		},
   303  
   304  		"distinct": {
   305  			{
   306  				`distinct(["a", "b", "a", "b"])`,
   307  				cty.ListVal([]cty.Value{
   308  					cty.StringVal("a"), cty.StringVal("b"),
   309  				}),
   310  			},
   311  		},
   312  
   313  		"element": {
   314  			{
   315  				`element(["hello"], 0)`,
   316  				cty.StringVal("hello"),
   317  			},
   318  		},
   319  
   320  		"endswith": {
   321  			{
   322  				`endswith("hello world", "world")`,
   323  				cty.True,
   324  			},
   325  			{
   326  				`endswith("hello world", "hello")`,
   327  				cty.False,
   328  			},
   329  		},
   330  
   331  		"file": {
   332  			{
   333  				`file("hello.txt")`,
   334  				cty.StringVal("hello!"),
   335  			},
   336  		},
   337  
   338  		"fileexists": {
   339  			{
   340  				`fileexists("hello.txt")`,
   341  				cty.BoolVal(true),
   342  			},
   343  		},
   344  
   345  		"fileset": {
   346  			{
   347  				`fileset(".", "*/hello.*")`,
   348  				cty.SetVal([]cty.Value{
   349  					cty.StringVal("subdirectory/hello.tmpl"),
   350  					cty.StringVal("subdirectory/hello.txt"),
   351  				}),
   352  			},
   353  			{
   354  				`fileset(".", "subdirectory/hello.*")`,
   355  				cty.SetVal([]cty.Value{
   356  					cty.StringVal("subdirectory/hello.tmpl"),
   357  					cty.StringVal("subdirectory/hello.txt"),
   358  				}),
   359  			},
   360  			{
   361  				`fileset(".", "hello.*")`,
   362  				cty.SetVal([]cty.Value{
   363  					cty.StringVal("hello.tmpl"),
   364  					cty.StringVal("hello.txt"),
   365  				}),
   366  			},
   367  			{
   368  				`fileset("subdirectory", "hello.*")`,
   369  				cty.SetVal([]cty.Value{
   370  					cty.StringVal("hello.tmpl"),
   371  					cty.StringVal("hello.txt"),
   372  				}),
   373  			},
   374  		},
   375  
   376  		"filebase64": {
   377  			{
   378  				`filebase64("hello.txt")`,
   379  				cty.StringVal("aGVsbG8h"),
   380  			},
   381  		},
   382  
   383  		"filebase64sha256": {
   384  			{
   385  				`filebase64sha256("hello.txt")`,
   386  				cty.StringVal("zgYJL7lI2f+sfRo3bkBLJrdXW8wR7gWkYV/vT+w6MIs="),
   387  			},
   388  		},
   389  
   390  		"filebase64sha512": {
   391  			{
   392  				`filebase64sha512("hello.txt")`,
   393  				cty.StringVal("xvgdsOn4IGyXHJ5YJuO6gj/7saOpAPgEdlKov3jqmP38dFhVo4U6Y1Z1RY620arxIJ6I6tLRkjgrXEy91oUOAg=="),
   394  			},
   395  		},
   396  
   397  		"filemd5": {
   398  			{
   399  				`filemd5("hello.txt")`,
   400  				cty.StringVal("5a8dd3ad0756a93ded72b823b19dd877"),
   401  			},
   402  		},
   403  
   404  		"filesha1": {
   405  			{
   406  				`filesha1("hello.txt")`,
   407  				cty.StringVal("8f7d88e901a5ad3a05d8cc0de93313fd76028f8c"),
   408  			},
   409  		},
   410  
   411  		"filesha256": {
   412  			{
   413  				`filesha256("hello.txt")`,
   414  				cty.StringVal("ce06092fb948d9ffac7d1a376e404b26b7575bcc11ee05a4615fef4fec3a308b"),
   415  			},
   416  		},
   417  
   418  		"filesha512": {
   419  			{
   420  				`filesha512("hello.txt")`,
   421  				cty.StringVal("c6f81db0e9f8206c971c9e5826e3ba823ffbb1a3a900f8047652a8bf78ea98fdfc745855a3853a635675458eb6d1aaf1209e88ead2d192382b5c4cbdd6850e02"),
   422  			},
   423  		},
   424  
   425  		"flatten": {
   426  			{
   427  				`flatten([["a", "b"], ["c", "d"]])`,
   428  				cty.TupleVal([]cty.Value{
   429  					cty.StringVal("a"),
   430  					cty.StringVal("b"),
   431  					cty.StringVal("c"),
   432  					cty.StringVal("d"),
   433  				}),
   434  			},
   435  		},
   436  
   437  		"floor": {
   438  			{
   439  				`floor(-1.8)`,
   440  				cty.NumberFloatVal(-2),
   441  			},
   442  		},
   443  
   444  		"format": {
   445  			{
   446  				`format("Hello, %s!", "Ander")`,
   447  				cty.StringVal("Hello, Ander!"),
   448  			},
   449  		},
   450  
   451  		"formatlist": {
   452  			{
   453  				`formatlist("Hello, %s!", ["Valentina", "Ander", "Olivia", "Sam"])`,
   454  				cty.ListVal([]cty.Value{
   455  					cty.StringVal("Hello, Valentina!"),
   456  					cty.StringVal("Hello, Ander!"),
   457  					cty.StringVal("Hello, Olivia!"),
   458  					cty.StringVal("Hello, Sam!"),
   459  				}),
   460  			},
   461  		},
   462  
   463  		"formatdate": {
   464  			{
   465  				`formatdate("DD MMM YYYY hh:mm ZZZ", "2018-01-04T23:12:01Z")`,
   466  				cty.StringVal("04 Jan 2018 23:12 UTC"),
   467  			},
   468  		},
   469  
   470  		"indent": {
   471  			{
   472  				fmt.Sprintf("indent(4, %#v)", Poem),
   473  				cty.StringVal("Fleas:\n    Adam\n    Had'em\n    \n    E.E. Cummings"),
   474  			},
   475  		},
   476  
   477  		"index": {
   478  			{
   479  				`index(["a", "b", "c"], "a")`,
   480  				cty.NumberIntVal(0),
   481  			},
   482  		},
   483  
   484  		"issensitive": {
   485  			{
   486  				`issensitive(1)`,
   487  				cty.False,
   488  			},
   489  		},
   490  
   491  		"join": {
   492  			{
   493  				`join(" ", ["Hello", "World"])`,
   494  				cty.StringVal("Hello World"),
   495  			},
   496  		},
   497  
   498  		"jsondecode": {
   499  			{
   500  				`jsondecode("{\"hello\": \"world\"}")`,
   501  				cty.ObjectVal(map[string]cty.Value{
   502  					"hello": cty.StringVal("world"),
   503  				}),
   504  			},
   505  		},
   506  
   507  		"jsonencode": {
   508  			{
   509  				`jsonencode({"hello"="world"})`,
   510  				cty.StringVal("{\"hello\":\"world\"}"),
   511  			},
   512  			// We are intentionally choosing to escape <, >, and & characters
   513  			// to preserve backwards compatibility with Terraform 0.11
   514  			{
   515  				`jsonencode({"hello"="<cats & kittens>"})`,
   516  				cty.StringVal("{\"hello\":\"\\u003ccats \\u0026 kittens\\u003e\"}"),
   517  			},
   518  		},
   519  
   520  		"keys": {
   521  			{
   522  				`keys({"hello"=1, "goodbye"=42})`,
   523  				cty.TupleVal([]cty.Value{
   524  					cty.StringVal("goodbye"),
   525  					cty.StringVal("hello"),
   526  				}),
   527  			},
   528  		},
   529  
   530  		"length": {
   531  			{
   532  				`length(["the", "quick", "brown", "bear"])`,
   533  				cty.NumberIntVal(4),
   534  			},
   535  		},
   536  
   537  		"list": {
   538  			// There are intentionally no test cases for "list" because
   539  			// it is a stub that always returns an error.
   540  		},
   541  
   542  		"log": {
   543  			{
   544  				`log(1, 10)`,
   545  				cty.NumberFloatVal(0),
   546  			},
   547  		},
   548  
   549  		"lookup": {
   550  			{
   551  				`lookup({hello=1, goodbye=42}, "goodbye")`,
   552  				cty.NumberIntVal(42),
   553  			},
   554  		},
   555  
   556  		"lower": {
   557  			{
   558  				`lower("HELLO")`,
   559  				cty.StringVal("hello"),
   560  			},
   561  		},
   562  
   563  		"map": {
   564  			// There are intentionally no test cases for "map" because
   565  			// it is a stub that always returns an error.
   566  		},
   567  
   568  		"matchkeys": {
   569  			{
   570  				`matchkeys(["a", "b", "c"], ["ref1", "ref2", "ref3"], ["ref1"])`,
   571  				cty.ListVal([]cty.Value{
   572  					cty.StringVal("a"),
   573  				}),
   574  			},
   575  			{ // mixing types in searchset
   576  				`matchkeys(["a", "b", "c"], [1, 2, 3], [1, "3"])`,
   577  				cty.ListVal([]cty.Value{
   578  					cty.StringVal("a"),
   579  					cty.StringVal("c"),
   580  				}),
   581  			},
   582  		},
   583  
   584  		"max": {
   585  			{
   586  				`max(12, 54, 3)`,
   587  				cty.NumberIntVal(54),
   588  			},
   589  		},
   590  
   591  		"md5": {
   592  			{
   593  				`md5("tada")`,
   594  				cty.StringVal("ce47d07243bb6eaf5e1322c81baf9bbf"),
   595  			},
   596  		},
   597  
   598  		"merge": {
   599  			{
   600  				`merge({"a"="b"}, {"c"="d"})`,
   601  				cty.ObjectVal(map[string]cty.Value{
   602  					"a": cty.StringVal("b"),
   603  					"c": cty.StringVal("d"),
   604  				}),
   605  			},
   606  		},
   607  
   608  		"min": {
   609  			{
   610  				`min(12, 54, 3)`,
   611  				cty.NumberIntVal(3),
   612  			},
   613  		},
   614  
   615  		"nonsensitive": {
   616  			{
   617  				// Due to how this test is set up we have no way to get
   618  				// a sensitive value other than to generate one with
   619  				// another function, so this is a bit odd but does still
   620  				// meet the goal of verifying that the "nonsensitive"
   621  				// function is correctly registered.
   622  				`nonsensitive(sensitive(1))`,
   623  				cty.NumberIntVal(1),
   624  			},
   625  		},
   626  
   627  		"one": {
   628  			{
   629  				`one([])`,
   630  				cty.NullVal(cty.DynamicPseudoType),
   631  			},
   632  			{
   633  				`one([true])`,
   634  				cty.True,
   635  			},
   636  		},
   637  
   638  		"parseint": {
   639  			{
   640  				`parseint("100", 10)`,
   641  				cty.NumberIntVal(100),
   642  			},
   643  		},
   644  
   645  		"pathexpand": {
   646  			{
   647  				`pathexpand("~/test-file")`,
   648  				cty.StringVal(filepath.Join(homePath, "test-file")),
   649  			},
   650  		},
   651  
   652  		"plantimestamp": {
   653  			{
   654  				`plantimestamp()`,
   655  				cty.UnknownVal(cty.String),
   656  			},
   657  		},
   658  
   659  		"pow": {
   660  			{
   661  				`pow(1,0)`,
   662  				cty.NumberFloatVal(1),
   663  			},
   664  		},
   665  
   666  		"range": {
   667  			{
   668  				`range(3)`,
   669  				cty.ListVal([]cty.Value{
   670  					cty.NumberIntVal(0),
   671  					cty.NumberIntVal(1),
   672  					cty.NumberIntVal(2),
   673  				}),
   674  			},
   675  			{
   676  				`range(1, 4)`,
   677  				cty.ListVal([]cty.Value{
   678  					cty.NumberIntVal(1),
   679  					cty.NumberIntVal(2),
   680  					cty.NumberIntVal(3),
   681  				}),
   682  			},
   683  			{
   684  				`range(1, 8, 2)`,
   685  				cty.ListVal([]cty.Value{
   686  					cty.NumberIntVal(1),
   687  					cty.NumberIntVal(3),
   688  					cty.NumberIntVal(5),
   689  					cty.NumberIntVal(7),
   690  				}),
   691  			},
   692  		},
   693  
   694  		"regex": {
   695  			{
   696  				`regex("(\\d+)([a-z]+)", "aaa111bbb222")`,
   697  				cty.TupleVal([]cty.Value{cty.StringVal("111"), cty.StringVal("bbb")}),
   698  			},
   699  		},
   700  
   701  		"regexall": {
   702  			{
   703  				`regexall("(\\d+)([a-z]+)", "...111aaa222bbb...")`,
   704  				cty.ListVal([]cty.Value{
   705  					cty.TupleVal([]cty.Value{cty.StringVal("111"), cty.StringVal("aaa")}),
   706  					cty.TupleVal([]cty.Value{cty.StringVal("222"), cty.StringVal("bbb")}),
   707  				}),
   708  			},
   709  		},
   710  
   711  		"replace": {
   712  			{
   713  				`replace("hello", "hel", "bel")`,
   714  				cty.StringVal("bello"),
   715  			},
   716  		},
   717  
   718  		"reverse": {
   719  			{
   720  				`reverse(["a", true, 0])`,
   721  				cty.TupleVal([]cty.Value{cty.Zero, cty.True, cty.StringVal("a")}),
   722  			},
   723  		},
   724  
   725  		"rsadecrypt": {
   726  			{
   727  				fmt.Sprintf("rsadecrypt(%#v, %#v)", CipherBase64, PrivateKey),
   728  				cty.StringVal("message"),
   729  			},
   730  		},
   731  
   732  		"sensitive": {
   733  			{
   734  				`sensitive(1)`,
   735  				cty.NumberIntVal(1).Mark(marks.Sensitive),
   736  			},
   737  		},
   738  
   739  		"setintersection": {
   740  			{
   741  				`setintersection(["a", "b"], ["b", "c"], ["b", "d"])`,
   742  				cty.SetVal([]cty.Value{
   743  					cty.StringVal("b"),
   744  				}),
   745  			},
   746  		},
   747  
   748  		"setproduct": {
   749  			{
   750  				`setproduct(["development", "staging", "production"], ["app1", "app2"])`,
   751  				cty.ListVal([]cty.Value{
   752  					cty.TupleVal([]cty.Value{cty.StringVal("development"), cty.StringVal("app1")}),
   753  					cty.TupleVal([]cty.Value{cty.StringVal("development"), cty.StringVal("app2")}),
   754  					cty.TupleVal([]cty.Value{cty.StringVal("staging"), cty.StringVal("app1")}),
   755  					cty.TupleVal([]cty.Value{cty.StringVal("staging"), cty.StringVal("app2")}),
   756  					cty.TupleVal([]cty.Value{cty.StringVal("production"), cty.StringVal("app1")}),
   757  					cty.TupleVal([]cty.Value{cty.StringVal("production"), cty.StringVal("app2")}),
   758  				}),
   759  			},
   760  		},
   761  
   762  		"setsubtract": {
   763  			{
   764  				`setsubtract(["a", "b", "c"], ["a", "c"])`,
   765  				cty.SetVal([]cty.Value{
   766  					cty.StringVal("b"),
   767  				}),
   768  			},
   769  		},
   770  
   771  		"setunion": {
   772  			{
   773  				`setunion(["a", "b"], ["b", "c"], ["d"])`,
   774  				cty.SetVal([]cty.Value{
   775  					cty.StringVal("d"),
   776  					cty.StringVal("b"),
   777  					cty.StringVal("a"),
   778  					cty.StringVal("c"),
   779  				}),
   780  			},
   781  		},
   782  
   783  		"sha1": {
   784  			{
   785  				`sha1("test")`,
   786  				cty.StringVal("a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"),
   787  			},
   788  		},
   789  
   790  		"sha256": {
   791  			{
   792  				`sha256("test")`,
   793  				cty.StringVal("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"),
   794  			},
   795  		},
   796  
   797  		"sha512": {
   798  			{
   799  				`sha512("test")`,
   800  				cty.StringVal("ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff"),
   801  			},
   802  		},
   803  
   804  		"signum": {
   805  			{
   806  				`signum(12)`,
   807  				cty.NumberFloatVal(1),
   808  			},
   809  		},
   810  
   811  		"slice": {
   812  			{
   813  				// force a list type here for testing
   814  				`slice(tolist(["a", "b", "c", "d"]), 1, 3)`,
   815  				cty.ListVal([]cty.Value{
   816  					cty.StringVal("b"), cty.StringVal("c"),
   817  				}),
   818  			},
   819  			{
   820  				`slice(["a", "b", 3, 4], 1, 3)`,
   821  				cty.TupleVal([]cty.Value{
   822  					cty.StringVal("b"), cty.NumberIntVal(3),
   823  				}),
   824  			},
   825  		},
   826  
   827  		"sort": {
   828  			{
   829  				`sort(["banana", "apple"])`,
   830  				cty.ListVal([]cty.Value{
   831  					cty.StringVal("apple"),
   832  					cty.StringVal("banana"),
   833  				}),
   834  			},
   835  		},
   836  
   837  		"split": {
   838  			{
   839  				`split(" ", "Hello World")`,
   840  				cty.ListVal([]cty.Value{
   841  					cty.StringVal("Hello"),
   842  					cty.StringVal("World"),
   843  				}),
   844  			},
   845  		},
   846  
   847  		"startswith": {
   848  			{
   849  				`startswith("hello world", "hello")`,
   850  				cty.True,
   851  			},
   852  			{
   853  				`startswith("hello world", "world")`,
   854  				cty.False,
   855  			},
   856  		},
   857  
   858  		"strcontains": {
   859  			{
   860  				`strcontains("hello", "llo")`,
   861  				cty.BoolVal(true),
   862  			},
   863  			{
   864  				`strcontains("hello", "a")`,
   865  				cty.BoolVal(false),
   866  			},
   867  		},
   868  
   869  		"strrev": {
   870  			{
   871  				`strrev("hello world")`,
   872  				cty.StringVal("dlrow olleh"),
   873  			},
   874  		},
   875  
   876  		"substr": {
   877  			{
   878  				`substr("hello world", 1, 4)`,
   879  				cty.StringVal("ello"),
   880  			},
   881  		},
   882  
   883  		"sum": {
   884  			{
   885  				`sum([2340.5,10,3])`,
   886  				cty.NumberFloatVal(2353.5),
   887  			},
   888  		},
   889  
   890  		"textdecodebase64": {
   891  			{
   892  				`textdecodebase64("dABlAHMAdAA=", "UTF-16LE")`,
   893  				cty.StringVal("test"),
   894  			},
   895  		},
   896  
   897  		"textencodebase64": {
   898  			{
   899  				`textencodebase64("test", "UTF-16LE")`,
   900  				cty.StringVal("dABlAHMAdAA="),
   901  			},
   902  		},
   903  
   904  		"templatefile": {
   905  			{
   906  				`templatefile("hello.tmpl", {name = "Jodie"})`,
   907  				cty.StringVal("Hello, Jodie!"),
   908  			},
   909  			{
   910  				`core::templatefile("hello.tmpl", {name = "Namespaced Jodie"})`,
   911  				cty.StringVal("Hello, Namespaced Jodie!"),
   912  			},
   913  		},
   914  
   915  		"timeadd": {
   916  			{
   917  				`timeadd("2017-11-22T00:00:00Z", "1s")`,
   918  				cty.StringVal("2017-11-22T00:00:01Z"),
   919  			},
   920  		},
   921  
   922  		"timecmp": {
   923  			{
   924  				`timecmp("2017-11-22T00:00:00Z", "2017-11-22T00:00:00Z")`,
   925  				cty.Zero,
   926  			},
   927  		},
   928  
   929  		"title": {
   930  			{
   931  				`title("hello")`,
   932  				cty.StringVal("Hello"),
   933  			},
   934  		},
   935  
   936  		"tobool": {
   937  			{
   938  				`tobool("false")`,
   939  				cty.False,
   940  			},
   941  		},
   942  
   943  		"tolist": {
   944  			{
   945  				`tolist(["a", "b", "c"])`,
   946  				cty.ListVal([]cty.Value{
   947  					cty.StringVal("a"), cty.StringVal("b"), cty.StringVal("c"),
   948  				}),
   949  			},
   950  		},
   951  
   952  		"tomap": {
   953  			{
   954  				`tomap({"a" = 1, "b" = 2})`,
   955  				cty.MapVal(map[string]cty.Value{
   956  					"a": cty.NumberIntVal(1),
   957  					"b": cty.NumberIntVal(2),
   958  				}),
   959  			},
   960  		},
   961  
   962  		"tonumber": {
   963  			{
   964  				`tonumber("42")`,
   965  				cty.NumberIntVal(42),
   966  			},
   967  		},
   968  
   969  		"toset": {
   970  			{
   971  				`toset(["a", "b", "c"])`,
   972  				cty.SetVal([]cty.Value{
   973  					cty.StringVal("a"), cty.StringVal("b"), cty.StringVal("c"),
   974  				}),
   975  			},
   976  		},
   977  
   978  		"tostring": {
   979  			{
   980  				`tostring("a")`,
   981  				cty.StringVal("a"),
   982  			},
   983  		},
   984  
   985  		"transpose": {
   986  			{
   987  				`transpose({"a" = ["1", "2"], "b" = ["2", "3"]})`,
   988  				cty.MapVal(map[string]cty.Value{
   989  					"1": cty.ListVal([]cty.Value{cty.StringVal("a")}),
   990  					"2": cty.ListVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")}),
   991  					"3": cty.ListVal([]cty.Value{cty.StringVal("b")}),
   992  				}),
   993  			},
   994  		},
   995  
   996  		"trim": {
   997  			{
   998  				`trim("?!hello?!", "!?")`,
   999  				cty.StringVal("hello"),
  1000  			},
  1001  		},
  1002  
  1003  		"trimprefix": {
  1004  			{
  1005  				`trimprefix("helloworld", "hello")`,
  1006  				cty.StringVal("world"),
  1007  			},
  1008  		},
  1009  
  1010  		"trimspace": {
  1011  			{
  1012  				`trimspace(" hello ")`,
  1013  				cty.StringVal("hello"),
  1014  			},
  1015  		},
  1016  
  1017  		"trimsuffix": {
  1018  			{
  1019  				`trimsuffix("helloworld", "world")`,
  1020  				cty.StringVal("hello"),
  1021  			},
  1022  		},
  1023  
  1024  		"try": {
  1025  			{
  1026  				// Note: "try" only works with expressions that pass static
  1027  				// validation, because it only gets an opportunity to run in
  1028  				// that case. The following "works" (captures the error) because
  1029  				// Terraform understands it as a reference to an attribute
  1030  				// that does not exist during dynamic evaluation.
  1031  				//
  1032  				// "try" doesn't work with references that could never possibly
  1033  				// be valid and are thus caught during static validation, such
  1034  				// as an expression like "foo" alone which would be understood
  1035  				// as an invalid resource reference. That's okay because this
  1036  				// function exists primarily to ease access to dynamically-typed
  1037  				// structures that Terraform can't statically validate by
  1038  				// definition.
  1039  				`try({}.baz, "fallback")`,
  1040  				cty.StringVal("fallback"),
  1041  			},
  1042  			{
  1043  				`try("fallback")`,
  1044  				cty.StringVal("fallback"),
  1045  			},
  1046  		},
  1047  
  1048  		"upper": {
  1049  			{
  1050  				`upper("hello")`,
  1051  				cty.StringVal("HELLO"),
  1052  			},
  1053  			{
  1054  				`core::upper("hello")`,
  1055  				cty.StringVal("HELLO"),
  1056  			},
  1057  		},
  1058  
  1059  		"urlencode": {
  1060  			{
  1061  				`urlencode("foo:bar@localhost?foo=bar&bar=baz")`,
  1062  				cty.StringVal("foo%3Abar%40localhost%3Ffoo%3Dbar%26bar%3Dbaz"),
  1063  			},
  1064  		},
  1065  
  1066  		"uuidv5": {
  1067  			{
  1068  				`uuidv5("dns", "tada")`,
  1069  				cty.StringVal("faa898db-9b9d-5b75-86a9-149e7bb8e3b8"),
  1070  			},
  1071  			{
  1072  				`uuidv5("url", "tada")`,
  1073  				cty.StringVal("2c1ff6b4-211f-577e-94de-d978b0caa16e"),
  1074  			},
  1075  			{
  1076  				`uuidv5("oid", "tada")`,
  1077  				cty.StringVal("61eeea26-5176-5288-87fc-232d6ed30d2f"),
  1078  			},
  1079  			{
  1080  				`uuidv5("x500", "tada")`,
  1081  				cty.StringVal("7e12415e-f7c9-57c3-9e43-52dc9950d264"),
  1082  			},
  1083  			{
  1084  				`uuidv5("6ba7b810-9dad-11d1-80b4-00c04fd430c8", "tada")`,
  1085  				cty.StringVal("faa898db-9b9d-5b75-86a9-149e7bb8e3b8"),
  1086  			},
  1087  		},
  1088  
  1089  		"values": {
  1090  			{
  1091  				`values({"hello"="world", "what's"="up"})`,
  1092  				cty.TupleVal([]cty.Value{
  1093  					cty.StringVal("world"),
  1094  					cty.StringVal("up"),
  1095  				}),
  1096  			},
  1097  		},
  1098  
  1099  		"yamldecode": {
  1100  			{
  1101  				`yamldecode("true")`,
  1102  				cty.True,
  1103  			},
  1104  			{
  1105  				`yamldecode("key: 0ba")`,
  1106  				cty.ObjectVal(map[string]cty.Value{
  1107  					"key": cty.StringVal("0ba"),
  1108  				}),
  1109  			},
  1110  		},
  1111  
  1112  		"yamlencode": {
  1113  			{
  1114  				`yamlencode(["foo", "bar", true])`,
  1115  				cty.StringVal("- \"foo\"\n- \"bar\"\n- true\n"),
  1116  			},
  1117  			{
  1118  				`yamlencode({a = "b", c = "d"})`,
  1119  				cty.StringVal("\"a\": \"b\"\n\"c\": \"d\"\n"),
  1120  			},
  1121  			{
  1122  				`yamlencode(true)`,
  1123  				// the ... here is an "end of document" marker, produced for implied primitive types only
  1124  				cty.StringVal("true\n...\n"),
  1125  			},
  1126  		},
  1127  
  1128  		"zipmap": {
  1129  			{
  1130  				`zipmap(["hello", "bar"], ["world", "baz"])`,
  1131  				cty.ObjectVal(map[string]cty.Value{
  1132  					"hello": cty.StringVal("world"),
  1133  					"bar":   cty.StringVal("baz"),
  1134  				}),
  1135  			},
  1136  		},
  1137  	}
  1138  
  1139  	t.Run("all functions are tested", func(t *testing.T) {
  1140  		data := &dataForTests{} // no variables available; we only need literals here
  1141  		scope := &Scope{
  1142  			Data:    data,
  1143  			BaseDir: "./testdata/functions-test", // for the functions that read from the filesystem
  1144  		}
  1145  
  1146  		// Check that there is at least one test case for each function, omitting
  1147  		// those functions that do not return consistent values
  1148  		allFunctions := scope.Functions()
  1149  
  1150  		// TODO: we can test the impure functions partially by configuring the scope
  1151  		// with PureOnly: true and then verify that they return unknown values of a
  1152  		// suitable type.
  1153  		for _, impureFunc := range impureFunctions {
  1154  			delete(allFunctions, impureFunc)
  1155  		}
  1156  		for f := range scope.Functions() {
  1157  			if strings.Contains(f, "::") {
  1158  				// Only non-namespaced functions are absolutely required to
  1159  				// have at least one test. (Others _may_ have tests.)
  1160  				continue
  1161  			}
  1162  			if _, ok := tests[f]; !ok {
  1163  				t.Errorf("Missing test for function %s\n", f)
  1164  			}
  1165  		}
  1166  	})
  1167  
  1168  	for funcName, funcTests := range tests {
  1169  		t.Run(funcName, func(t *testing.T) {
  1170  			for _, test := range funcTests {
  1171  				t.Run(test.src, func(t *testing.T) {
  1172  					data := &dataForTests{} // no variables available; we only need literals here
  1173  					scope := &Scope{
  1174  						Data:    data,
  1175  						BaseDir: "./testdata/functions-test", // for the functions that read from the filesystem
  1176  					}
  1177  
  1178  					expr, parseDiags := hclsyntax.ParseExpression([]byte(test.src), "test.hcl", hcl.Pos{Line: 1, Column: 1})
  1179  					if parseDiags.HasErrors() {
  1180  						for _, diag := range parseDiags {
  1181  							t.Error(diag.Error())
  1182  						}
  1183  						return
  1184  					}
  1185  
  1186  					got, diags := scope.EvalExpr(expr, cty.DynamicPseudoType)
  1187  					if diags.HasErrors() {
  1188  						for _, diag := range diags {
  1189  							t.Errorf("%s: %s", diag.Summary, diag.Detail)
  1190  						}
  1191  						return
  1192  					}
  1193  
  1194  					if !test.want.RawEquals(got) {
  1195  						t.Errorf("wrong result\nexpr: %s\ngot:  %#v\nwant: %#v", test.src, got, test.want)
  1196  					}
  1197  				})
  1198  			}
  1199  		})
  1200  	}
  1201  }
  1202  
  1203  const (
  1204  	CipherBase64 = "eczGaDhXDbOFRZGhjx2etVzWbRqWDlmq0bvNt284JHVbwCgObiuyX9uV0LSAMY707IEgMkExJqXmsB4OWKxvB7epRB9G/3+F+pcrQpODlDuL9oDUAsa65zEpYF0Wbn7Oh7nrMQncyUPpyr9WUlALl0gRWytOA23S+y5joa4M34KFpawFgoqTu/2EEH4Xl1zo+0fy73fEto+nfkUY+meuyGZ1nUx/+DljP7ZqxHBFSlLODmtuTMdswUbHbXbWneW51D7Jm7xB8nSdiA2JQNK5+Sg5x8aNfgvFTt/m2w2+qpsyFa5Wjeu6fZmXSl840CA07aXbk9vN4I81WmJyblD/ZA=="
  1205  	PrivateKey   = `
  1206  -----BEGIN RSA PRIVATE KEY-----
  1207  MIIEowIBAAKCAQEAgUElV5mwqkloIrM8ZNZ72gSCcnSJt7+/Usa5G+D15YQUAdf9
  1208  c1zEekTfHgDP+04nw/uFNFaE5v1RbHaPxhZYVg5ZErNCa/hzn+x10xzcepeS3KPV
  1209  Xcxae4MR0BEegvqZqJzN9loXsNL/c3H/B+2Gle3hTxjlWFb3F5qLgR+4Mf4ruhER
  1210  1v6eHQa/nchi03MBpT4UeJ7MrL92hTJYLdpSyCqmr8yjxkKJDVC2uRrr+sTSxfh7
  1211  r6v24u/vp/QTmBIAlNPgadVAZw17iNNb7vjV7Gwl/5gHXonCUKURaV++dBNLrHIZ
  1212  pqcAM8wHRph8mD1EfL9hsz77pHewxolBATV+7QIDAQABAoIBAC1rK+kFW3vrAYm3
  1213  +8/fQnQQw5nec4o6+crng6JVQXLeH32qXShNf8kLLG/Jj0vaYcTPPDZw9JCKkTMQ
  1214  0mKj9XR/5DLbBMsV6eNXXuvJJ3x4iKW5eD9WkLD4FKlNarBRyO7j8sfPTqXW7uat
  1215  NxWdFH7YsSRvNh/9pyQHLWA5OituidMrYbc3EUx8B1GPNyJ9W8Q8znNYLfwYOjU4
  1216  Wv1SLE6qGQQH9Q0WzA2WUf8jklCYyMYTIywAjGb8kbAJlKhmj2t2Igjmqtwt1PYc
  1217  pGlqbtQBDUiWXt5S4YX/1maIQ/49yeNUajjpbJiH3DbhJbHwFTzP3pZ9P9GHOzlG
  1218  kYR+wSECgYEAw/Xida8kSv8n86V3qSY/I+fYQ5V+jDtXIE+JhRnS8xzbOzz3v0WS
  1219  Oo5H+o4nJx5eL3Ghb3Gcm0Jn46dHrxinHbm+3RjXv/X6tlbxIYjRSQfHOTSMCTvd
  1220  qcliF5vC6RCLXuc7R+IWR1Ky6eDEZGtrvt3DyeYABsp9fRUFR/6NluUCgYEAqNsw
  1221  1aSl7WJa27F0DoJdlU9LWerpXcazlJcIdOz/S9QDmSK3RDQTdqfTxRmrxiYI9LEs
  1222  mkOkvzlnnOBMpnZ3ZOU5qIRfprecRIi37KDAOHWGnlC0EWGgl46YLb7/jXiWf0AG
  1223  Y+DfJJNd9i6TbIDWu8254/erAS6bKMhW/3q7f2kCgYAZ7Id/BiKJAWRpqTRBXlvw
  1224  BhXoKvjI2HjYP21z/EyZ+PFPzur/lNaZhIUlMnUfibbwE9pFggQzzf8scM7c7Sf+
  1225  mLoVSdoQ/Rujz7CqvQzi2nKSsM7t0curUIb3lJWee5/UeEaxZcmIufoNUrzohAWH
  1226  BJOIPDM4ssUTLRq7wYM9uQKBgHCBau5OP8gE6mjKuXsZXWUoahpFLKwwwmJUp2vQ
  1227  pOFPJ/6WZOlqkTVT6QPAcPUbTohKrF80hsZqZyDdSfT3peFx4ZLocBrS56m6NmHR
  1228  UYHMvJ8rQm76T1fryHVidz85g3zRmfBeWg8yqT5oFg4LYgfLsPm1gRjOhs8LfPvI
  1229  OLlRAoGBAIZ5Uv4Z3s8O7WKXXUe/lq6j7vfiVkR1NW/Z/WLKXZpnmvJ7FgxN4e56
  1230  RXT7GwNQHIY8eDjDnsHxzrxd+raOxOZeKcMHj3XyjCX3NHfTscnsBPAGYpY/Wxzh
  1231  T8UYnFu6RzkixElTf2rseEav7rkdKkI3LAeIZy7B0HulKKsmqVQ7
  1232  -----END RSA PRIVATE KEY-----
  1233  `
  1234  	Poem = `Fleas:
  1235  Adam
  1236  Had'em
  1237  
  1238  E.E. Cummings`
  1239  )
  1240  
  1241  func TestNewMockFunction(t *testing.T) {
  1242  	tests := []struct {
  1243  		name string
  1244  		call *FunctionCall
  1245  		args []cty.Value
  1246  	}{
  1247  		{
  1248  			name: "no args",
  1249  			call: &FunctionCall{
  1250  				Name:      "foo",
  1251  				ArgsCount: 0,
  1252  			},
  1253  			args: []cty.Value{},
  1254  		},
  1255  		{
  1256  			name: "single arg",
  1257  			call: &FunctionCall{
  1258  				Name:      "bar",
  1259  				ArgsCount: 1,
  1260  			},
  1261  			args: []cty.Value{cty.StringVal("hello")},
  1262  		},
  1263  		{
  1264  			name: "multiple args",
  1265  			call: &FunctionCall{
  1266  				Name:      "baz",
  1267  				ArgsCount: 2,
  1268  			},
  1269  			args: []cty.Value{cty.BoolVal(false), cty.NumberIntVal(1)},
  1270  		},
  1271  		{
  1272  			name: "null arg",
  1273  			call: &FunctionCall{
  1274  				Name:      "null",
  1275  				ArgsCount: 1,
  1276  			},
  1277  			args: []cty.Value{cty.NullVal(cty.String)},
  1278  		},
  1279  		{
  1280  			name: "unknown arg",
  1281  			call: &FunctionCall{
  1282  				Name:      "unknown",
  1283  				ArgsCount: 1,
  1284  			},
  1285  			args: []cty.Value{cty.UnknownVal(cty.Number)},
  1286  		},
  1287  		{
  1288  			name: "dynamic value arg",
  1289  			call: &FunctionCall{
  1290  				Name:      "dynamic",
  1291  				ArgsCount: 1,
  1292  			},
  1293  			args: []cty.Value{cty.DynamicVal},
  1294  		},
  1295  		{
  1296  			name: "marked value arg",
  1297  			call: &FunctionCall{
  1298  				Name:      "marked",
  1299  				ArgsCount: 1,
  1300  			},
  1301  			args: []cty.Value{cty.StringVal("marked").Mark(marks.Sensitive)},
  1302  		},
  1303  	}
  1304  
  1305  	for _, test := range tests {
  1306  		t.Run(test.name, func(t *testing.T) {
  1307  			fn := NewMockFunction(test.call)
  1308  
  1309  			got, err := fn.Call(test.args)
  1310  			if err != nil {
  1311  				t.Fatal(err)
  1312  			}
  1313  
  1314  			if !got.RawEquals(cty.DynamicVal) {
  1315  				t.Errorf("want: cty.DynamicVal, got: %s", got.GoString())
  1316  			}
  1317  		})
  1318  	}
  1319  }