Add regression coverage for KDBX reopen cycles

This commit is contained in:
Joe Julian
2026-03-29 11:26:14 -07:00
parent d3be07f252
commit b043ecdc83
3 changed files with 330 additions and 34 deletions
+109
View File
@@ -3,6 +3,7 @@ package vault
import (
"bytes"
"errors"
"slices"
"testing"
"github.com/tobischo/gokeepasslib/v3"
@@ -602,6 +603,114 @@ func TestKDBXRoundTripsEntryAttachments(t *testing.T) {
}
}
func TestKDBXReopenCyclesPreserveStableIDsAndCrossFeatureState(t *testing.T) {
t.Parallel()
model := Model{
Entries: []Entry{
{
ID: "entry-1",
Title: "Git Server",
Username: "joejulian",
Password: "token-2",
URL: "https://git.julianfamily.org",
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: "Git Server",
Username: "joejulian",
Password: "token-1",
URL: "https://git.julianfamily.org",
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 mustGroup(name string, children ...any) gokeepasslib.Group {
group := gokeepasslib.NewGroup()
group.Name = name