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 { type ui struct {
mode string mode string
theme *material.Theme theme *material.Theme
fileExplorer *explorer.Explorer fileExplorer *explorer.Explorer
logoHorizontal paint.ImageOp logoHorizontal paint.ImageOp
splashSquare paint.ImageOp splashSquare paint.ImageOp
search widget.Editor search widget.Editor
vaultPath widget.Editor vaultPath widget.Editor
saveAsPath widget.Editor saveAsPath widget.Editor
remoteBaseURL widget.Editor remoteBaseURL widget.Editor
remotePath widget.Editor remotePath widget.Editor
remoteUsername widget.Editor remoteUsername widget.Editor
remotePassword widget.Editor remotePassword widget.Editor
masterPassword widget.Editor masterPassword widget.Editor
keyFilePath widget.Editor keyFilePath widget.Editor
apiTokenName widget.Editor apiTokenName widget.Editor
apiTokenClientName widget.Editor apiTokenClientName widget.Editor
apiTokenExpiresAt widget.Editor apiTokenExpiresAt widget.Editor
apiPolicyOperation widget.Editor apiPolicyOperation widget.Editor
apiPolicyPath widget.Editor apiPolicyPath widget.Editor
apiPolicyEntryID widget.Editor apiPolicyEntryID widget.Editor
securityCipher widget.Editor securityCipher widget.Editor
securityKDF widget.Editor securityKDF widget.Editor
entryID widget.Editor entryID widget.Editor
entryTitle widget.Editor entryTitle widget.Editor
entryUsername widget.Editor entryUsername widget.Editor
entryPassword widget.Editor entryPassword widget.Editor
entryURL widget.Editor entryURL widget.Editor
entryNotes widget.Editor entryNotes widget.Editor
entryTags widget.Editor entryTags widget.Editor
entryPath widget.Editor entryPath widget.Editor
entryFields widget.Editor entryFields widget.Editor
customFieldKeys []widget.Editor customFieldKeys []widget.Editor
customFieldValues []widget.Editor customFieldValues []widget.Editor
historyIndex widget.Editor historyIndex widget.Editor
groupName widget.Editor groupName widget.Editor
groupParentPath widget.Editor groupParentPath widget.Editor
passwordProfile widget.Editor passwordProfile widget.Editor
attachmentName widget.Editor attachmentName widget.Editor
attachmentPath widget.Editor attachmentPath widget.Editor
exportAttachmentPath widget.Editor exportAttachmentPath widget.Editor
autofillBrowserAllowlist widget.Editor autofillBrowserAllowlist widget.Editor
autofillAppAllowlist widget.Editor autofillAppAllowlist widget.Editor
autofillPackageRules widget.Editor autofillPackageRules widget.Editor
list widget.List list widget.List
groupList widget.List groupList widget.List
detailList widget.List detailList widget.List
apiPolicyList widget.List apiPolicyList widget.List
lifecycleList widget.List lifecycleList widget.List
phonePanelList widget.List phonePanelList widget.List
securityDialogList widget.List securityDialogList widget.List
remotePrefsDialogList widget.List remotePrefsDialogList widget.List
recentVaultListState widget.List recentVaultListState widget.List
recentRemoteListState widget.List recentRemoteListState widget.List
copyUser widget.Clickable copyUser widget.Clickable
copyPass widget.Clickable copyPass widget.Clickable
copyURL widget.Clickable copyURL widget.Clickable
lockVault widget.Clickable lockVault widget.Clickable
unlockVault widget.Clickable unlockVault widget.Clickable
createVault widget.Clickable createVault widget.Clickable
openVault widget.Clickable openVault widget.Clickable
saveVault widget.Clickable saveVault widget.Clickable
saveAsVault widget.Clickable saveAsVault widget.Clickable
openRemote widget.Clickable openRemote widget.Clickable
changeMasterKey widget.Clickable changeMasterKey widget.Clickable
synchronizeVault widget.Clickable synchronizeVault widget.Clickable
toggleSyncMenu widget.Clickable toggleSyncMenu widget.Clickable
toggleMainMenu widget.Clickable toggleMainMenu widget.Clickable
openAdvancedSync widget.Clickable openAdvancedSync widget.Clickable
openSecuritySettings widget.Clickable openSecuritySettings widget.Clickable
openRemotePrefsHelp widget.Clickable openRemotePrefsHelp widget.Clickable
closeAdvancedSync widget.Clickable closeAdvancedSync widget.Clickable
closeSecuritySettings widget.Clickable closeSecuritySettings widget.Clickable
closeRemotePrefsHelp widget.Clickable closeRemotePrefsHelp widget.Clickable
runAdvancedSync widget.Clickable runAdvancedSync widget.Clickable
saveSecuritySettings widget.Clickable saveSecuritySettings widget.Clickable
settingsDensityDense widget.Clickable settingsDensityDense widget.Clickable
settingsDensityComfortable widget.Clickable settingsDensityComfortable widget.Clickable
settingsContrastStandard widget.Clickable settingsContrastStandard widget.Clickable
settingsContrastHigh widget.Clickable settingsContrastHigh widget.Clickable
settingsReducedMotionOff widget.Clickable settingsReducedMotionOff widget.Clickable
settingsReducedMotionOn widget.Clickable settingsReducedMotionOn widget.Clickable
settingsKeyboardFocusStandard widget.Clickable settingsKeyboardFocusStandard widget.Clickable
settingsKeyboardFocusProminent widget.Clickable settingsKeyboardFocusProminent widget.Clickable
showSettingsSyncLocal widget.Clickable showSettingsSyncLocal widget.Clickable
showSettingsSyncRemote widget.Clickable showSettingsSyncRemote widget.Clickable
showSettingsSyncPull widget.Clickable showSettingsSyncPull widget.Clickable
showSettingsSyncPush widget.Clickable showSettingsSyncPush widget.Clickable
editEntry widget.Clickable editEntry widget.Clickable
cancelEdit widget.Clickable cancelEdit widget.Clickable
pickVaultPath widget.Clickable pickVaultPath widget.Clickable
pickKeyFile widget.Clickable pickKeyFile widget.Clickable
pickSyncLocalPath widget.Clickable pickSyncLocalPath widget.Clickable
clearVaultSelection widget.Clickable clearVaultSelection widget.Clickable
clearRemoteSelection widget.Clickable clearRemoteSelection widget.Clickable
dismissBanner widget.Clickable dismissBanner widget.Clickable
addEntry widget.Clickable addEntry widget.Clickable
saveEntry widget.Clickable saveEntry widget.Clickable
duplicateEntry widget.Clickable duplicateEntry widget.Clickable
deleteEntry widget.Clickable deleteEntry widget.Clickable
restoreEntry widget.Clickable restoreEntry widget.Clickable
saveTemplate widget.Clickable saveTemplate widget.Clickable
deleteTemplate widget.Clickable deleteTemplate widget.Clickable
instantiateTemplate widget.Clickable instantiateTemplate widget.Clickable
addAttachment widget.Clickable addAttachment widget.Clickable
replaceAttachment widget.Clickable replaceAttachment widget.Clickable
removeAttachment widget.Clickable removeAttachment widget.Clickable
exportAttachment widget.Clickable exportAttachment widget.Clickable
restoreHistory widget.Clickable restoreHistory widget.Clickable
generatePassword widget.Clickable generatePassword widget.Clickable
goToRootGroup widget.Clickable goToRootGroup widget.Clickable
goToParentGroup widget.Clickable goToParentGroup widget.Clickable
createGroup widget.Clickable createGroup widget.Clickable
moveGroup widget.Clickable moveGroup widget.Clickable
renameGroup widget.Clickable renameGroup widget.Clickable
deleteGroup widget.Clickable deleteGroup widget.Clickable
confirmDeleteGroup widget.Clickable confirmDeleteGroup widget.Clickable
cancelDeleteGroup widget.Clickable cancelDeleteGroup widget.Clickable
addCustomField widget.Clickable addCustomField widget.Clickable
toggleGroupControls widget.Clickable toggleGroupControls widget.Clickable
toggleLifecycleAdvanced widget.Clickable toggleLifecycleAdvanced widget.Clickable
toggleHistory widget.Clickable toggleHistory widget.Clickable
togglePasswordInline widget.Clickable togglePasswordInline widget.Clickable
toggleSyncPassword widget.Clickable toggleSyncPassword widget.Clickable
setStatusBannerShort widget.Clickable setStatusBannerShort widget.Clickable
setStatusBannerStandard widget.Clickable setStatusBannerStandard widget.Clickable
setStatusBannerLong widget.Clickable setStatusBannerLong widget.Clickable
showAllAutofillNotices widget.Clickable showAllAutofillNotices widget.Clickable
showApprovalAutofillOnly widget.Clickable showApprovalAutofillOnly widget.Clickable
hideAutofillNotices widget.Clickable hideAutofillNotices widget.Clickable
showEntries widget.Clickable showEntries widget.Clickable
showTemplates widget.Clickable showTemplates widget.Clickable
showRecycle widget.Clickable showRecycle widget.Clickable
showAPITokens widget.Clickable showAPITokens widget.Clickable
showAPIAudit widget.Clickable showAPIAudit widget.Clickable
showAbout widget.Clickable showAbout widget.Clickable
showLocalLifecycle widget.Clickable showLocalLifecycle widget.Clickable
showRemoteLifecycle widget.Clickable showRemoteLifecycle widget.Clickable
showSyncLocal widget.Clickable showSyncLocal widget.Clickable
showSyncRemote widget.Clickable showSyncRemote widget.Clickable
showSyncPull widget.Clickable showSyncPull widget.Clickable
showSyncPush widget.Clickable showSyncPush widget.Clickable
showAutofillApprovalAsk widget.Clickable showAutofillApprovalAsk widget.Clickable
showAutofillApprovalAllow widget.Clickable showAutofillApprovalAllow widget.Clickable
showAutofillApprovalBlock widget.Clickable showAutofillApprovalBlock widget.Clickable
allowApproval widget.Clickable allowApproval widget.Clickable
denyApproval widget.Clickable denyApproval widget.Clickable
cancelApproval widget.Clickable cancelApproval widget.Clickable
cancelLifecycleProgress widget.Clickable cancelLifecycleProgress widget.Clickable
retryLifecycleOpen widget.Clickable retryLifecycleOpen widget.Clickable
approvalPermanent widget.Bool approvalPermanent widget.Bool
apiPolicyAllow widget.Bool apiPolicyAllow widget.Bool
apiPolicyGroupScopeW widget.Bool apiPolicyGroupScopeW widget.Bool
apiTokenDisabled widget.Bool apiTokenDisabled widget.Bool
settingsGroupControls widget.Bool settingsGroupControls widget.Bool
settingsLifecycleAdvanced widget.Bool settingsLifecycleAdvanced widget.Bool
settingsHistory widget.Bool settingsHistory widget.Bool
settingsDenseLayout widget.Bool settingsDenseLayout widget.Bool
entryClicks []widget.Clickable entryClicks []widget.Clickable
apiTokenClicks []widget.Clickable apiTokenClicks []widget.Clickable
apiPolicyRemoves []widget.Clickable apiPolicyRemoves []widget.Clickable
apiAuditClicks []widget.Clickable apiAuditClicks []widget.Clickable
apiAuditTokenFilters []widget.Clickable apiAuditTokenFilters []widget.Clickable
apiAuditDecisionFilters []widget.Clickable apiAuditDecisionFilters []widget.Clickable
apiAuditOperationFilters []widget.Clickable apiAuditOperationFilters []widget.Clickable
clearAPIAuditFilters widget.Clickable clearAPIAuditFilters widget.Clickable
historyClicks []widget.Clickable historyClicks []widget.Clickable
attachmentClicks []widget.Clickable attachmentClicks []widget.Clickable
breadcrumbs []widget.Clickable breadcrumbs []widget.Clickable
groupClicks []widget.Clickable groupClicks []widget.Clickable
recentVaultClicks []widget.Clickable recentVaultClicks []widget.Clickable
recentRemoteClicks []widget.Clickable recentRemoteClicks []widget.Clickable
removeCustomFields []widget.Clickable removeCustomFields []widget.Clickable
state appstate.State state appstate.State
visible []entry visible []entry
currentPath []string currentPath []string
syncedPath []string syncedPath []string
selectedHistoryIndex int selectedHistoryIndex int
showPassword bool showPassword bool
generatedPasswordDraft bool generatedPasswordDraft bool
togglePassword widget.Clickable togglePassword widget.Clickable
copyAPITokenSecret widget.Clickable copyAPITokenSecret widget.Clickable
issueAPIToken widget.Clickable issueAPIToken widget.Clickable
saveAPIToken widget.Clickable saveAPIToken widget.Clickable
rotateAPIToken widget.Clickable rotateAPIToken widget.Clickable
disableAPIToken widget.Clickable disableAPIToken widget.Clickable
revokeAPIToken widget.Clickable revokeAPIToken widget.Clickable
deleteAPIToken widget.Clickable deleteAPIToken widget.Clickable
useCurrentGroupForPolicy widget.Clickable useCurrentGroupForPolicy widget.Clickable
useSelectedEntryForPolicy widget.Clickable useSelectedEntryForPolicy widget.Clickable
clearAPIPolicyTarget widget.Clickable clearAPIPolicyTarget widget.Clickable
addAPIPolicyRule widget.Clickable addAPIPolicyRule widget.Clickable
phoneSplit widget.Float phoneSplit widget.Float
splitDrag gesture.Drag splitDrag gesture.Drag
splitBase float32 splitBase float32
splitStartY float32 splitStartY float32
phoneSpan int phoneSpan int
phoneGroupBrowserExpanded bool phoneGroupBrowserExpanded bool
eyeIcon *widget.Icon eyeIcon *widget.Icon
eyeOffIcon *widget.Icon eyeOffIcon *widget.Icon
copyIcon *widget.Icon copyIcon *widget.Icon
expandMoreIcon *widget.Icon expandMoreIcon *widget.Icon
expandLessIcon *widget.Icon expandLessIcon *widget.Icon
chevronRightIcon *widget.Icon chevronRightIcon *widget.Icon
chevronDownIcon *widget.Icon chevronDownIcon *widget.Icon
settingsIcon *widget.Icon settingsIcon *widget.Icon
menuIcon *widget.Icon menuIcon *widget.Icon
clipboardWriter clipboard.Writer clipboardWriter clipboard.Writer
loadingMessage string loadingMessage string
loadingActionLabel string loadingActionLabel string
lifecycleMode string lifecycleMode string
syncSourceMode syncSourceMode syncSourceMode syncSourceMode
syncDirection syncDirection syncDirection syncDirection
syncLocalImportName string syncLocalImportName string
syncLocalImportContent []byte syncLocalImportContent []byte
syncLocalPath widget.Editor syncLocalPath widget.Editor
syncRemoteBaseURL widget.Editor syncRemoteBaseURL widget.Editor
syncRemotePath widget.Editor syncRemotePath widget.Editor
syncRemoteUsername widget.Editor syncRemoteUsername widget.Editor
syncRemotePassword widget.Editor syncRemotePassword widget.Editor
syncDialogOpen bool syncDialogOpen bool
syncMenuOpen bool syncMenuOpen bool
mainMenuOpen bool mainMenuOpen bool
selectedRemoteConnection bool selectedRemoteConnection bool
securityDialogOpen bool selectedVaultRemoteProfileID string
remotePrefsDialogOpen bool selectedVaultRemoteCredentialEntryID string
showSyncPassword bool securityDialogOpen bool
keyboardFocus focusID remotePrefsDialogOpen bool
defaultSaveAsPath string showSyncPassword bool
recentVaultsPath string keyboardFocus focusID
settingsPath string defaultSaveAsPath string
uiPreferencesPath string recentVaultsPath string
recentRemotesPath string settingsPath string
autofillCachePath string uiPreferencesPath string
editingEntry bool recentRemotesPath string
syncDefaultSourceMode syncSourceMode autofillCachePath string
syncDefaultDirection syncDirection editingEntry bool
groupControlsHidden bool syncDefaultSourceMode syncSourceMode
lifecycleAdvancedHidden bool syncDefaultDirection syncDirection
historyHidden bool groupControlsHidden bool
denseLayout bool lifecycleAdvancedHidden bool
statusBannerTTL time.Duration historyHidden bool
autofillNoticePreference autofillNoticeMode denseLayout bool
autofillFirstFillApprovalMode autofillFirstFillApprovalMode statusBannerTTL time.Duration
accessibilityPrefs accessibilityPreferences autofillNoticePreference autofillNoticeMode
settingsDraft settingsDraft autofillFirstFillApprovalMode autofillFirstFillApprovalMode
recentVaults []string accessibilityPrefs accessibilityPreferences
recentRemotes []recentRemoteRecord settingsDraft settingsDraft
recentVaultGroups map[string][]string recentVaults []string
recentVaultUsedAt map[string]time.Time recentRemotes []recentRemoteRecord
entriesState entriesSectionState recentVaultGroups map[string][]string
deleteGroupPath []string recentVaultUsedAt map[string]time.Time
apiPolicyGroupScope bool entriesState entriesSectionState
apiTokenSecret string deleteGroupPath []string
selectedAuditIndex int apiPolicyGroupScope bool
statusExpiresAt time.Time apiTokenSecret string
now func() time.Time selectedAuditIndex int
apiHost *api.Host statusExpiresAt time.Time
auditLog *apiaudit.Log now func() time.Time
grpcAddress string apiHost *api.Host
backgroundResults chan backgroundActionResult auditLog *apiaudit.Log
backgroundActionSerial int grpcAddress string
activeBackgroundAction int backgroundResults chan backgroundActionResult
lastLifecycleAction string backgroundActionSerial int
requestMasterPassFocus bool activeBackgroundAction int
invalidate func() lastLifecycleAction string
requestMasterPassFocus bool
invalidate func()
} }
type backgroundActionResult struct { type backgroundActionResult struct {
@@ -1069,6 +1071,22 @@ func (u *ui) openRemoteAction() error {
if err != nil { if err != nil {
return err 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{ client := webdav.Client{
BaseURL: strings.TrimSpace(u.remoteBaseURL.Text()), BaseURL: strings.TrimSpace(u.remoteBaseURL.Text()),
Username: strings.TrimSpace(u.remoteUsername.Text()), Username: strings.TrimSpace(u.remoteUsername.Text()),
@@ -1108,6 +1126,20 @@ func (u *ui) startOpenRemoteAction() {
Password: u.remotePassword.Text(), Password: u.remotePassword.Text(),
} }
remotePath := strings.TrimSpace(u.remotePath.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.lastLifecycleAction = "open remote vault"
u.runBackgroundAction("open remote vault", func() (func() error, error) { u.runBackgroundAction("open remote vault", func() (func() error, error) {
prepared, err := session.PrepareRemoteOpen(client, remotePath, key) prepared, err := session.PrepareRemoteOpen(client, remotePath, key)
@@ -1904,6 +1936,33 @@ func (u *ui) availableRemoteCredentialEntries() []vault.Entry {
return entries 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() { func (u *ui) noteCurrentRemotePath() {
status, ok := u.state.Session.(sessionStatus) status, ok := u.state.Session.(sessionStatus)
if !ok || !status.IsRemote() || status.IsLocked() { 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) IsLocked() bool { return s.locked }
func (s summarySession) IsRemote() bool { return s.remote } 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) { func TestUIFiltersUsingVaultModelPathsAndSearch(t *testing.T) {
t.Parallel() 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) { func TestUIRemoteSaveConflictShowsVisibleErrorAndKeepsDirtyState(t *testing.T) {
t.Parallel() t.Parallel()