Add advanced synchronize dialog and APK tooling

This commit is contained in:
Joe Julian
2026-03-29 21:38:46 -07:00
parent 9dea7a4a34
commit fc34508689
11 changed files with 1154 additions and 80 deletions
+153 -1
View File
@@ -3,6 +3,7 @@ package main
import (
"bytes"
"errors"
"io"
"net/http"
"net/http/httptest"
"os"
@@ -12,8 +13,8 @@ import (
"testing"
"time"
"gioui.org/layout"
"gioui.org/io/key"
"gioui.org/layout"
"gioui.org/unit"
"gioui.org/widget"
@@ -678,6 +679,145 @@ func TestUIRemoteSaveConflictShowsVisibleErrorAndKeepsDirtyState(t *testing.T) {
}
}
func TestUIAdvancedSynchronizeFromLocalMergesIntoCurrentVault(t *testing.T) {
t.Parallel()
key := vault.MasterKey{Password: "correct horse battery staple"}
currentPath := filepath.Join(t.TempDir(), "current.kdbx")
otherPath := filepath.Join(t.TempDir(), "other.kdbx")
writeKDBXMainTestFile(t, currentPath, vault.Model{
Entries: []vault.Entry{{
ID: "entry-current",
Title: "Git Server",
Username: "joejulian",
Password: "token-current",
URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"},
}},
}, key)
writeKDBXMainTestFile(t, otherPath, vault.Model{
Entries: []vault.Entry{{
ID: "entry-other",
Title: "Dynadot",
Username: "jjulian",
Password: "token-other",
URL: "https://www.dynadot.com",
Path: []string{"Root", "Internet"},
}},
}, key)
u := newUIWithSession("desktop", &session.Manager{})
u.masterPassword.SetText(key.Password)
u.vaultPath.SetText(currentPath)
if err := u.openVaultAction(); err != nil {
t.Fatalf("openVaultAction() error = %v", err)
}
u.openAdvancedSyncDialog()
u.syncDirection = syncDirectionPull
u.syncSourceMode = syncSourceLocal
u.syncLocalPath.SetText(otherPath)
if err := u.advancedSyncAction(); err != nil {
t.Fatalf("advancedSyncAction() error = %v", err)
}
var reopened session.Manager
if err := reopened.Open(currentPath, key); err != nil {
t.Fatalf("reopen Open(current) error = %v", err)
}
model, err := reopened.Current()
if err != nil {
t.Fatalf("reopened Current() error = %v", err)
}
if got := len(model.EntriesInPath([]string{"Root", "Internet"})); got != 2 {
t.Fatalf("len(EntriesInPath(Root/Internet)) = %d, want 2", got)
}
}
func TestUIAdvancedSynchronizeToRemoteWritesMergedVaultToTarget(t *testing.T) {
t.Parallel()
key := vault.MasterKey{Password: "correct horse battery staple"}
currentPath := filepath.Join(t.TempDir(), "current.kdbx")
writeKDBXMainTestFile(t, currentPath, vault.Model{
Entries: []vault.Entry{{
ID: "entry-current",
Title: "Git Server",
Username: "joejulian",
Password: "token-current",
URL: "https://git.julianfamily.org",
Path: []string{"Root", "Internet"},
}},
}, key)
var remoteBytes bytes.Buffer
if err := vault.SaveKDBXWithKey(&remoteBytes, vault.Model{
Entries: []vault.Entry{{
ID: "entry-remote",
Title: "Dynadot",
Username: "jjulian",
Password: "token-remote",
URL: "https://www.dynadot.com",
Path: []string{"Root", "Internet"},
}},
}, key); err != nil {
t.Fatalf("SaveKDBXWithKey(remote) error = %v", err)
}
etag := "\"v1\""
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
w.Header().Set("ETag", etag)
_, _ = w.Write(remoteBytes.Bytes())
case http.MethodPut:
payload, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("ReadAll(PUT body) error = %v", err)
}
remoteBytes.Reset()
if _, err := remoteBytes.Write(payload); err != nil {
t.Fatalf("Write(remoteBytes) error = %v", err)
}
etag = "\"v2\""
w.Header().Set("ETag", etag)
w.WriteHeader(http.StatusNoContent)
default:
t.Fatalf("unexpected method %s", r.Method)
}
}))
defer server.Close()
u := newUIWithSession("desktop", &session.Manager{})
u.masterPassword.SetText(key.Password)
u.vaultPath.SetText(currentPath)
if err := u.openVaultAction(); err != nil {
t.Fatalf("openVaultAction() error = %v", err)
}
u.openAdvancedSyncDialog()
u.syncDirection = syncDirectionPush
u.syncSourceMode = syncSourceRemote
u.syncRemoteBaseURL.SetText(server.URL)
u.syncRemotePath.SetText("vaults/other.kdbx")
if err := u.advancedSyncAction(); err != nil {
t.Fatalf("advancedSyncAction() error = %v", err)
}
var reopened session.Manager
if err := reopened.OpenRemote(webdav.Client{BaseURL: server.URL}, "vaults/other.kdbx", key); err != nil {
t.Fatalf("OpenRemote(reopened) error = %v", err)
}
model, err := reopened.Current()
if err != nil {
t.Fatalf("reopened Current() error = %v", err)
}
if got := len(model.EntriesInPath([]string{"Root", "Internet"})); got != 2 {
t.Fatalf("len(EntriesInPath(Root/Internet)) = %d, want 2", got)
}
}
func TestUIMasterKeyInputSupportsKeyFileAndCompositeKeys(t *testing.T) {
t.Parallel()
@@ -2839,3 +2979,15 @@ func TestUICurrentMasterKeyReportsUnreadableKeyFile(t *testing.T) {
t.Fatalf("currentMasterKey() error = %v, want os.ErrNotExist", err)
}
}
func writeKDBXMainTestFile(t *testing.T, path string, model vault.Model, key vault.MasterKey) {
t.Helper()
var encoded bytes.Buffer
if err := vault.SaveKDBXWithKey(&encoded, model, key); err != nil {
t.Fatalf("SaveKDBXWithKey(%s) error = %v", path, err)
}
if err := os.WriteFile(path, encoded.Bytes(), 0o600); err != nil {
t.Fatalf("WriteFile(%s) error = %v", path, err)
}
}