Add secret-safe copy and reveal UX coverage

This commit is contained in:
Joe Julian
2026-03-29 13:32:21 -07:00
parent dd0b0b6f6e
commit 55ca6352b4
5 changed files with 204 additions and 10 deletions
+143
View File
@@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"slices"
"strings"
"testing"
"git.julianfamily.org/keepassgo/clipboard"
@@ -891,3 +892,145 @@ func TestUIUsesKeePassGOProductCopy(t *testing.T) {
t.Fatalf("desktopSubtitle = %q, want updated product subtitle", desktopSubtitle)
}
}
func TestUICopyActionsWriteExpectedClipboardContentsAndSanitizedFeedback(t *testing.T) {
t.Parallel()
model := vault.Model{
Entries: []vault.Entry{
{
ID: "git-server",
Title: "Git Server",
Username: "joejulian",
Password: "token-1",
URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"},
},
},
}
tests := []struct {
name string
target clipboard.Target
label string
want string
}{
{name: "username", target: clipboard.TargetUsername, label: "copy username", want: "joejulian"},
{name: "password", target: clipboard.TargetPassword, label: "copy password", want: "token-1"},
{name: "url", target: clipboard.TargetURL, label: "copy URL", want: "https://git.julianfamily.org"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u := newUIWithModel("desktop", model)
writer := &memoryClipboardWriter{}
u.clipboardWriter = writer
u.showEntriesSection()
u.currentPath = []string{"Root", "Internet"}
u.filter()
u.state.SelectedEntryID = "git-server"
u.runAction(tt.label, func() error { return u.copySelectedFieldAction(tt.target) })
if writer.content != tt.want {
t.Fatalf("clipboard content = %q, want %q", writer.content, tt.want)
}
if u.statusMessage != tt.label+" complete" {
t.Fatalf("statusMessage = %q, want %q", u.statusMessage, tt.label+" complete")
}
if u.errorMessage != "" {
t.Fatalf("errorMessage = %q, want empty", u.errorMessage)
}
if strings.Contains(u.statusMessage, tt.want) {
t.Fatalf("statusMessage = %q, must not contain copied secret or field value %q", u.statusMessage, tt.want)
}
})
}
}
func TestUICopyActionSanitizesClipboardBackendErrors(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{
Entries: []vault.Entry{
{
ID: "git-server",
Title: "Git Server",
Username: "joejulian",
Password: "token-1",
URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"},
},
},
})
u.clipboardWriter = failingClipboardWriter{err: os.ErrPermission}
u.showEntriesSection()
u.currentPath = []string{"Root", "Internet"}
u.filter()
u.state.SelectedEntryID = "git-server"
u.runAction("copy password", func() error { return u.copySelectedFieldAction(clipboard.TargetPassword) })
if u.errorMessage != clipboard.ErrWriteFailed.Error() {
t.Fatalf("errorMessage = %q, want %q", u.errorMessage, clipboard.ErrWriteFailed.Error())
}
if strings.Contains(u.errorMessage, "token-1") {
t.Fatalf("errorMessage = %q, must not contain copied password", u.errorMessage)
}
if u.statusMessage != "" {
t.Fatalf("statusMessage = %q, want empty on copy failure", u.statusMessage)
}
}
func TestUIPasswordRevealTogglesDisplayedPasswordAndLockResetsIt(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{
Entries: []vault.Entry{
{
ID: "git-server",
Title: "Git Server",
Username: "joejulian",
Password: "token-1",
Path: []string{"Root", "Internet"},
},
},
})
u.showEntriesSection()
u.currentPath = []string{"Root", "Internet"}
u.filter()
u.state.SelectedEntryID = "git-server"
if got := u.detailPasswordValue(); got != "••••••••" {
t.Fatalf("detailPasswordValue() hidden = %q, want %q", got, "••••••••")
}
u.showPassword = true
if got := u.detailPasswordValue(); got != "token-1" {
t.Fatalf("detailPasswordValue() revealed = %q, want %q", got, "token-1")
}
if err := u.lockAction(); err != nil {
t.Fatalf("lockAction() error = %v", err)
}
if u.showPassword {
t.Fatal("showPassword = true after lockAction(), want false")
}
}
type memoryClipboardWriter struct {
content string
}
func (w *memoryClipboardWriter) WriteText(text string) error {
w.content = text
return nil
}
type failingClipboardWriter struct {
err error
}
func (w failingClipboardWriter) WriteText(string) error {
return w.err
}