github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/lang/funcs/filesystem_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package funcs 5 6 import ( 7 "fmt" 8 "os" 9 "path/filepath" 10 "testing" 11 12 homedir "github.com/mitchellh/go-homedir" 13 "github.com/terramate-io/tf/lang/marks" 14 "github.com/zclconf/go-cty/cty" 15 "github.com/zclconf/go-cty/cty/function" 16 "github.com/zclconf/go-cty/cty/function/stdlib" 17 ) 18 19 func TestFile(t *testing.T) { 20 tests := []struct { 21 Path cty.Value 22 Want cty.Value 23 Err string 24 }{ 25 { 26 cty.StringVal("testdata/hello.txt"), 27 cty.StringVal("Hello World"), 28 ``, 29 }, 30 { 31 cty.StringVal("testdata/icon.png"), 32 cty.NilVal, 33 `contents of "testdata/icon.png" are not valid UTF-8; use the filebase64 function to obtain the Base64 encoded contents or the other file functions (e.g. filemd5, filesha256) to obtain file hashing results instead`, 34 }, 35 { 36 cty.StringVal("testdata/icon.png").Mark(marks.Sensitive), 37 cty.NilVal, 38 `contents of (sensitive value) are not valid UTF-8; use the filebase64 function to obtain the Base64 encoded contents or the other file functions (e.g. filemd5, filesha256) to obtain file hashing results instead`, 39 }, 40 { 41 cty.StringVal("testdata/missing"), 42 cty.NilVal, 43 `no file exists at "testdata/missing"; this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource`, 44 }, 45 { 46 cty.StringVal("testdata/missing").Mark(marks.Sensitive), 47 cty.NilVal, 48 `no file exists at (sensitive value); this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource`, 49 }, 50 } 51 52 for _, test := range tests { 53 t.Run(fmt.Sprintf("File(\".\", %#v)", test.Path), func(t *testing.T) { 54 got, err := File(".", test.Path) 55 56 if test.Err != "" { 57 if err == nil { 58 t.Fatal("succeeded; want error") 59 } 60 if got, want := err.Error(), test.Err; got != want { 61 t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) 62 } 63 return 64 } else if err != nil { 65 t.Fatalf("unexpected error: %s", err) 66 } 67 68 if !got.RawEquals(test.Want) { 69 t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 70 } 71 }) 72 } 73 } 74 75 func TestTemplateFile(t *testing.T) { 76 tests := []struct { 77 Path cty.Value 78 Vars cty.Value 79 Want cty.Value 80 Err string 81 }{ 82 { 83 cty.StringVal("testdata/hello.txt"), 84 cty.EmptyObjectVal, 85 cty.StringVal("Hello World"), 86 ``, 87 }, 88 { 89 cty.StringVal("testdata/icon.png"), 90 cty.EmptyObjectVal, 91 cty.NilVal, 92 `contents of "testdata/icon.png" are not valid UTF-8; use the filebase64 function to obtain the Base64 encoded contents or the other file functions (e.g. filemd5, filesha256) to obtain file hashing results instead`, 93 }, 94 { 95 cty.StringVal("testdata/missing"), 96 cty.EmptyObjectVal, 97 cty.NilVal, 98 `no file exists at "testdata/missing"; this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource`, 99 }, 100 { 101 cty.StringVal("testdata/secrets.txt").Mark(marks.Sensitive), 102 cty.EmptyObjectVal, 103 cty.NilVal, 104 `no file exists at (sensitive value); this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource`, 105 }, 106 { 107 cty.StringVal("testdata/hello.tmpl"), 108 cty.MapVal(map[string]cty.Value{ 109 "name": cty.StringVal("Jodie"), 110 }), 111 cty.StringVal("Hello, Jodie!"), 112 ``, 113 }, 114 { 115 cty.StringVal("testdata/hello.tmpl"), 116 cty.MapVal(map[string]cty.Value{ 117 "name!": cty.StringVal("Jodie"), 118 }), 119 cty.NilVal, 120 `invalid template variable name "name!": must start with a letter, followed by zero or more letters, digits, and underscores`, 121 }, 122 { 123 cty.StringVal("testdata/hello.tmpl"), 124 cty.ObjectVal(map[string]cty.Value{ 125 "name": cty.StringVal("Jimbo"), 126 }), 127 cty.StringVal("Hello, Jimbo!"), 128 ``, 129 }, 130 { 131 cty.StringVal("testdata/hello.tmpl"), 132 cty.EmptyObjectVal, 133 cty.NilVal, 134 `vars map does not contain key "name", referenced at testdata/hello.tmpl:1,10-14`, 135 }, 136 { 137 cty.StringVal("testdata/func.tmpl"), 138 cty.ObjectVal(map[string]cty.Value{ 139 "list": cty.ListVal([]cty.Value{ 140 cty.StringVal("a"), 141 cty.StringVal("b"), 142 cty.StringVal("c"), 143 }), 144 }), 145 cty.StringVal("The items are a, b, c"), 146 ``, 147 }, 148 { 149 cty.StringVal("testdata/recursive.tmpl"), 150 cty.MapValEmpty(cty.String), 151 cty.NilVal, 152 `testdata/recursive.tmpl:1,3-16: Error in function call; Call to function "templatefile" failed: cannot recursively call templatefile from inside templatefile call.`, 153 }, 154 { 155 cty.StringVal("testdata/list.tmpl"), 156 cty.ObjectVal(map[string]cty.Value{ 157 "list": cty.ListVal([]cty.Value{ 158 cty.StringVal("a"), 159 cty.StringVal("b"), 160 cty.StringVal("c"), 161 }), 162 }), 163 cty.StringVal("- a\n- b\n- c\n"), 164 ``, 165 }, 166 { 167 cty.StringVal("testdata/list.tmpl"), 168 cty.ObjectVal(map[string]cty.Value{ 169 "list": cty.True, 170 }), 171 cty.NilVal, 172 `testdata/list.tmpl:1,13-17: Iteration over non-iterable value; A value of type bool cannot be used as the collection in a 'for' expression.`, 173 }, 174 { 175 cty.StringVal("testdata/bare.tmpl"), 176 cty.ObjectVal(map[string]cty.Value{ 177 "val": cty.True, 178 }), 179 cty.True, // since this template contains only an interpolation, its true value shines through 180 ``, 181 }, 182 } 183 184 templateFileFn := MakeTemplateFileFunc(".", func() map[string]function.Function { 185 return map[string]function.Function{ 186 "join": stdlib.JoinFunc, 187 "templatefile": MakeFileFunc(".", false), // just a placeholder, since templatefile itself overrides this 188 } 189 }) 190 191 for _, test := range tests { 192 t.Run(fmt.Sprintf("TemplateFile(%#v, %#v)", test.Path, test.Vars), func(t *testing.T) { 193 got, err := templateFileFn.Call([]cty.Value{test.Path, test.Vars}) 194 195 if argErr, ok := err.(function.ArgError); ok { 196 if argErr.Index < 0 || argErr.Index > 1 { 197 t.Errorf("ArgError index %d is out of range for templatefile (must be 0 or 1)", argErr.Index) 198 } 199 } 200 201 if test.Err != "" { 202 if err == nil { 203 t.Fatal("succeeded; want error") 204 } 205 if got, want := err.Error(), test.Err; got != want { 206 t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) 207 } 208 return 209 } else if err != nil { 210 t.Fatalf("unexpected error: %s", err) 211 } 212 213 if !got.RawEquals(test.Want) { 214 t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 215 } 216 }) 217 } 218 } 219 220 func TestFileExists(t *testing.T) { 221 tests := []struct { 222 Path cty.Value 223 Want cty.Value 224 Err string 225 }{ 226 { 227 cty.StringVal("testdata/hello.txt"), 228 cty.BoolVal(true), 229 ``, 230 }, 231 { 232 cty.StringVal(""), 233 cty.BoolVal(false), 234 `"." is a directory, not a file`, 235 }, 236 { 237 cty.StringVal("testdata").Mark(marks.Sensitive), 238 cty.BoolVal(false), 239 `(sensitive value) is a directory, not a file`, 240 }, 241 { 242 cty.StringVal("testdata/missing"), 243 cty.BoolVal(false), 244 ``, 245 }, 246 { 247 cty.StringVal("testdata/unreadable/foobar"), 248 cty.BoolVal(false), 249 `failed to stat "testdata/unreadable/foobar"`, 250 }, 251 { 252 cty.StringVal("testdata/unreadable/foobar").Mark(marks.Sensitive), 253 cty.BoolVal(false), 254 `failed to stat (sensitive value)`, 255 }, 256 } 257 258 // Ensure "unreadable" directory cannot be listed during the test run 259 fi, err := os.Lstat("testdata/unreadable") 260 if err != nil { 261 t.Fatal(err) 262 } 263 os.Chmod("testdata/unreadable", 0000) 264 defer func(mode os.FileMode) { 265 os.Chmod("testdata/unreadable", mode) 266 }(fi.Mode()) 267 268 for _, test := range tests { 269 t.Run(fmt.Sprintf("FileExists(\".\", %#v)", test.Path), func(t *testing.T) { 270 got, err := FileExists(".", test.Path) 271 272 if test.Err != "" { 273 if err == nil { 274 t.Fatal("succeeded; want error") 275 } 276 if got, want := err.Error(), test.Err; got != want { 277 t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) 278 } 279 return 280 } else if err != nil { 281 t.Fatalf("unexpected error: %s", err) 282 } 283 284 if !got.RawEquals(test.Want) { 285 t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 286 } 287 }) 288 } 289 } 290 291 func TestFileSet(t *testing.T) { 292 tests := []struct { 293 Path cty.Value 294 Pattern cty.Value 295 Want cty.Value 296 Err string 297 }{ 298 { 299 cty.StringVal("."), 300 cty.StringVal("testdata*"), 301 cty.SetValEmpty(cty.String), 302 ``, 303 }, 304 { 305 cty.StringVal("."), 306 cty.StringVal("testdata"), 307 cty.SetValEmpty(cty.String), 308 ``, 309 }, 310 { 311 cty.StringVal("."), 312 cty.StringVal("{testdata,missing}"), 313 cty.SetValEmpty(cty.String), 314 ``, 315 }, 316 { 317 cty.StringVal("."), 318 cty.StringVal("testdata/missing"), 319 cty.SetValEmpty(cty.String), 320 ``, 321 }, 322 { 323 cty.StringVal("."), 324 cty.StringVal("testdata/missing*"), 325 cty.SetValEmpty(cty.String), 326 ``, 327 }, 328 { 329 cty.StringVal("."), 330 cty.StringVal("*/missing"), 331 cty.SetValEmpty(cty.String), 332 ``, 333 }, 334 { 335 cty.StringVal("."), 336 cty.StringVal("**/missing"), 337 cty.SetValEmpty(cty.String), 338 ``, 339 }, 340 { 341 cty.StringVal("."), 342 cty.StringVal("testdata/*.txt"), 343 cty.SetVal([]cty.Value{ 344 cty.StringVal("testdata/hello.txt"), 345 }), 346 ``, 347 }, 348 { 349 cty.StringVal("."), 350 cty.StringVal("testdata/hello.txt"), 351 cty.SetVal([]cty.Value{ 352 cty.StringVal("testdata/hello.txt"), 353 }), 354 ``, 355 }, 356 { 357 cty.StringVal("."), 358 cty.StringVal("testdata/hello.???"), 359 cty.SetVal([]cty.Value{ 360 cty.StringVal("testdata/hello.txt"), 361 }), 362 ``, 363 }, 364 { 365 cty.StringVal("."), 366 cty.StringVal("testdata/hello*"), 367 cty.SetVal([]cty.Value{ 368 cty.StringVal("testdata/hello.tmpl"), 369 cty.StringVal("testdata/hello.txt"), 370 }), 371 ``, 372 }, 373 { 374 cty.StringVal("."), 375 cty.StringVal("testdata/hello.{tmpl,txt}"), 376 cty.SetVal([]cty.Value{ 377 cty.StringVal("testdata/hello.tmpl"), 378 cty.StringVal("testdata/hello.txt"), 379 }), 380 ``, 381 }, 382 { 383 cty.StringVal("."), 384 cty.StringVal("*/hello.txt"), 385 cty.SetVal([]cty.Value{ 386 cty.StringVal("testdata/hello.txt"), 387 }), 388 ``, 389 }, 390 { 391 cty.StringVal("."), 392 cty.StringVal("*/*.txt"), 393 cty.SetVal([]cty.Value{ 394 cty.StringVal("testdata/hello.txt"), 395 }), 396 ``, 397 }, 398 { 399 cty.StringVal("."), 400 cty.StringVal("*/hello*"), 401 cty.SetVal([]cty.Value{ 402 cty.StringVal("testdata/hello.tmpl"), 403 cty.StringVal("testdata/hello.txt"), 404 }), 405 ``, 406 }, 407 { 408 cty.StringVal("."), 409 cty.StringVal("**/hello*"), 410 cty.SetVal([]cty.Value{ 411 cty.StringVal("testdata/hello.tmpl"), 412 cty.StringVal("testdata/hello.txt"), 413 }), 414 ``, 415 }, 416 { 417 cty.StringVal("."), 418 cty.StringVal("**/hello.{tmpl,txt}"), 419 cty.SetVal([]cty.Value{ 420 cty.StringVal("testdata/hello.tmpl"), 421 cty.StringVal("testdata/hello.txt"), 422 }), 423 ``, 424 }, 425 { 426 cty.StringVal("."), 427 cty.StringVal("["), 428 cty.SetValEmpty(cty.String), 429 `failed to glob pattern "[": syntax error in pattern`, 430 }, 431 { 432 cty.StringVal("."), 433 cty.StringVal("[").Mark(marks.Sensitive), 434 cty.SetValEmpty(cty.String), 435 `failed to glob pattern (sensitive value): syntax error in pattern`, 436 }, 437 { 438 cty.StringVal("."), 439 cty.StringVal("\\"), 440 cty.SetValEmpty(cty.String), 441 `failed to glob pattern "\\": syntax error in pattern`, 442 }, 443 { 444 cty.StringVal("testdata"), 445 cty.StringVal("missing"), 446 cty.SetValEmpty(cty.String), 447 ``, 448 }, 449 { 450 cty.StringVal("testdata"), 451 cty.StringVal("missing*"), 452 cty.SetValEmpty(cty.String), 453 ``, 454 }, 455 { 456 cty.StringVal("testdata"), 457 cty.StringVal("*.txt"), 458 cty.SetVal([]cty.Value{ 459 cty.StringVal("hello.txt"), 460 }), 461 ``, 462 }, 463 { 464 cty.StringVal("testdata"), 465 cty.StringVal("hello.txt"), 466 cty.SetVal([]cty.Value{ 467 cty.StringVal("hello.txt"), 468 }), 469 ``, 470 }, 471 { 472 cty.StringVal("testdata"), 473 cty.StringVal("hello.???"), 474 cty.SetVal([]cty.Value{ 475 cty.StringVal("hello.txt"), 476 }), 477 ``, 478 }, 479 { 480 cty.StringVal("testdata"), 481 cty.StringVal("hello*"), 482 cty.SetVal([]cty.Value{ 483 cty.StringVal("hello.tmpl"), 484 cty.StringVal("hello.txt"), 485 }), 486 ``, 487 }, 488 } 489 490 for _, test := range tests { 491 t.Run(fmt.Sprintf("FileSet(\".\", %#v, %#v)", test.Path, test.Pattern), func(t *testing.T) { 492 got, err := FileSet(".", test.Path, test.Pattern) 493 494 if test.Err != "" { 495 if err == nil { 496 t.Fatal("succeeded; want error") 497 } 498 if got, want := err.Error(), test.Err; got != want { 499 t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) 500 } 501 return 502 } else if err != nil { 503 t.Fatalf("unexpected error: %s", err) 504 } 505 506 if !got.RawEquals(test.Want) { 507 t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 508 } 509 }) 510 } 511 } 512 513 func TestFileBase64(t *testing.T) { 514 tests := []struct { 515 Path cty.Value 516 Want cty.Value 517 Err bool 518 }{ 519 { 520 cty.StringVal("testdata/hello.txt"), 521 cty.StringVal("SGVsbG8gV29ybGQ="), 522 false, 523 }, 524 { 525 cty.StringVal("testdata/icon.png"), 526 cty.StringVal("iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAq1BMVEX///9cTuVeUeRcTuZcTuZcT+VbSe1cTuVdT+MAAP9JSbZcT+VcTuZAQLFAQLJcTuVcTuZcUuBBQbA/P7JAQLJaTuRcT+RcTuVGQ7xAQLJVVf9cTuVcTuVGRMFeUeRbTeJcTuU/P7JeTeZbTOVcTeZAQLJBQbNAQLNaUORcTeZbT+VcTuRAQLNAQLRdTuRHR8xgUOdgUN9cTuVdTeRdT+VZTulcTuVAQLL///8+GmETAAAANnRSTlMApibw+osO6DcBB3fIX87+oRk3yehB0/Nj/gNs7nsTRv3dHmu//JYUMLVr3bssjxkgEK5CaxeK03nIAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAADoQAAA6EBvJf9gwAAAAd0SU1FB+EEBRIQDxZNTKsAAACCSURBVBjTfc7JFsFQEATQQpCYxyBEzJ55rvf/f0ZHcyQLvelTd1GngEwWycs5+UISyKLraSi9geWKK9Gr1j7AeqOJVtt2XtD1Bchef2BjQDAcCTC0CsA4mihMtXw2XwgsV2sFw812F+4P3y2GdI6nn3FGSs//4HJNAXDzU4Dg/oj/E+bsEbhf5cMsAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE3LTA0LTA1VDE4OjE2OjE1KzAyOjAws5bLVQAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxNy0wNC0wNVQxODoxNjoxNSswMjowMMLLc+kAAAAZdEVYdFNvZnR3YXJlAHd3dy5pbmtzY2FwZS5vcmeb7jwaAAAAC3RFWHRUaXRsZQBHcm91cJYfIowAAABXelRYdFJhdyBwcm9maWxlIHR5cGUgaXB0YwAAeJzj8gwIcVYoKMpPy8xJ5VIAAyMLLmMLEyMTS5MUAxMgRIA0w2QDI7NUIMvY1MjEzMQcxAfLgEigSi4A6hcRdPJCNZUAAAAASUVORK5CYII="), 527 false, 528 }, 529 { 530 cty.StringVal("testdata/missing"), 531 cty.NilVal, 532 true, // no file exists 533 }, 534 } 535 536 for _, test := range tests { 537 t.Run(fmt.Sprintf("FileBase64(\".\", %#v)", test.Path), func(t *testing.T) { 538 got, err := FileBase64(".", test.Path) 539 540 if test.Err { 541 if err == nil { 542 t.Fatal("succeeded; want error") 543 } 544 return 545 } else if err != nil { 546 t.Fatalf("unexpected error: %s", err) 547 } 548 549 if !got.RawEquals(test.Want) { 550 t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 551 } 552 }) 553 } 554 } 555 556 func TestBasename(t *testing.T) { 557 tests := []struct { 558 Path cty.Value 559 Want cty.Value 560 Err bool 561 }{ 562 { 563 cty.StringVal("testdata/hello.txt"), 564 cty.StringVal("hello.txt"), 565 false, 566 }, 567 { 568 cty.StringVal("hello.txt"), 569 cty.StringVal("hello.txt"), 570 false, 571 }, 572 { 573 cty.StringVal(""), 574 cty.StringVal("."), 575 false, 576 }, 577 } 578 579 for _, test := range tests { 580 t.Run(fmt.Sprintf("Basename(%#v)", test.Path), func(t *testing.T) { 581 got, err := Basename(test.Path) 582 583 if test.Err { 584 if err == nil { 585 t.Fatal("succeeded; want error") 586 } 587 return 588 } else if err != nil { 589 t.Fatalf("unexpected error: %s", err) 590 } 591 592 if !got.RawEquals(test.Want) { 593 t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 594 } 595 }) 596 } 597 } 598 599 func TestDirname(t *testing.T) { 600 tests := []struct { 601 Path cty.Value 602 Want cty.Value 603 Err bool 604 }{ 605 { 606 cty.StringVal("testdata/hello.txt"), 607 cty.StringVal("testdata"), 608 false, 609 }, 610 { 611 cty.StringVal("testdata/foo/hello.txt"), 612 cty.StringVal("testdata/foo"), 613 false, 614 }, 615 { 616 cty.StringVal("hello.txt"), 617 cty.StringVal("."), 618 false, 619 }, 620 { 621 cty.StringVal(""), 622 cty.StringVal("."), 623 false, 624 }, 625 } 626 627 for _, test := range tests { 628 t.Run(fmt.Sprintf("Dirname(%#v)", test.Path), func(t *testing.T) { 629 got, err := Dirname(test.Path) 630 631 if test.Err { 632 if err == nil { 633 t.Fatal("succeeded; want error") 634 } 635 return 636 } else if err != nil { 637 t.Fatalf("unexpected error: %s", err) 638 } 639 640 if !got.RawEquals(test.Want) { 641 t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 642 } 643 }) 644 } 645 } 646 647 func TestPathExpand(t *testing.T) { 648 homePath, err := homedir.Dir() 649 if err != nil { 650 t.Fatalf("Error getting home directory: %v", err) 651 } 652 653 tests := []struct { 654 Path cty.Value 655 Want cty.Value 656 Err bool 657 }{ 658 { 659 cty.StringVal("~/test-file"), 660 cty.StringVal(filepath.Join(homePath, "test-file")), 661 false, 662 }, 663 { 664 cty.StringVal("~/another/test/file"), 665 cty.StringVal(filepath.Join(homePath, "another/test/file")), 666 false, 667 }, 668 { 669 cty.StringVal("/root/file"), 670 cty.StringVal("/root/file"), 671 false, 672 }, 673 { 674 cty.StringVal("/"), 675 cty.StringVal("/"), 676 false, 677 }, 678 } 679 680 for _, test := range tests { 681 t.Run(fmt.Sprintf("Dirname(%#v)", test.Path), func(t *testing.T) { 682 got, err := Pathexpand(test.Path) 683 684 if test.Err { 685 if err == nil { 686 t.Fatal("succeeded; want error") 687 } 688 return 689 } else if err != nil { 690 t.Fatalf("unexpected error: %s", err) 691 } 692 693 if !got.RawEquals(test.Want) { 694 t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 695 } 696 }) 697 } 698 }