diff --git a/main.go b/main.go index 2e3d315..cc93e64 100644 --- a/main.go +++ b/main.go @@ -1035,6 +1035,7 @@ func (u *ui) openVaultAction() error { u.resetPasswordPeek() u.currentPath = append([]string(nil), u.state.CurrentPath...) u.restoreRecentVaultGroup(path) + u.syncSavedRemoteBindingSelection() u.loadSecuritySettingsFromSession() u.editingEntry = false u.filter() @@ -1072,6 +1073,7 @@ func (u *ui) startOpenVaultAction() { u.resetPasswordPeek() u.currentPath = append([]string(nil), u.state.CurrentPath...) u.restoreRecentVaultGroup(path) + u.syncSavedRemoteBindingSelection() u.loadSecuritySettingsFromSession() u.editingEntry = false u.filter() @@ -2199,6 +2201,49 @@ func (u *ui) selectedVaultRemoteBinding() (appstate.RemoteBinding, bool) { }, true } +func (u *ui) syncSavedRemoteBindingSelection() { + profiles := u.availableRemoteProfiles() + entries := u.availableRemoteCredentialEntries() + + profileID := strings.TrimSpace(u.selectedVaultRemoteProfileID) + if profileID != "" { + var found bool + for _, profile := range profiles { + if profile.ID == profileID { + found = true + break + } + } + if !found { + u.selectedVaultRemoteProfileID = "" + } + } + if strings.TrimSpace(u.selectedVaultRemoteProfileID) == "" && len(profiles) == 1 { + u.selectedVaultRemoteProfileID = profiles[0].ID + } + if profile, ok := u.selectedVaultRemoteProfile(); ok { + u.remoteBaseURL.SetText(profile.BaseURL) + u.remotePath.SetText(profile.Path) + } + + entryID := strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) + if entryID != "" { + var found bool + for _, entry := range entries { + if entry.ID == entryID { + found = true + break + } + } + if !found { + u.selectedVaultRemoteCredentialEntryID = "" + } + } + if strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) == "" && len(entries) == 1 { + u.selectedVaultRemoteCredentialEntryID = entries[0].ID + } +} + func (u *ui) shouldShowSavedRemoteBindingSelectors() bool { profiles := u.availableRemoteProfiles() entries := u.availableRemoteCredentialEntries() diff --git a/main_test.go b/main_test.go index 95d3257..d95a3e7 100644 --- a/main_test.go +++ b/main_test.go @@ -2159,6 +2159,118 @@ func TestUIStartOpenRemoteActionBootstrapsFromLocalVaultBinding(t *testing.T) { } } +func TestUIOpenVaultActionSelectsSoleSavedRemoteBinding(t *testing.T) { + t.Parallel() + + key := vault.MasterKey{Password: "correct horse battery staple"} + path := filepath.Join(t.TempDir(), "family.kdbx") + model := vault.Model{ + Entries: []vault.Entry{{ + ID: "remote-creds-1", + Title: "WebDAV Sign-In", + Username: "tjulian", + Password: "token-1", + Path: []string{"Crew", "Internet"}, + }}, + RemoteProfiles: []vault.RemoteProfile{{ + ID: "family-webdav", + Name: "Family Vault", + Backend: vault.RemoteBackendWebDAV, + BaseURL: "https://dav.example.invalid/remote.php/dav", + Path: "files/family/keepass.kdbx", + }}, + } + writeKDBXMainTestFile(t, path, model, key) + + u := newUIWithState("desktop", &session.Manager{}, statePaths{ + DefaultSaveAsPath: filepath.Join(t.TempDir(), "default.kdbx"), + RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"), + RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"), + UIPreferencesPath: filepath.Join(t.TempDir(), "ui-prefs.json"), + }) + u.vaultPath.SetText(path) + u.masterPassword.SetText(key.Password) + u.selectedVaultRemoteProfileID = "stale-profile" + u.selectedVaultRemoteCredentialEntryID = "stale-credential" + u.remoteBaseURL.SetText("https://stale.example.invalid") + u.remotePath.SetText("stale/path.kdbx") + + if err := u.openVaultAction(); err != nil { + t.Fatalf("openVaultAction() error = %v", err) + } + + if got := u.selectedVaultRemoteProfileID; got != "family-webdav" { + t.Fatalf("selectedVaultRemoteProfileID = %q, want family-webdav", got) + } + if got := u.selectedVaultRemoteCredentialEntryID; got != "remote-creds-1" { + t.Fatalf("selectedVaultRemoteCredentialEntryID = %q, want remote-creds-1", got) + } + if got := u.remoteBaseURL.Text(); got != "https://dav.example.invalid/remote.php/dav" { + t.Fatalf("remoteBaseURL = %q, want resolved profile base URL", got) + } + if got := u.remotePath.Text(); got != "files/family/keepass.kdbx" { + t.Fatalf("remotePath = %q, want resolved profile path", got) + } +} + +func TestUIStartOpenVaultActionSelectsSoleSavedRemoteBinding(t *testing.T) { + t.Parallel() + + key := vault.MasterKey{Password: "correct horse battery staple"} + path := filepath.Join(t.TempDir(), "family.kdbx") + model := vault.Model{ + Entries: []vault.Entry{{ + ID: "remote-creds-1", + Title: "WebDAV Sign-In", + Username: "tjulian", + Password: "token-1", + Path: []string{"Crew", "Internet"}, + }}, + RemoteProfiles: []vault.RemoteProfile{{ + ID: "family-webdav", + Name: "Family Vault", + Backend: vault.RemoteBackendWebDAV, + BaseURL: "https://dav.example.invalid/remote.php/dav", + Path: "files/family/keepass.kdbx", + }}, + } + writeKDBXMainTestFile(t, path, model, key) + + u := newUIWithState("desktop", &session.Manager{}, statePaths{ + DefaultSaveAsPath: filepath.Join(t.TempDir(), "default.kdbx"), + RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"), + RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"), + UIPreferencesPath: filepath.Join(t.TempDir(), "ui-prefs.json"), + }) + u.vaultPath.SetText(path) + u.masterPassword.SetText(key.Password) + u.selectedVaultRemoteProfileID = "stale-profile" + u.selectedVaultRemoteCredentialEntryID = "stale-credential" + u.remoteBaseURL.SetText("https://stale.example.invalid") + u.remotePath.SetText("stale/path.kdbx") + + u.startOpenVaultAction() + + result := waitForBackgroundResult(t, u) + u.applyBackgroundResult(result) + + if got := u.state.ErrorMessage; got != "" { + t.Fatalf("ErrorMessage after apply = %q, want empty", got) + } + if got := u.selectedVaultRemoteProfileID; got != "family-webdav" { + t.Fatalf("selectedVaultRemoteProfileID = %q, want family-webdav", got) + } + if got := u.selectedVaultRemoteCredentialEntryID; got != "remote-creds-1" { + t.Fatalf("selectedVaultRemoteCredentialEntryID = %q, want remote-creds-1", got) + } + if got := u.remoteBaseURL.Text(); got != "https://dav.example.invalid/remote.php/dav" { + t.Fatalf("remoteBaseURL = %q, want resolved profile base URL", got) + } + if got := u.remotePath.Text(); got != "files/family/keepass.kdbx" { + t.Fatalf("remotePath = %q, want resolved profile path", got) + } +} + func TestUIRemoteSaveConflictShowsVisibleErrorAndKeepsDirtyState(t *testing.T) { t.Parallel()