github.com/opentofu/opentofu@v1.7.1/internal/command/cliconfig/cliconfig_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 cliconfig
     7  
     8  import (
     9  	"os"
    10  	"path/filepath"
    11  	"reflect"
    12  	"testing"
    13  
    14  	"github.com/davecgh/go-spew/spew"
    15  	"github.com/google/go-cmp/cmp"
    16  	"github.com/opentofu/opentofu/internal/tfdiags"
    17  )
    18  
    19  // This is the directory where our test fixtures are.
    20  const fixtureDir = "./testdata"
    21  
    22  func TestLoadConfig(t *testing.T) {
    23  	c, err := loadConfigFile(filepath.Join(fixtureDir, "config"))
    24  	if err != nil {
    25  		t.Fatalf("err: %s", err)
    26  	}
    27  
    28  	expected := &Config{
    29  		Providers: map[string]string{
    30  			"aws": "foo",
    31  			"do":  "bar",
    32  		},
    33  	}
    34  
    35  	if !reflect.DeepEqual(c, expected) {
    36  		t.Fatalf("bad: %#v", c)
    37  	}
    38  }
    39  
    40  func TestLoadConfig_envSubst(t *testing.T) {
    41  	t.Setenv("TFTEST", "hello")
    42  
    43  	c, err := loadConfigFile(filepath.Join(fixtureDir, "config-env"))
    44  	if err != nil {
    45  		t.Fatalf("err: %s", err)
    46  	}
    47  
    48  	expected := &Config{
    49  		Providers: map[string]string{
    50  			"aws":    "hello",
    51  			"google": "bar",
    52  		},
    53  		Provisioners: map[string]string{
    54  			"local": "hello",
    55  		},
    56  	}
    57  
    58  	if !reflect.DeepEqual(c, expected) {
    59  		t.Fatalf("bad: %#v", c)
    60  	}
    61  }
    62  
    63  func TestLoadConfig_non_existing_file(t *testing.T) {
    64  	tmpDir := os.TempDir()
    65  	cliTmpFile := filepath.Join(tmpDir, "dev.tfrc")
    66  
    67  	t.Setenv("TF_CLI_CONFIG_FILE", cliTmpFile)
    68  
    69  	c, errs := LoadConfig()
    70  	if errs.HasErrors() || c.Validate().HasErrors() {
    71  		t.Fatalf("err: %s", errs)
    72  	}
    73  
    74  	hasOpenFileWarn := false
    75  	for _, err := range errs {
    76  		if err.Severity() == tfdiags.Warning && err.Description().Summary == "Unable to open CLI configuration file" {
    77  			hasOpenFileWarn = true
    78  			break
    79  		}
    80  	}
    81  
    82  	if !hasOpenFileWarn {
    83  		t.Fatal("expecting a warning message because of nonexisting CLI configuration file")
    84  	}
    85  }
    86  
    87  func TestEnvConfig(t *testing.T) {
    88  	tests := map[string]struct {
    89  		env  map[string]string
    90  		want *Config
    91  	}{
    92  		"no environment variables": {
    93  			nil,
    94  			&Config{},
    95  		},
    96  		"TF_PLUGIN_CACHE_DIR=boop": {
    97  			map[string]string{
    98  				"TF_PLUGIN_CACHE_DIR": "boop",
    99  			},
   100  			&Config{
   101  				PluginCacheDir: "boop",
   102  			},
   103  		},
   104  		"TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE=anything_except_zero": {
   105  			map[string]string{
   106  				"TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE": "anything_except_zero",
   107  			},
   108  			&Config{
   109  				PluginCacheMayBreakDependencyLockFile: true,
   110  			},
   111  		},
   112  		"TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE=0": {
   113  			map[string]string{
   114  				"TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE": "0",
   115  			},
   116  			&Config{},
   117  		},
   118  		"TF_PLUGIN_CACHE_DIR and TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE": {
   119  			map[string]string{
   120  				"TF_PLUGIN_CACHE_DIR":                            "beep",
   121  				"TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE": "1",
   122  			},
   123  			&Config{
   124  				PluginCacheDir:                        "beep",
   125  				PluginCacheMayBreakDependencyLockFile: true,
   126  			},
   127  		},
   128  	}
   129  
   130  	for name, test := range tests {
   131  		t.Run(name, func(t *testing.T) {
   132  			got := envConfig(test.env)
   133  			want := test.want
   134  
   135  			if diff := cmp.Diff(want, got); diff != "" {
   136  				t.Errorf("wrong result\n%s", diff)
   137  			}
   138  		})
   139  	}
   140  }
   141  
   142  func TestMakeEnvMap(t *testing.T) {
   143  	tests := map[string]struct {
   144  		environ []string
   145  		want    map[string]string
   146  	}{
   147  		"nil": {
   148  			nil,
   149  			nil,
   150  		},
   151  		"one": {
   152  			[]string{
   153  				"FOO=bar",
   154  			},
   155  			map[string]string{
   156  				"FOO": "bar",
   157  			},
   158  		},
   159  		"many": {
   160  			[]string{
   161  				"FOO=1",
   162  				"BAR=2",
   163  				"BAZ=3",
   164  			},
   165  			map[string]string{
   166  				"FOO": "1",
   167  				"BAR": "2",
   168  				"BAZ": "3",
   169  			},
   170  		},
   171  		"conflict": {
   172  			[]string{
   173  				"FOO=1",
   174  				"BAR=1",
   175  				"FOO=2",
   176  			},
   177  			map[string]string{
   178  				"BAR": "1",
   179  				"FOO": "2", // Last entry of each name wins
   180  			},
   181  		},
   182  		"empty_val": {
   183  			[]string{
   184  				"FOO=",
   185  			},
   186  			map[string]string{
   187  				"FOO": "",
   188  			},
   189  		},
   190  		"no_equals": {
   191  			[]string{
   192  				"FOO=bar",
   193  				"INVALID",
   194  			},
   195  			map[string]string{
   196  				"FOO": "bar",
   197  			},
   198  		},
   199  		"multi_equals": {
   200  			[]string{
   201  				"FOO=bar=baz=boop",
   202  			},
   203  			map[string]string{
   204  				"FOO": "bar=baz=boop",
   205  			},
   206  		},
   207  	}
   208  
   209  	for name, test := range tests {
   210  		t.Run(name, func(t *testing.T) {
   211  			got := makeEnvMap(test.environ)
   212  			want := test.want
   213  
   214  			if diff := cmp.Diff(want, got); diff != "" {
   215  				t.Errorf("wrong result\n%s", diff)
   216  			}
   217  		})
   218  	}
   219  
   220  }
   221  
   222  func TestLoadConfig_hosts(t *testing.T) {
   223  	got, diags := loadConfigFile(filepath.Join(fixtureDir, "hosts"))
   224  	if len(diags) != 0 {
   225  		t.Fatalf("%s", diags.Err())
   226  	}
   227  
   228  	want := &Config{
   229  		Hosts: map[string]*ConfigHost{
   230  			"example.com": {
   231  				Services: map[string]interface{}{
   232  					"modules.v1": "https://example.com/",
   233  				},
   234  			},
   235  		},
   236  	}
   237  
   238  	if !reflect.DeepEqual(got, want) {
   239  		t.Errorf("wrong result\ngot:  %swant: %s", spew.Sdump(got), spew.Sdump(want))
   240  	}
   241  }
   242  
   243  func TestLoadConfig_credentials(t *testing.T) {
   244  	got, err := loadConfigFile(filepath.Join(fixtureDir, "credentials"))
   245  	if err != nil {
   246  		t.Fatal(err)
   247  	}
   248  
   249  	want := &Config{
   250  		Credentials: map[string]map[string]interface{}{
   251  			"example.com": map[string]interface{}{
   252  				"token": "foo the bar baz",
   253  			},
   254  			"example.net": map[string]interface{}{
   255  				"username": "foo",
   256  				"password": "baz",
   257  			},
   258  		},
   259  		CredentialsHelpers: map[string]*ConfigCredentialsHelper{
   260  			"foo": &ConfigCredentialsHelper{
   261  				Args: []string{"bar", "baz"},
   262  			},
   263  		},
   264  	}
   265  
   266  	if !reflect.DeepEqual(got, want) {
   267  		t.Errorf("wrong result\ngot:  %swant: %s", spew.Sdump(got), spew.Sdump(want))
   268  	}
   269  }
   270  
   271  func TestConfigValidate(t *testing.T) {
   272  	tests := map[string]struct {
   273  		Config    *Config
   274  		DiagCount int
   275  	}{
   276  		"nil": {
   277  			nil,
   278  			0,
   279  		},
   280  		"empty": {
   281  			&Config{},
   282  			0,
   283  		},
   284  		"host good": {
   285  			&Config{
   286  				Hosts: map[string]*ConfigHost{
   287  					"example.com": {},
   288  				},
   289  			},
   290  			0,
   291  		},
   292  		"host with bad hostname": {
   293  			&Config{
   294  				Hosts: map[string]*ConfigHost{
   295  					"example..com": {},
   296  				},
   297  			},
   298  			1, // host block has invalid hostname
   299  		},
   300  		"credentials good": {
   301  			&Config{
   302  				Credentials: map[string]map[string]interface{}{
   303  					"example.com": map[string]interface{}{
   304  						"token": "foo",
   305  					},
   306  				},
   307  			},
   308  			0,
   309  		},
   310  		"credentials with bad hostname": {
   311  			&Config{
   312  				Credentials: map[string]map[string]interface{}{
   313  					"example..com": map[string]interface{}{
   314  						"token": "foo",
   315  					},
   316  				},
   317  			},
   318  			1, // credentials block has invalid hostname
   319  		},
   320  		"credentials helper good": {
   321  			&Config{
   322  				CredentialsHelpers: map[string]*ConfigCredentialsHelper{
   323  					"foo": {},
   324  				},
   325  			},
   326  			0,
   327  		},
   328  		"credentials helper too many": {
   329  			&Config{
   330  				CredentialsHelpers: map[string]*ConfigCredentialsHelper{
   331  					"foo": {},
   332  					"bar": {},
   333  				},
   334  			},
   335  			1, // no more than one credentials_helper block allowed
   336  		},
   337  		"provider_installation good none": {
   338  			&Config{
   339  				ProviderInstallation: nil,
   340  			},
   341  			0,
   342  		},
   343  		"provider_installation good one": {
   344  			&Config{
   345  				ProviderInstallation: []*ProviderInstallation{
   346  					{},
   347  				},
   348  			},
   349  			0,
   350  		},
   351  		"provider_installation too many": {
   352  			&Config{
   353  				ProviderInstallation: []*ProviderInstallation{
   354  					{},
   355  					{},
   356  				},
   357  			},
   358  			1, // no more than one provider_installation block allowed
   359  		},
   360  		"plugin_cache_dir does not exist": {
   361  			&Config{
   362  				PluginCacheDir: "fake",
   363  			},
   364  			1, // The specified plugin cache dir %s cannot be opened
   365  		},
   366  	}
   367  
   368  	for name, test := range tests {
   369  		t.Run(name, func(t *testing.T) {
   370  			diags := test.Config.Validate()
   371  			if len(diags) != test.DiagCount {
   372  				t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount)
   373  				for _, diag := range diags {
   374  					t.Logf("- %#v", diag.Description())
   375  				}
   376  			}
   377  		})
   378  	}
   379  }
   380  
   381  func TestConfig_Merge(t *testing.T) {
   382  	c1 := &Config{
   383  		Providers: map[string]string{
   384  			"foo": "bar",
   385  			"bar": "blah",
   386  		},
   387  		Provisioners: map[string]string{
   388  			"local":  "local",
   389  			"remote": "bad",
   390  		},
   391  		Hosts: map[string]*ConfigHost{
   392  			"example.com": {
   393  				Services: map[string]interface{}{
   394  					"modules.v1": "http://example.com/",
   395  				},
   396  			},
   397  		},
   398  		Credentials: map[string]map[string]interface{}{
   399  			"foo": {
   400  				"bar": "baz",
   401  			},
   402  		},
   403  		CredentialsHelpers: map[string]*ConfigCredentialsHelper{
   404  			"buz": {},
   405  		},
   406  		ProviderInstallation: []*ProviderInstallation{
   407  			{
   408  				Methods: []*ProviderInstallationMethod{
   409  					{Location: ProviderInstallationFilesystemMirror("a")},
   410  					{Location: ProviderInstallationFilesystemMirror("b")},
   411  				},
   412  			},
   413  			{
   414  				Methods: []*ProviderInstallationMethod{
   415  					{Location: ProviderInstallationFilesystemMirror("c")},
   416  				},
   417  			},
   418  		},
   419  	}
   420  
   421  	c2 := &Config{
   422  		Providers: map[string]string{
   423  			"bar": "baz",
   424  			"baz": "what",
   425  		},
   426  		Provisioners: map[string]string{
   427  			"remote": "remote",
   428  		},
   429  		Hosts: map[string]*ConfigHost{
   430  			"example.net": {
   431  				Services: map[string]interface{}{
   432  					"modules.v1": "https://example.net/",
   433  				},
   434  			},
   435  		},
   436  		Credentials: map[string]map[string]interface{}{
   437  			"fee": {
   438  				"bur": "bez",
   439  			},
   440  		},
   441  		CredentialsHelpers: map[string]*ConfigCredentialsHelper{
   442  			"biz": {},
   443  		},
   444  		ProviderInstallation: []*ProviderInstallation{
   445  			{
   446  				Methods: []*ProviderInstallationMethod{
   447  					{Location: ProviderInstallationFilesystemMirror("d")},
   448  				},
   449  			},
   450  		},
   451  		PluginCacheMayBreakDependencyLockFile: true,
   452  	}
   453  
   454  	expected := &Config{
   455  		Providers: map[string]string{
   456  			"foo": "bar",
   457  			"bar": "baz",
   458  			"baz": "what",
   459  		},
   460  		Provisioners: map[string]string{
   461  			"local":  "local",
   462  			"remote": "remote",
   463  		},
   464  		Hosts: map[string]*ConfigHost{
   465  			"example.com": {
   466  				Services: map[string]interface{}{
   467  					"modules.v1": "http://example.com/",
   468  				},
   469  			},
   470  			"example.net": {
   471  				Services: map[string]interface{}{
   472  					"modules.v1": "https://example.net/",
   473  				},
   474  			},
   475  		},
   476  		Credentials: map[string]map[string]interface{}{
   477  			"foo": {
   478  				"bar": "baz",
   479  			},
   480  			"fee": {
   481  				"bur": "bez",
   482  			},
   483  		},
   484  		CredentialsHelpers: map[string]*ConfigCredentialsHelper{
   485  			"buz": {},
   486  			"biz": {},
   487  		},
   488  		ProviderInstallation: []*ProviderInstallation{
   489  			{
   490  				Methods: []*ProviderInstallationMethod{
   491  					{Location: ProviderInstallationFilesystemMirror("a")},
   492  					{Location: ProviderInstallationFilesystemMirror("b")},
   493  				},
   494  			},
   495  			{
   496  				Methods: []*ProviderInstallationMethod{
   497  					{Location: ProviderInstallationFilesystemMirror("c")},
   498  				},
   499  			},
   500  			{
   501  				Methods: []*ProviderInstallationMethod{
   502  					{Location: ProviderInstallationFilesystemMirror("d")},
   503  				},
   504  			},
   505  		},
   506  		PluginCacheMayBreakDependencyLockFile: true,
   507  	}
   508  
   509  	actual := c1.Merge(c2)
   510  	if diff := cmp.Diff(expected, actual); diff != "" {
   511  		t.Fatalf("wrong result\n%s", diff)
   512  	}
   513  }