diff --git a/appstate/state.go b/appstate/state.go index 7f3b5eb..12a0d54 100644 --- a/appstate/state.go +++ b/appstate/state.go @@ -853,6 +853,27 @@ func (s *State) OpenBoundRemoteVault(binding RemoteBinding, key vault.MasterKey) return s.OpenRemoteVault(client, resolved.Profile.Path, key) } +func (s *State) ConfigureRemoteBinding(input RemoteBindingInput) (RemoteBinding, error) { + session, ok := s.Session.(MutableSession) + if !ok { + return RemoteBinding{}, fmt.Errorf("session is not mutable") + } + + model, err := session.Current() + if err != nil { + return RemoteBinding{}, err + } + + binding, err := ConfigureRemoteBinding(&model, input) + if err != nil { + return RemoteBinding{}, err + } + + session.Replace(model) + s.Dirty = true + return binding, nil +} + 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 3c11eef..e72265f 100644 --- a/appstate/state_test.go +++ b/appstate/state_test.go @@ -1045,6 +1045,64 @@ func TestOpenBoundRemoteVaultReturnsResolutionErrors(t *testing.T) { } } +func TestConfigureRemoteBindingPersistsIntoCurrentVaultAndMarksDirty(t *testing.T) { + t.Parallel() + + sess := &mutableStubSession{model: vault.Model{}} + state := State{Session: sess} + + binding, err := state.ConfigureRemoteBinding(RemoteBindingInput{ + LocalVaultPath: "/tmp/family.kdbx", + RemoteProfileID: "family-webdav", + RemoteProfileName: "Family Vault", + BaseURL: "https://dav.example.invalid/remote.php/dav", + RemotePath: "files/family/keepass.kdbx", + CredentialEntryID: "remote-creds-1", + CredentialTitle: "WebDAV Sign-In", + Username: "tjulian", + Password: "token-1", + CredentialPath: []string{"Crew", "Internet"}, + SyncMode: SyncModeAutomaticOnOpenSave, + }) + if err != nil { + t.Fatalf("ConfigureRemoteBinding() error = %v", err) + } + + if !state.Dirty { + t.Fatal("Dirty = false, want true after ConfigureRemoteBinding") + } + if got := binding.RemoteProfileID; got != "family-webdav" { + t.Fatalf("binding.RemoteProfileID = %q, want family-webdav", got) + } + if got := len(sess.model.RemoteProfiles); got != 1 { + t.Fatalf("len(RemoteProfiles) = %d, want 1", got) + } + credentials, err := sess.model.EntryByID("remote-creds-1") + if err != nil { + t.Fatalf("EntryByID(remote-creds-1) error = %v", err) + } + if credentials.Username != "tjulian" || credentials.Password != "token-1" { + t.Fatalf("stored credential entry = %#v, want tjulian/token-1", credentials) + } +} + +func TestConfigureRemoteBindingRequiresMutableSession(t *testing.T) { + t.Parallel() + + state := State{Session: stubSession{model: vault.Model{}}} + _, err := state.ConfigureRemoteBinding(RemoteBindingInput{ + LocalVaultPath: "/tmp/family.kdbx", + RemoteProfileID: "family-webdav", + BaseURL: "https://dav.example.invalid/remote.php/dav", + RemotePath: "files/family/keepass.kdbx", + CredentialEntryID: "remote-creds-1", + Password: "token-1", + }) + if err == nil { + t.Fatal("ConfigureRemoteBinding() error = nil, want mutability error") + } +} + func TestLockClearsSelectionAndMakesVaultUnavailable(t *testing.T) { t.Parallel()