github.com/opentofu/opentofu@v1.7.1/internal/lang/funcs/string_test.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package funcs
     7  
     8  import (
     9  	"fmt"
    10  	"testing"
    11  
    12  	"github.com/opentofu/opentofu/internal/lang/marks"
    13  	"github.com/zclconf/go-cty/cty"
    14  	"github.com/zclconf/go-cty/cty/function"
    15  )
    16  
    17  func TestReplace(t *testing.T) {
    18  	tests := []struct {
    19  		String  cty.Value
    20  		Substr  cty.Value
    21  		Replace cty.Value
    22  		Want    cty.Value
    23  		Err     bool
    24  	}{
    25  		{ // Regular search and replace
    26  			cty.StringVal("hello"),
    27  			cty.StringVal("hel"),
    28  			cty.StringVal("bel"),
    29  			cty.StringVal("bello"),
    30  			false,
    31  		},
    32  		{ // Search string doesn't match
    33  			cty.StringVal("hello"),
    34  			cty.StringVal("nope"),
    35  			cty.StringVal("bel"),
    36  			cty.StringVal("hello"),
    37  			false,
    38  		},
    39  		{ // Regular expression
    40  			cty.StringVal("hello"),
    41  			cty.StringVal("/l/"),
    42  			cty.StringVal("L"),
    43  			cty.StringVal("heLLo"),
    44  			false,
    45  		},
    46  		{
    47  			cty.StringVal("helo"),
    48  			cty.StringVal("/(l)/"),
    49  			cty.StringVal("$1$1"),
    50  			cty.StringVal("hello"),
    51  			false,
    52  		},
    53  		{ // Bad regexp
    54  			cty.StringVal("hello"),
    55  			cty.StringVal("/(l/"),
    56  			cty.StringVal("$1$1"),
    57  			cty.UnknownVal(cty.String),
    58  			true,
    59  		},
    60  	}
    61  
    62  	for _, test := range tests {
    63  		t.Run(fmt.Sprintf("replace(%#v, %#v, %#v)", test.String, test.Substr, test.Replace), func(t *testing.T) {
    64  			got, err := Replace(test.String, test.Substr, test.Replace)
    65  
    66  			if test.Err {
    67  				if err == nil {
    68  					t.Fatal("succeeded; want error")
    69  				}
    70  				return
    71  			} else if err != nil {
    72  				t.Fatalf("unexpected error: %s", err)
    73  			}
    74  
    75  			if !got.RawEquals(test.Want) {
    76  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
    77  			}
    78  		})
    79  	}
    80  }
    81  
    82  func TestStrContains(t *testing.T) {
    83  	tests := []struct {
    84  		String cty.Value
    85  		Substr cty.Value
    86  		Want   cty.Value
    87  		Err    bool
    88  	}{
    89  		{
    90  			cty.StringVal("hello"),
    91  			cty.StringVal("hel"),
    92  			cty.BoolVal(true),
    93  			false,
    94  		},
    95  		{
    96  			cty.StringVal("hello"),
    97  			cty.StringVal("lo"),
    98  			cty.BoolVal(true),
    99  			false,
   100  		},
   101  		{
   102  			cty.StringVal("hello1"),
   103  			cty.StringVal("1"),
   104  			cty.BoolVal(true),
   105  			false,
   106  		},
   107  		{
   108  			cty.StringVal("hello1"),
   109  			cty.StringVal("heo"),
   110  			cty.BoolVal(false),
   111  			false,
   112  		},
   113  		{
   114  			cty.StringVal("hello1"),
   115  			cty.NumberIntVal(1),
   116  			cty.UnknownVal(cty.Bool),
   117  			true,
   118  		},
   119  	}
   120  
   121  	for _, test := range tests {
   122  		t.Run(fmt.Sprintf("includes(%#v, %#v)", test.String, test.Substr), func(t *testing.T) {
   123  			got, err := StrContains(test.String, test.Substr)
   124  
   125  			if test.Err {
   126  				if err == nil {
   127  					t.Fatal("succeeded; want error")
   128  				}
   129  				return
   130  			} else if err != nil {
   131  				t.Fatalf("unexpected error: %s", err)
   132  			}
   133  
   134  			if !got.RawEquals(test.Want) {
   135  				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
   136  			}
   137  		})
   138  	}
   139  }
   140  
   141  func TestStartsWith(t *testing.T) {
   142  	tests := []struct {
   143  		String, Prefix cty.Value
   144  		Want           cty.Value
   145  		WantError      string
   146  	}{
   147  		{
   148  			cty.StringVal("hello world"),
   149  			cty.StringVal("hello"),
   150  			cty.True,
   151  			``,
   152  		},
   153  		{
   154  			cty.StringVal("hey world"),
   155  			cty.StringVal("hello"),
   156  			cty.False,
   157  			``,
   158  		},
   159  		{
   160  			cty.StringVal(""),
   161  			cty.StringVal(""),
   162  			cty.True,
   163  			``,
   164  		},
   165  		{
   166  			cty.StringVal("a"),
   167  			cty.StringVal(""),
   168  			cty.True,
   169  			``,
   170  		},
   171  		{
   172  			cty.StringVal(""),
   173  			cty.StringVal("a"),
   174  			cty.False,
   175  			``,
   176  		},
   177  		{
   178  			cty.UnknownVal(cty.String),
   179  			cty.StringVal("a"),
   180  			cty.UnknownVal(cty.Bool).RefineNotNull(),
   181  			``,
   182  		},
   183  		{
   184  			cty.UnknownVal(cty.String),
   185  			cty.StringVal(""),
   186  			cty.True,
   187  			``,
   188  		},
   189  		{
   190  			cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
   191  			cty.StringVal(""),
   192  			cty.True,
   193  			``,
   194  		},
   195  		{
   196  			cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
   197  			cty.StringVal("a"),
   198  			cty.False,
   199  			``,
   200  		},
   201  		{
   202  			cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
   203  			cty.StringVal("ht"),
   204  			cty.True,
   205  			``,
   206  		},
   207  		{
   208  			cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
   209  			cty.StringVal("https:"),
   210  			cty.True,
   211  			``,
   212  		},
   213  		{
   214  			cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
   215  			cty.StringVal("https-"),
   216  			cty.False,
   217  			``,
   218  		},
   219  		{
   220  			cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(),
   221  			cty.StringVal("https://"),
   222  			cty.UnknownVal(cty.Bool).RefineNotNull(),
   223  			``,
   224  		},
   225  		{
   226  			// Unicode combining characters edge-case: we match the prefix
   227  			// in terms of unicode code units rather than grapheme clusters,
   228  			// which is inconsistent with our string processing elsewhere but
   229  			// would be a breaking change to fix that bug now.
   230  			cty.StringVal("\U0001f937\u200d\u2642"), // "Man Shrugging" is encoded as "Person Shrugging" followed by zero-width joiner and then the masculine gender presentation modifier
   231  			cty.StringVal("\U0001f937"),             // Just the "Person Shrugging" character without any modifiers
   232  			cty.True,
   233  			``,
   234  		},
   235  	}
   236  
   237  	for _, test := range tests {
   238  		t.Run(fmt.Sprintf("StartsWith(%#v, %#v)", test.String, test.Prefix), func(t *testing.T) {
   239  			got, err := StartsWithFunc.Call([]cty.Value{test.String, test.Prefix})
   240  
   241  			if test.WantError != "" {
   242  				gotErr := fmt.Sprintf("%s", err)
   243  				if gotErr != test.WantError {
   244  					t.Errorf("wrong error\ngot:  %s\nwant: %s", gotErr, test.WantError)
   245  				}
   246  				return
   247  			} else if err != nil {
   248  				t.Fatalf("unexpected error: %s", err)
   249  			}
   250  
   251  			if !got.RawEquals(test.Want) {
   252  				t.Errorf(
   253  					"wrong result\nstring: %#v\nprefix: %#v\ngot:    %#v\nwant:   %#v",
   254  					test.String, test.Prefix, got, test.Want,
   255  				)
   256  			}
   257  		})
   258  	}
   259  }
   260  
   261  func TestTemplateString(t *testing.T) {
   262  	tests := map[string]struct {
   263  		Content cty.Value
   264  		Vars    cty.Value
   265  		Want    cty.Value
   266  		Err     string
   267  	}{
   268  		"Simple string template": {
   269  			cty.StringVal("Hello, Jodie!"),
   270  			cty.EmptyObjectVal,
   271  			cty.StringVal("Hello, Jodie!"),
   272  			``,
   273  		},
   274  		"String interpolation with variable": {
   275  			cty.StringVal("Hello, ${name}!"),
   276  			cty.MapVal(map[string]cty.Value{
   277  				"name": cty.StringVal("Jodie"),
   278  			}),
   279  			cty.StringVal("Hello, Jodie!"),
   280  			``,
   281  		},
   282  		"Looping through list": {
   283  			cty.StringVal("Items: %{ for x in list ~} ${x} %{ endfor ~}"),
   284  			cty.ObjectVal(map[string]cty.Value{
   285  				"list": cty.ListVal([]cty.Value{
   286  					cty.StringVal("a"),
   287  					cty.StringVal("b"),
   288  					cty.StringVal("c"),
   289  				}),
   290  			}),
   291  			cty.StringVal("Items: a b c "),
   292  			``,
   293  		},
   294  		"Looping through map": {
   295  			cty.StringVal("%{ for key, value in list ~} ${key}:${value} %{ endfor ~}"),
   296  			cty.ObjectVal(map[string]cty.Value{
   297  				"list": cty.ObjectVal(map[string]cty.Value{
   298  					"item1": cty.StringVal("a"),
   299  					"item2": cty.StringVal("b"),
   300  					"item3": cty.StringVal("c"),
   301  				}),
   302  			}),
   303  			cty.StringVal("item1:a item2:b item3:c "),
   304  			``,
   305  		},
   306  		"Invalid template variable name": {
   307  			cty.StringVal("Hello, ${1}!"),
   308  			cty.MapVal(map[string]cty.Value{
   309  				"1": cty.StringVal("Jodie"),
   310  			}),
   311  			cty.NilVal,
   312  			`invalid template variable name "1": must start with a letter, followed by zero or more letters, digits, and underscores`,
   313  		},
   314  		"Variable not present in vars map": {
   315  			cty.StringVal("Hello, ${name}!"),
   316  			cty.EmptyObjectVal,
   317  			cty.NilVal,
   318  			`vars map does not contain key "name"`,
   319  		},
   320  		"Interpolation of a boolean value": {
   321  			cty.StringVal("${val}"),
   322  			cty.ObjectVal(map[string]cty.Value{
   323  				"val": cty.True,
   324  			}),
   325  			cty.True,
   326  			``,
   327  		},
   328  		"Sensitive string template": {
   329  			cty.StringVal("My password is 1234").Mark(marks.Sensitive),
   330  			cty.EmptyObjectVal,
   331  			cty.StringVal("My password is 1234").Mark(marks.Sensitive),
   332  			``,
   333  		},
   334  		"Sensitive template variable": {
   335  			cty.StringVal("My password is ${pass}"),
   336  			cty.ObjectVal(map[string]cty.Value{
   337  				"pass": cty.StringVal("secret").Mark(marks.Sensitive),
   338  			}),
   339  			cty.StringVal("My password is secret").Mark(marks.Sensitive),
   340  			``,
   341  		},
   342  	}
   343  
   344  	templateStringFn := MakeTemplateStringFunc(".", func() map[string]function.Function {
   345  		return map[string]function.Function{}
   346  	})
   347  
   348  	for _, test := range tests {
   349  		t.Run(fmt.Sprintf("TemplateString(%#v, %#v)", test.Content, test.Vars), func(t *testing.T) {
   350  			got, err := templateStringFn.Call([]cty.Value{test.Content, test.Vars})
   351  
   352  			if argErr, ok := err.(function.ArgError); ok {
   353  				if argErr.Index < 0 || argErr.Index > 1 {
   354  					t.Errorf("ArgError index %d is out of range for templatestring (must be 0 or 1)", argErr.Index)
   355  				}
   356  			}
   357  
   358  			if err != nil {
   359  				if test.Err == "" {
   360  					t.Fatalf("unexpected error: %s", err)
   361  				} else {
   362  					if got, want := err.Error(), test.Err; got != want {
   363  						t.Errorf("wrong error\ngot:  %s\nwant: %s", got, want)
   364  					}
   365  				}
   366  			} else if test.Err != "" {
   367  				t.Fatal("succeeded; want error")
   368  			} else {
   369  				if !got.RawEquals(test.Want) {
   370  					t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
   371  				}
   372  			}
   373  		})
   374  	}
   375  }