github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli/config/configfile/file_test.go (about)

     1  package configfile
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"os"
     8  	"testing"
     9  
    10  	"github.com/khulnasoft/cli/cli/config/credentials"
    11  	"github.com/khulnasoft/cli/cli/config/types"
    12  	"gotest.tools/v3/assert"
    13  	is "gotest.tools/v3/assert/cmp"
    14  	"gotest.tools/v3/fs"
    15  	"gotest.tools/v3/golden"
    16  )
    17  
    18  func TestEncodeAuth(t *testing.T) {
    19  	newAuthConfig := &types.AuthConfig{Username: "ken", Password: "test"}
    20  	authStr := encodeAuth(newAuthConfig)
    21  
    22  	expected := &types.AuthConfig{}
    23  	var err error
    24  	expected.Username, expected.Password, err = decodeAuth(authStr)
    25  	assert.NilError(t, err)
    26  	assert.Check(t, is.DeepEqual(expected, newAuthConfig))
    27  }
    28  
    29  func TestProxyConfig(t *testing.T) {
    30  	var (
    31  		httpProxy  = "http://proxy.mycorp.example.com:3128"
    32  		httpsProxy = "https://user:password@proxy.mycorp.example.com:3129"
    33  		ftpProxy   = "http://ftpproxy.mycorp.example.com:21"
    34  		noProxy    = "*.intra.mycorp.example.com"
    35  		allProxy   = "socks://example.com:1234"
    36  
    37  		defaultProxyConfig = ProxyConfig{
    38  			HTTPProxy:  httpProxy,
    39  			HTTPSProxy: httpsProxy,
    40  			FTPProxy:   ftpProxy,
    41  			NoProxy:    noProxy,
    42  			AllProxy:   allProxy,
    43  		}
    44  	)
    45  
    46  	cfg := ConfigFile{
    47  		Proxies: map[string]ProxyConfig{
    48  			"default": defaultProxyConfig,
    49  		},
    50  	}
    51  
    52  	proxyConfig := cfg.ParseProxyConfig("/var/run/docker.sock", nil)
    53  	expected := map[string]*string{
    54  		"HTTP_PROXY":  &httpProxy,
    55  		"http_proxy":  &httpProxy,
    56  		"HTTPS_PROXY": &httpsProxy,
    57  		"https_proxy": &httpsProxy,
    58  		"FTP_PROXY":   &ftpProxy,
    59  		"ftp_proxy":   &ftpProxy,
    60  		"NO_PROXY":    &noProxy,
    61  		"no_proxy":    &noProxy,
    62  		"ALL_PROXY":   &allProxy,
    63  		"all_proxy":   &allProxy,
    64  	}
    65  	assert.Check(t, is.DeepEqual(expected, proxyConfig))
    66  }
    67  
    68  func TestProxyConfigOverride(t *testing.T) {
    69  	var (
    70  		httpProxy         = "http://proxy.mycorp.example.com:3128"
    71  		httpProxyOverride = "http://proxy.example.com:3128"
    72  		httpsProxy        = "https://user:password@proxy.mycorp.example.com:3129"
    73  		ftpProxy          = "http://ftpproxy.mycorp.example.com:21"
    74  		noProxy           = "*.intra.mycorp.example.com"
    75  		noProxyOverride   = ""
    76  
    77  		defaultProxyConfig = ProxyConfig{
    78  			HTTPProxy:  httpProxy,
    79  			HTTPSProxy: httpsProxy,
    80  			FTPProxy:   ftpProxy,
    81  			NoProxy:    noProxy,
    82  		}
    83  	)
    84  
    85  	cfg := ConfigFile{
    86  		Proxies: map[string]ProxyConfig{
    87  			"default": defaultProxyConfig,
    88  		},
    89  	}
    90  
    91  	clone := func(s string) *string {
    92  		s2 := s
    93  		return &s2
    94  	}
    95  
    96  	ropts := map[string]*string{
    97  		"HTTP_PROXY": clone(httpProxyOverride),
    98  		"NO_PROXY":   clone(noProxyOverride),
    99  	}
   100  	proxyConfig := cfg.ParseProxyConfig("/var/run/docker.sock", ropts)
   101  	expected := map[string]*string{
   102  		"HTTP_PROXY":  &httpProxyOverride,
   103  		"http_proxy":  &httpProxy,
   104  		"HTTPS_PROXY": &httpsProxy,
   105  		"https_proxy": &httpsProxy,
   106  		"FTP_PROXY":   &ftpProxy,
   107  		"ftp_proxy":   &ftpProxy,
   108  		"NO_PROXY":    &noProxyOverride,
   109  		"no_proxy":    &noProxy,
   110  	}
   111  	assert.Check(t, is.DeepEqual(expected, proxyConfig))
   112  }
   113  
   114  func TestProxyConfigPerHost(t *testing.T) {
   115  	var (
   116  		httpProxy  = "http://proxy.mycorp.example.com:3128"
   117  		httpsProxy = "https://user:password@proxy.mycorp.example.com:3129"
   118  		ftpProxy   = "http://ftpproxy.mycorp.example.com:21"
   119  		noProxy    = "*.intra.mycorp.example.com"
   120  
   121  		extHTTPProxy  = "http://proxy.example.com:3128"
   122  		extHTTPSProxy = "https://user:password@proxy.example.com:3129"
   123  		extFTPProxy   = "http://ftpproxy.example.com:21"
   124  		extNoProxy    = "*.intra.example.com"
   125  
   126  		defaultProxyConfig = ProxyConfig{
   127  			HTTPProxy:  httpProxy,
   128  			HTTPSProxy: httpsProxy,
   129  			FTPProxy:   ftpProxy,
   130  			NoProxy:    noProxy,
   131  		}
   132  
   133  		externalProxyConfig = ProxyConfig{
   134  			HTTPProxy:  extHTTPProxy,
   135  			HTTPSProxy: extHTTPSProxy,
   136  			FTPProxy:   extFTPProxy,
   137  			NoProxy:    extNoProxy,
   138  		}
   139  	)
   140  
   141  	cfg := ConfigFile{
   142  		Proxies: map[string]ProxyConfig{
   143  			"default":                       defaultProxyConfig,
   144  			"tcp://example.docker.com:2376": externalProxyConfig,
   145  		},
   146  	}
   147  
   148  	proxyConfig := cfg.ParseProxyConfig("tcp://example.docker.com:2376", nil)
   149  	expected := map[string]*string{
   150  		"HTTP_PROXY":  &extHTTPProxy,
   151  		"http_proxy":  &extHTTPProxy,
   152  		"HTTPS_PROXY": &extHTTPSProxy,
   153  		"https_proxy": &extHTTPSProxy,
   154  		"FTP_PROXY":   &extFTPProxy,
   155  		"ftp_proxy":   &extFTPProxy,
   156  		"NO_PROXY":    &extNoProxy,
   157  		"no_proxy":    &extNoProxy,
   158  	}
   159  	assert.Check(t, is.DeepEqual(expected, proxyConfig))
   160  }
   161  
   162  func TestConfigFile(t *testing.T) {
   163  	configFilename := "configFilename"
   164  	configFile := New(configFilename)
   165  
   166  	assert.Check(t, is.Equal(configFilename, configFile.Filename))
   167  }
   168  
   169  type mockNativeStore struct {
   170  	GetAllCallCount  int
   171  	authConfigs      map[string]types.AuthConfig
   172  	authConfigErrors map[string]error
   173  }
   174  
   175  func (c *mockNativeStore) Erase(registryHostname string) error {
   176  	delete(c.authConfigs, registryHostname)
   177  	return nil
   178  }
   179  
   180  func (c *mockNativeStore) Get(registryHostname string) (types.AuthConfig, error) {
   181  	return c.authConfigs[registryHostname], c.authConfigErrors[registryHostname]
   182  }
   183  
   184  func (c *mockNativeStore) GetAll() (map[string]types.AuthConfig, error) {
   185  	c.GetAllCallCount++
   186  	return c.authConfigs, nil
   187  }
   188  
   189  func (c *mockNativeStore) Store(_ types.AuthConfig) error {
   190  	return nil
   191  }
   192  
   193  // make sure it satisfies the interface
   194  var _ credentials.Store = (*mockNativeStore)(nil)
   195  
   196  func NewMockNativeStore(authConfigs map[string]types.AuthConfig, authConfigErrors map[string]error) credentials.Store {
   197  	return &mockNativeStore{authConfigs: authConfigs, authConfigErrors: authConfigErrors}
   198  }
   199  
   200  func TestGetAllCredentialsFileStoreOnly(t *testing.T) {
   201  	configFile := New("filename")
   202  	exampleAuth := types.AuthConfig{
   203  		Username: "user",
   204  		Password: "pass",
   205  	}
   206  	configFile.AuthConfigs["example.com/foo"] = exampleAuth
   207  
   208  	authConfigs, err := configFile.GetAllCredentials()
   209  	assert.NilError(t, err)
   210  
   211  	expected := make(map[string]types.AuthConfig)
   212  	expected["example.com/foo"] = exampleAuth
   213  	assert.Check(t, is.DeepEqual(expected, authConfigs))
   214  }
   215  
   216  func TestGetAllCredentialsCredsStore(t *testing.T) {
   217  	configFile := New("filename")
   218  	configFile.CredentialsStore = "test_creds_store"
   219  	testRegistryHostname := "example.com"
   220  	expectedAuth := types.AuthConfig{
   221  		Username: "user",
   222  		Password: "pass",
   223  	}
   224  
   225  	testCredsStore := NewMockNativeStore(map[string]types.AuthConfig{testRegistryHostname: expectedAuth}, nil)
   226  
   227  	tmpNewNativeStore := newNativeStore
   228  	defer func() { newNativeStore = tmpNewNativeStore }()
   229  	newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store {
   230  		return testCredsStore
   231  	}
   232  
   233  	authConfigs, err := configFile.GetAllCredentials()
   234  	assert.NilError(t, err)
   235  
   236  	expected := make(map[string]types.AuthConfig)
   237  	expected[testRegistryHostname] = expectedAuth
   238  	assert.Check(t, is.DeepEqual(expected, authConfigs))
   239  	assert.Check(t, is.Equal(1, testCredsStore.(*mockNativeStore).GetAllCallCount))
   240  }
   241  
   242  func TestGetAllCredentialsCredStoreErrorHandling(t *testing.T) {
   243  	const (
   244  		workingHelperRegistryHostname = "working-helper.example.com"
   245  		brokenHelperRegistryHostname  = "broken-helper.example.com"
   246  	)
   247  	configFile := New("filename")
   248  	configFile.CredentialHelpers = map[string]string{
   249  		workingHelperRegistryHostname: "cred_helper",
   250  		brokenHelperRegistryHostname:  "broken_cred_helper",
   251  	}
   252  	expectedAuth := types.AuthConfig{
   253  		Username: "username",
   254  		Password: "pass",
   255  	}
   256  	// configure the mock store to throw an error
   257  	// when calling the helper for this registry
   258  	authErrors := map[string]error{
   259  		brokenHelperRegistryHostname: errors.New("an error"),
   260  	}
   261  
   262  	testCredsStore := NewMockNativeStore(map[string]types.AuthConfig{
   263  		workingHelperRegistryHostname: expectedAuth,
   264  		// configure an auth entry for the "broken" credential
   265  		// helper that will throw an error
   266  		brokenHelperRegistryHostname: {},
   267  	}, authErrors)
   268  
   269  	tmpNewNativeStore := newNativeStore
   270  	defer func() { newNativeStore = tmpNewNativeStore }()
   271  	newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store {
   272  		return testCredsStore
   273  	}
   274  
   275  	authConfigs, err := configFile.GetAllCredentials()
   276  
   277  	// make sure we're still returning the expected credentials
   278  	// and skipping the ones throwing an error
   279  	assert.NilError(t, err)
   280  	assert.Check(t, is.Equal(1, len(authConfigs)))
   281  	assert.Check(t, is.DeepEqual(expectedAuth, authConfigs[workingHelperRegistryHostname]))
   282  }
   283  
   284  func TestGetAllCredentialsCredHelper(t *testing.T) {
   285  	const (
   286  		testCredHelperSuffix                = "test_cred_helper"
   287  		testCredHelperRegistryHostname      = "credhelper.com"
   288  		testExtraCredHelperRegistryHostname = "somethingweird.com"
   289  	)
   290  
   291  	unexpectedCredHelperAuth := types.AuthConfig{
   292  		Username: "file_store_user",
   293  		Password: "file_store_pass",
   294  	}
   295  	expectedCredHelperAuth := types.AuthConfig{
   296  		Username: "cred_helper_user",
   297  		Password: "cred_helper_pass",
   298  	}
   299  
   300  	configFile := New("filename")
   301  	configFile.CredentialHelpers = map[string]string{testCredHelperRegistryHostname: testCredHelperSuffix}
   302  
   303  	testCredHelper := NewMockNativeStore(map[string]types.AuthConfig{
   304  		testCredHelperRegistryHostname: expectedCredHelperAuth,
   305  		// Add in an extra auth entry which doesn't appear in CredentialHelpers section of the configFile.
   306  		// This verifies that only explicitly configured registries are being requested from the cred helpers.
   307  		testExtraCredHelperRegistryHostname: unexpectedCredHelperAuth,
   308  	}, nil)
   309  
   310  	tmpNewNativeStore := newNativeStore
   311  	defer func() { newNativeStore = tmpNewNativeStore }()
   312  	newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store {
   313  		return testCredHelper
   314  	}
   315  
   316  	authConfigs, err := configFile.GetAllCredentials()
   317  	assert.NilError(t, err)
   318  
   319  	expected := make(map[string]types.AuthConfig)
   320  	expected[testCredHelperRegistryHostname] = expectedCredHelperAuth
   321  	assert.Check(t, is.DeepEqual(expected, authConfigs))
   322  	assert.Check(t, is.Equal(0, testCredHelper.(*mockNativeStore).GetAllCallCount))
   323  }
   324  
   325  func TestGetAllCredentialsFileStoreAndCredHelper(t *testing.T) {
   326  	const (
   327  		testFileStoreRegistryHostname  = "example.com"
   328  		testCredHelperSuffix           = "test_cred_helper"
   329  		testCredHelperRegistryHostname = "credhelper.com"
   330  	)
   331  
   332  	expectedFileStoreAuth := types.AuthConfig{
   333  		Username: "file_store_user",
   334  		Password: "file_store_pass",
   335  	}
   336  	expectedCredHelperAuth := types.AuthConfig{
   337  		Username: "cred_helper_user",
   338  		Password: "cred_helper_pass",
   339  	}
   340  
   341  	configFile := New("filename")
   342  	configFile.CredentialHelpers = map[string]string{testCredHelperRegistryHostname: testCredHelperSuffix}
   343  	configFile.AuthConfigs[testFileStoreRegistryHostname] = expectedFileStoreAuth
   344  
   345  	testCredHelper := NewMockNativeStore(map[string]types.AuthConfig{testCredHelperRegistryHostname: expectedCredHelperAuth}, nil)
   346  
   347  	newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store {
   348  		return testCredHelper
   349  	}
   350  
   351  	tmpNewNativeStore := newNativeStore
   352  	defer func() { newNativeStore = tmpNewNativeStore }()
   353  	authConfigs, err := configFile.GetAllCredentials()
   354  	assert.NilError(t, err)
   355  
   356  	expected := make(map[string]types.AuthConfig)
   357  	expected[testFileStoreRegistryHostname] = expectedFileStoreAuth
   358  	expected[testCredHelperRegistryHostname] = expectedCredHelperAuth
   359  	assert.Check(t, is.DeepEqual(expected, authConfigs))
   360  	assert.Check(t, is.Equal(0, testCredHelper.(*mockNativeStore).GetAllCallCount))
   361  }
   362  
   363  func TestGetAllCredentialsCredStoreAndCredHelper(t *testing.T) {
   364  	const (
   365  		testCredStoreSuffix            = "test_creds_store"
   366  		testCredStoreRegistryHostname  = "credstore.com"
   367  		testCredHelperSuffix           = "test_cred_helper"
   368  		testCredHelperRegistryHostname = "credhelper.com"
   369  	)
   370  
   371  	configFile := New("filename")
   372  	configFile.CredentialsStore = testCredStoreSuffix
   373  	configFile.CredentialHelpers = map[string]string{testCredHelperRegistryHostname: testCredHelperSuffix}
   374  
   375  	expectedCredStoreAuth := types.AuthConfig{
   376  		Username: "cred_store_user",
   377  		Password: "cred_store_pass",
   378  	}
   379  	expectedCredHelperAuth := types.AuthConfig{
   380  		Username: "cred_helper_user",
   381  		Password: "cred_helper_pass",
   382  	}
   383  
   384  	testCredHelper := NewMockNativeStore(map[string]types.AuthConfig{testCredHelperRegistryHostname: expectedCredHelperAuth}, nil)
   385  	testCredsStore := NewMockNativeStore(map[string]types.AuthConfig{testCredStoreRegistryHostname: expectedCredStoreAuth}, nil)
   386  
   387  	tmpNewNativeStore := newNativeStore
   388  	defer func() { newNativeStore = tmpNewNativeStore }()
   389  	newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store {
   390  		if helperSuffix == testCredHelperSuffix {
   391  			return testCredHelper
   392  		}
   393  		return testCredsStore
   394  	}
   395  
   396  	authConfigs, err := configFile.GetAllCredentials()
   397  	assert.NilError(t, err)
   398  
   399  	expected := make(map[string]types.AuthConfig)
   400  	expected[testCredStoreRegistryHostname] = expectedCredStoreAuth
   401  	expected[testCredHelperRegistryHostname] = expectedCredHelperAuth
   402  	assert.Check(t, is.DeepEqual(expected, authConfigs))
   403  	assert.Check(t, is.Equal(1, testCredsStore.(*mockNativeStore).GetAllCallCount))
   404  	assert.Check(t, is.Equal(0, testCredHelper.(*mockNativeStore).GetAllCallCount))
   405  }
   406  
   407  func TestGetAllCredentialsCredHelperOverridesDefaultStore(t *testing.T) {
   408  	const (
   409  		testCredStoreSuffix  = "test_creds_store"
   410  		testCredHelperSuffix = "test_cred_helper"
   411  		testRegistryHostname = "example.com"
   412  	)
   413  
   414  	configFile := New("filename")
   415  	configFile.CredentialsStore = testCredStoreSuffix
   416  	configFile.CredentialHelpers = map[string]string{testRegistryHostname: testCredHelperSuffix}
   417  
   418  	unexpectedCredStoreAuth := types.AuthConfig{
   419  		Username: "cred_store_user",
   420  		Password: "cred_store_pass",
   421  	}
   422  	expectedCredHelperAuth := types.AuthConfig{
   423  		Username: "cred_helper_user",
   424  		Password: "cred_helper_pass",
   425  	}
   426  
   427  	testCredHelper := NewMockNativeStore(map[string]types.AuthConfig{testRegistryHostname: expectedCredHelperAuth}, nil)
   428  	testCredsStore := NewMockNativeStore(map[string]types.AuthConfig{testRegistryHostname: unexpectedCredStoreAuth}, nil)
   429  
   430  	tmpNewNativeStore := newNativeStore
   431  	defer func() { newNativeStore = tmpNewNativeStore }()
   432  	newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store {
   433  		if helperSuffix == testCredHelperSuffix {
   434  			return testCredHelper
   435  		}
   436  		return testCredsStore
   437  	}
   438  
   439  	authConfigs, err := configFile.GetAllCredentials()
   440  	assert.NilError(t, err)
   441  
   442  	expected := make(map[string]types.AuthConfig)
   443  	expected[testRegistryHostname] = expectedCredHelperAuth
   444  	assert.Check(t, is.DeepEqual(expected, authConfigs))
   445  	assert.Check(t, is.Equal(1, testCredsStore.(*mockNativeStore).GetAllCallCount))
   446  	assert.Check(t, is.Equal(0, testCredHelper.(*mockNativeStore).GetAllCallCount))
   447  }
   448  
   449  func TestLoadFromReaderWithUsernamePassword(t *testing.T) {
   450  	configFile := New("test-load")
   451  	defer os.Remove("test-load")
   452  
   453  	want := types.AuthConfig{
   454  		Username: "user",
   455  		Password: "pass",
   456  	}
   457  
   458  	for _, tc := range []types.AuthConfig{
   459  		want,
   460  		{
   461  			Auth: encodeAuth(&want),
   462  		},
   463  	} {
   464  		cf := ConfigFile{
   465  			AuthConfigs: map[string]types.AuthConfig{
   466  				"example.com/foo": tc,
   467  			},
   468  		}
   469  
   470  		b, err := json.Marshal(cf)
   471  		assert.NilError(t, err)
   472  
   473  		err = configFile.LoadFromReader(bytes.NewReader(b))
   474  		assert.NilError(t, err)
   475  
   476  		got, err := configFile.GetAuthConfig("example.com/foo")
   477  		assert.NilError(t, err)
   478  
   479  		assert.Check(t, is.DeepEqual(want.Username, got.Username))
   480  		assert.Check(t, is.DeepEqual(want.Password, got.Password))
   481  	}
   482  }
   483  
   484  func TestSave(t *testing.T) {
   485  	configFile := New("test-save")
   486  	defer os.Remove("test-save")
   487  	err := configFile.Save()
   488  	assert.NilError(t, err)
   489  	cfg, err := os.ReadFile("test-save")
   490  	assert.NilError(t, err)
   491  	assert.Equal(t, string(cfg), `{
   492  	"auths": {}
   493  }`)
   494  }
   495  
   496  func TestSaveCustomHTTPHeaders(t *testing.T) {
   497  	configFile := New(t.Name())
   498  	defer os.Remove(t.Name())
   499  	configFile.HTTPHeaders["CUSTOM-HEADER"] = "custom-value"
   500  	configFile.HTTPHeaders["User-Agent"] = "user-agent 1"
   501  	configFile.HTTPHeaders["user-agent"] = "user-agent 2"
   502  	err := configFile.Save()
   503  	assert.NilError(t, err)
   504  	cfg, err := os.ReadFile(t.Name())
   505  	assert.NilError(t, err)
   506  	assert.Equal(t, string(cfg), `{
   507  	"auths": {},
   508  	"HttpHeaders": {
   509  		"CUSTOM-HEADER": "custom-value"
   510  	}
   511  }`)
   512  }
   513  
   514  func TestSaveWithSymlink(t *testing.T) {
   515  	dir := fs.NewDir(t, t.Name(), fs.WithFile("real-config.json", `{}`))
   516  	defer dir.Remove()
   517  
   518  	symLink := dir.Join("config.json")
   519  	realFile := dir.Join("real-config.json")
   520  	err := os.Symlink(realFile, symLink)
   521  	assert.NilError(t, err)
   522  
   523  	configFile := New(symLink)
   524  
   525  	err = configFile.Save()
   526  	assert.NilError(t, err)
   527  
   528  	fi, err := os.Lstat(symLink)
   529  	assert.NilError(t, err)
   530  	assert.Assert(t, fi.Mode()&os.ModeSymlink != 0, "expected %s to be a symlink", symLink)
   531  
   532  	cfg, err := os.ReadFile(symLink)
   533  	assert.NilError(t, err)
   534  	assert.Check(t, is.Equal(string(cfg), "{\n	\"auths\": {}\n}"))
   535  
   536  	cfg, err = os.ReadFile(realFile)
   537  	assert.NilError(t, err)
   538  	assert.Check(t, is.Equal(string(cfg), "{\n	\"auths\": {}\n}"))
   539  }
   540  
   541  func TestPluginConfig(t *testing.T) {
   542  	configFile := New("test-plugin")
   543  	defer os.Remove("test-plugin")
   544  
   545  	// Populate some initial values
   546  	configFile.SetPluginConfig("plugin1", "data1", "some string")
   547  	configFile.SetPluginConfig("plugin1", "data2", "42")
   548  	configFile.SetPluginConfig("plugin2", "data3", "some other string")
   549  
   550  	// Save a config file with some plugin config
   551  	err := configFile.Save()
   552  	assert.NilError(t, err)
   553  
   554  	// Read it back and check it has the expected content
   555  	cfg, err := os.ReadFile("test-plugin")
   556  	assert.NilError(t, err)
   557  	golden.Assert(t, string(cfg), "plugin-config.golden")
   558  
   559  	// Load it, resave and check again that the content is
   560  	// preserved through a load/save cycle.
   561  	configFile = New("test-plugin2")
   562  	defer os.Remove("test-plugin2")
   563  	assert.NilError(t, configFile.LoadFromReader(bytes.NewReader(cfg)))
   564  	err = configFile.Save()
   565  	assert.NilError(t, err)
   566  	cfg, err = os.ReadFile("test-plugin2")
   567  	assert.NilError(t, err)
   568  	golden.Assert(t, string(cfg), "plugin-config.golden")
   569  
   570  	// Check that the contents was reloaded properly
   571  	v, ok := configFile.PluginConfig("plugin1", "data1")
   572  	assert.Assert(t, ok)
   573  	assert.Equal(t, v, "some string")
   574  	v, ok = configFile.PluginConfig("plugin1", "data2")
   575  	assert.Assert(t, ok)
   576  	assert.Equal(t, v, "42")
   577  	v, ok = configFile.PluginConfig("plugin1", "data3")
   578  	assert.Assert(t, !ok)
   579  	assert.Equal(t, v, "")
   580  	v, ok = configFile.PluginConfig("plugin2", "data3")
   581  	assert.Assert(t, ok)
   582  	assert.Equal(t, v, "some other string")
   583  	v, ok = configFile.PluginConfig("plugin2", "data4")
   584  	assert.Assert(t, !ok)
   585  	assert.Equal(t, v, "")
   586  	v, ok = configFile.PluginConfig("plugin3", "data5")
   587  	assert.Assert(t, !ok)
   588  	assert.Equal(t, v, "")
   589  
   590  	// Add, remove and modify
   591  	configFile.SetPluginConfig("plugin1", "data1", "some replacement string") // replacing a key
   592  	configFile.SetPluginConfig("plugin1", "data2", "")                        // deleting a key
   593  	configFile.SetPluginConfig("plugin1", "data3", "some additional string")  // new key
   594  	configFile.SetPluginConfig("plugin2", "data3", "")                        // delete the whole plugin, since this was the only data
   595  	configFile.SetPluginConfig("plugin3", "data5", "a new plugin")            // add a new plugin
   596  
   597  	err = configFile.Save()
   598  	assert.NilError(t, err)
   599  
   600  	// Read it back and check it has the expected content again
   601  	cfg, err = os.ReadFile("test-plugin2")
   602  	assert.NilError(t, err)
   603  	golden.Assert(t, string(cfg), "plugin-config-2.golden")
   604  }