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 }