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