github.com/justincormack/cli@v0.0.0-20201215022714-831ebeae9675/cli/config/configfile/file_test.go (about)

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