From 677464dc9add631bfcb963d593c51ea1f3c1fe2d Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Mon, 6 Apr 2026 12:09:31 -0700 Subject: [PATCH] Open remote vaults from vault bindings --- appstate/state.go | 19 ++++++++ appstate/state_test.go | 98 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 110 insertions(+), 7 deletions(-) diff --git a/appstate/state.go b/appstate/state.go index 45dcc1a..7f3b5eb 100644 --- a/appstate/state.go +++ b/appstate/state.go @@ -834,6 +834,25 @@ func (s *State) OpenRemoteVault(client webdav.Client, path string, key vault.Mas return nil } +func (s *State) OpenBoundRemoteVault(binding RemoteBinding, key vault.MasterKey) error { + model, err := s.currentModel() + if err != nil { + return err + } + + resolved, err := binding.Resolve(model) + if err != nil { + return err + } + + client := webdav.Client{ + BaseURL: resolved.Profile.BaseURL, + Username: resolved.Credentials.Username, + Password: resolved.Credentials.Password, + } + return s.OpenRemoteVault(client, resolved.Profile.Path, key) +} + func (s *State) CreateGroup(name string) error { session, ok := s.Session.(MutableSession) if !ok { diff --git a/appstate/state_test.go b/appstate/state_test.go index 270d5f3..3c11eef 100644 --- a/appstate/state_test.go +++ b/appstate/state_test.go @@ -964,6 +964,87 @@ func TestOpenRemoteVaultResetsSelectionPathAndDirtyState(t *testing.T) { } } +func TestOpenBoundRemoteVaultResolvesClientFromVaultBinding(t *testing.T) { + t.Parallel() + + sess := &lifecycleStubSession{ + 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", + }, + }, + }, + } + state := State{ + Session: sess, + CurrentPath: []string{"Root", "Internet"}, + SelectedEntryID: "vault-console", + Dirty: true, + } + + err := state.OpenBoundRemoteVault(RemoteBinding{ + LocalVaultPath: "/tmp/family.kdbx", + RemoteProfileID: "family-webdav", + CredentialEntryID: "remote-creds-1", + SyncMode: SyncModeAutomaticOnOpenSave, + }, vault.MasterKey{Password: "correct horse battery staple"}) + if err != nil { + t.Fatalf("OpenBoundRemoteVault() error = %v", err) + } + + if got := sess.remoteClient.BaseURL; got != "https://dav.example.invalid/remote.php/dav" { + t.Fatalf("remote client base URL = %q, want remote.php/dav URL", got) + } + if got := sess.remoteClient.Username; got != "tjulian" { + t.Fatalf("remote client username = %q, want tjulian", got) + } + if got := sess.remoteClient.Password; got != "token-1" { + t.Fatalf("remote client 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 len(state.CurrentPath) != 0 { + t.Fatalf("CurrentPath = %v, want empty", state.CurrentPath) + } + if state.SelectedEntryID != "" { + t.Fatalf("SelectedEntryID = %q, want empty", state.SelectedEntryID) + } + if state.Dirty { + t.Fatal("Dirty = true, want false after bound remote open") + } +} + +func TestOpenBoundRemoteVaultReturnsResolutionErrors(t *testing.T) { + t.Parallel() + + sess := &lifecycleStubSession{model: vault.Model{}} + state := State{Session: sess} + + err := state.OpenBoundRemoteVault(RemoteBinding{ + LocalVaultPath: "/tmp/family.kdbx", + RemoteProfileID: "missing-profile", + CredentialEntryID: "remote-creds-1", + }, vault.MasterKey{Password: "correct horse battery staple"}) + if !errors.Is(err, vault.ErrRemoteProfileNotFound) { + t.Fatalf("OpenBoundRemoteVault() error = %v, want ErrRemoteProfileNotFound", err) + } +} + func TestLockClearsSelectionAndMakesVaultUnavailable(t *testing.T) { t.Parallel() @@ -1553,15 +1634,17 @@ func (s *saveableStubSession) Save() error { } type lifecycleStubSession struct { - createCalls int - openPath string - saveAsPath string - remotePath string - changedKey vault.MasterKey + createCalls int + model vault.Model + openPath string + saveAsPath string + remoteClient webdav.Client + remotePath string + changedKey vault.MasterKey } func (s *lifecycleStubSession) Current() (vault.Model, error) { - return vault.Model{}, nil + return s.model, nil } func (s *lifecycleStubSession) Create(_ vault.Model, _ vault.MasterKey) error { @@ -1579,7 +1662,8 @@ func (s *lifecycleStubSession) SaveAs(path string) error { return nil } -func (s *lifecycleStubSession) OpenRemote(_ webdav.Client, path string, _ vault.MasterKey) error { +func (s *lifecycleStubSession) OpenRemote(client webdav.Client, path string, _ vault.MasterKey) error { + s.remoteClient = client s.remotePath = path return nil }