751 lines
21 KiB
Go
751 lines
21 KiB
Go
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", "token-1"),
|
|
),
|
|
mustGroup("Home Assistant",
|
|
mustEntry("Surveillance Console", "codex", "https://surveillance.crew.example.invalid", "token-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] != "Home Assistant" || groups[1] != "Internet" {
|
|
t.Fatalf("ChildGroups() = %v, want [Home Assistant Internet]", groups)
|
|
}
|
|
}
|
|
|
|
func TestLoadKDBXPreservesEntryDetails(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
entry := mustEntry("Surveillance Console", "codex", "https://surveillance.crew.example.invalid", "token-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("Home Assistant", 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", "Home Assistant"})
|
|
if len(got) != 1 {
|
|
t.Fatalf("len(EntriesInPath()) = %d, want 1", len(got))
|
|
}
|
|
|
|
if got[0].Password != "token-2" {
|
|
t.Fatalf("Entry.Password = %q, want %q", got[0].Password, "token-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: "token-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: "codex",
|
|
Password: "token-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", "Home Assistant"},
|
|
},
|
|
},
|
|
}
|
|
|
|
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", "Home Assistant"})
|
|
if len(homeAssistant) != 1 {
|
|
t.Fatalf("len(EntriesInPath(Home Assistant)) = %d, want 1", len(homeAssistant))
|
|
}
|
|
|
|
if homeAssistant[0].Password != "token-2" {
|
|
t.Fatalf("Home Assistant password = %q, want %q", homeAssistant[0].Password, "token-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 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: "codex",
|
|
Password: "token-2",
|
|
URL: "https://surveillance.crew.example.invalid",
|
|
Path: []string{"Root", "Home Assistant"},
|
|
},
|
|
},
|
|
}
|
|
|
|
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] != "Home Assistant" {
|
|
t.Fatalf("RecycleBin[0].Path = %v, want [Root Home Assistant]", 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(`<?xml version="1.0" encoding="utf-8"?>
|
|
<KeyFile>
|
|
<Meta>
|
|
<Version>1.0</Version>
|
|
</Meta>
|
|
<Key>
|
|
<Data>PbLBYmgEXFhLWf2gxoBMARXgDZGE7f34tr+anCw52LI=</Data>
|
|
</Key>
|
|
</KeyFile>
|
|
`)
|
|
|
|
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", "token-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 != "token-1" {
|
|
t.Fatalf("LoadKDBXWithKey() = %#v, want password-preserving vault entry", got)
|
|
}
|
|
}
|
|
|
|
func TestLoadKDBXWithCompositeCredentials(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keyData := []byte(`<?xml version="1.0" encoding="utf-8"?>
|
|
<KeyFile>
|
|
<Meta>
|
|
<Version>1.0</Version>
|
|
</Meta>
|
|
<Key>
|
|
<Data>PbLBYmgEXFhLWf2gxoBMARXgDZGE7f34tr+anCw52LI=</Data>
|
|
</Key>
|
|
</KeyFile>
|
|
`)
|
|
|
|
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("Home Assistant", mustEntry("Surveillance Console", "codex", "https://surveillance.crew.example.invalid", "token-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", "Home Assistant"})
|
|
if len(got) != 1 || got[0].Password != "token-2" {
|
|
t.Fatalf("LoadKDBXWithKey() = %#v, want Home Assistant 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", "token-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(`<?xml version="1.0" encoding="utf-8"?>
|
|
<KeyFile>
|
|
<Meta>
|
|
<Version>1.0</Version>
|
|
</Meta>
|
|
<Key>
|
|
<Data>PbLBYmgEXFhLWf2gxoBMARXgDZGE7f34tr+anCw52LI=</Data>
|
|
</Key>
|
|
</KeyFile>
|
|
`)
|
|
|
|
model := Model{
|
|
Entries: []Entry{
|
|
{
|
|
ID: "vault-console",
|
|
Title: "Vault Console",
|
|
Username: "dannyocean",
|
|
Password: "token-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 != "token-1" {
|
|
t.Fatalf("round-trip with key file = %#v, want vault entry with password", got)
|
|
}
|
|
}
|
|
|
|
func TestSaveKDBXWithCompositeKeyRoundTripsModel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keyData := []byte(`<?xml version="1.0" encoding="utf-8"?>
|
|
<KeyFile>
|
|
<Meta>
|
|
<Version>1.0</Version>
|
|
</Meta>
|
|
<Key>
|
|
<Data>PbLBYmgEXFhLWf2gxoBMARXgDZGE7f34tr+anCw52LI=</Data>
|
|
</Key>
|
|
</KeyFile>
|
|
`)
|
|
|
|
model := Model{
|
|
Entries: []Entry{
|
|
{
|
|
ID: "surveillance-console",
|
|
Title: "Surveillance Console",
|
|
Username: "codex",
|
|
Password: "token-2",
|
|
URL: "https://surveillance.crew.example.invalid",
|
|
Path: []string{"Root", "Home Assistant"},
|
|
},
|
|
},
|
|
}
|
|
|
|
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", "Home Assistant"})
|
|
if len(got) != 1 || got[0].Password != "token-2" {
|
|
t.Fatalf("composite key round-trip = %#v, want Home Assistant entry with password", got)
|
|
}
|
|
}
|
|
|
|
func TestKDBXRoundTripsEntryAttachments(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
model := Model{
|
|
Entries: []Entry{
|
|
{
|
|
ID: "vault-console",
|
|
Title: "Vault Console",
|
|
Username: "dannyocean",
|
|
Password: "token-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: "token-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: "token-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 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)},
|
|
}
|
|
}
|