Before this change the environment variable getters in fs/configmap.go logged the option value with %q, so a password set via RCLONE_CONFIG_remote_pass (or RCLONE_remote_pass) was printed in full to the debug log. Values from the config file were already redacted, which made the leak easy to miss. This change routes both getters through fs.RedactOptionValue, which looks up the option in the backend's option list: options marked IsPassword or Sensitive log as XXX, unknown options are conservatively redacted, and --dump auth still shows the value for debugging. Fixes #5794
334 lines
9.1 KiB
Go
334 lines
9.1 KiB
Go
package fs
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/rclone/rclone/fs/config/configmap"
|
|
"github.com/spf13/pflag"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// Check it satisfies the interface
|
|
var _ pflag.Value = (*Option)(nil)
|
|
|
|
func TestOption(t *testing.T) {
|
|
d := &Option{
|
|
Name: "potato",
|
|
Value: SizeSuffix(17 << 20),
|
|
}
|
|
assert.Equal(t, "17Mi", d.String())
|
|
assert.Equal(t, "SizeSuffix", d.Type())
|
|
err := d.Set("18M")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, SizeSuffix(18<<20), d.Value)
|
|
err = d.Set("sdfsdf")
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
// Test options
|
|
var (
|
|
nouncOption = Option{
|
|
Name: "nounc",
|
|
}
|
|
copyLinksOption = Option{
|
|
Name: "copy_links",
|
|
Default: false,
|
|
NoPrefix: true,
|
|
ShortOpt: "L",
|
|
Advanced: true,
|
|
}
|
|
caseInsensitiveOption = Option{
|
|
Name: "case_insensitive",
|
|
Default: false,
|
|
Value: true,
|
|
set: true, // set as if from a flag
|
|
Advanced: true,
|
|
}
|
|
// setToDefaultOption has been set explicitly to a value which
|
|
// happens to equal its default - it must still override the config
|
|
setToDefaultOption = Option{
|
|
Name: "set_to_default",
|
|
Default: "deflt",
|
|
Value: "deflt",
|
|
set: true,
|
|
}
|
|
// envDefaultOption has had a value loaded as a default from the
|
|
// environment (so set is false). It is only honoured over more
|
|
// specific sources when it differs from the default.
|
|
envDefaultOption = Option{
|
|
Name: "env_default",
|
|
Default: "deflt",
|
|
Value: "deflt", // equal to default, so must not be surfaced
|
|
}
|
|
envDefaultDiffOption = Option{
|
|
Name: "env_default_diff",
|
|
Default: "deflt",
|
|
Value: "fromenv", // differs from default, so must be surfaced
|
|
}
|
|
testOptions = Options{nouncOption, copyLinksOption, caseInsensitiveOption, setToDefaultOption, envDefaultOption, envDefaultDiffOption}
|
|
)
|
|
|
|
func TestOptionsSetValues(t *testing.T) {
|
|
assert.Nil(t, testOptions[0].Default)
|
|
assert.Equal(t, false, testOptions[1].Default)
|
|
assert.Equal(t, false, testOptions[2].Default)
|
|
testOptions.setValues()
|
|
assert.Equal(t, "", testOptions[0].Default)
|
|
assert.Equal(t, false, testOptions[1].Default)
|
|
assert.Equal(t, false, testOptions[2].Default)
|
|
}
|
|
|
|
func TestOptionsGet(t *testing.T) {
|
|
opt := testOptions.Get("copy_links")
|
|
assert.Equal(t, ©LinksOption, opt)
|
|
opt = testOptions.Get("not_found")
|
|
assert.Nil(t, opt)
|
|
}
|
|
|
|
func TestOptionsOveridden(t *testing.T) {
|
|
m := configmap.New()
|
|
m1 := configmap.Simple{
|
|
"nounc": "m1",
|
|
"copy_links": "m1",
|
|
}
|
|
m.AddGetter(m1, configmap.PriorityNormal)
|
|
m2 := configmap.Simple{
|
|
"nounc": "m2",
|
|
"case_insensitive": "m2",
|
|
}
|
|
m.AddGetter(m2, configmap.PriorityConfig)
|
|
m3 := configmap.Simple{
|
|
"nounc": "m3",
|
|
}
|
|
m.AddGetter(m3, configmap.PriorityDefault)
|
|
got := testOptions.Overridden(m)
|
|
assert.Equal(t, configmap.Simple{
|
|
"copy_links": "m1",
|
|
"nounc": "m1",
|
|
}, got)
|
|
}
|
|
|
|
func TestOptionsNonDefault(t *testing.T) {
|
|
m := configmap.Simple{}
|
|
got := testOptions.NonDefault(m)
|
|
assert.Equal(t, configmap.Simple{}, got)
|
|
|
|
m["case_insensitive"] = "false"
|
|
got = testOptions.NonDefault(m)
|
|
assert.Equal(t, configmap.Simple{}, got)
|
|
|
|
m["case_insensitive"] = "true"
|
|
got = testOptions.NonDefault(m)
|
|
assert.Equal(t, configmap.Simple{"case_insensitive": "true"}, got)
|
|
}
|
|
|
|
func TestOptionMarshalJSON(t *testing.T) {
|
|
out, err := json.MarshalIndent(&caseInsensitiveOption, "", "")
|
|
assert.NoError(t, err)
|
|
require.Equal(t, `{
|
|
"Name": "case_insensitive",
|
|
"FieldName": "",
|
|
"Help": "",
|
|
"Default": false,
|
|
"Value": true,
|
|
"Hide": 0,
|
|
"Required": false,
|
|
"IsPassword": false,
|
|
"NoPrefix": false,
|
|
"Advanced": true,
|
|
"Exclusive": false,
|
|
"Sensitive": false,
|
|
"DefaultStr": "false",
|
|
"ValueStr": "true",
|
|
"Type": "bool"
|
|
}`, string(out))
|
|
}
|
|
|
|
func TestOptionGetValue(t *testing.T) {
|
|
assert.Equal(t, "", nouncOption.GetValue())
|
|
assert.Equal(t, false, copyLinksOption.GetValue())
|
|
assert.Equal(t, true, caseInsensitiveOption.GetValue())
|
|
}
|
|
|
|
func TestOptionString(t *testing.T) {
|
|
assert.Equal(t, "", nouncOption.String())
|
|
assert.Equal(t, "false", copyLinksOption.String())
|
|
assert.Equal(t, "true", caseInsensitiveOption.String())
|
|
}
|
|
|
|
func TestOptionStringStringArray(t *testing.T) {
|
|
opt := Option{
|
|
Name: "string_array",
|
|
Default: []string(nil),
|
|
}
|
|
assert.Equal(t, "", opt.String())
|
|
opt.Default = []string{}
|
|
assert.Equal(t, "", opt.String())
|
|
opt.Default = []string{"a", "b"}
|
|
assert.Equal(t, "a,b", opt.String())
|
|
opt.Default = []string{"hello, world!", "goodbye, world!"}
|
|
assert.Equal(t, `"hello, world!","goodbye, world!"`, opt.String())
|
|
}
|
|
|
|
func TestOptionStringSizeSuffix(t *testing.T) {
|
|
opt := Option{
|
|
Name: "size_suffix",
|
|
Default: SizeSuffix(0),
|
|
}
|
|
assert.Equal(t, "0", opt.String())
|
|
opt.Default = SizeSuffix(-1)
|
|
assert.Equal(t, "off", opt.String())
|
|
opt.Default = SizeSuffix(100)
|
|
assert.Equal(t, "100B", opt.String())
|
|
opt.Default = SizeSuffix(1024)
|
|
assert.Equal(t, "1Ki", opt.String())
|
|
}
|
|
|
|
func TestOptionSet(t *testing.T) {
|
|
o := caseInsensitiveOption
|
|
assert.Equal(t, true, o.Value)
|
|
err := o.Set("FALSE")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, false, o.Value)
|
|
|
|
o = copyLinksOption
|
|
assert.Equal(t, nil, o.Value)
|
|
err = o.Set("True")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, true, o.Value)
|
|
|
|
err = o.Set("INVALID")
|
|
assert.Error(t, err)
|
|
assert.Equal(t, true, o.Value)
|
|
}
|
|
|
|
func TestOptionType(t *testing.T) {
|
|
assert.Equal(t, "string", nouncOption.Type())
|
|
assert.Equal(t, "bool", copyLinksOption.Type())
|
|
assert.Equal(t, "bool", caseInsensitiveOption.Type())
|
|
}
|
|
|
|
func TestOptionFlagName(t *testing.T) {
|
|
assert.Equal(t, "local-nounc", nouncOption.FlagName("local"))
|
|
assert.Equal(t, "copy-links", copyLinksOption.FlagName("local"))
|
|
assert.Equal(t, "local-case-insensitive", caseInsensitiveOption.FlagName("local"))
|
|
}
|
|
|
|
func TestOptionEnvVarName(t *testing.T) {
|
|
assert.Equal(t, "RCLONE_LOCAL_NOUNC", nouncOption.EnvVarName("local"))
|
|
assert.Equal(t, "RCLONE_LOCAL_COPY_LINKS", copyLinksOption.EnvVarName("local"))
|
|
assert.Equal(t, "RCLONE_LOCAL_CASE_INSENSITIVE", caseInsensitiveOption.EnvVarName("local"))
|
|
}
|
|
|
|
func TestOptionGetters(t *testing.T) {
|
|
// Set up env vars
|
|
envVars := [][2]string{
|
|
{"RCLONE_CONFIG_LOCAL_POTATO_PIE", "yes"},
|
|
{"RCLONE_COPY_LINKS", "TRUE"},
|
|
{"RCLONE_LOCAL_NOUNC", "NOUNC"},
|
|
}
|
|
for _, ev := range envVars {
|
|
assert.NoError(t, os.Setenv(ev[0], ev[1]))
|
|
}
|
|
defer func() {
|
|
for _, ev := range envVars {
|
|
assert.NoError(t, os.Unsetenv(ev[0]))
|
|
}
|
|
}()
|
|
|
|
oldConfigFileGet := ConfigFileGet
|
|
ConfigFileGet = func(section, key string) (string, bool) {
|
|
if section == "sausage" && key == "key1" {
|
|
return "value1", true
|
|
}
|
|
return "", false
|
|
}
|
|
defer func() {
|
|
ConfigFileGet = oldConfigFileGet
|
|
}()
|
|
|
|
// set up getters
|
|
|
|
// A configmap.Getter to read from the environment RCLONE_CONFIG_backend_option_name
|
|
configEnvVarsGetter := configEnvVars{configName: "local"}
|
|
|
|
// A configmap.Getter to read from the environment RCLONE_option_name
|
|
optionEnvVarsGetter := optionEnvVars{"local", testOptions}
|
|
|
|
// A configmap.Getter to read either the default value or the set
|
|
// value from the RegInfo.Options
|
|
regInfoValuesGetterFalse := ®InfoValues{
|
|
options: testOptions,
|
|
useDefault: false,
|
|
}
|
|
regInfoValuesGetterTrue := ®InfoValues{
|
|
options: testOptions,
|
|
useDefault: true,
|
|
}
|
|
|
|
// A configmap.Setter to read from the config file
|
|
configFileGetter := getConfigFile("sausage")
|
|
|
|
for i, test := range []struct {
|
|
get configmap.Getter
|
|
key string
|
|
wantValue string
|
|
wantOk bool
|
|
}{
|
|
{configEnvVarsGetter, "not_found", "", false},
|
|
{configEnvVarsGetter, "potato_pie", "yes", true},
|
|
{optionEnvVarsGetter, "not_found", "", false},
|
|
{optionEnvVarsGetter, "copy_links", "TRUE", true},
|
|
{optionEnvVarsGetter, "nounc", "NOUNC", true},
|
|
{optionEnvVarsGetter, "case_insensitive", "", false},
|
|
{regInfoValuesGetterFalse, "not_found", "", false},
|
|
{regInfoValuesGetterFalse, "case_insensitive", "true", true},
|
|
{regInfoValuesGetterFalse, "copy_links", "", false},
|
|
{regInfoValuesGetterFalse, "set_to_default", "deflt", true},
|
|
{regInfoValuesGetterFalse, "env_default", "", false},
|
|
{regInfoValuesGetterFalse, "env_default_diff", "fromenv", true},
|
|
{regInfoValuesGetterTrue, "not_found", "", false},
|
|
{regInfoValuesGetterTrue, "case_insensitive", "true", true},
|
|
{regInfoValuesGetterTrue, "copy_links", "false", true},
|
|
{configFileGetter, "not_found", "", false},
|
|
{configFileGetter, "key1", "value1", true},
|
|
} {
|
|
what := fmt.Sprintf("%d: %+v: %q", i, test.get, test.key)
|
|
gotValue, gotOk := test.get.Get(test.key)
|
|
assert.Equal(t, test.wantValue, gotValue, what)
|
|
assert.Equal(t, test.wantOk, gotOk, what)
|
|
}
|
|
|
|
}
|
|
|
|
func TestOptionsNonDefaultRC(t *testing.T) {
|
|
type cfg struct {
|
|
X string `config:"x"`
|
|
Y int `config:"y"`
|
|
}
|
|
c := &cfg{X: "a", Y: 6}
|
|
opts := Options{
|
|
{Name: "x", Default: "a"}, // at default, should be omitted
|
|
{Name: "y", Default: 5}, // non-default, should be included
|
|
}
|
|
|
|
got, err := opts.NonDefaultRC(c)
|
|
require.NoError(t, err)
|
|
require.Equal(t, map[string]any{"Y": 6}, got)
|
|
}
|
|
|
|
func TestOptionsNonDefaultRCMissingKey(t *testing.T) {
|
|
type cfg struct {
|
|
X string `config:"x"`
|
|
}
|
|
c := &cfg{X: "a"}
|
|
// Options refers to a key not present in the struct -> expect error
|
|
opts := Options{{Name: "missing", Default: ""}}
|
|
_, err := opts.NonDefaultRC(c)
|
|
assert.ErrorContains(t, err, "not found")
|
|
}
|