784 lines
24 KiB
Go
784 lines
24 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"testing"
|
|
|
|
"git.julianfamily.org/keepassgo/clipboard"
|
|
"git.julianfamily.org/keepassgo/session"
|
|
"git.julianfamily.org/keepassgo/vault"
|
|
)
|
|
|
|
func TestUIFiltersUsingVaultModelPathsAndSearch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
u := newUIWithModel("desktop", vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "1", Title: "Bellagio", Username: "rustyryan", URL: "https://bellagio.example.invalid", Path: []string{"Crew", "Internet"}},
|
|
{ID: "2", Title: "Vault Console", Username: "dannyocean", URL: "https://vault.crew.example.invalid", Path: []string{"Crew", "Internet"}},
|
|
{ID: "3", Title: "Surveillance Console", Username: "codex", URL: "https://surveillance.crew.example.invalid", Path: []string{"Crew", "Home Assistant"}},
|
|
},
|
|
})
|
|
|
|
u.currentPath = []string{"Crew", "Internet"}
|
|
u.filter()
|
|
if got := u.filteredTitles(); !slices.Equal(got, []string{"Bellagio", "Vault Console"}) {
|
|
t.Fatalf("filteredTitles() = %v, want [Bellagio Vault Console]", got)
|
|
}
|
|
|
|
u.search.SetText("surveillance")
|
|
u.filter()
|
|
if got := u.filteredTitles(); !slices.Equal(got, []string{"Surveillance Console"}) {
|
|
t.Fatalf("search filteredTitles() = %v, want [Surveillance Console]", got)
|
|
}
|
|
}
|
|
|
|
func TestUIChildGroupsComeFromVaultModel(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
u := newUIWithModel("desktop", vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "1", Title: "Bellagio", Path: []string{"Crew", "Internet"}},
|
|
{ID: "2", Title: "Surveillance Console", Path: []string{"Crew", "Home Assistant"}},
|
|
{ID: "3", Title: "Alma (WA Prep)", Path: []string{"Tricia", "School"}},
|
|
},
|
|
})
|
|
|
|
u.currentPath = []string{"Crew"}
|
|
if got := u.childGroups(); !slices.Equal(got, []string{"Home Assistant", "Internet"}) {
|
|
t.Fatalf("childGroups() = %v, want [Home Assistant Internet]", got)
|
|
}
|
|
}
|
|
|
|
func TestUISelectedEntryFollowsApplicationStateSelection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
u := newUIWithModel("desktop", vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "1", Title: "Bellagio", Path: []string{"Crew", "Internet"}},
|
|
{ID: "2", Title: "Vault Console", Path: []string{"Crew", "Internet"}},
|
|
},
|
|
})
|
|
|
|
u.currentPath = []string{"Crew", "Internet"}
|
|
u.filter()
|
|
u.state.SelectedEntryID = "2"
|
|
|
|
got, ok := u.selectedEntry()
|
|
if !ok {
|
|
t.Fatal("selectedEntry() ok = false, want true")
|
|
}
|
|
|
|
if got.Title != "Vault Console" {
|
|
t.Fatalf("selectedEntry().Title = %q, want %q", got.Title, "Vault Console")
|
|
}
|
|
}
|
|
|
|
func TestUILockHidesVisibleEntries(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
u := newUIWithModel("desktop", vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "1", Title: "Bellagio", Path: []string{"Crew", "Internet"}},
|
|
},
|
|
})
|
|
|
|
if err := u.state.Lock(); err != nil {
|
|
t.Fatalf("state.Lock() error = %v", err)
|
|
}
|
|
u.filter()
|
|
|
|
if got := u.filteredTitles(); len(got) != 0 {
|
|
t.Fatalf("filteredTitles() = %v, want empty while locked", got)
|
|
}
|
|
}
|
|
|
|
func TestUILifecycleActionsCreateSaveOpenLockAndUnlockLocalVault(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.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)
|
|
}
|
|
|
|
path := filepath.Join(t.TempDir(), "keepassgo.kdbx")
|
|
u.saveAsPath.SetText(path)
|
|
if err := u.saveAsAction(); err != nil {
|
|
t.Fatalf("saveAsAction() error = %v", err)
|
|
}
|
|
|
|
if err := u.lockAction(); err != nil {
|
|
t.Fatalf("lockAction() error = %v", err)
|
|
}
|
|
u.currentPath = []string{"Root", "Internet"}
|
|
u.filter()
|
|
if got := u.filteredTitles(); len(got) != 0 {
|
|
t.Fatalf("filteredTitles() = %v, want empty while locked", got)
|
|
}
|
|
|
|
if err := u.unlockAction(); err != nil {
|
|
t.Fatalf("unlockAction() error = %v", err)
|
|
}
|
|
u.filter()
|
|
if got := u.filteredTitles(); !slices.Equal(got, []string{"Vault Console"}) {
|
|
t.Fatalf("filteredTitles() after unlock = %v, want [Vault Console]", got)
|
|
}
|
|
|
|
reopened := newUIWithSession("desktop", &session.Manager{})
|
|
reopened.masterPassword.SetText("correct horse battery staple")
|
|
reopened.vaultPath.SetText(path)
|
|
if err := reopened.openVaultAction(); err != nil {
|
|
t.Fatalf("openVaultAction() error = %v", err)
|
|
}
|
|
reopened.currentPath = []string{"Root", "Internet"}
|
|
reopened.filter()
|
|
if got := reopened.filteredTitles(); !slices.Equal(got, []string{"Vault Console"}) {
|
|
t.Fatalf("reopened filteredTitles() = %v, want [Vault Console]", got)
|
|
}
|
|
}
|
|
|
|
func TestUIMasterKeyModesCreateOpenAndUnlockLocalVault(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
mode vault.MasterKeyMode
|
|
password string
|
|
keyFileData []byte
|
|
}{
|
|
{
|
|
name: "password only",
|
|
mode: vault.MasterKeyModePasswordOnly,
|
|
password: "correct horse battery staple",
|
|
},
|
|
{
|
|
name: "key file only",
|
|
mode: vault.MasterKeyModeKeyFileOnly,
|
|
keyFileData: []byte("key-file-only-material"),
|
|
},
|
|
{
|
|
name: "composite",
|
|
mode: vault.MasterKeyModePasswordAndKeyFile,
|
|
password: "correct horse battery staple",
|
|
keyFileData: []byte("composite-key-material"),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keyFile := ""
|
|
if len(tt.keyFileData) > 0 {
|
|
keyFile = filepath.Join(t.TempDir(), "master.key")
|
|
if err := os.WriteFile(keyFile, tt.keyFileData, 0o600); err != nil {
|
|
t.Fatalf("WriteFile(master.key) error = %v", err)
|
|
}
|
|
}
|
|
|
|
u := newUIWithSession("desktop", &session.Manager{})
|
|
u.setMasterKeyMode(tt.mode)
|
|
u.masterPassword.SetText(tt.password)
|
|
u.keyFilePath.SetText(keyFile)
|
|
|
|
if err := u.createVaultAction(); err != nil {
|
|
t.Fatalf("createVaultAction() error = %v", err)
|
|
}
|
|
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)
|
|
}
|
|
|
|
path := filepath.Join(t.TempDir(), "keepassgo.kdbx")
|
|
u.saveAsPath.SetText(path)
|
|
if err := u.saveAsAction(); err != nil {
|
|
t.Fatalf("saveAsAction() error = %v", err)
|
|
}
|
|
|
|
if err := u.lockAction(); err != nil {
|
|
t.Fatalf("lockAction() error = %v", err)
|
|
}
|
|
if err := u.unlockAction(); err != nil {
|
|
t.Fatalf("unlockAction() error = %v", err)
|
|
}
|
|
|
|
reopened := newUIWithSession("desktop", &session.Manager{})
|
|
reopened.setMasterKeyMode(tt.mode)
|
|
reopened.masterPassword.SetText(tt.password)
|
|
reopened.keyFilePath.SetText(keyFile)
|
|
reopened.vaultPath.SetText(path)
|
|
if err := reopened.openVaultAction(); err != nil {
|
|
t.Fatalf("openVaultAction() error = %v", err)
|
|
}
|
|
reopened.currentPath = []string{"Root", "Internet"}
|
|
reopened.filter()
|
|
if got := reopened.filteredTitles(); !slices.Equal(got, []string{"Vault Console"}) {
|
|
t.Fatalf("reopened filteredTitles() = %v, want [Vault Console]", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUIChangeMasterKeyModeForExistingVault(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
updated := filepath.Join(t.TempDir(), "updated.key")
|
|
if err := os.WriteFile(updated, []byte("updated-key"), 0o600); err != nil {
|
|
t.Fatalf("WriteFile(updated.key) error = %v", err)
|
|
}
|
|
|
|
u := newUIWithSession("desktop", &session.Manager{})
|
|
u.setMasterKeyMode(vault.MasterKeyModePasswordOnly)
|
|
u.masterPassword.SetText("old-password")
|
|
if err := u.createVaultAction(); err != nil {
|
|
t.Fatalf("createVaultAction() error = %v", err)
|
|
}
|
|
if err := u.state.UpsertEntry(vault.Entry{
|
|
ID: "vault-console",
|
|
Title: "Vault Console",
|
|
Path: []string{"Root", "Internet"},
|
|
}); err != nil {
|
|
t.Fatalf("UpsertEntry() error = %v", err)
|
|
}
|
|
|
|
path := filepath.Join(t.TempDir(), "keepassgo.kdbx")
|
|
u.saveAsPath.SetText(path)
|
|
if err := u.saveAsAction(); err != nil {
|
|
t.Fatalf("saveAsAction() error = %v", err)
|
|
}
|
|
|
|
u.setMasterKeyMode(vault.MasterKeyModePasswordAndKeyFile)
|
|
u.masterPassword.SetText("new-password")
|
|
u.keyFilePath.SetText(updated)
|
|
if err := u.changeMasterKeyAction(); err != nil {
|
|
t.Fatalf("changeMasterKeyAction() error = %v", err)
|
|
}
|
|
if err := u.saveAction(); err != nil {
|
|
t.Fatalf("saveAction() error = %v", err)
|
|
}
|
|
if err := u.lockAction(); err != nil {
|
|
t.Fatalf("lockAction() error = %v", err)
|
|
}
|
|
|
|
u.masterPassword.SetText("old-password")
|
|
u.keyFilePath.SetText("")
|
|
u.setMasterKeyMode(vault.MasterKeyModePasswordOnly)
|
|
u.runAction("unlock vault", u.unlockAction)
|
|
if u.errorMessage == "" {
|
|
t.Fatal("errorMessage = empty, want visible invalid master key error")
|
|
}
|
|
|
|
u.setMasterKeyMode(vault.MasterKeyModePasswordAndKeyFile)
|
|
u.masterPassword.SetText("new-password")
|
|
u.keyFilePath.SetText(updated)
|
|
if err := u.unlockAction(); err != nil {
|
|
t.Fatalf("unlockAction() with updated key error = %v", err)
|
|
}
|
|
|
|
reopened := newUIWithSession("desktop", &session.Manager{})
|
|
reopened.setMasterKeyMode(vault.MasterKeyModePasswordAndKeyFile)
|
|
reopened.masterPassword.SetText("new-password")
|
|
reopened.keyFilePath.SetText(updated)
|
|
reopened.vaultPath.SetText(path)
|
|
if err := reopened.openVaultAction(); err != nil {
|
|
t.Fatalf("openVaultAction() with updated key error = %v", err)
|
|
}
|
|
reopened.currentPath = []string{"Root", "Internet"}
|
|
reopened.filter()
|
|
if got := reopened.filteredTitles(); !slices.Equal(got, []string{"Vault Console"}) {
|
|
t.Fatalf("reopened filteredTitles() = %v, want [Vault Console]", got)
|
|
}
|
|
}
|
|
|
|
func TestUIMasterKeyValidationErrorsAreVisible(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
mode vault.MasterKeyMode
|
|
password string
|
|
keyFile string
|
|
wantError string
|
|
}{
|
|
{
|
|
name: "password mode requires password",
|
|
mode: vault.MasterKeyModePasswordOnly,
|
|
wantError: "master password is required",
|
|
},
|
|
{
|
|
name: "key file mode requires path",
|
|
mode: vault.MasterKeyModeKeyFileOnly,
|
|
wantError: "key file is required",
|
|
},
|
|
{
|
|
name: "composite mode requires password",
|
|
mode: vault.MasterKeyModePasswordAndKeyFile,
|
|
keyFile: filepath.Join("/tmp", "ignored.key"),
|
|
wantError: "master password is required",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
u := newUIWithSession("desktop", &session.Manager{})
|
|
u.setMasterKeyMode(tt.mode)
|
|
u.masterPassword.SetText(tt.password)
|
|
u.keyFilePath.SetText(tt.keyFile)
|
|
|
|
u.runAction("create vault", u.createVaultAction)
|
|
|
|
if got := u.errorMessage; got != tt.wantError {
|
|
t.Fatalf("errorMessage = %q, want %q", got, tt.wantError)
|
|
}
|
|
if got := u.statusMessage; got != "" {
|
|
t.Fatalf("statusMessage = %q, want empty on validation error", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUIUnreadableAndInvalidMasterKeyErrorsAreVisible(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keyFile := filepath.Join(t.TempDir(), "master.key")
|
|
if err := os.WriteFile(keyFile, []byte("key-material"), 0o600); err != nil {
|
|
t.Fatalf("WriteFile(master.key) error = %v", err)
|
|
}
|
|
|
|
create := newUIWithSession("desktop", &session.Manager{})
|
|
create.setMasterKeyMode(vault.MasterKeyModeKeyFileOnly)
|
|
create.keyFilePath.SetText(keyFile)
|
|
if err := create.createVaultAction(); err != nil {
|
|
t.Fatalf("createVaultAction() error = %v", err)
|
|
}
|
|
|
|
path := filepath.Join(t.TempDir(), "keepassgo.kdbx")
|
|
create.saveAsPath.SetText(path)
|
|
if err := create.saveAsAction(); err != nil {
|
|
t.Fatalf("saveAsAction() error = %v", err)
|
|
}
|
|
|
|
unreadable := newUIWithSession("desktop", &session.Manager{})
|
|
unreadable.setMasterKeyMode(vault.MasterKeyModeKeyFileOnly)
|
|
unreadable.keyFilePath.SetText(filepath.Join(t.TempDir(), "missing.key"))
|
|
unreadable.runAction("open vault", unreadable.openVaultAction)
|
|
if got := unreadable.errorMessage; got == "" || got[:14] != "read key file:" {
|
|
t.Fatalf("errorMessage = %q, want read key file error", got)
|
|
}
|
|
|
|
wrong := newUIWithSession("desktop", &session.Manager{})
|
|
wrong.setMasterKeyMode(vault.MasterKeyModeKeyFileOnly)
|
|
wrong.keyFilePath.SetText(filepath.Join(t.TempDir(), "wrong.key"))
|
|
if err := os.WriteFile(wrong.keyFilePath.Text(), []byte("wrong-key"), 0o600); err != nil {
|
|
t.Fatalf("WriteFile(wrong.key) error = %v", err)
|
|
}
|
|
wrong.vaultPath.SetText(path)
|
|
wrong.runAction("open vault", wrong.openVaultAction)
|
|
if got := wrong.errorMessage; got == "" || !bytes.Contains([]byte(got), []byte(vault.ErrInvalidMasterKey.Error())) {
|
|
t.Fatalf("errorMessage = %q, want invalid master key error", got)
|
|
}
|
|
}
|
|
|
|
func TestUIOpenRemoteAndSaveThroughConfiguredWebDAVTarget(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
key := vault.MasterKey{Password: "correct horse battery staple"}
|
|
model := vault.Model{
|
|
Entries: []vault.Entry{
|
|
{
|
|
ID: "vault-console",
|
|
Title: "Vault Console",
|
|
Username: "dannyocean",
|
|
Password: "token-1",
|
|
URL: "https://vault.crew.example.invalid",
|
|
Path: []string{"Root", "Internet"},
|
|
},
|
|
},
|
|
}
|
|
|
|
var putCount int
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
var encoded bytes.Buffer
|
|
if err := vault.SaveKDBXWithKey(&encoded, model, key); err != nil {
|
|
t.Fatalf("SaveKDBXWithKey() error = %v", err)
|
|
}
|
|
w.Header().Set("ETag", "\"v1\"")
|
|
_, _ = w.Write(encoded.Bytes())
|
|
case http.MethodPut:
|
|
putCount++
|
|
w.Header().Set("ETag", "\"v2\"")
|
|
w.WriteHeader(http.StatusNoContent)
|
|
default:
|
|
t.Fatalf("unexpected method %s", r.Method)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
u := newUIWithSession("desktop", &session.Manager{})
|
|
u.masterPassword.SetText("correct horse battery staple")
|
|
u.remoteBaseURL.SetText(server.URL)
|
|
u.remotePath.SetText("vaults/main.kdbx")
|
|
|
|
if err := u.openRemoteAction(); err != nil {
|
|
t.Fatalf("openRemoteAction() error = %v", err)
|
|
}
|
|
|
|
if err := u.state.UpsertEntry(vault.Entry{
|
|
ID: "vault-console",
|
|
Title: "Vault Console",
|
|
Username: "dannyocean",
|
|
Password: "token-2",
|
|
URL: "https://vault.crew.example.invalid",
|
|
Path: []string{"Root", "Internet"},
|
|
}); err != nil {
|
|
t.Fatalf("UpsertEntry() error = %v", err)
|
|
}
|
|
|
|
if err := u.saveAction(); err != nil {
|
|
t.Fatalf("saveAction() error = %v", err)
|
|
}
|
|
|
|
if putCount != 1 {
|
|
t.Fatalf("remote PUT count = %d, want 1", putCount)
|
|
}
|
|
}
|
|
|
|
func TestUIMasterKeyInputSupportsKeyFileAndCompositeKeys(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
keyFile := filepath.Join(t.TempDir(), "master.key")
|
|
keyData := []byte("key-file-bytes")
|
|
if err := os.WriteFile(keyFile, keyData, 0o600); err != nil {
|
|
t.Fatalf("WriteFile(keyFile) error = %v", err)
|
|
}
|
|
|
|
u := newUIWithSession("desktop", &session.Manager{})
|
|
u.setMasterKeyMode(vault.MasterKeyModePasswordAndKeyFile)
|
|
u.masterPassword.SetText("correct horse battery staple")
|
|
u.keyFilePath.SetText(keyFile)
|
|
|
|
key, err := u.currentMasterKey()
|
|
if err != nil {
|
|
t.Fatalf("currentMasterKey() error = %v", err)
|
|
}
|
|
|
|
if key.Password != "correct horse battery staple" {
|
|
t.Fatalf("MasterKey.Password = %q, want correct horse battery staple", key.Password)
|
|
}
|
|
if !bytes.Equal(key.KeyFileData, keyData) {
|
|
t.Fatalf("MasterKey.KeyFileData = %q, want %q", key.KeyFileData, keyData)
|
|
}
|
|
}
|
|
|
|
func TestUISectionNavigationShowsTemplatesAndRecycleBin(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
u := newUIWithModel("desktop", vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "entry-1", Title: "Vault Console", Path: []string{"Root", "Internet"}},
|
|
},
|
|
Templates: []vault.Entry{
|
|
{ID: "tpl-1", Title: "Website Login", Path: []string{"Templates", "Web"}},
|
|
},
|
|
RecycleBin: []vault.Entry{
|
|
{ID: "deleted-1", Title: "Deleted Entry", Path: []string{"Root", "Internet"}},
|
|
},
|
|
})
|
|
|
|
u.showTemplatesSection()
|
|
if got := u.filteredTitles(); !slices.Equal(got, []string{"Website Login"}) {
|
|
t.Fatalf("template filteredTitles() = %v, want [Website Login]", got)
|
|
}
|
|
|
|
u.showRecycleBinSection()
|
|
if got := u.filteredTitles(); !slices.Equal(got, []string{"Deleted Entry"}) {
|
|
t.Fatalf("recycle filteredTitles() = %v, want [Deleted Entry]", got)
|
|
}
|
|
|
|
u.showEntriesSection()
|
|
u.currentPath = []string{"Root", "Internet"}
|
|
u.filter()
|
|
if got := u.filteredTitles(); !slices.Equal(got, []string{"Vault Console"}) {
|
|
t.Fatalf("entry filteredTitles() = %v, want [Vault Console]", got)
|
|
}
|
|
}
|
|
|
|
func TestUISavesDuplicatesDeletesAndRestoresEntriesFromTheEditor(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
u := newUIWithModel("desktop", vault.Model{
|
|
Entries: []vault.Entry{
|
|
{
|
|
ID: "vault-console",
|
|
Title: "Vault Console",
|
|
Username: "dannyocean",
|
|
Password: "token-1",
|
|
URL: "https://vault.crew.example.invalid",
|
|
Path: []string{"Root", "Internet"},
|
|
},
|
|
},
|
|
})
|
|
u.showEntriesSection()
|
|
u.currentPath = []string{"Root", "Internet"}
|
|
u.filter()
|
|
u.state.SelectedEntryID = "vault-console"
|
|
u.loadSelectedEntryIntoEditor()
|
|
u.entryPassword.SetText("token-2")
|
|
|
|
if err := u.saveEntryAction(); err != nil {
|
|
t.Fatalf("saveEntryAction() error = %v", err)
|
|
}
|
|
u.filter()
|
|
if entry, ok := u.selectedEntry(); !ok || entry.Password != "token-2" {
|
|
t.Fatalf("selectedEntry() = %#v, want updated password token-2", entry)
|
|
}
|
|
|
|
if err := u.duplicateSelectedEntryAction(); err != nil {
|
|
t.Fatalf("duplicateSelectedEntryAction() error = %v", err)
|
|
}
|
|
u.filter()
|
|
if got := u.filteredTitles(); !slices.Equal(got, []string{"Vault Console", "Vault Console (Copy)"}) {
|
|
t.Fatalf("filteredTitles() after duplicate = %v, want copy present", got)
|
|
}
|
|
|
|
if err := u.deleteSelectedEntryAction(); err != nil {
|
|
t.Fatalf("deleteSelectedEntryAction() error = %v", err)
|
|
}
|
|
u.showRecycleBinSection()
|
|
if got := u.filteredTitles(); !slices.Equal(got, []string{"Vault Console (Copy)"}) {
|
|
t.Fatalf("recycle filteredTitles() = %v, want deleted copy", got)
|
|
}
|
|
|
|
u.state.SelectedEntryID = "vault-console-copy"
|
|
if err := u.restoreSelectedRecycleEntryAction(); err != nil {
|
|
t.Fatalf("restoreSelectedRecycleEntryAction() error = %v", err)
|
|
}
|
|
u.showEntriesSection()
|
|
u.currentPath = []string{"Root", "Internet"}
|
|
u.filter()
|
|
if got := u.filteredTitles(); !slices.Equal(got, []string{"Vault Console", "Vault Console (Copy)"}) {
|
|
t.Fatalf("filteredTitles() after restore = %v, want restored copy", got)
|
|
}
|
|
}
|
|
|
|
func TestUITemplateAndAttachmentActionsWorkThroughEditor(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
u := newUIWithModel("desktop", vault.Model{
|
|
Templates: []vault.Entry{
|
|
{
|
|
ID: "tpl-1",
|
|
Title: "Website Login",
|
|
Username: "template-user",
|
|
Password: "template-password",
|
|
Notes: "Reusable template",
|
|
Path: []string{"Templates", "Web"},
|
|
},
|
|
},
|
|
})
|
|
|
|
u.showTemplatesSection()
|
|
u.filter()
|
|
u.state.SelectedEntryID = "tpl-1"
|
|
u.loadSelectedEntryIntoEditor()
|
|
u.entryTitle.SetText("Website Login Updated")
|
|
|
|
if err := u.saveTemplateAction(); err != nil {
|
|
t.Fatalf("saveTemplateAction() error = %v", err)
|
|
}
|
|
|
|
u.entryID.SetText("entry-1")
|
|
u.entryTitle.SetText("Bellagio")
|
|
u.entryUsername.SetText("rustyryan")
|
|
u.entryPassword.SetText("token-1")
|
|
u.entryURL.SetText("https://bellagio.example.invalid")
|
|
u.entryPath.SetText("Root / Internet")
|
|
if err := u.instantiateSelectedTemplateAction(); err != nil {
|
|
t.Fatalf("instantiateSelectedTemplateAction() error = %v", err)
|
|
}
|
|
|
|
u.showEntriesSection()
|
|
u.currentPath = []string{"Root", "Internet"}
|
|
u.filter()
|
|
u.state.SelectedEntryID = "entry-1"
|
|
u.loadSelectedEntryIntoEditor()
|
|
|
|
attachmentPath := filepath.Join(t.TempDir(), "token.txt")
|
|
attachmentExportPath := filepath.Join(t.TempDir(), "exported.txt")
|
|
content := []byte("attachment-content")
|
|
if err := os.WriteFile(attachmentPath, content, 0o600); err != nil {
|
|
t.Fatalf("WriteFile(attachmentPath) error = %v", err)
|
|
}
|
|
|
|
u.attachmentPath.SetText(attachmentPath)
|
|
u.attachmentName.SetText("token.txt")
|
|
if err := u.addAttachmentAction(); err != nil {
|
|
t.Fatalf("addAttachmentAction() error = %v", err)
|
|
}
|
|
|
|
u.exportAttachmentPath.SetText(attachmentExportPath)
|
|
if err := u.exportAttachmentAction(); err != nil {
|
|
t.Fatalf("exportAttachmentAction() error = %v", err)
|
|
}
|
|
|
|
exported, err := os.ReadFile(attachmentExportPath)
|
|
if err != nil {
|
|
t.Fatalf("ReadFile(exportAttachmentPath) error = %v", err)
|
|
}
|
|
if !bytes.Equal(exported, content) {
|
|
t.Fatalf("exported attachment = %q, want %q", exported, content)
|
|
}
|
|
|
|
if err := u.removeAttachmentAction(); err != nil {
|
|
t.Fatalf("removeAttachmentAction() error = %v", err)
|
|
}
|
|
|
|
u.showTemplatesSection()
|
|
u.filter()
|
|
u.state.SelectedEntryID = "tpl-1"
|
|
if err := u.deleteSelectedTemplateAction(); err != nil {
|
|
t.Fatalf("deleteSelectedTemplateAction() error = %v", err)
|
|
}
|
|
u.filter()
|
|
if got := u.filteredTitles(); len(got) != 0 {
|
|
t.Fatalf("template filteredTitles() after delete = %v, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestUIRestoresSelectedEntryHistoryVersion(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
u := newUIWithModel("desktop", vault.Model{
|
|
Entries: []vault.Entry{
|
|
{
|
|
ID: "vault-console",
|
|
Title: "Vault Console",
|
|
Username: "dannyocean",
|
|
Password: "token-2",
|
|
URL: "https://vault.crew.example.invalid",
|
|
Path: []string{"Root", "Internet"},
|
|
History: []vault.Entry{
|
|
{
|
|
ID: "vault-console-h1",
|
|
Title: "Vault Console",
|
|
Username: "dannyocean",
|
|
Password: "token-1",
|
|
URL: "https://vault.crew.example.invalid",
|
|
Path: []string{"Root", "Internet"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
u.showEntriesSection()
|
|
u.currentPath = []string{"Root", "Internet"}
|
|
u.filter()
|
|
u.state.SelectedEntryID = "vault-console"
|
|
u.loadSelectedEntryIntoEditor()
|
|
u.historyIndex.SetText("0")
|
|
|
|
if err := u.restoreSelectedHistoryAction(); err != nil {
|
|
t.Fatalf("restoreSelectedHistoryAction() error = %v", err)
|
|
}
|
|
u.filter()
|
|
if entry, ok := u.selectedEntry(); !ok || entry.Password != "token-1" {
|
|
t.Fatalf("selectedEntry() = %#v, want restored password token-1", entry)
|
|
}
|
|
}
|
|
|
|
func TestUIKeyboardShortcutActionsDispatchExpectedCommands(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
u := newUIWithModel("desktop", vault.Model{
|
|
Entries: []vault.Entry{
|
|
{
|
|
ID: "vault-console",
|
|
Title: "Vault Console",
|
|
Username: "dannyocean",
|
|
Password: "token-1",
|
|
URL: "https://vault.crew.example.invalid",
|
|
Path: []string{"Root", "Internet"},
|
|
},
|
|
},
|
|
})
|
|
u.showEntriesSection()
|
|
u.currentPath = []string{"Root", "Internet"}
|
|
u.filter()
|
|
u.state.SelectedEntryID = "vault-console"
|
|
u.loadSelectedEntryIntoEditor()
|
|
|
|
if err := u.performShortcut(shortcutNewEntry); err != nil {
|
|
t.Fatalf("performShortcut(new-entry) error = %v", err)
|
|
}
|
|
if u.state.SelectedEntryID != "" {
|
|
t.Fatalf("SelectedEntryID = %q, want empty after new-entry shortcut", u.state.SelectedEntryID)
|
|
}
|
|
|
|
u.state.SelectedEntryID = "vault-console"
|
|
if err := u.performShortcut(shortcutCopyUser); err != nil {
|
|
t.Fatalf("performShortcut(copy-user) error = %v", err)
|
|
}
|
|
if err := u.performShortcut(shortcutCopyPassword); err != nil {
|
|
t.Fatalf("performShortcut(copy-password) error = %v", err)
|
|
}
|
|
if err := u.performShortcut(shortcutCopyURL); err != nil {
|
|
t.Fatalf("performShortcut(copy-url) error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestUIActionErrorsAndStatusMessagesAreCapturedForDisplay(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
u := newUIWithSession("desktop", &session.Manager{})
|
|
u.vaultPath.SetText("/does/not/exist.kdbx")
|
|
u.masterPassword.SetText("correct horse battery staple")
|
|
|
|
u.runAction("open vault", u.openVaultAction)
|
|
if u.errorMessage == "" {
|
|
t.Fatal("errorMessage = empty, want visible action error")
|
|
}
|
|
|
|
u = newUIWithModel("desktop", vault.Model{
|
|
Entries: []vault.Entry{
|
|
{ID: "vault-console", Title: "Vault Console", Username: "dannyocean", Password: "token-1", URL: "https://vault.crew.example.invalid", Path: []string{"Root", "Internet"}},
|
|
},
|
|
})
|
|
u.showEntriesSection()
|
|
u.currentPath = []string{"Root", "Internet"}
|
|
u.filter()
|
|
u.state.SelectedEntryID = "vault-console"
|
|
u.runAction("copy username", func() error { return u.copySelectedFieldAction(clipboard.TargetUsername) })
|
|
if u.statusMessage == "" {
|
|
t.Fatal("statusMessage = empty, want visible success status")
|
|
}
|
|
}
|