Add local vault lifecycle UI coverage

This commit is contained in:
Joe Julian
2026-03-29 11:21:30 -07:00
parent 3f29fae12f
commit 2b535a90e4
2 changed files with 307 additions and 14 deletions
+248
View File
@@ -1817,3 +1817,251 @@ type failingClipboardWriter struct {
func (w failingClipboardWriter) WriteText(string) error {
return w.err
}
func TestUILocalLifecycleActionsUpdateVisibleStatusMessages(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
u.masterPassword.SetText("correct horse battery staple")
u.runAction("create vault", u.createVaultAction)
if got := u.statusMessage; got != "create vault complete" {
t.Fatalf("status after create = %q, want %q", got, "create vault complete")
}
if got := u.errorMessage; got != "" {
t.Fatalf("error after create = %q, want empty", got)
}
path := filepath.Join(t.TempDir(), "keepassgo.kdbx")
u.saveAsPath.SetText(path)
u.runAction("save-as vault", u.saveAsAction)
if got := u.statusMessage; got != "save-as vault complete" {
t.Fatalf("status after save-as = %q, want %q", got, "save-as vault complete")
}
if got := u.errorMessage; got != "" {
t.Fatalf("error after save-as = %q, want empty", got)
}
if err := u.state.UpsertEntry(vault.Entry{
ID: "vault-console",
Title: "Vault Console",
Username: "dannyocean",
Password: "token-1",
URL: "https://vault.crew.example.invalid",
Path: []string{"Root", "Internet"},
}); err != nil {
t.Fatalf("UpsertEntry() error = %v", err)
}
u.runAction("save vault", u.saveAction)
if got := u.statusMessage; got != "save vault complete" {
t.Fatalf("status after save = %q, want %q", got, "save vault complete")
}
if got := u.errorMessage; got != "" {
t.Fatalf("error after save = %q, want empty", got)
}
u.runAction("lock vault", u.lockAction)
if got := u.statusMessage; got != "lock vault complete" {
t.Fatalf("status after lock = %q, want %q", got, "lock vault complete")
}
if got := u.errorMessage; got != "" {
t.Fatalf("error after lock = %q, want empty", got)
}
u.runAction("unlock vault", u.unlockAction)
if got := u.statusMessage; got != "unlock vault complete" {
t.Fatalf("status after unlock = %q, want %q", got, "unlock vault complete")
}
if got := u.errorMessage; got != "" {
t.Fatalf("error after unlock = %q, want empty", got)
}
reopened := newUIWithSession("desktop", &session.Manager{})
reopened.masterPassword.SetText("correct horse battery staple")
reopened.vaultPath.SetText(path)
reopened.runAction("open vault", reopened.openVaultAction)
if got := reopened.statusMessage; got != "open vault complete" {
t.Fatalf("status after open = %q, want %q", got, "open vault complete")
}
if got := reopened.errorMessage; got != "" {
t.Fatalf("error after open = %q, want empty", got)
}
}
func TestUILocalLifecycleActionErrorsAreVisibleAndSpecific(t *testing.T) {
t.Parallel()
t.Run("save without configured path", func(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
u.masterPassword.SetText("correct horse battery staple")
u.runAction("create vault", u.createVaultAction)
u.runAction("save vault", u.saveAction)
if got := u.statusMessage; got != "" {
t.Fatalf("status after failed save = %q, want empty", got)
}
if got := u.errorMessage; !strings.Contains(got, session.ErrNoPath.Error()) {
t.Fatalf("error after failed save = %q, want %q", got, session.ErrNoPath.Error())
}
})
t.Run("save-as without target path", func(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
u.masterPassword.SetText("correct horse battery staple")
u.runAction("create vault", u.createVaultAction)
u.runAction("save-as vault", u.saveAsAction)
if got := u.statusMessage; got != "" {
t.Fatalf("status after failed save-as = %q, want empty", got)
}
if got := u.errorMessage; got != "save-as path is required" {
t.Fatalf("error after failed save-as = %q, want %q", got, "save-as path is required")
}
})
t.Run("open without target path", func(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
u.masterPassword.SetText("correct horse battery staple")
u.runAction("open vault", u.openVaultAction)
if got := u.statusMessage; got != "" {
t.Fatalf("status after failed open = %q, want empty", got)
}
if got := u.errorMessage; got != "vault path is required" {
t.Fatalf("error after failed open = %q, want %q", got, "vault path is required")
}
})
t.Run("open unreadable path", func(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
u.masterPassword.SetText("correct horse battery staple")
u.vaultPath.SetText(filepath.Join(t.TempDir(), "missing.kdbx"))
u.runAction("open vault", u.openVaultAction)
if got := u.statusMessage; got != "" {
t.Fatalf("status after unreadable open = %q, want empty", got)
}
if got := u.errorMessage; got == "" || !strings.Contains(got, "read ") {
t.Fatalf("error after unreadable open = %q, want read failure", got)
}
})
t.Run("open decode failure", func(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "corrupt.kdbx")
if err := os.WriteFile(path, []byte("not-a-kdbx"), 0o600); err != nil {
t.Fatalf("WriteFile(corrupt) error = %v", err)
}
u := newUIWithSession("desktop", &session.Manager{})
u.masterPassword.SetText("correct horse battery staple")
u.vaultPath.SetText(path)
u.runAction("open vault", u.openVaultAction)
if got := u.statusMessage; got != "" {
t.Fatalf("status after decode failure = %q, want empty", got)
}
if got := u.errorMessage; got == "" || !strings.Contains(got, "decode kdbx") {
t.Fatalf("error after decode failure = %q, want decode kdbx failure", got)
}
})
t.Run("open invalid master key", func(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "vault.kdbx")
var encoded bytes.Buffer
if err := vault.SaveKDBX(&encoded, vault.Model{}, "correct horse battery staple"); err != nil {
t.Fatalf("SaveKDBX() error = %v", err)
}
if err := os.WriteFile(path, encoded.Bytes(), 0o600); err != nil {
t.Fatalf("WriteFile(vault) error = %v", err)
}
u := newUIWithSession("desktop", &session.Manager{})
u.masterPassword.SetText("wrong password")
u.vaultPath.SetText(path)
u.runAction("open vault", u.openVaultAction)
if got := u.statusMessage; got != "" {
t.Fatalf("status after invalid master open = %q, want empty", got)
}
if got := u.errorMessage; !strings.Contains(got, vault.ErrInvalidMasterKey.Error()) {
t.Fatalf("error after invalid master open = %q, want %q", got, vault.ErrInvalidMasterKey.Error())
}
})
t.Run("unlock invalid master key", func(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
u.masterPassword.SetText("correct horse battery staple")
if err := u.createVaultAction(); err != nil {
t.Fatalf("createVaultAction() error = %v", err)
}
if err := u.lockAction(); err != nil {
t.Fatalf("lockAction() error = %v", err)
}
u.masterPassword.SetText("wrong password")
u.runAction("unlock vault", u.unlockAction)
if got := u.statusMessage; got != "" {
t.Fatalf("status after invalid unlock = %q, want empty", got)
}
if got := u.errorMessage; !strings.Contains(got, vault.ErrInvalidMasterKey.Error()) {
t.Fatalf("error after invalid unlock = %q, want %q", got, vault.ErrInvalidMasterKey.Error())
}
})
}
func TestUILocalLifecycleActionsClearStaleMessagesOnSuccess(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
u.masterPassword.SetText("correct horse battery staple")
u.runAction("save vault", u.saveAction)
if u.errorMessage == "" {
t.Fatal("error after failed save = empty, want visible failure")
}
u.runAction("create vault", u.createVaultAction)
if got := u.errorMessage; got != "" {
t.Fatalf("error after create = %q, want cleared", got)
}
if got := u.statusMessage; got != "create vault complete" {
t.Fatalf("status after create = %q, want %q", got, "create vault complete")
}
}
func TestUICurrentMasterKeyReportsUnreadableKeyFile(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
u.keyFilePath.SetText(filepath.Join(t.TempDir(), "missing.key"))
_, err := u.currentMasterKey()
if err == nil {
t.Fatal("currentMasterKey() error = nil, want read failure")
}
if !errors.Is(err, os.ErrNotExist) {
t.Fatalf("currentMasterKey() error = %v, want os.ErrNotExist", err)
}
}