diff --git a/backend/zoho/zoho.go b/backend/zoho/zoho.go index 4d4f26ac8..092f8c25a 100644 --- a/backend/zoho/zoho.go +++ b/backend/zoho/zoho.go @@ -151,6 +151,20 @@ func init() { } } + // If a root_folder_id is already set (from config, or a previous + // setup) don't overwrite it on update/reconnect unless the user + // asks to, mirroring how the drive backend gates its team drive id. + if rootID, _ := m.Get(configRootID); rootID != "" { + return fs.ConfigConfirm("root_change", false, "config_change_root", fmt.Sprintf("Change current root folder id %q?\n", rootID)) + } + return fs.ConfigGoto("select_edition") + case "root_change": + if config.Result == "false" { + // Keep the existing root_folder_id; the token has already been refreshed. + return nil, nil + } + return fs.ConfigGoto("select_edition") + case "select_edition": _, apiSrv, err := getSrvs() if err != nil { return nil, err @@ -252,6 +266,11 @@ browser.`, Value: "com.au", Help: "Australia", }}, + }, { + Name: "root_folder_id", + Help: "ID of the root folder.\n\nLeave blank normally.\n\nFill in to make rclone use a non root folder as its starting point.", + Advanced: true, + Sensitive: true, }, { Name: "upload_cutoff", Help: "Cutoff for switching to large file upload api (>= 10 MiB).", diff --git a/backend/zoho/zoho_internal_test.go b/backend/zoho/zoho_internal_test.go new file mode 100644 index 000000000..a1e73195b --- /dev/null +++ b/backend/zoho/zoho_internal_test.go @@ -0,0 +1,71 @@ +package zoho + +import ( + "context" + "testing" + + "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/fs/config/configmap" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestConfigRootFolderID drives the interactive Config state machine directly +// and checks that an existing root_folder_id is preserved on update/reconnect: +// workspace selection only runs when the id is empty or the user opts in. The +// seeded token is already the Zoho type and no region service is contacted, so +// these cases make no network calls. +func TestConfigRootFolderID(t *testing.T) { + regInfo := fs.MustFind("zoho") + const rootID = "abc123rootfolderid" + // Already the Zoho custom type, so the "type" state's token rewrite is a + // no-op and nothing is sent to the network. + const token = `{"access_token":"x","token_type":"Zoho-oauthtoken"}` + + newMapper := func(withRoot bool) configmap.Simple { + m := configmap.Simple{"region": "eu", "token": token} + if withRoot { + m[configRootID] = rootID + } + return m + } + + ctx := context.Background() + + t.Run("SetRootAsksBeforeChanging", func(t *testing.T) { + m := newMapper(true) + out, err := regInfo.Config(ctx, "zoho", m, fs.ConfigIn{State: "type"}) + require.NoError(t, err) + require.NotNil(t, out) + assert.Equal(t, "root_change", out.State) + require.NotNil(t, out.Option) + got, _ := m.Get(configRootID) + assert.Equal(t, rootID, got, "id must not change before the user answers") + }) + + t.Run("EmptyRootGoesToEdition", func(t *testing.T) { + m := newMapper(false) + out, err := regInfo.Config(ctx, "zoho", m, fs.ConfigIn{State: "type"}) + require.NoError(t, err) + require.NotNil(t, out) + assert.Equal(t, "select_edition", out.State) + assert.Nil(t, out.Option, "goto, not a question") + }) + + t.Run("KeepRootOnNo", func(t *testing.T) { + m := newMapper(true) + out, err := regInfo.Config(ctx, "zoho", m, fs.ConfigIn{State: "root_change", Result: "false"}) + require.NoError(t, err) + assert.Nil(t, out, "answering No ends config and keeps the id") + got, _ := m.Get(configRootID) + assert.Equal(t, rootID, got) + }) + + t.Run("ChangeRootOnYes", func(t *testing.T) { + m := newMapper(true) + out, err := regInfo.Config(ctx, "zoho", m, fs.ConfigIn{State: "root_change", Result: "true"}) + require.NoError(t, err) + require.NotNil(t, out) + assert.Equal(t, "select_edition", out.State) + }) +}