diff --git a/main.go b/main.go index 4191d7a..de1647a 100644 --- a/main.go +++ b/main.go @@ -2143,31 +2143,55 @@ func (u *ui) selectVaultRemoteCredentialEntry(id string) { } func (u *ui) selectedVaultRemoteProfile() (vault.RemoteProfile, bool) { - for _, profile := range u.availableRemoteProfiles() { - if profile.ID == strings.TrimSpace(u.selectedVaultRemoteProfileID) { + selectedID := strings.TrimSpace(u.selectedVaultRemoteProfileID) + profiles := u.availableRemoteProfiles() + for _, profile := range profiles { + if profile.ID == selectedID { return profile, true } } + if selectedID == "" && len(profiles) == 1 { + return profiles[0], true + } return vault.RemoteProfile{}, false } func (u *ui) selectedVaultRemoteCredentialEntry() (vault.Entry, bool) { - for _, entry := range u.availableRemoteCredentialEntries() { - if entry.ID == strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) { + selectedID := strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) + entries := u.availableRemoteCredentialEntries() + for _, entry := range entries { + if entry.ID == selectedID { return entry, true } } + if selectedID == "" && len(entries) == 1 { + return entries[0], true + } return vault.Entry{}, false } func (u *ui) selectedVaultRemoteBinding() (appstate.RemoteBinding, bool) { - if strings.TrimSpace(u.selectedVaultRemoteProfileID) == "" || strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) == "" { + profileID := strings.TrimSpace(u.selectedVaultRemoteProfileID) + entryID := strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID) + if profileID != "" && entryID != "" { + return appstate.RemoteBinding{ + LocalVaultPath: strings.TrimSpace(u.vaultPath.Text()), + RemoteProfileID: profileID, + CredentialEntryID: entryID, + }, true + } + profile, ok := u.selectedVaultRemoteProfile() + if !ok { + return appstate.RemoteBinding{}, false + } + entry, ok := u.selectedVaultRemoteCredentialEntry() + if !ok { return appstate.RemoteBinding{}, false } return appstate.RemoteBinding{ LocalVaultPath: strings.TrimSpace(u.vaultPath.Text()), - RemoteProfileID: strings.TrimSpace(u.selectedVaultRemoteProfileID), - CredentialEntryID: strings.TrimSpace(u.selectedVaultRemoteCredentialEntryID), + RemoteProfileID: profile.ID, + CredentialEntryID: entry.ID, }, true } diff --git a/main_test.go b/main_test.go index 8192033..9b41952 100644 --- a/main_test.go +++ b/main_test.go @@ -1804,6 +1804,50 @@ func TestUIOpenRemoteActionUsesSelectedVaultBinding(t *testing.T) { } } +func TestUIOpenRemoteActionUsesImplicitSingleVaultBinding(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.vaultPath.SetText("/vaults/family.kdbx") + + 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) + } +} + func TestUIOpenRemoteActionBootstrapsFromLocalVaultBinding(t *testing.T) { t.Parallel() @@ -1885,6 +1929,7 @@ func TestUIStartOpenRemoteActionUsesSelectedVaultBinding(t *testing.T) { t.Parallel() localKey := vault.MasterKey{Password: "correct horse battery staple"} + localPath := filepath.Join(t.TempDir(), "family.kdbx") localModel := vault.Model{ Entries: []vault.Entry{{ ID: "remote-creds-1", @@ -1935,6 +1980,7 @@ func TestUIStartOpenRemoteActionUsesSelectedVaultBinding(t *testing.T) { u := newUIWithSession("desktop", manager) u.masterPassword.SetText(localKey.Password) + u.vaultPath.SetText(localPath) u.selectedVaultRemoteProfileID = "family-webdav" u.selectedVaultRemoteCredentialEntryID = "remote-creds-1" @@ -1954,6 +2000,79 @@ func TestUIStartOpenRemoteActionUsesSelectedVaultBinding(t *testing.T) { } } +func TestUIStartOpenRemoteActionUsesImplicitSingleVaultBinding(t *testing.T) { + t.Parallel() + + localKey := vault.MasterKey{Password: "correct horse battery staple"} + localPath := filepath.Join(t.TempDir(), "family.kdbx") + 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.vaultPath.SetText(localPath) + + 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 implicit profile", got) + } + if got := u.remotePath.Text(); got != "files/family/keepass.kdbx" { + t.Fatalf("remotePath = %q, want implicit profile path", got) + } +} + func TestUIStartOpenRemoteActionBootstrapsFromLocalVaultBinding(t *testing.T) { t.Parallel()