package vault import ( "errors" "slices" "testing" ) func testModel() Model { return Model{ Entries: []Entry{ {ID: "1", Title: "Bellagio", Username: "rustyryan", URL: "https://bellagio.example.invalid", Path: []string{"Crew", "Internet"}}, {ID: "2", Title: "Vault Console", Username: "dannyocean", URL: "https://vault.crew.example.invalid", Path: []string{"Crew", "Internet"}}, {ID: "3", Title: "Surveillance Console", Username: "codex", URL: "https://surveillance.crew.example.invalid", Path: []string{"Crew", "Home Assistant"}}, {ID: "4", Title: "Alma (WA Prep)", Username: "christina.julian", URL: "https://waprep.getalma.com", Path: []string{"Tricia", "School"}}, }, } } func TestChildGroupsReturnsImmediateGroupsOnly(t *testing.T) { model := testModel() got := model.ChildGroups([]string{"Crew"}) want := []string{"Home Assistant", "Internet"} if !slices.Equal(got, want) { t.Fatalf("ChildGroups() = %v, want %v", got, want) } } func TestEntriesInPathReturnsOnlyDirectEntries(t *testing.T) { model := testModel() got := model.EntriesInPath([]string{"Crew", "Internet"}) if len(got) != 2 { t.Fatalf("len(EntriesInPath()) = %d, want 2", len(got)) } if got[0].Title != "Bellagio" || got[1].Title != "Vault Console" { t.Fatalf("EntriesInPath() titles = %q, %q", got[0].Title, got[1].Title) } } func TestEntriesUnderPathReturnsDescendantEntries(t *testing.T) { t.Parallel() model := testModel() got := model.EntriesUnderPath([]string{"Crew"}) if len(got) != 3 { t.Fatalf("len(EntriesUnderPath(Crew)) = %d, want 3", len(got)) } if got[0].Title != "Bellagio" || got[1].Title != "Surveillance Console" || got[2].Title != "Vault Console" { t.Fatalf("EntriesUnderPath(Crew) titles = %q, %q, %q", got[0].Title, got[1].Title, got[2].Title) } } func TestSearchReturnsMatchesWithFullPathContext(t *testing.T) { model := testModel() got := model.Search("vault") if len(got) != 1 { t.Fatalf("len(Search()) = %d, want 1", len(got)) } if got[0].Entry.Title != "Vault Console" { t.Fatalf("Search() title = %q, want %q", got[0].Entry.Title, "Vault Console") } if got[0].Path != "Crew / Internet" { t.Fatalf("Search() path = %q, want %q", got[0].Path, "Crew / Internet") } } func TestTemplateEntriesAreStoredSeparatelyFromNormalEntries(t *testing.T) { model := testModel() model.UpsertTemplate(Entry{ ID: "tpl-1", Title: "Website Login", Username: "template-user", Password: "template-password", URL: "https://example.com", Notes: "Reusable template for website accounts.", Tags: []string{"template", "web"}, Path: []string{"Templates"}, }) if len(model.Entries) != 4 { t.Fatalf("len(Entries) = %d, want 4", len(model.Entries)) } if len(model.Templates) != 1 { t.Fatalf("len(Templates) = %d, want 1", len(model.Templates)) } if got := model.Templates[0].Title; got != "Website Login" { t.Fatalf("Templates[0].Title = %q, want %q", got, "Website Login") } } func TestInstantiateTemplateCreatesNormalEntryWithOverrides(t *testing.T) { model := Model{ Templates: []Entry{ { ID: "tpl-1", Title: "Website Login", Username: "template-user", Password: "template-password", URL: "https://example.com", Notes: "Reusable template for website accounts.", Tags: []string{"template", "web"}, Fields: map[string]string{ "Environment": "prod", }, Path: []string{"Templates"}, }, }, } entry, err := model.InstantiateTemplate("tpl-1", Entry{ ID: "entry-1", Title: "Bellagio", Username: "rustyryan", Password: "hunter2", URL: "https://bellagio.example.invalid", Path: []string{"Crew", "Internet"}, Tags: []string{"dns"}, }) if err != nil { t.Fatalf("InstantiateTemplate() error = %v", err) } if entry.ID != "entry-1" { t.Fatalf("entry.ID = %q, want %q", entry.ID, "entry-1") } if entry.Title != "Bellagio" { t.Fatalf("entry.Title = %q, want %q", entry.Title, "Bellagio") } if entry.Username != "rustyryan" || entry.Password != "hunter2" || entry.URL != "https://bellagio.example.invalid" { t.Fatalf("entry credentials = %#v, want override values", entry) } if entry.Notes != "Reusable template for website accounts." { t.Fatalf("entry.Notes = %q, want %q", entry.Notes, "Reusable template for website accounts.") } if !slices.Equal(entry.Tags, []string{"dns"}) { t.Fatalf("entry.Tags = %v, want [dns]", entry.Tags) } if entry.Fields["Environment"] != "prod" { t.Fatalf("entry.Fields[Environment] = %q, want %q", entry.Fields["Environment"], "prod") } got := model.EntriesInPath([]string{"Crew", "Internet"}) if len(got) != 1 || got[0].Title != "Bellagio" { t.Fatalf("EntriesInPath() = %#v, want instantiated Bellagio entry", got) } } func TestInstantiateTemplateFailsForUnknownTemplate(t *testing.T) { model := Model{} _, err := model.InstantiateTemplate("missing-template", Entry{ID: "entry-1"}) if err == nil { t.Fatal("InstantiateTemplate() error = nil, want ErrEntryNotFound") } if !errors.Is(err, ErrEntryNotFound) { t.Fatalf("InstantiateTemplate() error = %v, want ErrEntryNotFound", err) } } func TestDeleteTemplateRemovesTemplateWithoutTouchingEntries(t *testing.T) { t.Parallel() model := Model{ Entries: []Entry{ {ID: "entry-1", Title: "Vault Console", Path: []string{"Root", "Internet"}}, }, Templates: []Entry{ {ID: "tpl-1", Title: "Website Login", Path: []string{"Templates"}}, }, } if err := model.DeleteTemplate("tpl-1"); err != nil { t.Fatalf("DeleteTemplate() error = %v", err) } if len(model.Templates) != 0 { t.Fatalf("len(Templates) = %d, want 0", len(model.Templates)) } if len(model.Entries) != 1 || model.Entries[0].ID != "entry-1" { t.Fatalf("Entries = %#v, want unchanged normal entry", model.Entries) } } func TestMoveTemplateChangesItsPath(t *testing.T) { t.Parallel() model := Model{ Templates: []Entry{ {ID: "tpl-1", Title: "Website Login", Path: []string{"Templates", "Web"}}, }, } if err := model.MoveTemplate("tpl-1", []string{"Templates", "Infra"}); err != nil { t.Fatalf("MoveTemplate() error = %v", err) } if got := model.Templates[0].Path; !slices.Equal(got, []string{"Templates", "Infra"}) { t.Fatalf("Templates[0].Path = %v, want [Templates Infra]", got) } } func TestDuplicateEntryCopiesEntryWithNewIDAndTitle(t *testing.T) { t.Parallel() model := Model{ Entries: []Entry{ { ID: "entry-1", Title: "Vault Console", Username: "dannyocean", Password: "token-1", Path: []string{"Root", "Internet"}, }, }, } duplicate, err := model.DuplicateEntry("entry-1", "entry-2") if err != nil { t.Fatalf("DuplicateEntry() error = %v", err) } if duplicate.ID != "entry-2" { t.Fatalf("duplicate.ID = %q, want %q", duplicate.ID, "entry-2") } if duplicate.Title != "Vault Console (Copy)" { t.Fatalf("duplicate.Title = %q, want %q", duplicate.Title, "Vault Console (Copy)") } got := model.EntriesInPath([]string{"Root", "Internet"}) if len(got) != 2 { t.Fatalf("len(EntriesInPath()) = %d, want 2", len(got)) } } func TestCreateGroupMakesItVisibleAsChildGroup(t *testing.T) { model := testModel() model.CreateGroup([]string{"Crew"}, "Finance") got := model.ChildGroups([]string{"Crew"}) want := []string{"Finance", "Home Assistant", "Internet"} if !slices.Equal(got, want) { t.Fatalf("ChildGroups() = %v, want %v", got, want) } } func TestCreateGroupSupportsNestedRelativePath(t *testing.T) { t.Parallel() model := testModel() model.CreateGroup([]string{"Crew"}, "Infrastructure / Prod") got := model.ChildGroups([]string{"Crew"}) if !slices.Equal(got, []string{"Home Assistant", "Infrastructure", "Internet"}) { t.Fatalf("ChildGroups(Crew) = %v, want [Home Assistant Infrastructure Internet]", got) } got = model.ChildGroups([]string{"Crew", "Infrastructure"}) if !slices.Equal(got, []string{"Prod"}) { t.Fatalf("ChildGroups(Crew/Infrastructure) = %v, want [Prod]", got) } } func TestRenameGroupMovesEntriesAndKeepsHierarchy(t *testing.T) { model := testModel() if err := model.RenameGroup([]string{"Crew", "Internet"}, "Infra"); err != nil { t.Fatalf("RenameGroup() error = %v", err) } got := model.EntriesInPath([]string{"Crew", "Infra"}) if len(got) != 2 { t.Fatalf("len(EntriesInPath(Crew/Infra)) = %d, want 2", len(got)) } if len(model.EntriesInPath([]string{"Crew", "Internet"})) != 0 { t.Fatal("EntriesInPath(Crew/Internet) should be empty after rename") } } func TestMoveEntryChangesItsPath(t *testing.T) { model := testModel() if err := model.MoveEntry("1", []string{"Tricia", "School"}); err != nil { t.Fatalf("MoveEntry() error = %v", err) } got := model.EntriesInPath([]string{"Tricia", "School"}) if len(got) != 2 { t.Fatalf("len(EntriesInPath(Tricia/School)) = %d, want 2", len(got)) } } func TestMoveGroupMovesEntriesAndNestedGroups(t *testing.T) { t.Parallel() model := testModel() model.CreateGroup([]string{"Crew", "Internet"}, "Infrastructure") if err := model.MoveGroup([]string{"Crew", "Internet"}, []string{"Tricia"}); err != nil { t.Fatalf("MoveGroup() error = %v", err) } got := model.EntriesInPath([]string{"Tricia", "Internet"}) if len(got) != 2 { t.Fatalf("len(EntriesInPath(Tricia/Internet)) = %d, want 2", len(got)) } if len(model.EntriesInPath([]string{"Crew", "Internet"})) != 0 { t.Fatal("EntriesInPath(Crew/Internet) should be empty after move") } gotGroups := model.ChildGroups([]string{"Tricia", "Internet"}) if !slices.Equal(gotGroups, []string{"Infrastructure"}) { t.Fatalf("ChildGroups(Tricia/Internet) = %v, want [Infrastructure]", gotGroups) } } func TestDeleteEmptyGroupRemovesItFromNavigation(t *testing.T) { model := testModel() model.CreateGroup([]string{"Crew"}, "Finance") if err := model.DeleteGroup([]string{"Crew", "Finance"}); err != nil { t.Fatalf("DeleteGroup() error = %v", err) } got := model.ChildGroups([]string{"Crew"}) want := []string{"Home Assistant", "Internet"} if !slices.Equal(got, want) { t.Fatalf("ChildGroups() = %v, want %v", got, want) } }