package vault import ( "bytes" "errors" "slices" "testing" "github.com/tobischo/gokeepasslib/v3" w "github.com/tobischo/gokeepasslib/v3/wrappers" ) func TestLoadKDBXBuildsModelFromNestedGroups(t *testing.T) { t.Parallel() db := &gokeepasslib.Database{ Header: gokeepasslib.NewHeader(), Credentials: gokeepasslib.NewPasswordCredentials("correct horse battery staple"), Content: &gokeepasslib.DBContent{ Meta: gokeepasslib.NewMetaData(), Root: &gokeepasslib.RootData{ Groups: []gokeepasslib.Group{ mustGroup("Root", mustGroup("Internet", mustEntry("Bellagio", "rustyryan", "https://bellagio.example.invalid", "hunter2"), mustEntry("Vault Console", "dannyocean", "https://vault.crew.example.invalid", "bellagio-pass-1"), ), mustGroup("Security Office", mustEntry("Surveillance Console", "bashertarr", "https://surveillance.crew.example.invalid", "bellagio-pass-2"), ), ), }, }, }, } if err := db.LockProtectedEntries(); err != nil { t.Fatalf("LockProtectedEntries failed: %v", err) } var encoded bytes.Buffer if err := gokeepasslib.NewEncoder(&encoded).Encode(db); err != nil { t.Fatalf("Encode failed: %v", err) } model, err := LoadKDBX(bytes.NewReader(encoded.Bytes()), "correct horse battery staple") if err != nil { t.Fatalf("LoadKDBX failed: %v", err) } got := model.EntriesInPath([]string{"Root", "Internet"}) if len(got) != 2 { t.Fatalf("len(EntriesInPath()) = %d, want 2", len(got)) } if got[0].Title != "Bellagio" || got[0].Username != "rustyryan" || got[0].URL != "https://bellagio.example.invalid" { t.Fatalf("unexpected first entry: %#v", got[0]) } if got[1].Title != "Vault Console" || got[1].Username != "dannyocean" || got[1].URL != "https://vault.crew.example.invalid" { t.Fatalf("unexpected second entry: %#v", got[1]) } groups := model.ChildGroups([]string{"Root"}) if len(groups) != 2 || groups[0] != "Internet" || groups[1] != "Security Office" { t.Fatalf("ChildGroups() = %v, want [Internet Security Office]", groups) } } func TestLoadKDBXPreservesEntryDetails(t *testing.T) { t.Parallel() entry := mustEntry("Surveillance Console", "bashertarr", "https://surveillance.crew.example.invalid", "bellagio-pass-2") entry.Tags = "automation; home" entry.Values = append(entry.Values, mkValue("Notes", "Long-lived token used by Codex for home automation tasks."), mkValue("X-Role", "automation"), ) db := &gokeepasslib.Database{ Header: gokeepasslib.NewHeader(), Credentials: gokeepasslib.NewPasswordCredentials("correct horse battery staple"), Content: &gokeepasslib.DBContent{ Meta: gokeepasslib.NewMetaData(), Root: &gokeepasslib.RootData{ Groups: []gokeepasslib.Group{ mustGroup("Root", mustGroup("Security Office", entry)), }, }, }, } if err := db.LockProtectedEntries(); err != nil { t.Fatalf("LockProtectedEntries failed: %v", err) } var encoded bytes.Buffer if err := gokeepasslib.NewEncoder(&encoded).Encode(db); err != nil { t.Fatalf("Encode failed: %v", err) } model, err := LoadKDBX(bytes.NewReader(encoded.Bytes()), "correct horse battery staple") if err != nil { t.Fatalf("LoadKDBX failed: %v", err) } got := model.EntriesInPath([]string{"Root", "Security Office"}) if len(got) != 1 { t.Fatalf("len(EntriesInPath()) = %d, want 1", len(got)) } if got[0].Password != "bellagio-pass-2" { t.Fatalf("Entry.Password = %q, want %q", got[0].Password, "bellagio-pass-2") } if got[0].Notes != "Long-lived token used by Codex for home automation tasks." { t.Fatalf("Entry.Notes = %q, want %q", got[0].Notes, "Long-lived token used by Codex for home automation tasks.") } if len(got[0].Tags) != 2 || got[0].Tags[0] != "automation" || got[0].Tags[1] != "home" { t.Fatalf("Entry.Tags = %v, want [automation home]", got[0].Tags) } if got[0].Fields["X-Role"] != "automation" { t.Fatalf("Entry.Fields[\"X-Role\"] = %q, want %q", got[0].Fields["X-Role"], "automation") } } func TestSaveKDBXRoundTripsModel(t *testing.T) { t.Parallel() model := Model{ Entries: []Entry{ { ID: "entry-1", Title: "Vault Console", Username: "dannyocean", Password: "bellagio-pass-1", URL: "https://vault.crew.example.invalid", Notes: "Personal git server token entry used for automation and CLI auth.", Tags: []string{"git", "infra"}, Fields: map[string]string{ "X-Role": "automation", }, Path: []string{"Root", "Internet"}, }, { ID: "entry-2", Title: "Surveillance Console", Username: "bashertarr", Password: "bellagio-pass-2", URL: "https://surveillance.crew.example.invalid", Notes: "Long-lived token used by Codex for home automation tasks.", Tags: []string{"automation", "home"}, Path: []string{"Root", "Security Office"}, }, }, } var encoded bytes.Buffer if err := SaveKDBX(&encoded, model, "correct horse battery staple"); err != nil { t.Fatalf("SaveKDBX() error = %v", err) } loaded, err := LoadKDBX(bytes.NewReader(encoded.Bytes()), "correct horse battery staple") if err != nil { t.Fatalf("LoadKDBX() error = %v", err) } got := loaded.Search("vault") if len(got) != 1 { t.Fatalf("len(Search(\"git\")) = %d, want 1", len(got)) } if got[0].Entry.Notes != "Personal git server token entry used for automation and CLI auth." { t.Fatalf("Search(\"git\") notes = %q, want %q", got[0].Entry.Notes, "Personal git server token entry used for automation and CLI auth.") } if got[0].Entry.Fields["X-Role"] != "automation" { t.Fatalf("Search(\"git\") X-Role = %q, want %q", got[0].Entry.Fields["X-Role"], "automation") } homeAssistant := loaded.EntriesInPath([]string{"Root", "Security Office"}) if len(homeAssistant) != 1 { t.Fatalf("len(EntriesInPath(Security Office)) = %d, want 1", len(homeAssistant)) } if homeAssistant[0].Password != "bellagio-pass-2" { t.Fatalf("Security Office password = %q, want %q", homeAssistant[0].Password, "bellagio-pass-2") } } func TestSaveKDBXRoundTripsTemplates(t *testing.T) { t.Parallel() 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"}, }, }, } var encoded bytes.Buffer if err := SaveKDBX(&encoded, model, "correct horse battery staple"); err != nil { t.Fatalf("SaveKDBX() error = %v", err) } loaded, err := LoadKDBX(bytes.NewReader(encoded.Bytes()), "correct horse battery staple") if err != nil { t.Fatalf("LoadKDBX() error = %v", err) } if len(loaded.Templates) != 1 { t.Fatalf("len(Templates) = %d, want 1", len(loaded.Templates)) } if loaded.Templates[0].Title != "Website Login" { t.Fatalf("Templates[0].Title = %q, want %q", loaded.Templates[0].Title, "Website Login") } if loaded.Templates[0].Fields["Environment"] != "prod" { t.Fatalf("Templates[0].Fields[Environment] = %q, want %q", loaded.Templates[0].Fields["Environment"], "prod") } if len(loaded.Entries) != 0 { t.Fatalf("len(Entries) = %d, want 0", len(loaded.Entries)) } } func TestSaveKDBXRoundTripsRemoteProfiles(t *testing.T) { t.Parallel() model := Model{ RemoteProfiles: []RemoteProfile{ { ID: "bellagio-webdav", Name: "Bellagio Vault", Backend: RemoteBackendWebDAV, BaseURL: "https://dav.example.invalid/remote.php/dav", Path: "files/bellagio/keepass.kdbx", }, }, } var encoded bytes.Buffer if err := SaveKDBX(&encoded, model, "correct horse battery staple"); err != nil { t.Fatalf("SaveKDBX() error = %v", err) } loaded, err := LoadKDBX(bytes.NewReader(encoded.Bytes()), "correct horse battery staple") if err != nil { t.Fatalf("LoadKDBX() error = %v", err) } if len(loaded.RemoteProfiles) != 1 { t.Fatalf("len(RemoteProfiles) = %d, want 1", len(loaded.RemoteProfiles)) } got := loaded.RemoteProfiles[0] if got.ID != "bellagio-webdav" || got.Name != "Bellagio Vault" { t.Fatalf("loaded remote profile = %#v, want bellagio-webdav Bellagio Vault", got) } if got.Backend != RemoteBackendWebDAV { t.Fatalf("remote backend = %q, want %q", got.Backend, RemoteBackendWebDAV) } if got.BaseURL != "https://dav.example.invalid/remote.php/dav" { t.Fatalf("remote base URL = %q, want remote.php/dav URL", got.BaseURL) } if got.Path != "files/bellagio/keepass.kdbx" { t.Fatalf("remote path = %q, want files/bellagio/keepass.kdbx", got.Path) } } func TestSaveKDBXRoundTripsEntryHistory(t *testing.T) { t.Parallel() model := Model{ Entries: []Entry{ { ID: "entry-1", Title: "Vault Console", Username: "dannyocean", Password: "new-token", URL: "https://vault.crew.example.invalid", Path: []string{"Root", "Internet"}, History: []Entry{ { ID: "entry-1-old", Title: "Vault Console", Username: "dannyocean", Password: "old-token", URL: "https://vault.crew.example.invalid", Path: []string{"Root", "Internet"}, Notes: "Original version", }, }, }, }, } var encoded bytes.Buffer if err := SaveKDBX(&encoded, model, "correct horse battery staple"); err != nil { t.Fatalf("SaveKDBX() error = %v", err) } loaded, err := LoadKDBX(bytes.NewReader(encoded.Bytes()), "correct horse battery staple") if err != nil { t.Fatalf("LoadKDBX() error = %v", err) } got := loaded.EntriesInPath([]string{"Root", "Internet"}) if len(got) != 1 { t.Fatalf("len(EntriesInPath()) = %d, want 1", len(got)) } if len(got[0].History) != 1 { t.Fatalf("len(History) = %d, want 1", len(got[0].History)) } if got[0].History[0].Password != "old-token" || got[0].History[0].Notes != "Original version" { t.Fatalf("History[0] = %#v, want preserved prior version", got[0].History[0]) } } func TestSaveKDBXRoundTripsRecycleBinEntries(t *testing.T) { t.Parallel() model := Model{ RecycleBin: []Entry{ { ID: "entry-1", Title: "Surveillance Console", Username: "bashertarr", Password: "bellagio-pass-2", URL: "https://surveillance.crew.example.invalid", Path: []string{"Root", "Security Office"}, }, }, } var encoded bytes.Buffer if err := SaveKDBX(&encoded, model, "correct horse battery staple"); err != nil { t.Fatalf("SaveKDBX() error = %v", err) } loaded, err := LoadKDBX(bytes.NewReader(encoded.Bytes()), "correct horse battery staple") if err != nil { t.Fatalf("LoadKDBX() error = %v", err) } if len(loaded.RecycleBin) != 1 { t.Fatalf("len(RecycleBin) = %d, want 1", len(loaded.RecycleBin)) } if loaded.RecycleBin[0].Title != "Surveillance Console" { t.Fatalf("RecycleBin[0].Title = %q, want %q", loaded.RecycleBin[0].Title, "Surveillance Console") } if len(loaded.RecycleBin[0].Path) != 2 || loaded.RecycleBin[0].Path[0] != "Root" || loaded.RecycleBin[0].Path[1] != "Security Office" { t.Fatalf("RecycleBin[0].Path = %v, want [Root Security Office]", loaded.RecycleBin[0].Path) } if len(loaded.Entries) != 0 { t.Fatalf("len(Entries) = %d, want 0", len(loaded.Entries)) } } func TestLoadKDBXWithKeyFileCredentials(t *testing.T) { t.Parallel() keyData := []byte(` 1.0 PbLBYmgEXFhLWf2gxoBMARXgDZGE7f34tr+anCw52LI= `) credentials, err := newCredentials(MasterKey{KeyFileData: keyData}) if err != nil { t.Fatalf("newCredentials() error = %v", err) } db := &gokeepasslib.Database{ Header: gokeepasslib.NewHeader(), Credentials: credentials, Content: &gokeepasslib.DBContent{ Meta: gokeepasslib.NewMetaData(), Root: &gokeepasslib.RootData{ Groups: []gokeepasslib.Group{ mustGroup("Root", mustGroup("Internet", mustEntry("Vault Console", "dannyocean", "https://vault.crew.example.invalid", "bellagio-pass-1"))), }, }, }, } if err := db.LockProtectedEntries(); err != nil { t.Fatalf("LockProtectedEntries failed: %v", err) } var encoded bytes.Buffer if err := gokeepasslib.NewEncoder(&encoded).Encode(db); err != nil { t.Fatalf("Encode failed: %v", err) } model, err := LoadKDBXWithKey(bytes.NewReader(encoded.Bytes()), MasterKey{KeyFileData: keyData}) if err != nil { t.Fatalf("LoadKDBXWithKey() error = %v", err) } got := model.Search("vault") if len(got) != 1 || got[0].Entry.Password != "bellagio-pass-1" { t.Fatalf("LoadKDBXWithKey() = %#v, want password-preserving vault entry", got) } } func TestLoadKDBXWithCompositeCredentials(t *testing.T) { t.Parallel() keyData := []byte(` 1.0 PbLBYmgEXFhLWf2gxoBMARXgDZGE7f34tr+anCw52LI= `) credentials, err := newCredentials(MasterKey{ Password: "correct horse battery staple", KeyFileData: keyData, }) if err != nil { t.Fatalf("newCredentials() error = %v", err) } db := &gokeepasslib.Database{ Header: gokeepasslib.NewHeader(), Credentials: credentials, Content: &gokeepasslib.DBContent{ Meta: gokeepasslib.NewMetaData(), Root: &gokeepasslib.RootData{ Groups: []gokeepasslib.Group{ mustGroup("Root", mustGroup("Security Office", mustEntry("Surveillance Console", "bashertarr", "https://surveillance.crew.example.invalid", "bellagio-pass-2"))), }, }, }, } if err := db.LockProtectedEntries(); err != nil { t.Fatalf("LockProtectedEntries failed: %v", err) } var encoded bytes.Buffer if err := gokeepasslib.NewEncoder(&encoded).Encode(db); err != nil { t.Fatalf("Encode failed: %v", err) } model, err := LoadKDBXWithKey(bytes.NewReader(encoded.Bytes()), MasterKey{ Password: "correct horse battery staple", KeyFileData: keyData, }) if err != nil { t.Fatalf("LoadKDBXWithKey() error = %v", err) } got := model.EntriesInPath([]string{"Root", "Security Office"}) if len(got) != 1 || got[0].Password != "bellagio-pass-2" { t.Fatalf("LoadKDBXWithKey() = %#v, want Security Office entry with password", got) } } func TestLoadKDBXReturnsInvalidCredentialsError(t *testing.T) { t.Parallel() db := &gokeepasslib.Database{ Header: gokeepasslib.NewHeader(), Credentials: gokeepasslib.NewPasswordCredentials("correct horse battery staple"), Content: &gokeepasslib.DBContent{ Meta: gokeepasslib.NewMetaData(), Root: &gokeepasslib.RootData{ Groups: []gokeepasslib.Group{ mustGroup("Root", mustGroup("Internet", mustEntry("Vault Console", "dannyocean", "https://vault.crew.example.invalid", "bellagio-pass-1"))), }, }, }, } if err := db.LockProtectedEntries(); err != nil { t.Fatalf("LockProtectedEntries failed: %v", err) } var encoded bytes.Buffer if err := gokeepasslib.NewEncoder(&encoded).Encode(db); err != nil { t.Fatalf("Encode failed: %v", err) } _, err := LoadKDBX(bytes.NewReader(encoded.Bytes()), "definitely wrong password") if !errors.Is(err, ErrInvalidMasterKey) { t.Fatalf("LoadKDBX() error = %v, want %v", err, ErrInvalidMasterKey) } } func TestSaveKDBXWithKeyRoundTripsModel(t *testing.T) { t.Parallel() keyData := []byte(` 1.0 PbLBYmgEXFhLWf2gxoBMARXgDZGE7f34tr+anCw52LI= `) model := Model{ Entries: []Entry{ { ID: "vault-console", Title: "Vault Console", Username: "dannyocean", Password: "bellagio-pass-1", URL: "https://vault.crew.example.invalid", Path: []string{"Root", "Internet"}, }, }, } var encoded bytes.Buffer if err := SaveKDBXWithKey(&encoded, model, MasterKey{KeyFileData: keyData}); err != nil { t.Fatalf("SaveKDBXWithKey() error = %v", err) } loaded, err := LoadKDBXWithKey(bytes.NewReader(encoded.Bytes()), MasterKey{KeyFileData: keyData}) if err != nil { t.Fatalf("LoadKDBXWithKey() error = %v", err) } got := loaded.Search("vault") if len(got) != 1 || got[0].Entry.Password != "bellagio-pass-1" { t.Fatalf("round-trip with key file = %#v, want vault entry with password", got) } } func TestSaveKDBXWithCompositeKeyRoundTripsModel(t *testing.T) { t.Parallel() keyData := []byte(` 1.0 PbLBYmgEXFhLWf2gxoBMARXgDZGE7f34tr+anCw52LI= `) model := Model{ Entries: []Entry{ { ID: "surveillance-console", Title: "Surveillance Console", Username: "bashertarr", Password: "bellagio-pass-2", URL: "https://surveillance.crew.example.invalid", Path: []string{"Root", "Security Office"}, }, }, } key := MasterKey{ Password: "correct horse battery staple", KeyFileData: keyData, } var encoded bytes.Buffer if err := SaveKDBXWithKey(&encoded, model, key); err != nil { t.Fatalf("SaveKDBXWithKey() error = %v", err) } loaded, err := LoadKDBXWithKey(bytes.NewReader(encoded.Bytes()), key) if err != nil { t.Fatalf("LoadKDBXWithKey() error = %v", err) } got := loaded.EntriesInPath([]string{"Root", "Security Office"}) if len(got) != 1 || got[0].Password != "bellagio-pass-2" { t.Fatalf("composite key round-trip = %#v, want Security Office entry with password", got) } } func TestKDBXRoundTripsEntryAttachments(t *testing.T) { t.Parallel() model := Model{ Entries: []Entry{ { ID: "vault-console", Title: "Vault Console", Username: "dannyocean", Password: "bellagio-pass-1", URL: "https://vault.crew.example.invalid", Path: []string{"Root", "Internet"}, Attachments: map[string][]byte{ "token.txt": []byte("secret attachment contents"), }, }, }, } var encoded bytes.Buffer if err := SaveKDBX(&encoded, model, "correct horse battery staple"); err != nil { t.Fatalf("SaveKDBX() error = %v", err) } loaded, err := LoadKDBX(bytes.NewReader(encoded.Bytes()), "correct horse battery staple") if err != nil { t.Fatalf("LoadKDBX() error = %v", err) } got := loaded.EntriesInPath([]string{"Root", "Internet"}) if len(got) != 1 { t.Fatalf("len(EntriesInPath()) = %d, want 1", len(got)) } if string(got[0].Attachments["token.txt"]) != "secret attachment contents" { t.Fatalf("attachment contents = %q, want %q", string(got[0].Attachments["token.txt"]), "secret attachment contents") } } func TestKDBXReopenCyclesPreserveStableIDsAndCrossFeatureState(t *testing.T) { t.Parallel() model := Model{ Entries: []Entry{ { ID: "entry-1", Title: "Vault Console", Username: "dannyocean", Password: "bellagio-pass-2", URL: "https://vault.crew.example.invalid", Notes: "Current credential", Path: []string{"Root", "Internet"}, Attachments: map[string][]byte{ "token.txt": []byte("secret attachment contents"), }, History: []Entry{ { ID: "entry-1-history-1", Title: "Vault Console", Username: "dannyocean", Password: "bellagio-pass-1", URL: "https://vault.crew.example.invalid", Notes: "Original credential", Path: []string{"Root", "Internet"}, }, }, }, }, Templates: []Entry{ { ID: "tpl-1", Title: "Website Login", Username: "template-user", Password: "template-password", Path: []string{"Templates", "Web"}, }, }, RecycleBin: []Entry{ { ID: "deleted-1", Title: "Retired Entry", Username: "archived-user", Password: "retired-token", Path: []string{"Root", "Archive"}, }, }, Groups: [][]string{ {"Root", "Archive"}, {"Root", "Empty Group"}, {"Templates", "Web"}, }, } var encoded bytes.Buffer if err := SaveKDBX(&encoded, model, "correct horse battery staple"); err != nil { t.Fatalf("SaveKDBX(first cycle) error = %v", err) } reopened, err := LoadKDBX(bytes.NewReader(encoded.Bytes()), "correct horse battery staple") if err != nil { t.Fatalf("LoadKDBX(first cycle) error = %v", err) } encoded.Reset() if err := SaveKDBX(&encoded, reopened, "correct horse battery staple"); err != nil { t.Fatalf("SaveKDBX(second cycle) error = %v", err) } reopened, err = LoadKDBX(bytes.NewReader(encoded.Bytes()), "correct horse battery staple") if err != nil { t.Fatalf("LoadKDBX(second cycle) error = %v", err) } got := reopened.EntriesInPath([]string{"Root", "Internet"}) if len(got) != 1 { t.Fatalf("len(EntriesInPath(Root/Internet)) = %d, want 1", len(got)) } if got[0].ID != "entry-1" { t.Fatalf("entry ID after reopen cycles = %q, want %q", got[0].ID, "entry-1") } if len(got[0].History) != 1 { t.Fatalf("len(History) after reopen cycles = %d, want 1", len(got[0].History)) } if got[0].History[0].ID != "entry-1-history-1" { t.Fatalf("history ID after reopen cycles = %q, want %q", got[0].History[0].ID, "entry-1-history-1") } if string(got[0].Attachments["token.txt"]) != "secret attachment contents" { t.Fatalf("attachment after reopen cycles = %q, want %q", string(got[0].Attachments["token.txt"]), "secret attachment contents") } if len(reopened.Templates) != 1 || reopened.Templates[0].Path[1] != "Web" { t.Fatalf("Templates after reopen cycles = %#v, want Website Login in Templates/Web", reopened.Templates) } if len(reopened.RecycleBin) != 1 || reopened.RecycleBin[0].Path[1] != "Archive" { t.Fatalf("RecycleBin after reopen cycles = %#v, want recycled entry in Root/Archive", reopened.RecycleBin) } rootGroups := reopened.ChildGroups([]string{"Root"}) if !slices.Equal(rootGroups, []string{"Archive", "Empty Group", "Internet"}) { t.Fatalf("ChildGroups(Root) after reopen cycles = %v, want [Archive Empty Group Internet]", rootGroups) } templateGroups := reopened.ChildGroups([]string{"Templates"}) if !slices.Equal(templateGroups, []string{"Web"}) { t.Fatalf("ChildGroups(Templates) after reopen cycles = %v, want [Web]", templateGroups) } } func TestKDBXKeepassRootEntriesPreserveAttachmentsWithTemplates(t *testing.T) { t.Parallel() model := Model{ Entries: []Entry{ { ID: "entry-1", Title: "Vault Console", Username: "dannyocean", Password: "bellagio-pass-2", URL: "https://vault.crew.example.invalid", Path: []string{"keepass", "Internet"}, Attachments: map[string][]byte{ "token.txt": []byte("secret attachment contents"), }, }, }, Templates: []Entry{ { ID: "tpl-1", Title: "Website Login", Username: "template-user", Password: "template-password", Path: []string{"Templates", "Web"}, }, }, Groups: [][]string{ {"keepass", "Internet"}, {"Templates", "Web"}, }, } var encoded bytes.Buffer if err := SaveKDBX(&encoded, model, "correct horse battery staple"); err != nil { t.Fatalf("SaveKDBX() error = %v", err) } loaded, err := LoadKDBX(bytes.NewReader(encoded.Bytes()), "correct horse battery staple") if err != nil { t.Fatalf("LoadKDBX() error = %v", err) } got := loaded.EntriesInPath([]string{"keepass", "Internet"}) if len(got) != 1 { t.Fatalf("len(EntriesInPath()) = %d, want 1", len(got)) } if string(got[0].Attachments["token.txt"]) != "secret attachment contents" { t.Fatalf("attachment contents = %q, want %q", string(got[0].Attachments["token.txt"]), "secret attachment contents") } } func mustGroup(name string, children ...any) gokeepasslib.Group { group := gokeepasslib.NewGroup() group.Name = name for _, child := range children { switch value := child.(type) { case gokeepasslib.Group: group.Groups = append(group.Groups, value) case gokeepasslib.Entry: group.Entries = append(group.Entries, value) default: panic("unsupported child type") } } return group } func mustEntry(title, username, url, password string) gokeepasslib.Entry { entry := gokeepasslib.NewEntry() entry.Values = append(entry.Values, mkValue("Title", title), mkValue("UserName", username), mkValue("URL", url), mkProtectedValue("Password", password), ) return entry } func mkValue(key, value string) gokeepasslib.ValueData { return gokeepasslib.ValueData{Key: key, Value: gokeepasslib.V{Content: value}} } func mkProtectedValue(key, value string) gokeepasslib.ValueData { return gokeepasslib.ValueData{ Key: key, Value: gokeepasslib.V{Content: value, Protected: w.NewBoolWrapper(true)}, } }