Add UI master key setup and change flows

This commit is contained in:
Joe Julian
2026-03-29 11:23:54 -07:00
parent e15cfb1535
commit 44bba18149
10 changed files with 675 additions and 147 deletions
+253
View File
@@ -154,6 +154,258 @@ func TestUILifecycleActionsCreateSaveOpenLockAndUnlockLocalVault(t *testing.T) {
}
}
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()
@@ -230,6 +482,7 @@ func TestUIMasterKeyInputSupportsKeyFileAndCompositeKeys(t *testing.T) {
}
u := newUIWithSession("desktop", &session.Manager{})
u.setMasterKeyMode(vault.MasterKeyModePasswordAndKeyFile)
u.masterPassword.SetText("correct horse battery staple")
u.keyFilePath.SetText(keyFile)