From f6aff3e255d14ec58603f2d8a19d950796d3f6ae Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Mon, 6 Apr 2026 20:30:04 -0700 Subject: [PATCH] Persist remote binding after setup push --- main.go | 50 +++++++++++++++++++++++++++++++++++++-- main_test.go | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 4fb1ef6..397d6be 100644 --- a/main.go +++ b/main.go @@ -1523,14 +1523,21 @@ func (u *ui) startImportSharedVaultAction() { func (u *ui) advancedSyncToAction() error { switch u.syncSourceMode { case syncSourceRemote: + baseURL := strings.TrimSpace(u.syncRemoteBaseURL.Text()) + remotePath := strings.TrimSpace(u.syncRemotePath.Text()) client := webdav.Client{ - BaseURL: strings.TrimSpace(u.syncRemoteBaseURL.Text()), + BaseURL: baseURL, Username: strings.TrimSpace(u.syncRemoteUsername.Text()), Password: u.syncRemotePassword.Text(), } - if err := u.state.SynchronizeToRemote(client, strings.TrimSpace(u.syncRemotePath.Text())); err != nil { + if err := u.state.SynchronizeToRemote(client, remotePath); err != nil { return err } + if u.syncDialogPurpose == syncDialogPurposeRemoteSetup { + if err := u.persistSyncDialogRemoteBinding(baseURL, remotePath); err != nil { + return err + } + } default: path := strings.TrimSpace(u.syncLocalPath.Text()) if path == "" { @@ -1546,6 +1553,45 @@ func (u *ui) advancedSyncToAction() error { return nil } +func (u *ui) persistSyncDialogRemoteBinding(baseURL, remotePath string) error { + baseURL = strings.TrimSpace(baseURL) + remotePath = strings.TrimSpace(remotePath) + if baseURL == "" || remotePath == "" { + return fmt.Errorf("remote setup requires base URL and path") + } + input := appstate.RemoteBindingInput{ + LocalVaultPath: strings.TrimSpace(u.vaultPath.Text()), + RemoteProfileID: "remote-profile-" + remoteBindingSuffix(baseURL, remotePath, strings.TrimSpace(u.syncRemoteUsername.Text())), + RemoteProfileName: friendlyRecentRemoteLabel(recentRemoteRecord{BaseURL: baseURL, Path: remotePath}), + BaseURL: baseURL, + RemotePath: remotePath, + CredentialEntryID: "remote-credential-" + remoteBindingSuffix(baseURL, remotePath, strings.TrimSpace(u.syncRemoteUsername.Text())), + CredentialTitle: "WebDAV Sign-In" + func() string { + if user := strings.TrimSpace(u.syncRemoteUsername.Text()); user != "" { + return " ยท " + user + } + return "" + }(), + Username: strings.TrimSpace(u.syncRemoteUsername.Text()), + Password: u.syncRemotePassword.Text(), + CredentialPath: append([]string(nil), u.currentPath...), + SyncMode: u.newRemoteBindingSyncMode(), + } + binding, err := u.state.ConfigureRemoteBinding(input) + if err != nil { + return err + } + u.selectedVaultRemoteProfileID = binding.RemoteProfileID + u.selectedVaultRemoteCredentialEntryID = binding.CredentialEntryID + u.selectedVaultRemoteSyncMode = binding.SyncMode + u.remoteBaseURL.SetText(baseURL) + u.remotePath.SetText(remotePath) + u.remoteUsername.SetText(strings.TrimSpace(u.syncRemoteUsername.Text())) + u.remotePassword.SetText(u.syncRemotePassword.Text()) + u.noteRecentRemote(baseURL, remotePath) + return nil +} + func (u *ui) saveAsTargetPath() string { path := strings.TrimSpace(u.saveAsPath.Text()) if path != "" { diff --git a/main_test.go b/main_test.go index 76f4a6d..a552d08 100644 --- a/main_test.go +++ b/main_test.go @@ -5974,6 +5974,72 @@ func TestUISyncDialogUsesAdvancedCopy(t *testing.T) { } } +func TestUIRemoteSyncSetupPersistsBindingAfterSuccessfulPush(t *testing.T) { + t.Parallel() + + key := vault.MasterKey{Password: "correct horse battery staple"} + currentPath := filepath.Join(t.TempDir(), "current.kdbx") + writeKDBXMainTestFile(t, currentPath, vault.Model{ + Entries: []vault.Entry{{ + ID: "entry-current", + Title: "Vault Console", + Username: "dannyocean", + Password: "token-current", + URL: "https://vault.crew.example.invalid", + Path: []string{"Root", "Internet"}, + }}, + }, key) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.WriteHeader(http.StatusNotFound) + case http.MethodPut: + w.Header().Set("ETag", "\"v1\"") + w.WriteHeader(http.StatusNoContent) + default: + t.Fatalf("unexpected method %s", r.Method) + } + })) + defer server.Close() + + u := newUIWithSession("desktop", &session.Manager{}) + u.masterPassword.SetText(key.Password) + u.vaultPath.SetText(currentPath) + if err := u.openVaultAction(); err != nil { + t.Fatalf("openVaultAction() error = %v", err) + } + + u.openRemoteSyncSetupDialog() + u.syncRemoteBaseURL.SetText(server.URL) + u.syncRemotePath.SetText("vaults/other.kdbx") + u.syncRemoteUsername.SetText("tjulian") + u.syncRemotePassword.SetText("token-1") + + if err := u.advancedSyncAction(); err != nil { + t.Fatalf("advancedSyncAction() error = %v", err) + } + + if got := u.selectedVaultRemoteProfileID; got == "" { + t.Fatal("selectedVaultRemoteProfileID = empty, want saved binding") + } + if got := u.selectedVaultRemoteCredentialEntryID; got == "" { + t.Fatal("selectedVaultRemoteCredentialEntryID = empty, want saved credential binding") + } + if got := u.remoteBaseURL.Text(); got != server.URL { + t.Fatalf("remoteBaseURL = %q, want %q", got, server.URL) + } + if got := u.remotePath.Text(); got != "vaults/other.kdbx" { + t.Fatalf("remotePath = %q, want vaults/other.kdbx", got) + } + if got := len(u.recentRemotes); got != 1 { + t.Fatalf("len(recentRemotes) = %d, want 1", got) + } + if got := u.recentRemotes[0].LocalVaultPath; got != currentPath { + t.Fatalf("recentRemotes[0].LocalVaultPath = %q, want %q", got, currentPath) + } +} + func TestUISaveCurrentRemoteBindingActionPersistsBindingIntoVault(t *testing.T) { t.Parallel()