Use vault bindings for remote opens

This commit is contained in:
Joe Julian
2026-04-06 15:54:52 -07:00
parent 4da9c07b32
commit dc03d6a0a5
2 changed files with 464 additions and 265 deletions
+324 -265
View File
@@ -207,271 +207,273 @@ const (
)
type ui struct {
mode string
theme *material.Theme
fileExplorer *explorer.Explorer
logoHorizontal paint.ImageOp
splashSquare paint.ImageOp
search widget.Editor
vaultPath widget.Editor
saveAsPath widget.Editor
remoteBaseURL widget.Editor
remotePath widget.Editor
remoteUsername widget.Editor
remotePassword widget.Editor
masterPassword widget.Editor
keyFilePath widget.Editor
apiTokenName widget.Editor
apiTokenClientName widget.Editor
apiTokenExpiresAt widget.Editor
apiPolicyOperation widget.Editor
apiPolicyPath widget.Editor
apiPolicyEntryID widget.Editor
securityCipher widget.Editor
securityKDF widget.Editor
entryID widget.Editor
entryTitle widget.Editor
entryUsername widget.Editor
entryPassword widget.Editor
entryURL widget.Editor
entryNotes widget.Editor
entryTags widget.Editor
entryPath widget.Editor
entryFields widget.Editor
customFieldKeys []widget.Editor
customFieldValues []widget.Editor
historyIndex widget.Editor
groupName widget.Editor
groupParentPath widget.Editor
passwordProfile widget.Editor
attachmentName widget.Editor
attachmentPath widget.Editor
exportAttachmentPath widget.Editor
autofillBrowserAllowlist widget.Editor
autofillAppAllowlist widget.Editor
autofillPackageRules widget.Editor
list widget.List
groupList widget.List
detailList widget.List
apiPolicyList widget.List
lifecycleList widget.List
phonePanelList widget.List
securityDialogList widget.List
remotePrefsDialogList widget.List
recentVaultListState widget.List
recentRemoteListState widget.List
copyUser widget.Clickable
copyPass widget.Clickable
copyURL widget.Clickable
lockVault widget.Clickable
unlockVault widget.Clickable
createVault widget.Clickable
openVault widget.Clickable
saveVault widget.Clickable
saveAsVault widget.Clickable
openRemote widget.Clickable
changeMasterKey widget.Clickable
synchronizeVault widget.Clickable
toggleSyncMenu widget.Clickable
toggleMainMenu widget.Clickable
openAdvancedSync widget.Clickable
openSecuritySettings widget.Clickable
openRemotePrefsHelp widget.Clickable
closeAdvancedSync widget.Clickable
closeSecuritySettings widget.Clickable
closeRemotePrefsHelp widget.Clickable
runAdvancedSync widget.Clickable
saveSecuritySettings widget.Clickable
settingsDensityDense widget.Clickable
settingsDensityComfortable widget.Clickable
settingsContrastStandard widget.Clickable
settingsContrastHigh widget.Clickable
settingsReducedMotionOff widget.Clickable
settingsReducedMotionOn widget.Clickable
settingsKeyboardFocusStandard widget.Clickable
settingsKeyboardFocusProminent widget.Clickable
showSettingsSyncLocal widget.Clickable
showSettingsSyncRemote widget.Clickable
showSettingsSyncPull widget.Clickable
showSettingsSyncPush widget.Clickable
editEntry widget.Clickable
cancelEdit widget.Clickable
pickVaultPath widget.Clickable
pickKeyFile widget.Clickable
pickSyncLocalPath widget.Clickable
clearVaultSelection widget.Clickable
clearRemoteSelection widget.Clickable
dismissBanner widget.Clickable
addEntry widget.Clickable
saveEntry widget.Clickable
duplicateEntry widget.Clickable
deleteEntry widget.Clickable
restoreEntry widget.Clickable
saveTemplate widget.Clickable
deleteTemplate widget.Clickable
instantiateTemplate widget.Clickable
addAttachment widget.Clickable
replaceAttachment widget.Clickable
removeAttachment widget.Clickable
exportAttachment widget.Clickable
restoreHistory widget.Clickable
generatePassword widget.Clickable
goToRootGroup widget.Clickable
goToParentGroup widget.Clickable
createGroup widget.Clickable
moveGroup widget.Clickable
renameGroup widget.Clickable
deleteGroup widget.Clickable
confirmDeleteGroup widget.Clickable
cancelDeleteGroup widget.Clickable
addCustomField widget.Clickable
toggleGroupControls widget.Clickable
toggleLifecycleAdvanced widget.Clickable
toggleHistory widget.Clickable
togglePasswordInline widget.Clickable
toggleSyncPassword widget.Clickable
setStatusBannerShort widget.Clickable
setStatusBannerStandard widget.Clickable
setStatusBannerLong widget.Clickable
showAllAutofillNotices widget.Clickable
showApprovalAutofillOnly widget.Clickable
hideAutofillNotices widget.Clickable
showEntries widget.Clickable
showTemplates widget.Clickable
showRecycle widget.Clickable
showAPITokens widget.Clickable
showAPIAudit widget.Clickable
showAbout widget.Clickable
showLocalLifecycle widget.Clickable
showRemoteLifecycle widget.Clickable
showSyncLocal widget.Clickable
showSyncRemote widget.Clickable
showSyncPull widget.Clickable
showSyncPush widget.Clickable
showAutofillApprovalAsk widget.Clickable
showAutofillApprovalAllow widget.Clickable
showAutofillApprovalBlock widget.Clickable
allowApproval widget.Clickable
denyApproval widget.Clickable
cancelApproval widget.Clickable
cancelLifecycleProgress widget.Clickable
retryLifecycleOpen widget.Clickable
approvalPermanent widget.Bool
apiPolicyAllow widget.Bool
apiPolicyGroupScopeW widget.Bool
apiTokenDisabled widget.Bool
settingsGroupControls widget.Bool
settingsLifecycleAdvanced widget.Bool
settingsHistory widget.Bool
settingsDenseLayout widget.Bool
entryClicks []widget.Clickable
apiTokenClicks []widget.Clickable
apiPolicyRemoves []widget.Clickable
apiAuditClicks []widget.Clickable
apiAuditTokenFilters []widget.Clickable
apiAuditDecisionFilters []widget.Clickable
apiAuditOperationFilters []widget.Clickable
clearAPIAuditFilters widget.Clickable
historyClicks []widget.Clickable
attachmentClicks []widget.Clickable
breadcrumbs []widget.Clickable
groupClicks []widget.Clickable
recentVaultClicks []widget.Clickable
recentRemoteClicks []widget.Clickable
removeCustomFields []widget.Clickable
state appstate.State
visible []entry
currentPath []string
syncedPath []string
selectedHistoryIndex int
showPassword bool
generatedPasswordDraft bool
togglePassword widget.Clickable
copyAPITokenSecret widget.Clickable
issueAPIToken widget.Clickable
saveAPIToken widget.Clickable
rotateAPIToken widget.Clickable
disableAPIToken widget.Clickable
revokeAPIToken widget.Clickable
deleteAPIToken widget.Clickable
useCurrentGroupForPolicy widget.Clickable
useSelectedEntryForPolicy widget.Clickable
clearAPIPolicyTarget widget.Clickable
addAPIPolicyRule widget.Clickable
phoneSplit widget.Float
splitDrag gesture.Drag
splitBase float32
splitStartY float32
phoneSpan int
phoneGroupBrowserExpanded bool
eyeIcon *widget.Icon
eyeOffIcon *widget.Icon
copyIcon *widget.Icon
expandMoreIcon *widget.Icon
expandLessIcon *widget.Icon
chevronRightIcon *widget.Icon
chevronDownIcon *widget.Icon
settingsIcon *widget.Icon
menuIcon *widget.Icon
clipboardWriter clipboard.Writer
loadingMessage string
loadingActionLabel string
lifecycleMode string
syncSourceMode syncSourceMode
syncDirection syncDirection
syncLocalImportName string
syncLocalImportContent []byte
syncLocalPath widget.Editor
syncRemoteBaseURL widget.Editor
syncRemotePath widget.Editor
syncRemoteUsername widget.Editor
syncRemotePassword widget.Editor
syncDialogOpen bool
syncMenuOpen bool
mainMenuOpen bool
selectedRemoteConnection bool
securityDialogOpen bool
remotePrefsDialogOpen bool
showSyncPassword bool
keyboardFocus focusID
defaultSaveAsPath string
recentVaultsPath string
settingsPath string
uiPreferencesPath string
recentRemotesPath string
autofillCachePath string
editingEntry bool
syncDefaultSourceMode syncSourceMode
syncDefaultDirection syncDirection
groupControlsHidden bool
lifecycleAdvancedHidden bool
historyHidden bool
denseLayout bool
statusBannerTTL time.Duration
autofillNoticePreference autofillNoticeMode
autofillFirstFillApprovalMode autofillFirstFillApprovalMode
accessibilityPrefs accessibilityPreferences
settingsDraft settingsDraft
recentVaults []string
recentRemotes []recentRemoteRecord
recentVaultGroups map[string][]string
recentVaultUsedAt map[string]time.Time
entriesState entriesSectionState
deleteGroupPath []string
apiPolicyGroupScope bool
apiTokenSecret string
selectedAuditIndex int
statusExpiresAt time.Time
now func() time.Time
apiHost *api.Host
auditLog *apiaudit.Log
grpcAddress string
backgroundResults chan backgroundActionResult
backgroundActionSerial int
activeBackgroundAction int
lastLifecycleAction string
requestMasterPassFocus bool
invalidate func()
mode string
theme *material.Theme
fileExplorer *explorer.Explorer
logoHorizontal paint.ImageOp
splashSquare paint.ImageOp
search widget.Editor
vaultPath widget.Editor
saveAsPath widget.Editor
remoteBaseURL widget.Editor
remotePath widget.Editor
remoteUsername widget.Editor
remotePassword widget.Editor
masterPassword widget.Editor
keyFilePath widget.Editor
apiTokenName widget.Editor
apiTokenClientName widget.Editor
apiTokenExpiresAt widget.Editor
apiPolicyOperation widget.Editor
apiPolicyPath widget.Editor
apiPolicyEntryID widget.Editor
securityCipher widget.Editor
securityKDF widget.Editor
entryID widget.Editor
entryTitle widget.Editor
entryUsername widget.Editor
entryPassword widget.Editor
entryURL widget.Editor
entryNotes widget.Editor
entryTags widget.Editor
entryPath widget.Editor
entryFields widget.Editor
customFieldKeys []widget.Editor
customFieldValues []widget.Editor
historyIndex widget.Editor
groupName widget.Editor
groupParentPath widget.Editor
passwordProfile widget.Editor
attachmentName widget.Editor
attachmentPath widget.Editor
exportAttachmentPath widget.Editor
autofillBrowserAllowlist widget.Editor
autofillAppAllowlist widget.Editor
autofillPackageRules widget.Editor
list widget.List
groupList widget.List
detailList widget.List
apiPolicyList widget.List
lifecycleList widget.List
phonePanelList widget.List
securityDialogList widget.List
remotePrefsDialogList widget.List
recentVaultListState widget.List
recentRemoteListState widget.List
copyUser widget.Clickable
copyPass widget.Clickable
copyURL widget.Clickable
lockVault widget.Clickable
unlockVault widget.Clickable
createVault widget.Clickable
openVault widget.Clickable
saveVault widget.Clickable
saveAsVault widget.Clickable
openRemote widget.Clickable
changeMasterKey widget.Clickable
synchronizeVault widget.Clickable
toggleSyncMenu widget.Clickable
toggleMainMenu widget.Clickable
openAdvancedSync widget.Clickable
openSecuritySettings widget.Clickable
openRemotePrefsHelp widget.Clickable
closeAdvancedSync widget.Clickable
closeSecuritySettings widget.Clickable
closeRemotePrefsHelp widget.Clickable
runAdvancedSync widget.Clickable
saveSecuritySettings widget.Clickable
settingsDensityDense widget.Clickable
settingsDensityComfortable widget.Clickable
settingsContrastStandard widget.Clickable
settingsContrastHigh widget.Clickable
settingsReducedMotionOff widget.Clickable
settingsReducedMotionOn widget.Clickable
settingsKeyboardFocusStandard widget.Clickable
settingsKeyboardFocusProminent widget.Clickable
showSettingsSyncLocal widget.Clickable
showSettingsSyncRemote widget.Clickable
showSettingsSyncPull widget.Clickable
showSettingsSyncPush widget.Clickable
editEntry widget.Clickable
cancelEdit widget.Clickable
pickVaultPath widget.Clickable
pickKeyFile widget.Clickable
pickSyncLocalPath widget.Clickable
clearVaultSelection widget.Clickable
clearRemoteSelection widget.Clickable
dismissBanner widget.Clickable
addEntry widget.Clickable
saveEntry widget.Clickable
duplicateEntry widget.Clickable
deleteEntry widget.Clickable
restoreEntry widget.Clickable
saveTemplate widget.Clickable
deleteTemplate widget.Clickable
instantiateTemplate widget.Clickable
addAttachment widget.Clickable
replaceAttachment widget.Clickable
removeAttachment widget.Clickable
exportAttachment widget.Clickable
restoreHistory widget.Clickable
generatePassword widget.Clickable
goToRootGroup widget.Clickable
goToParentGroup widget.Clickable
createGroup widget.Clickable
moveGroup widget.Clickable
renameGroup widget.Clickable
deleteGroup widget.Clickable
confirmDeleteGroup widget.Clickable
cancelDeleteGroup widget.Clickable
addCustomField widget.Clickable
toggleGroupControls widget.Clickable
toggleLifecycleAdvanced widget.Clickable
toggleHistory widget.Clickable
togglePasswordInline widget.Clickable
toggleSyncPassword widget.Clickable
setStatusBannerShort widget.Clickable
setStatusBannerStandard widget.Clickable
setStatusBannerLong widget.Clickable
showAllAutofillNotices widget.Clickable
showApprovalAutofillOnly widget.Clickable
hideAutofillNotices widget.Clickable
showEntries widget.Clickable
showTemplates widget.Clickable
showRecycle widget.Clickable
showAPITokens widget.Clickable
showAPIAudit widget.Clickable
showAbout widget.Clickable
showLocalLifecycle widget.Clickable
showRemoteLifecycle widget.Clickable
showSyncLocal widget.Clickable
showSyncRemote widget.Clickable
showSyncPull widget.Clickable
showSyncPush widget.Clickable
showAutofillApprovalAsk widget.Clickable
showAutofillApprovalAllow widget.Clickable
showAutofillApprovalBlock widget.Clickable
allowApproval widget.Clickable
denyApproval widget.Clickable
cancelApproval widget.Clickable
cancelLifecycleProgress widget.Clickable
retryLifecycleOpen widget.Clickable
approvalPermanent widget.Bool
apiPolicyAllow widget.Bool
apiPolicyGroupScopeW widget.Bool
apiTokenDisabled widget.Bool
settingsGroupControls widget.Bool
settingsLifecycleAdvanced widget.Bool
settingsHistory widget.Bool
settingsDenseLayout widget.Bool
entryClicks []widget.Clickable
apiTokenClicks []widget.Clickable
apiPolicyRemoves []widget.Clickable
apiAuditClicks []widget.Clickable
apiAuditTokenFilters []widget.Clickable
apiAuditDecisionFilters []widget.Clickable
apiAuditOperationFilters []widget.Clickable
clearAPIAuditFilters widget.Clickable
historyClicks []widget.Clickable
attachmentClicks []widget.Clickable
breadcrumbs []widget.Clickable
groupClicks []widget.Clickable
recentVaultClicks []widget.Clickable
recentRemoteClicks []widget.Clickable
removeCustomFields []widget.Clickable
state appstate.State
visible []entry
currentPath []string
syncedPath []string
selectedHistoryIndex int
showPassword bool
generatedPasswordDraft bool
togglePassword widget.Clickable
copyAPITokenSecret widget.Clickable
issueAPIToken widget.Clickable
saveAPIToken widget.Clickable
rotateAPIToken widget.Clickable
disableAPIToken widget.Clickable
revokeAPIToken widget.Clickable
deleteAPIToken widget.Clickable
useCurrentGroupForPolicy widget.Clickable
useSelectedEntryForPolicy widget.Clickable
clearAPIPolicyTarget widget.Clickable
addAPIPolicyRule widget.Clickable
phoneSplit widget.Float
splitDrag gesture.Drag
splitBase float32
splitStartY float32
phoneSpan int
phoneGroupBrowserExpanded bool
eyeIcon *widget.Icon
eyeOffIcon *widget.Icon
copyIcon *widget.Icon
expandMoreIcon *widget.Icon
expandLessIcon *widget.Icon
chevronRightIcon *widget.Icon
chevronDownIcon *widget.Icon
settingsIcon *widget.Icon
menuIcon *widget.Icon
clipboardWriter clipboard.Writer
loadingMessage string
loadingActionLabel string
lifecycleMode string
syncSourceMode syncSourceMode
syncDirection syncDirection
syncLocalImportName string
syncLocalImportContent []byte
syncLocalPath widget.Editor
syncRemoteBaseURL widget.Editor
syncRemotePath widget.Editor
syncRemoteUsername widget.Editor
syncRemotePassword widget.Editor
syncDialogOpen bool
syncMenuOpen bool
mainMenuOpen bool
selectedRemoteConnection bool
selectedVaultRemoteProfileID string
selectedVaultRemoteCredentialEntryID string
securityDialogOpen bool
remotePrefsDialogOpen bool
showSyncPassword bool
keyboardFocus focusID
defaultSaveAsPath string
recentVaultsPath string
settingsPath string
uiPreferencesPath string
recentRemotesPath string
autofillCachePath string
editingEntry bool
syncDefaultSourceMode syncSourceMode
syncDefaultDirection syncDirection
groupControlsHidden bool
lifecycleAdvancedHidden bool
historyHidden bool
denseLayout bool
statusBannerTTL time.Duration
autofillNoticePreference autofillNoticeMode
autofillFirstFillApprovalMode autofillFirstFillApprovalMode
accessibilityPrefs accessibilityPreferences
settingsDraft settingsDraft
recentVaults []string
recentRemotes []recentRemoteRecord
recentVaultGroups map[string][]string
recentVaultUsedAt map[string]time.Time
entriesState entriesSectionState
deleteGroupPath []string
apiPolicyGroupScope bool
apiTokenSecret string
selectedAuditIndex int
statusExpiresAt time.Time
now func() time.Time
apiHost *api.Host
auditLog *apiaudit.Log
grpcAddress string
backgroundResults chan backgroundActionResult
backgroundActionSerial int
activeBackgroundAction int
lastLifecycleAction string
requestMasterPassFocus bool
invalidate func()
}
type backgroundActionResult struct {
@@ -1069,6 +1071,22 @@ func (u *ui) openRemoteAction() error {
if err != nil {
return err
}
if binding, resolved, ok, err := u.resolvedSelectedVaultRemoteBinding(); err != nil {
return err
} else if ok {
if err := u.state.OpenBoundRemoteVault(binding, key); err != nil {
return err
}
u.remoteBaseURL.SetText(resolved.Profile.BaseURL)
u.remotePath.SetText(resolved.Profile.Path)
u.noteRecentRemote(resolved.Profile.BaseURL, resolved.Profile.Path)
u.resetPasswordPeek()
u.restoreRecentRemoteGroup(resolved.Profile.BaseURL, resolved.Profile.Path)
u.loadSecuritySettingsFromSession()
u.editingEntry = false
u.filter()
return nil
}
client := webdav.Client{
BaseURL: strings.TrimSpace(u.remoteBaseURL.Text()),
Username: strings.TrimSpace(u.remoteUsername.Text()),
@@ -1108,6 +1126,20 @@ func (u *ui) startOpenRemoteAction() {
Password: u.remotePassword.Text(),
}
remotePath := strings.TrimSpace(u.remotePath.Text())
if _, resolved, ok, err := u.resolvedSelectedVaultRemoteBinding(); err != nil {
u.state.ErrorMessage = u.describeActionError("open remote vault", err)
u.requestMasterPassFocus = true
return
} else if ok {
client = webdav.Client{
BaseURL: resolved.Profile.BaseURL,
Username: resolved.Credentials.Username,
Password: resolved.Credentials.Password,
}
remotePath = resolved.Profile.Path
u.remoteBaseURL.SetText(resolved.Profile.BaseURL)
u.remotePath.SetText(resolved.Profile.Path)
}
u.lastLifecycleAction = "open remote vault"
u.runBackgroundAction("open remote vault", func() (func() error, error) {
prepared, err := session.PrepareRemoteOpen(client, remotePath, key)
@@ -1904,6 +1936,33 @@ func (u *ui) availableRemoteCredentialEntries() []vault.Entry {
return entries
}
func (u *ui) selectedVaultRemoteBinding() (appstate.RemoteBinding, bool) {
if strings.TrimSpace(u.selectedVaultRemoteProfileID) == "" || strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) == "" {
return appstate.RemoteBinding{}, false
}
return appstate.RemoteBinding{
LocalVaultPath: strings.TrimSpace(u.vaultPath.Text()),
RemoteProfileID: strings.TrimSpace(u.selectedVaultRemoteProfileID),
CredentialEntryID: strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID),
}, true
}
func (u *ui) resolvedSelectedVaultRemoteBinding() (appstate.RemoteBinding, appstate.ResolvedRemoteBinding, bool, error) {
binding, ok := u.selectedVaultRemoteBinding()
if !ok {
return appstate.RemoteBinding{}, appstate.ResolvedRemoteBinding{}, false, nil
}
model, err := u.state.Session.Current()
if err != nil {
return appstate.RemoteBinding{}, appstate.ResolvedRemoteBinding{}, false, err
}
resolved, err := binding.Resolve(model)
if err != nil {
return appstate.RemoteBinding{}, appstate.ResolvedRemoteBinding{}, false, err
}
return binding, resolved, true, nil
}
func (u *ui) noteCurrentRemotePath() {
status, ok := u.state.Session.(sessionStatus)
if !ok || !status.IsRemote() || status.IsLocked() {
+140
View File
@@ -83,6 +83,22 @@ func (s summarySession) HasVault() bool
func (s summarySession) IsLocked() bool { return s.locked }
func (s summarySession) IsRemote() bool { return s.remote }
type remoteOpenCaptureSession struct {
model vault.Model
remoteClient webdav.Client
remotePath string
}
func (s *remoteOpenCaptureSession) Current() (vault.Model, error) {
return s.model, nil
}
func (s *remoteOpenCaptureSession) OpenRemote(client webdav.Client, path string, _ vault.MasterKey) error {
s.remoteClient = client
s.remotePath = path
return nil
}
func TestUIFiltersUsingVaultModelPathsAndSearch(t *testing.T) {
t.Parallel()
@@ -1658,6 +1674,130 @@ func TestUIOpenRemoteReportsTransportFailure(t *testing.T) {
}
}
func TestUIOpenRemoteActionUsesSelectedVaultBinding(t *testing.T) {
t.Parallel()
sess := &remoteOpenCaptureSession{
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",
}},
},
}
u := newUIWithSession("desktop", sess)
u.masterPassword.SetText("correct horse battery staple")
u.selectedVaultRemoteProfileID = "family-webdav"
u.selectedVaultRemoteCredentialEntryID = "remote-creds-1"
if err := u.openRemoteAction(); err != nil {
t.Fatalf("openRemoteAction() error = %v", err)
}
if got := sess.remoteClient.BaseURL; got != "https://dav.example.invalid/remote.php/dav" {
t.Fatalf("remoteClient.BaseURL = %q, want remote.php/dav URL", got)
}
if got := sess.remoteClient.Username; got != "tjulian" {
t.Fatalf("remoteClient.Username = %q, want tjulian", got)
}
if got := sess.remoteClient.Password; got != "token-1" {
t.Fatalf("remoteClient.Password = %q, want token-1", got)
}
if got := sess.remotePath; got != "files/family/keepass.kdbx" {
t.Fatalf("remotePath = %q, want files/family/keepass.kdbx", 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 editor = %q, want resolved profile path", got)
}
}
func TestUIStartOpenRemoteActionUsesSelectedVaultBinding(t *testing.T) {
t.Parallel()
localKey := vault.MasterKey{Password: "correct horse battery staple"}
localModel := 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: "",
Path: "files/family/keepass.kdbx",
}},
}
remoteModel := vault.Model{
Entries: []vault.Entry{{
ID: "vault-console",
Title: "Vault Console",
Username: "dannyocean",
Password: "token-1",
URL: "https://vault.crew.example.invalid",
Path: []string{"Root", "Internet"},
}},
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Fatalf("unexpected method %s", r.Method)
}
var encoded bytes.Buffer
if err := vault.SaveKDBXWithKey(&encoded, remoteModel, localKey); err != nil {
t.Fatalf("SaveKDBXWithKey() error = %v", err)
}
w.Header().Set("ETag", "\"v1\"")
_, _ = w.Write(encoded.Bytes())
}))
defer server.Close()
localModel.RemoteProfiles[0].BaseURL = server.URL
manager := &session.Manager{}
if err := manager.Create(localModel, localKey); err != nil {
t.Fatalf("manager.Create() error = %v", err)
}
u := newUIWithSession("desktop", manager)
u.masterPassword.SetText(localKey.Password)
u.selectedVaultRemoteProfileID = "family-webdav"
u.selectedVaultRemoteCredentialEntryID = "remote-creds-1"
u.startOpenRemoteAction()
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.remoteBaseURL.Text(); got != server.URL {
t.Fatalf("remoteBaseURL = %q, want server URL from selected profile", got)
}
if got := u.remotePath.Text(); got != "files/family/keepass.kdbx" {
t.Fatalf("remotePath = %q, want selected profile path", got)
}
}
func TestUIRemoteSaveConflictShowsVisibleErrorAndKeepsDirtyState(t *testing.T) {
t.Parallel()