github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/config/configfile/configfile_test.go (about)

     1  package configfile
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"runtime"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/rclone/rclone/fs/config"
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  )
    15  
    16  var configData = `[one]
    17  type = number1
    18  fruit = potato
    19  
    20  [two]
    21  type = number2
    22  fruit = apple
    23  topping = nuts
    24  
    25  [three]
    26  type = number3
    27  fruit = banana
    28  
    29  `
    30  
    31  // Fill up a temporary config file with the testdata filename passed in
    32  func setConfigFile(t *testing.T, data string) func() {
    33  	out, err := os.CreateTemp("", "rclone-configfile-test")
    34  	require.NoError(t, err)
    35  	filePath := out.Name()
    36  
    37  	_, err = out.Write([]byte(data))
    38  	require.NoError(t, err)
    39  
    40  	require.NoError(t, out.Close())
    41  
    42  	old := config.GetConfigPath()
    43  	assert.NoError(t, config.SetConfigPath(filePath))
    44  	return func() {
    45  		assert.NoError(t, config.SetConfigPath(old))
    46  		_ = os.Remove(filePath)
    47  	}
    48  }
    49  
    50  // toUnix converts \r\n to \n in buf
    51  func toUnix(buf string) string {
    52  	if runtime.GOOS == "windows" {
    53  		return strings.ReplaceAll(buf, "\r\n", "\n")
    54  	}
    55  	return buf
    56  }
    57  
    58  func TestConfigFile(t *testing.T) {
    59  	defer setConfigFile(t, configData)()
    60  	data := &Storage{}
    61  
    62  	require.NoError(t, data.Load())
    63  
    64  	t.Run("Read", func(t *testing.T) {
    65  		t.Run("Serialize", func(t *testing.T) {
    66  			buf, err := data.Serialize()
    67  			require.NoError(t, err)
    68  			assert.Equal(t, configData, toUnix(buf))
    69  		})
    70  		t.Run("HasSection", func(t *testing.T) {
    71  			assert.True(t, data.HasSection("one"))
    72  			assert.False(t, data.HasSection("missing"))
    73  		})
    74  		t.Run("GetSectionList", func(t *testing.T) {
    75  			assert.Equal(t, []string{
    76  				"one",
    77  				"two",
    78  				"three",
    79  			}, data.GetSectionList())
    80  		})
    81  		t.Run("GetKeyList", func(t *testing.T) {
    82  			assert.Equal(t, []string{
    83  				"type",
    84  				"fruit",
    85  				"topping",
    86  			}, data.GetKeyList("two"))
    87  			assert.Equal(t, []string(nil), data.GetKeyList("unicorn"))
    88  		})
    89  		t.Run("GetValue", func(t *testing.T) {
    90  			value, ok := data.GetValue("one", "type")
    91  			assert.True(t, ok)
    92  			assert.Equal(t, "number1", value)
    93  			value, ok = data.GetValue("three", "fruit")
    94  			assert.True(t, ok)
    95  			assert.Equal(t, "banana", value)
    96  			value, ok = data.GetValue("one", "typeX")
    97  			assert.False(t, ok)
    98  			assert.Equal(t, "", value)
    99  			value, ok = data.GetValue("threeX", "fruit")
   100  			assert.False(t, ok)
   101  			assert.Equal(t, "", value)
   102  		})
   103  	})
   104  
   105  	//defer setConfigFile(configData)()
   106  
   107  	t.Run("Write", func(t *testing.T) {
   108  		t.Run("SetValue", func(t *testing.T) {
   109  			data.SetValue("one", "extra", "42")
   110  			data.SetValue("two", "fruit", "acorn")
   111  
   112  			buf, err := data.Serialize()
   113  			require.NoError(t, err)
   114  			assert.Equal(t, `[one]
   115  type = number1
   116  fruit = potato
   117  extra = 42
   118  
   119  [two]
   120  type = number2
   121  fruit = acorn
   122  topping = nuts
   123  
   124  [three]
   125  type = number3
   126  fruit = banana
   127  
   128  `, toUnix(buf))
   129  			t.Run("DeleteKey", func(t *testing.T) {
   130  				data.DeleteKey("one", "type")
   131  				data.DeleteKey("two", "missing")
   132  				data.DeleteKey("three", "fruit")
   133  				buf, err := data.Serialize()
   134  				require.NoError(t, err)
   135  				assert.Equal(t, `[one]
   136  fruit = potato
   137  extra = 42
   138  
   139  [two]
   140  type = number2
   141  fruit = acorn
   142  topping = nuts
   143  
   144  [three]
   145  type = number3
   146  
   147  `, toUnix(buf))
   148  				t.Run("DeleteSection", func(t *testing.T) {
   149  					data.DeleteSection("two")
   150  					data.DeleteSection("missing")
   151  					buf, err := data.Serialize()
   152  					require.NoError(t, err)
   153  					assert.Equal(t, `[one]
   154  fruit = potato
   155  extra = 42
   156  
   157  [three]
   158  type = number3
   159  
   160  `, toUnix(buf))
   161  					t.Run("Save", func(t *testing.T) {
   162  						require.NoError(t, data.Save())
   163  						buf, err := os.ReadFile(config.GetConfigPath())
   164  						require.NoError(t, err)
   165  						assert.Equal(t, `[one]
   166  fruit = potato
   167  extra = 42
   168  
   169  [three]
   170  type = number3
   171  
   172  `, toUnix(string(buf)))
   173  					})
   174  				})
   175  			})
   176  		})
   177  	})
   178  }
   179  
   180  func TestConfigFileReload(t *testing.T) {
   181  	defer setConfigFile(t, configData)()
   182  	data := &Storage{}
   183  
   184  	require.NoError(t, data.Load())
   185  
   186  	value, ok := data.GetValue("three", "appended")
   187  	assert.False(t, ok)
   188  	assert.Equal(t, "", value)
   189  
   190  	// Now write a new value on the end
   191  	out, err := os.OpenFile(config.GetConfigPath(), os.O_APPEND|os.O_WRONLY, 0777)
   192  	require.NoError(t, err)
   193  	fmt.Fprintln(out, "appended = what magic")
   194  	require.NoError(t, out.Close())
   195  
   196  	// And check we magically reloaded it
   197  	value, ok = data.GetValue("three", "appended")
   198  	assert.True(t, ok)
   199  	assert.Equal(t, "what magic", value)
   200  }
   201  
   202  func TestConfigFileDoesNotExist(t *testing.T) {
   203  	defer setConfigFile(t, configData)()
   204  	data := &Storage{}
   205  
   206  	require.NoError(t, os.Remove(config.GetConfigPath()))
   207  
   208  	err := data.Load()
   209  	require.Equal(t, config.ErrorConfigFileNotFound, err)
   210  
   211  	// check that using data doesn't crash
   212  	value, ok := data.GetValue("three", "appended")
   213  	assert.False(t, ok)
   214  	assert.Equal(t, "", value)
   215  }
   216  
   217  func testConfigFileNoConfig(t *testing.T, configPath string) {
   218  	assert.NoError(t, config.SetConfigPath(configPath))
   219  	data := &Storage{}
   220  
   221  	err := data.Load()
   222  	require.Equal(t, config.ErrorConfigFileNotFound, err)
   223  
   224  	data.SetValue("one", "extra", "42")
   225  	value, ok := data.GetValue("one", "extra")
   226  	assert.True(t, ok)
   227  	assert.Equal(t, "42", value)
   228  
   229  	err = data.Save()
   230  	require.Error(t, err)
   231  }
   232  
   233  func TestConfigFileNoConfig(t *testing.T) {
   234  	old := config.GetConfigPath()
   235  	defer func() {
   236  		assert.NoError(t, config.SetConfigPath(old))
   237  	}()
   238  
   239  	t.Run("Empty", func(t *testing.T) {
   240  		testConfigFileNoConfig(t, "")
   241  	})
   242  	t.Run("NotFound", func(t *testing.T) {
   243  		testConfigFileNoConfig(t, "/notfound")
   244  	})
   245  }
   246  
   247  func TestConfigFileSave(t *testing.T) {
   248  	testDir := t.TempDir()
   249  	configPath := filepath.Join(testDir, "a", "b", "c", "configfile")
   250  
   251  	assert.NoError(t, config.SetConfigPath(configPath))
   252  	data := &Storage{}
   253  	require.Error(t, data.Load(), config.ErrorConfigFileNotFound)
   254  
   255  	t.Run("CreatesDirsAndFile", func(t *testing.T) {
   256  		err := data.Save()
   257  		require.NoError(t, err)
   258  		info, err := os.Stat(configPath)
   259  		require.NoError(t, err)
   260  		assert.False(t, info.IsDir())
   261  	})
   262  	t.Run("KeepsFileMode", func(t *testing.T) {
   263  		if runtime.GOOS != "linux" {
   264  			t.Skip("this is a Linux only test")
   265  		}
   266  		assert.NoError(t, os.Chmod(configPath, 0400)) // -r--------
   267  		defer func() {
   268  			_ = os.Chmod(configPath, 0644) // -rw-r--r--
   269  		}()
   270  		err := data.Save()
   271  		require.NoError(t, err)
   272  		info, err := os.Stat(configPath)
   273  		require.NoError(t, err)
   274  		assert.Equal(t, os.FileMode(0400), info.Mode().Perm())
   275  	})
   276  	t.Run("SucceedsEvenIfReadOnlyFile", func(t *testing.T) {
   277  		// Save succeeds even if file is read-only since it does not write directly to the file.
   278  		assert.NoError(t, os.Chmod(configPath, 0400)) // -r--------
   279  		defer func() {
   280  			_ = os.Chmod(configPath, 0644) // -rw-r--r--
   281  		}()
   282  		err := data.Save()
   283  		assert.NoError(t, err)
   284  	})
   285  	t.Run("FailsIfNotAccessToDir", func(t *testing.T) {
   286  		// Save fails if no access to the directory.
   287  		if runtime.GOOS != "linux" {
   288  			// On Windows the os.Chmod only affects the read-only attribute of files)
   289  			t.Skip("this is a Linux only test")
   290  		}
   291  		configDir := filepath.Dir(configPath)
   292  		assert.NoError(t, os.Chmod(configDir, 0400)) // -r--------
   293  		defer func() {
   294  			_ = os.Chmod(configDir, 0755) // -rwxr-xr-x
   295  		}()
   296  		err := data.Save()
   297  		require.Error(t, err)
   298  		assert.True(t, strings.HasPrefix(err.Error(), "failed to resolve config file path"))
   299  	})
   300  	t.Run("FailsIfNotAllowedToCreateNewFiles", func(t *testing.T) {
   301  		// Save fails if read-only access to the directory, since it needs to create temporary files in there.
   302  		if runtime.GOOS != "linux" {
   303  			// On Windows the os.Chmod only affects the read-only attribute of files)
   304  			t.Skip("this is a Linux only test")
   305  		}
   306  		configDir := filepath.Dir(configPath)
   307  		assert.NoError(t, os.Chmod(configDir, 0544)) // -r-xr--r--
   308  		defer func() {
   309  			_ = os.Chmod(configDir, 0755) // -rwxr-xr-x
   310  		}()
   311  		err := data.Save()
   312  		require.Error(t, err)
   313  		assert.True(t, strings.HasPrefix(err.Error(), "failed to create temp file for new config"))
   314  	})
   315  }
   316  
   317  func TestConfigFileSaveSymlinkAbsolute(t *testing.T) {
   318  	if runtime.GOOS != "linux" {
   319  		// Symlinks may require admin privileges on Windows and os.Symlink will then
   320  		// fail with "A required privilege is not held by the client."
   321  		t.Skip("this is a Linux only test")
   322  	}
   323  	testDir := t.TempDir()
   324  	linkDir := filepath.Join(testDir, "a")
   325  	err := os.Mkdir(linkDir, os.ModePerm)
   326  	require.NoError(t, err)
   327  
   328  	testSymlink := func(t *testing.T, link string, target string, resolvedTarget string) {
   329  		err = os.Symlink(target, link)
   330  		require.NoError(t, err)
   331  		defer func() {
   332  			_ = os.Remove(link)
   333  		}()
   334  
   335  		assert.NoError(t, config.SetConfigPath(link))
   336  		data := &Storage{}
   337  		require.Error(t, data.Load(), config.ErrorConfigFileNotFound)
   338  
   339  		err = data.Save()
   340  		require.NoError(t, err)
   341  
   342  		info, err := os.Lstat(link)
   343  		require.NoError(t, err)
   344  		assert.True(t, info.Mode()&os.ModeSymlink != 0)
   345  		assert.False(t, info.IsDir())
   346  
   347  		info, err = os.Lstat(resolvedTarget)
   348  		require.NoError(t, err)
   349  		assert.False(t, info.IsDir())
   350  	}
   351  
   352  	t.Run("Absolute", func(t *testing.T) {
   353  		link := filepath.Join(linkDir, "configfilelink")
   354  		target := filepath.Join(testDir, "b", "configfiletarget")
   355  		testSymlink(t, link, target, target)
   356  	})
   357  	t.Run("Relative", func(t *testing.T) {
   358  		link := filepath.Join(linkDir, "configfilelink")
   359  		target := filepath.Join("b", "c", "configfiletarget")
   360  		resolvedTarget := filepath.Join(filepath.Dir(link), target)
   361  		testSymlink(t, link, target, resolvedTarget)
   362  	})
   363  }