From c8f91b300bf14db9ac36af151e316d6784322bbe Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Sat, 11 Apr 2026 11:26:00 -0700 Subject: [PATCH] Share hidden vault root logic across UI and API --- internal/api/server.go | 16 ++------ internal/api/server_test.go | 67 +++++++++++++++++++++++++++++++++ internal/appui/recent_state.go | 17 +-------- internal/vaultview/root.go | 23 +++++++++++ internal/vaultview/root_test.go | 26 +++++++++++++ 5 files changed, 121 insertions(+), 28 deletions(-) create mode 100644 internal/vaultview/root.go create mode 100644 internal/vaultview/root_test.go diff --git a/internal/api/server.go b/internal/api/server.go index 6556c30..2a73fe2 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -19,6 +19,7 @@ import ( "git.julianfamily.org/keepassgo/internal/passwords" "git.julianfamily.org/keepassgo/internal/session" "git.julianfamily.org/keepassgo/internal/vault" + "git.julianfamily.org/keepassgo/internal/vaultview" "git.julianfamily.org/keepassgo/internal/webdav" keepassgov1 "git.julianfamily.org/keepassgo/proto/keepassgo/v1" "google.golang.org/grpc/codes" @@ -973,19 +974,8 @@ func entryFromProtoWithModel(model vault.Model, entry *keepassgov1.Entry) vault. } } -func hiddenVaultRoot(model vault.Model) string { - if len(model.EntriesInPath(nil)) != 0 { - return "" - } - groups := model.ChildGroups(nil) - if len(groups) != 1 { - return "" - } - return groups[0] -} - func expandClientPath(model vault.Model, path []string) []string { - root := hiddenVaultRoot(model) + root := vaultview.HiddenRoot(model) if root == "" { return append([]string(nil), path...) } @@ -999,7 +989,7 @@ func expandClientPath(model vault.Model, path []string) []string { } func collapseInternalPath(model vault.Model, path []string) []string { - root := hiddenVaultRoot(model) + root := vaultview.HiddenRoot(model) if root == "" || len(path) == 0 || path[0] != root { return append([]string(nil), path...) } diff --git a/internal/api/server_test.go b/internal/api/server_test.go index 1b86434..695e18a 100644 --- a/internal/api/server_test.go +++ b/internal/api/server_test.go @@ -265,6 +265,46 @@ func TestVaultServiceListEntriesHidesSingleInternalVaultRoot(t *testing.T) { } } +func TestVaultServiceListEntriesHidesSingleInternalVaultRootWhenRecycleBinExists(t *testing.T) { + t.Parallel() + + client, _, cleanup := newTestClientForModel(t, vault.Model{ + Entries: []vault.Entry{ + { + ID: "codex-nextcloud", + Title: "Nextcloud (codex)", + Username: "jjulian", + Password: "secret-1", + URL: "https://nextcloud.example.invalid", + Path: []string{"keepass", "Joe", "codex"}, + }, + testAPITokenEntry(t, + apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationListEntries, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"keepass", "Joe", "codex"}}}, + ), + }, + Groups: [][]string{ + {"keepass"}, + {"keepass", "Joe"}, + {"keepass", "Joe", "codex"}, + {"Recycle Bin"}, + }, + }) + defer cleanup() + + resp, err := client.ListEntries(tokenContext(defaultTestTokenSecret), &keepassgov1.ListEntriesRequest{ + Path: []string{"Joe", "codex"}, + }) + if err != nil { + t.Fatalf("ListEntries() error = %v", err) + } + if len(resp.Entries) != 1 { + t.Fatalf("len(ListEntries().Entries) = %d, want 1", len(resp.Entries)) + } + if got := resp.Entries[0].Path; !slices.Equal(got, []string{"Joe", "codex"}) { + t.Fatalf("ListEntries().Entries[0].Path = %v, want [Joe codex]", got) + } +} + func TestVaultServiceListGroupsHidesSingleInternalVaultRoot(t *testing.T) { t.Parallel() @@ -291,6 +331,33 @@ func TestVaultServiceListGroupsHidesSingleInternalVaultRoot(t *testing.T) { } } +func TestVaultServiceListGroupsHidesSingleInternalVaultRootWhenRecycleBinExists(t *testing.T) { + t.Parallel() + + client, _, cleanup := newTestClientForModel(t, vault.Model{ + Entries: []vault.Entry{ + testAPITokenEntry(t, + apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationListGroups, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"keepass"}}}, + ), + }, + Groups: [][]string{ + {"keepass"}, + {"keepass", "Joe"}, + {"keepass", "Shared"}, + {"Recycle Bin"}, + }, + }) + defer cleanup() + + resp, err := client.ListGroups(tokenContext(defaultTestTokenSecret), &keepassgov1.ListGroupsRequest{}) + if err != nil { + t.Fatalf("ListGroups() error = %v", err) + } + if !slices.Equal(resp.Names, []string{"Joe", "Shared"}) { + t.Fatalf("ListGroups().Names = %v, want [Joe Shared]", resp.Names) + } +} + func TestVaultServiceGetsBrowserCredentialForAuthorizedClients(t *testing.T) { t.Parallel() diff --git a/internal/appui/recent_state.go b/internal/appui/recent_state.go index 1628349..8cb49b4 100644 --- a/internal/appui/recent_state.go +++ b/internal/appui/recent_state.go @@ -18,6 +18,7 @@ import ( "git.julianfamily.org/keepassgo/internal/autofillcache" "git.julianfamily.org/keepassgo/internal/session" "git.julianfamily.org/keepassgo/internal/vault" + "git.julianfamily.org/keepassgo/internal/vaultview" "git.julianfamily.org/keepassgo/internal/webdav" ) @@ -1266,21 +1267,7 @@ func (u *ui) hiddenVaultRoot() string { if err != nil { return "" } - if len(model.EntriesInPath(nil)) != 0 { - return "" - } - groups := model.ChildGroups(nil) - roots := make([]string, 0, len(groups)) - for _, group := range groups { - if group == "Recycle Bin" { - continue - } - roots = append(roots, group) - } - if len(roots) != 1 { - return "" - } - return roots[0] + return vaultview.HiddenRoot(model) } func (u *ui) enterHiddenVaultRoot() { diff --git a/internal/vaultview/root.go b/internal/vaultview/root.go new file mode 100644 index 0000000..7b398ba --- /dev/null +++ b/internal/vaultview/root.go @@ -0,0 +1,23 @@ +package vaultview + +import "git.julianfamily.org/keepassgo/internal/vault" + +// HiddenRoot returns the single synthetic top-level vault group that should be +// treated as an internal storage root rather than as a user-visible group. +func HiddenRoot(model vault.Model) string { + if len(model.EntriesInPath(nil)) != 0 { + return "" + } + groups := model.ChildGroups(nil) + roots := make([]string, 0, len(groups)) + for _, group := range groups { + if group == "Recycle Bin" { + continue + } + roots = append(roots, group) + } + if len(roots) != 1 { + return "" + } + return roots[0] +} diff --git a/internal/vaultview/root_test.go b/internal/vaultview/root_test.go new file mode 100644 index 0000000..fee444e --- /dev/null +++ b/internal/vaultview/root_test.go @@ -0,0 +1,26 @@ +package vaultview + +import ( + "testing" + + "git.julianfamily.org/keepassgo/internal/vault" +) + +func TestHiddenRootIgnoresRecycleBin(t *testing.T) { + t.Parallel() + + model := vault.Model{ + Entries: []vault.Entry{ + {ID: "entry-1", Title: "Vault Console", Path: []string{"keepass", "Crew", "Internet"}}, + }, + Groups: [][]string{ + {"keepass"}, + {"keepass", "Crew"}, + {"Recycle Bin"}, + }, + } + + if got := HiddenRoot(model); got != "keepass" { + t.Fatalf("HiddenRoot() = %q, want %q", got, "keepass") + } +}