github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/depsfile/locks_file_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package depsfile
     5  
     6  import (
     7  	"bufio"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/google/go-cmp/cmp"
    15  	"github.com/terramate-io/tf/addrs"
    16  	"github.com/terramate-io/tf/getproviders"
    17  	"github.com/terramate-io/tf/tfdiags"
    18  )
    19  
    20  func TestLoadLocksFromFile(t *testing.T) {
    21  	// For ease of test maintenance we treat every file under
    22  	// test-data/locks-files as a test case which is subject
    23  	// at least to testing that it produces an expected set
    24  	// of diagnostics represented via specially-formatted comments
    25  	// in the fixture files (which might be the empty set, if
    26  	// there are no such comments).
    27  	//
    28  	// Some of the files also have additional assertions that
    29  	// are encoded in the test code below. These must pass
    30  	// in addition to the standard diagnostics tests, if present.
    31  	files, err := ioutil.ReadDir("testdata/locks-files")
    32  	if err != nil {
    33  		t.Fatal(err.Error())
    34  	}
    35  
    36  	for _, info := range files {
    37  		testName := filepath.Base(info.Name())
    38  		filename := filepath.Join("testdata/locks-files", testName)
    39  		t.Run(testName, func(t *testing.T) {
    40  			f, err := os.Open(filename)
    41  			if err != nil {
    42  				t.Fatal(err.Error())
    43  			}
    44  			defer f.Close()
    45  			const errorPrefix = "# ERROR: "
    46  			const warningPrefix = "# WARNING: "
    47  			wantErrors := map[int]string{}
    48  			wantWarnings := map[int]string{}
    49  			sc := bufio.NewScanner(f)
    50  			lineNum := 1
    51  			for sc.Scan() {
    52  				l := sc.Text()
    53  				if pos := strings.Index(l, errorPrefix); pos != -1 {
    54  					wantSummary := l[pos+len(errorPrefix):]
    55  					wantErrors[lineNum] = wantSummary
    56  				}
    57  				if pos := strings.Index(l, warningPrefix); pos != -1 {
    58  					wantSummary := l[pos+len(warningPrefix):]
    59  					wantWarnings[lineNum] = wantSummary
    60  				}
    61  				lineNum++
    62  			}
    63  			if err := sc.Err(); err != nil {
    64  				t.Fatal(err.Error())
    65  			}
    66  
    67  			locks, diags := LoadLocksFromFile(filename)
    68  			gotErrors := map[int]string{}
    69  			gotWarnings := map[int]string{}
    70  			for _, diag := range diags {
    71  				summary := diag.Description().Summary
    72  				if diag.Source().Subject == nil {
    73  					// We don't expect any sourceless diagnostics here.
    74  					t.Errorf("unexpected sourceless diagnostic: %s", summary)
    75  					continue
    76  				}
    77  				lineNum := diag.Source().Subject.Start.Line
    78  				switch sev := diag.Severity(); sev {
    79  				case tfdiags.Error:
    80  					gotErrors[lineNum] = summary
    81  				case tfdiags.Warning:
    82  					gotWarnings[lineNum] = summary
    83  				default:
    84  					t.Errorf("unexpected diagnostic severity %s", sev)
    85  				}
    86  			}
    87  
    88  			if diff := cmp.Diff(wantErrors, gotErrors); diff != "" {
    89  				t.Errorf("wrong errors\n%s", diff)
    90  			}
    91  			if diff := cmp.Diff(wantWarnings, gotWarnings); diff != "" {
    92  				t.Errorf("wrong warnings\n%s", diff)
    93  			}
    94  
    95  			switch testName {
    96  			// These are the file-specific test assertions. Not all files
    97  			// need custom test assertions in addition to the standard
    98  			// diagnostics assertions implemented above, so the cases here
    99  			// don't need to be exhaustive for all files.
   100  			//
   101  			// Please keep these in alphabetical order so the list is easy
   102  			// to scan!
   103  
   104  			case "empty.hcl":
   105  				if got, want := len(locks.providers), 0; got != want {
   106  					t.Errorf("wrong number of providers %d; want %d", got, want)
   107  				}
   108  
   109  			case "valid-provider-locks.hcl":
   110  				if got, want := len(locks.providers), 3; got != want {
   111  					t.Errorf("wrong number of providers %d; want %d", got, want)
   112  				}
   113  
   114  				t.Run("version-only", func(t *testing.T) {
   115  					if lock := locks.Provider(addrs.MustParseProviderSourceString("terraform.io/test/version-only")); lock != nil {
   116  						if got, want := lock.Version().String(), "1.0.0"; got != want {
   117  							t.Errorf("wrong version\ngot:  %s\nwant: %s", got, want)
   118  						}
   119  						if got, want := getproviders.VersionConstraintsString(lock.VersionConstraints()), ""; got != want {
   120  							t.Errorf("wrong version constraints\ngot:  %s\nwant: %s", got, want)
   121  						}
   122  						if got, want := len(lock.hashes), 0; got != want {
   123  							t.Errorf("wrong number of hashes %d; want %d", got, want)
   124  						}
   125  					}
   126  				})
   127  
   128  				t.Run("version-and-constraints", func(t *testing.T) {
   129  					if lock := locks.Provider(addrs.MustParseProviderSourceString("terraform.io/test/version-and-constraints")); lock != nil {
   130  						if got, want := lock.Version().String(), "1.2.0"; got != want {
   131  							t.Errorf("wrong version\ngot:  %s\nwant: %s", got, want)
   132  						}
   133  						if got, want := getproviders.VersionConstraintsString(lock.VersionConstraints()), "~> 1.2"; got != want {
   134  							t.Errorf("wrong version constraints\ngot:  %s\nwant: %s", got, want)
   135  						}
   136  						if got, want := len(lock.hashes), 0; got != want {
   137  							t.Errorf("wrong number of hashes %d; want %d", got, want)
   138  						}
   139  					}
   140  				})
   141  
   142  				t.Run("all-the-things", func(t *testing.T) {
   143  					if lock := locks.Provider(addrs.MustParseProviderSourceString("terraform.io/test/all-the-things")); lock != nil {
   144  						if got, want := lock.Version().String(), "3.0.10"; got != want {
   145  							t.Errorf("wrong version\ngot:  %s\nwant: %s", got, want)
   146  						}
   147  						if got, want := getproviders.VersionConstraintsString(lock.VersionConstraints()), ">= 3.0.2"; got != want {
   148  							t.Errorf("wrong version constraints\ngot:  %s\nwant: %s", got, want)
   149  						}
   150  						wantHashes := []getproviders.Hash{
   151  							getproviders.MustParseHash("test:placeholder-hash-1"),
   152  							getproviders.MustParseHash("test:placeholder-hash-2"),
   153  							getproviders.MustParseHash("test:placeholder-hash-3"),
   154  						}
   155  						if diff := cmp.Diff(wantHashes, lock.hashes); diff != "" {
   156  							t.Errorf("wrong hashes\n%s", diff)
   157  						}
   158  					}
   159  				})
   160  			}
   161  		})
   162  	}
   163  }
   164  
   165  func TestLoadLocksFromFileAbsent(t *testing.T) {
   166  	t.Run("lock file is a directory", func(t *testing.T) {
   167  		// This can never happen when Terraform is the one generating the
   168  		// lock file, but might arise if the user makes a directory with the
   169  		// lock file's name for some reason. (There is no actual reason to do
   170  		// so, so that would always be a mistake.)
   171  		locks, diags := LoadLocksFromFile("testdata")
   172  		if len(locks.providers) != 0 {
   173  			t.Errorf("returned locks has providers; expected empty locks")
   174  		}
   175  		if !diags.HasErrors() {
   176  			t.Fatalf("LoadLocksFromFile succeeded; want error")
   177  		}
   178  		// This is a generic error message from HCL itself, so upgrading HCL
   179  		// in future might cause a different error message here.
   180  		want := `Failed to read file: The configuration file "testdata" could not be read.`
   181  		got := diags.Err().Error()
   182  		if got != want {
   183  			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
   184  		}
   185  	})
   186  	t.Run("lock file doesn't exist", func(t *testing.T) {
   187  		locks, diags := LoadLocksFromFile("testdata/nonexist.hcl")
   188  		if len(locks.providers) != 0 {
   189  			t.Errorf("returned locks has providers; expected empty locks")
   190  		}
   191  		if !diags.HasErrors() {
   192  			t.Fatalf("LoadLocksFromFile succeeded; want error")
   193  		}
   194  		// This is a generic error message from HCL itself, so upgrading HCL
   195  		// in future might cause a different error message here.
   196  		want := `Failed to read file: The configuration file "testdata/nonexist.hcl" could not be read.`
   197  		got := diags.Err().Error()
   198  		if got != want {
   199  			t.Errorf("wrong error message\ngot:  %s\nwant: %s", got, want)
   200  		}
   201  	})
   202  }
   203  
   204  func TestSaveLocksToFile(t *testing.T) {
   205  	locks := NewLocks()
   206  
   207  	fooProvider := addrs.MustParseProviderSourceString("test/foo")
   208  	barProvider := addrs.MustParseProviderSourceString("test/bar")
   209  	bazProvider := addrs.MustParseProviderSourceString("test/baz")
   210  	booProvider := addrs.MustParseProviderSourceString("test/boo")
   211  	oneDotOh := getproviders.MustParseVersion("1.0.0")
   212  	oneDotTwo := getproviders.MustParseVersion("1.2.0")
   213  	atLeastOneDotOh := getproviders.MustParseVersionConstraints(">= 1.0.0")
   214  	pessimisticOneDotOh := getproviders.MustParseVersionConstraints("~> 1")
   215  	abbreviatedOneDotTwo := getproviders.MustParseVersionConstraints("1.2")
   216  	hashes := []getproviders.Hash{
   217  		getproviders.MustParseHash("test:cccccccccccccccccccccccccccccccccccccccccccccccc"),
   218  		getproviders.MustParseHash("test:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"),
   219  		getproviders.MustParseHash("test:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
   220  	}
   221  	locks.SetProvider(fooProvider, oneDotOh, atLeastOneDotOh, hashes)
   222  	locks.SetProvider(barProvider, oneDotTwo, pessimisticOneDotOh, nil)
   223  	locks.SetProvider(bazProvider, oneDotTwo, nil, nil)
   224  	locks.SetProvider(booProvider, oneDotTwo, abbreviatedOneDotTwo, nil)
   225  
   226  	dir := t.TempDir()
   227  
   228  	filename := filepath.Join(dir, LockFilePath)
   229  	diags := SaveLocksToFile(locks, filename)
   230  	if diags.HasErrors() {
   231  		t.Fatalf("unexpected errors\n%s", diags.Err().Error())
   232  	}
   233  
   234  	fileInfo, err := os.Stat(filename)
   235  	if err != nil {
   236  		t.Fatalf(err.Error())
   237  	}
   238  	if mode := fileInfo.Mode(); mode&0111 != 0 {
   239  		t.Fatalf("Expected lock file to be non-executable: %o", mode)
   240  	}
   241  
   242  	gotContentBytes, err := ioutil.ReadFile(filename)
   243  	if err != nil {
   244  		t.Fatalf(err.Error())
   245  	}
   246  	gotContent := string(gotContentBytes)
   247  	wantContent := `# This file is maintained automatically by "terraform init".
   248  # Manual edits may be lost in future updates.
   249  
   250  provider "registry.terraform.io/test/bar" {
   251    version     = "1.2.0"
   252    constraints = "~> 1.0"
   253  }
   254  
   255  provider "registry.terraform.io/test/baz" {
   256    version = "1.2.0"
   257  }
   258  
   259  provider "registry.terraform.io/test/boo" {
   260    version     = "1.2.0"
   261    constraints = "1.2.0"
   262  }
   263  
   264  provider "registry.terraform.io/test/foo" {
   265    version     = "1.0.0"
   266    constraints = ">= 1.0.0"
   267    hashes = [
   268      "test:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
   269      "test:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
   270      "test:cccccccccccccccccccccccccccccccccccccccccccccccc",
   271    ]
   272  }
   273  `
   274  	if diff := cmp.Diff(wantContent, gotContent); diff != "" {
   275  		t.Errorf("wrong result\n%s", diff)
   276  	}
   277  }