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

     1  // These are in an external package because we need to import configfile
     2  //
     3  // Internal tests are in ui_internal_test.go
     4  
     5  package config_test
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"os"
    11  	"testing"
    12  
    13  	"github.com/rclone/rclone/fs"
    14  	"github.com/rclone/rclone/fs/config"
    15  	"github.com/rclone/rclone/fs/config/configfile"
    16  	"github.com/rclone/rclone/fs/config/obscure"
    17  	"github.com/rclone/rclone/fs/rc"
    18  	"github.com/stretchr/testify/assert"
    19  	"github.com/stretchr/testify/require"
    20  )
    21  
    22  var simpleOptions = []fs.Option{{
    23  	Name:       "bool",
    24  	Default:    false,
    25  	IsPassword: false,
    26  }, {
    27  	Name:       "pass",
    28  	Default:    "",
    29  	IsPassword: true,
    30  }}
    31  
    32  func testConfigFile(t *testing.T, options []fs.Option, configFileName string) func() {
    33  	ctx := context.Background()
    34  	ci := fs.GetConfig(ctx)
    35  	config.ClearConfigPassword()
    36  	_ = os.Unsetenv("_RCLONE_CONFIG_KEY_FILE")
    37  	_ = os.Unsetenv("RCLONE_CONFIG_PASS")
    38  	// create temp config file
    39  	tempFile, err := os.CreateTemp("", configFileName)
    40  	assert.NoError(t, err)
    41  	path := tempFile.Name()
    42  	assert.NoError(t, tempFile.Close())
    43  
    44  	// temporarily adapt configuration
    45  	oldOsStdout := os.Stdout
    46  	oldConfigPath := config.GetConfigPath()
    47  	oldConfig := *ci
    48  	oldConfigFile := config.Data()
    49  	oldReadLine := config.ReadLine
    50  	oldPassword := config.Password
    51  	os.Stdout = nil
    52  	assert.NoError(t, config.SetConfigPath(path))
    53  	ci = &fs.ConfigInfo{}
    54  
    55  	configfile.Install()
    56  	assert.Equal(t, []string{}, config.Data().GetSectionList())
    57  
    58  	// Fake a filesystem/backend
    59  	backendName := "config_test_remote"
    60  	if regInfo, _ := fs.Find(backendName); regInfo != nil {
    61  		regInfo.Options = options
    62  	} else {
    63  		fs.Register(&fs.RegInfo{
    64  			Name:    backendName,
    65  			Options: options,
    66  		})
    67  	}
    68  
    69  	// Undo the above (except registered backend, unfortunately)
    70  	return func() {
    71  		err := os.Remove(path)
    72  		assert.NoError(t, err)
    73  
    74  		os.Stdout = oldOsStdout
    75  		assert.NoError(t, config.SetConfigPath(oldConfigPath))
    76  		config.ReadLine = oldReadLine
    77  		config.Password = oldPassword
    78  		*ci = oldConfig
    79  		config.SetData(oldConfigFile)
    80  
    81  		_ = os.Unsetenv("_RCLONE_CONFIG_KEY_FILE")
    82  		_ = os.Unsetenv("RCLONE_CONFIG_PASS")
    83  	}
    84  }
    85  
    86  // makeReadLine makes a simple readLine which returns a fixed list of
    87  // strings
    88  func makeReadLine(answers []string) func() string {
    89  	i := 0
    90  	return func() string {
    91  		i = i + 1
    92  		return answers[i-1]
    93  	}
    94  }
    95  
    96  func TestCRUD(t *testing.T) {
    97  	defer testConfigFile(t, simpleOptions, "crud.conf")()
    98  	ctx := context.Background()
    99  
   100  	// script for creating remote
   101  	config.ReadLine = makeReadLine([]string{
   102  		"config_test_remote", // type
   103  		"true",               // bool value
   104  		"y",                  // type my own password
   105  		"secret",             // password
   106  		"secret",             // repeat
   107  		"n",                  // don't edit advanced config
   108  		"y",                  // looks good, save
   109  	})
   110  	require.NoError(t, config.NewRemote(ctx, "test"))
   111  
   112  	assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
   113  	assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
   114  	assert.Equal(t, "true", config.FileGet("test", "bool"))
   115  	assert.Equal(t, "secret", obscure.MustReveal(config.FileGet("test", "pass")))
   116  
   117  	// normal rename, test → asdf
   118  	config.ReadLine = makeReadLine([]string{
   119  		"asdf",
   120  		"asdf",
   121  		"asdf",
   122  	})
   123  	config.RenameRemote("test")
   124  
   125  	assert.Equal(t, []string{"asdf"}, config.Data().GetSectionList())
   126  	assert.Equal(t, "config_test_remote", config.FileGet("asdf", "type"))
   127  	assert.Equal(t, "true", config.FileGet("asdf", "bool"))
   128  	assert.Equal(t, "secret", obscure.MustReveal(config.FileGet("asdf", "pass")))
   129  
   130  	// delete remote
   131  	config.DeleteRemote("asdf")
   132  	assert.Equal(t, []string{}, config.Data().GetSectionList())
   133  }
   134  
   135  func TestChooseOption(t *testing.T) {
   136  	defer testConfigFile(t, simpleOptions, "crud.conf")()
   137  	ctx := context.Background()
   138  
   139  	// script for creating remote
   140  	config.ReadLine = makeReadLine([]string{
   141  		"config_test_remote", // type
   142  		"false",              // bool value
   143  		"x",                  // bad choice
   144  		"g",                  // generate password
   145  		"1024",               // very big
   146  		"y",                  // password OK
   147  		"y",                  // looks good, save
   148  	})
   149  	config.Password = func(bits int) (string, error) {
   150  		assert.Equal(t, 1024, bits)
   151  		return "not very random password", nil
   152  	}
   153  	require.NoError(t, config.NewRemote(ctx, "test"))
   154  
   155  	assert.Equal(t, "", config.FileGet("test", "bool")) // this is the default now
   156  	assert.Equal(t, "not very random password", obscure.MustReveal(config.FileGet("test", "pass")))
   157  
   158  	// script for creating remote
   159  	config.ReadLine = makeReadLine([]string{
   160  		"config_test_remote", // type
   161  		"true",               // bool value
   162  		"n",                  // not required
   163  		"y",                  // looks good, save
   164  	})
   165  	require.NoError(t, config.NewRemote(ctx, "test"))
   166  
   167  	assert.Equal(t, "true", config.FileGet("test", "bool"))
   168  	assert.Equal(t, "", config.FileGet("test", "pass"))
   169  }
   170  
   171  func TestNewRemoteName(t *testing.T) {
   172  	defer testConfigFile(t, simpleOptions, "crud.conf")()
   173  	ctx := context.Background()
   174  
   175  	// script for creating remote
   176  	config.ReadLine = makeReadLine([]string{
   177  		"config_test_remote", // type
   178  		"true",               // bool value
   179  		"n",                  // not required
   180  		"y",                  // looks good, save
   181  	})
   182  	require.NoError(t, config.NewRemote(ctx, "test"))
   183  
   184  	config.ReadLine = makeReadLine([]string{
   185  		"test",           // already exists
   186  		"",               // empty string not allowed
   187  		"bad^characters", // bad characters
   188  		"newname",        // OK
   189  	})
   190  
   191  	assert.Equal(t, "newname", config.NewRemoteName())
   192  }
   193  
   194  func TestCreateUpdatePasswordRemote(t *testing.T) {
   195  	ctx := context.Background()
   196  	defer testConfigFile(t, simpleOptions, "update.conf")()
   197  
   198  	for _, doObscure := range []bool{false, true} {
   199  		for _, noObscure := range []bool{false, true} {
   200  			if doObscure && noObscure {
   201  				break
   202  			}
   203  			t.Run(fmt.Sprintf("doObscure=%v,noObscure=%v", doObscure, noObscure), func(t *testing.T) {
   204  				opt := config.UpdateRemoteOpt{
   205  					Obscure:   doObscure,
   206  					NoObscure: noObscure,
   207  				}
   208  				_, err := config.CreateRemote(ctx, "test2", "config_test_remote", rc.Params{
   209  					"bool": true,
   210  					"pass": "potato",
   211  				}, opt)
   212  				require.NoError(t, err)
   213  
   214  				assert.Equal(t, []string{"test2"}, config.Data().GetSectionList())
   215  				assert.Equal(t, "config_test_remote", config.FileGet("test2", "type"))
   216  				assert.Equal(t, "true", config.FileGet("test2", "bool"))
   217  				gotPw := config.FileGet("test2", "pass")
   218  				if !noObscure {
   219  					gotPw = obscure.MustReveal(gotPw)
   220  				}
   221  				assert.Equal(t, "potato", gotPw)
   222  
   223  				wantPw := obscure.MustObscure("potato2")
   224  				_, err = config.UpdateRemote(ctx, "test2", rc.Params{
   225  					"bool":  false,
   226  					"pass":  wantPw,
   227  					"spare": "spare",
   228  				}, opt)
   229  				require.NoError(t, err)
   230  
   231  				assert.Equal(t, []string{"test2"}, config.Data().GetSectionList())
   232  				assert.Equal(t, "config_test_remote", config.FileGet("test2", "type"))
   233  				assert.Equal(t, "false", config.FileGet("test2", "bool"))
   234  				gotPw = config.FileGet("test2", "pass")
   235  				if doObscure {
   236  					gotPw = obscure.MustReveal(gotPw)
   237  				}
   238  				assert.Equal(t, wantPw, gotPw)
   239  
   240  				require.NoError(t, config.PasswordRemote(ctx, "test2", rc.Params{
   241  					"pass": "potato3",
   242  				}))
   243  
   244  				assert.Equal(t, []string{"test2"}, config.Data().GetSectionList())
   245  				assert.Equal(t, "config_test_remote", config.FileGet("test2", "type"))
   246  				assert.Equal(t, "false", config.FileGet("test2", "bool"))
   247  				assert.Equal(t, "potato3", obscure.MustReveal(config.FileGet("test2", "pass")))
   248  			})
   249  		}
   250  	}
   251  }
   252  
   253  func TestDefaultRequired(t *testing.T) {
   254  	// By default options are optional (sic), regardless if a default value is defined.
   255  	// Setting Required=true means empty string is no longer allowed, except when
   256  	// a default value is set: Default value means empty string is always allowed!
   257  	options := []fs.Option{{
   258  		Name:     "string_required",
   259  		Required: true,
   260  	}, {
   261  		Name:    "string_default",
   262  		Default: "AAA",
   263  	}, {
   264  		Name:     "string_required_default",
   265  		Default:  "BBB",
   266  		Required: true,
   267  	}}
   268  
   269  	defer testConfigFile(t, options, "crud.conf")()
   270  	ctx := context.Background()
   271  
   272  	// script for creating remote
   273  	config.ReadLine = makeReadLine([]string{
   274  		"config_test_remote", // type
   275  		"111",                // string_required
   276  		"222",                // string_default
   277  		"333",                // string_required_default
   278  		"y",                  // looks good, save
   279  	})
   280  	require.NoError(t, config.NewRemote(ctx, "test"))
   281  
   282  	assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
   283  	assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
   284  	assert.Equal(t, "111", config.FileGet("test", "string_required"))
   285  	assert.Equal(t, "222", config.FileGet("test", "string_default"))
   286  	assert.Equal(t, "333", config.FileGet("test", "string_required_default"))
   287  
   288  	// delete remote
   289  	config.DeleteRemote("test")
   290  	assert.Equal(t, []string{}, config.Data().GetSectionList())
   291  
   292  	// script for creating remote
   293  	config.ReadLine = makeReadLine([]string{
   294  		"config_test_remote", // type
   295  		"",                   // string_required - invalid (empty string not allowed)
   296  		"111",                // string_required - valid
   297  		"",                   // string_default (empty string allowed, means use default)
   298  		"",                   // string_required_default (empty string allowed, means use default)
   299  		"y",                  // looks good, save
   300  	})
   301  	require.NoError(t, config.NewRemote(ctx, "test"))
   302  
   303  	assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
   304  	assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
   305  	assert.Equal(t, "111", config.FileGet("test", "string_required"))
   306  	assert.Equal(t, "", config.FileGet("test", "string_default"))
   307  	assert.Equal(t, "", config.FileGet("test", "string_required_default"))
   308  }
   309  
   310  func TestMultipleChoice(t *testing.T) {
   311  	// Multiple-choice options can be set to the number of a predefined choice, or
   312  	// its text. Unless Exclusive=true, tested later, any free text input is accepted.
   313  	//
   314  	// By default options are optional, regardless if a default value is defined.
   315  	// Setting Required=true means empty string is no longer allowed, except when
   316  	// a default value is set: Default value means empty string is always allowed!
   317  	options := []fs.Option{{
   318  		Name: "multiple_choice",
   319  		Examples: []fs.OptionExample{{
   320  			Value: "AAA",
   321  			Help:  "This is value AAA",
   322  		}, {
   323  			Value: "BBB",
   324  			Help:  "This is value BBB",
   325  		}, {
   326  			Value: "CCC",
   327  			Help:  "This is value CCC",
   328  		}},
   329  	}, {
   330  		Name:     "multiple_choice_required",
   331  		Required: true,
   332  		Examples: []fs.OptionExample{{
   333  			Value: "AAA",
   334  			Help:  "This is value AAA",
   335  		}, {
   336  			Value: "BBB",
   337  			Help:  "This is value BBB",
   338  		}, {
   339  			Value: "CCC",
   340  			Help:  "This is value CCC",
   341  		}},
   342  	}, {
   343  		Name:    "multiple_choice_default",
   344  		Default: "BBB",
   345  		Examples: []fs.OptionExample{{
   346  			Value: "AAA",
   347  			Help:  "This is value AAA",
   348  		}, {
   349  			Value: "BBB",
   350  			Help:  "This is value BBB",
   351  		}, {
   352  			Value: "CCC",
   353  			Help:  "This is value CCC",
   354  		}},
   355  	}, {
   356  		Name:     "multiple_choice_required_default",
   357  		Required: true,
   358  		Default:  "BBB",
   359  		Examples: []fs.OptionExample{{
   360  			Value: "AAA",
   361  			Help:  "This is value AAA",
   362  		}, {
   363  			Value: "BBB",
   364  			Help:  "This is value BBB",
   365  		}, {
   366  			Value: "CCC",
   367  			Help:  "This is value CCC",
   368  		}},
   369  	}}
   370  
   371  	defer testConfigFile(t, options, "crud.conf")()
   372  	ctx := context.Background()
   373  
   374  	// script for creating remote
   375  	config.ReadLine = makeReadLine([]string{
   376  		"config_test_remote", // type
   377  		"3",                  // multiple_choice
   378  		"3",                  // multiple_choice_required
   379  		"3",                  // multiple_choice_default
   380  		"3",                  // multiple_choice_required_default
   381  		"y",                  // looks good, save
   382  	})
   383  	require.NoError(t, config.NewRemote(ctx, "test"))
   384  
   385  	assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
   386  	assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
   387  	assert.Equal(t, "CCC", config.FileGet("test", "multiple_choice"))
   388  	assert.Equal(t, "CCC", config.FileGet("test", "multiple_choice_required"))
   389  	assert.Equal(t, "CCC", config.FileGet("test", "multiple_choice_default"))
   390  	assert.Equal(t, "CCC", config.FileGet("test", "multiple_choice_required_default"))
   391  
   392  	// delete remote
   393  	config.DeleteRemote("test")
   394  	assert.Equal(t, []string{}, config.Data().GetSectionList())
   395  
   396  	// script for creating remote
   397  	config.ReadLine = makeReadLine([]string{
   398  		"config_test_remote", // type
   399  		"XXX",                // multiple_choice
   400  		"XXX",                // multiple_choice_required
   401  		"XXX",                // multiple_choice_default
   402  		"XXX",                // multiple_choice_required_default
   403  		"y",                  // looks good, save
   404  	})
   405  	require.NoError(t, config.NewRemote(ctx, "test"))
   406  
   407  	assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
   408  	assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
   409  	assert.Equal(t, "XXX", config.FileGet("test", "multiple_choice"))
   410  	assert.Equal(t, "XXX", config.FileGet("test", "multiple_choice_required"))
   411  	assert.Equal(t, "XXX", config.FileGet("test", "multiple_choice_default"))
   412  	assert.Equal(t, "XXX", config.FileGet("test", "multiple_choice_required_default"))
   413  
   414  	// delete remote
   415  	config.DeleteRemote("test")
   416  	assert.Equal(t, []string{}, config.Data().GetSectionList())
   417  
   418  	// script for creating remote
   419  	config.ReadLine = makeReadLine([]string{
   420  		"config_test_remote", // type
   421  		"",                   // multiple_choice (empty string allowed)
   422  		"",                   // multiple_choice_required - invalid (empty string not allowed)
   423  		"XXX",                // multiple_choice_required - valid (value not restricted to examples)
   424  		"",                   // multiple_choice_default (empty string allowed)
   425  		"",                   // multiple_choice_required_default (required does nothing when default is set)
   426  		"y",                  // looks good, save
   427  	})
   428  	require.NoError(t, config.NewRemote(ctx, "test"))
   429  
   430  	assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
   431  	assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
   432  	assert.Equal(t, "", config.FileGet("test", "multiple_choice"))
   433  	assert.Equal(t, "XXX", config.FileGet("test", "multiple_choice_required"))
   434  	assert.Equal(t, "", config.FileGet("test", "multiple_choice_default"))
   435  	assert.Equal(t, "", config.FileGet("test", "multiple_choice_required_default"))
   436  }
   437  
   438  func TestMultipleChoiceExclusive(t *testing.T) {
   439  	// Setting Exclusive=true on multiple-choice option means any input
   440  	// value must be from the predefined list, but empty string is allowed.
   441  	// Setting a default value makes no difference.
   442  	options := []fs.Option{{
   443  		Name:      "multiple_choice_exclusive",
   444  		Exclusive: true,
   445  		Examples: []fs.OptionExample{{
   446  			Value: "AAA",
   447  			Help:  "This is value AAA",
   448  		}, {
   449  			Value: "BBB",
   450  			Help:  "This is value BBB",
   451  		}, {
   452  			Value: "CCC",
   453  			Help:  "This is value CCC",
   454  		}},
   455  	}, {
   456  		Name:      "multiple_choice_exclusive_default",
   457  		Exclusive: true,
   458  		Default:   "CCC",
   459  		Examples: []fs.OptionExample{{
   460  			Value: "AAA",
   461  			Help:  "This is value AAA",
   462  		}, {
   463  			Value: "BBB",
   464  			Help:  "This is value BBB",
   465  		}, {
   466  			Value: "CCC",
   467  			Help:  "This is value CCC",
   468  		}},
   469  	}}
   470  
   471  	defer testConfigFile(t, options, "crud.conf")()
   472  	ctx := context.Background()
   473  
   474  	// script for creating remote
   475  	config.ReadLine = makeReadLine([]string{
   476  		"config_test_remote", // type
   477  		"XXX",                // multiple_choice_exclusive - invalid (not a value from examples)
   478  		"",                   // multiple_choice_exclusive - valid (empty string allowed)
   479  		"YYY",                // multiple_choice_exclusive_default - invalid (not a value from examples)
   480  		"",                   // multiple_choice_exclusive_default - valid (empty string allowed)
   481  		"y",                  // looks good, save
   482  	})
   483  	require.NoError(t, config.NewRemote(ctx, "test"))
   484  
   485  	assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
   486  	assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
   487  	assert.Equal(t, "", config.FileGet("test", "multiple_choice_exclusive"))
   488  	assert.Equal(t, "", config.FileGet("test", "multiple_choice_exclusive_default"))
   489  }
   490  
   491  func TestMultipleChoiceExclusiveRequired(t *testing.T) {
   492  	// Setting Required=true together with Exclusive=true on multiple-choice option
   493  	// means empty string is no longer allowed, except when a default value is set
   494  	// (default value means empty string is always allowed).
   495  	options := []fs.Option{{
   496  		Name:      "multiple_choice_exclusive_required",
   497  		Exclusive: true,
   498  		Required:  true,
   499  		Examples: []fs.OptionExample{{
   500  			Value: "AAA",
   501  			Help:  "This is value AAA",
   502  		}, {
   503  			Value: "BBB",
   504  			Help:  "This is value BBB",
   505  		}, {
   506  			Value: "CCC",
   507  			Help:  "This is value CCC",
   508  		}},
   509  	}, {
   510  		Name:      "multiple_choice_exclusive_required_default",
   511  		Exclusive: true,
   512  		Required:  true,
   513  		Default:   "CCC",
   514  		Examples: []fs.OptionExample{{
   515  			Value: "AAA",
   516  			Help:  "This is value AAA",
   517  		}, {
   518  			Value: "BBB",
   519  			Help:  "This is value BBB",
   520  		}, {
   521  			Value: "CCC",
   522  			Help:  "This is value CCC",
   523  		}},
   524  	}}
   525  
   526  	defer testConfigFile(t, options, "crud.conf")()
   527  	ctx := context.Background()
   528  
   529  	// script for creating remote
   530  	config.ReadLine = makeReadLine([]string{
   531  		"config_test_remote", // type
   532  		"XXX",                // multiple_choice_exclusive_required - invalid (not a value from examples)
   533  		"",                   // multiple_choice_exclusive_required - invalid (empty string not allowed)
   534  		"CCC",                // multiple_choice_exclusive_required - valid
   535  		"XXX",                // multiple_choice_exclusive_required_default - invalid (not a value from examples)
   536  		"",                   // multiple_choice_exclusive_required_default - valid (empty string allowed)
   537  		"y",                  // looks good, save
   538  	})
   539  	require.NoError(t, config.NewRemote(ctx, "test"))
   540  
   541  	assert.Equal(t, []string{"test"}, config.Data().GetSectionList())
   542  	assert.Equal(t, "config_test_remote", config.FileGet("test", "type"))
   543  	assert.Equal(t, "CCC", config.FileGet("test", "multiple_choice_exclusive_required"))
   544  	assert.Equal(t, "", config.FileGet("test", "multiple_choice_exclusive_required_default"))
   545  }