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 }