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 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.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") } }