Materialize local cache on first remote open

This commit is contained in:
Joe Julian
2026-04-06 16:38:15 -07:00
parent db74a0a198
commit fc921ca30f
2 changed files with 233 additions and 6 deletions
+191 -6
View File
@@ -262,7 +262,12 @@ func TestUIClearingSearchResetsToCurrentSectionListing(t *testing.T) {
func TestUIRunBackgroundActionIgnoresDuplicateWhileLoading(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
u := newUIWithState("desktop", &session.Manager{}, statePaths{
DefaultSaveAsPath: filepath.Join(t.TempDir(), "default-save-path.kdbx"),
RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"),
RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"),
UIPreferencesPath: filepath.Join(t.TempDir(), "ui-preferences.json"),
})
started := make(chan struct{})
release := make(chan struct{})
runs := 0
@@ -298,7 +303,12 @@ func TestUIRunBackgroundActionIgnoresDuplicateWhileLoading(t *testing.T) {
func TestUICancelLifecycleBusyStateIgnoresLateResultAndKeepsRetryAvailable(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
u := newUIWithState("desktop", &session.Manager{}, statePaths{
DefaultSaveAsPath: filepath.Join(t.TempDir(), "default-save-path.kdbx"),
RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"),
RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"),
UIPreferencesPath: filepath.Join(t.TempDir(), "ui-preferences.json"),
})
u.vaultPath.SetText("/tmp/example.kdbx")
u.lastLifecycleAction = "open vault"
@@ -1191,7 +1201,12 @@ func TestUISaveSecuritySettingsUpdatesExistingVault(t *testing.T) {
t.Parallel()
manager := &session.Manager{}
u := newUIWithSession("desktop", manager)
u := newUIWithState("desktop", manager, statePaths{
DefaultSaveAsPath: filepath.Join(t.TempDir(), "default-save-path.kdbx"),
RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"),
RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"),
UIPreferencesPath: filepath.Join(t.TempDir(), "ui-preferences.json"),
})
u.masterPassword.SetText("correct horse battery staple")
if err := u.createVaultAction(); err != nil {
t.Fatalf("createVaultAction() error = %v", err)
@@ -1263,7 +1278,12 @@ func TestUISaveSettingsPersistsUIPreferences(t *testing.T) {
func TestUILockAndUnlockClearMasterPasswordField(t *testing.T) {
t.Parallel()
u := newUIWithSession("desktop", &session.Manager{})
u := newUIWithState("desktop", &session.Manager{}, statePaths{
DefaultSaveAsPath: filepath.Join(t.TempDir(), "default-save-path.kdbx"),
RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"),
RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"),
UIPreferencesPath: filepath.Join(t.TempDir(), "ui-preferences.json"),
})
u.masterPassword.SetText("correct horse battery staple")
if err := u.createVaultAction(); err != nil {
t.Fatalf("createVaultAction() error = %v", err)
@@ -1621,7 +1641,12 @@ func TestUIStartOpenRemoteActionAppliesResultOnMainThread(t *testing.T) {
defer server.Close()
manager := &session.Manager{}
u := newUIWithSession("desktop", manager)
u := newUIWithState("desktop", manager, statePaths{
DefaultSaveAsPath: filepath.Join(t.TempDir(), "default-save-path.kdbx"),
RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"),
RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"),
UIPreferencesPath: filepath.Join(t.TempDir(), "ui-preferences.json"),
})
u.masterPassword.SetText(key.Password)
u.remoteBaseURL.SetText(server.URL)
u.remotePath.SetText("vaults/main.kdbx")
@@ -1659,7 +1684,12 @@ func TestUIOpenRemoteReportsTransportFailure(t *testing.T) {
url := server.URL
server.Close()
u := newUIWithSession("desktop", &session.Manager{})
u := newUIWithState("desktop", &session.Manager{}, statePaths{
DefaultSaveAsPath: filepath.Join(t.TempDir(), "default-save-path.kdbx"),
RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"),
RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"),
UIPreferencesPath: filepath.Join(t.TempDir(), "ui-preferences.json"),
})
u.masterPassword.SetText("correct horse battery staple")
u.remoteBaseURL.SetText(url)
u.remotePath.SetText("vaults/main.kdbx")
@@ -5331,6 +5361,161 @@ func TestUIOpenRemoteVaultRestoresLastOpenedGroupForThatConnection(t *testing.T)
}
}
func TestUIOpenRemoteActionMaterializesLocalCacheAndBinding(t *testing.T) {
t.Parallel()
dir := t.TempDir()
cachePath := filepath.Join(dir, "remote-cache.kdbx")
paths := statePaths{
DefaultSaveAsPath: cachePath,
RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"),
RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"),
UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"),
}
key := vault.MasterKey{Password: "correct horse battery staple"}
var encoded bytes.Buffer
if err := vault.SaveKDBXWithKey(&encoded, vault.Model{
Entries: []vault.Entry{
{ID: "entry-1", Title: "Vault Console", Path: []string{"Root", "Internet"}},
},
}, key); err != nil {
t.Fatalf("SaveKDBXWithKey() error = %v", err)
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok || username != "tjulian" || password != "token-1" {
t.Fatalf("BasicAuth() = (%q, %q, %t), want (tjulian, token-1, true)", username, password, ok)
}
if r.Method != http.MethodGet {
t.Fatalf("method = %s, want GET", r.Method)
}
w.Header().Set("ETag", "\"v1\"")
_, _ = w.Write(encoded.Bytes())
}))
defer server.Close()
u := newUIWithState("desktop", &session.Manager{}, paths)
u.lifecycleMode = "remote"
u.masterPassword.SetText(key.Password)
u.remoteBaseURL.SetText(server.URL)
u.remotePath.SetText("vault.kdbx")
u.remoteUsername.SetText("tjulian")
u.remotePassword.SetText("token-1")
if err := u.openRemoteAction(); err != nil {
t.Fatalf("openRemoteAction() error = %v", err)
}
if got := u.vaultPath.Text(); got != cachePath {
t.Fatalf("vaultPath = %q, want %q", got, cachePath)
}
if _, err := os.Stat(cachePath); err != nil {
t.Fatalf("Stat(cachePath) error = %v", err)
}
if got := len(u.recentRemotes); got != 1 {
t.Fatalf("len(recentRemotes) = %d, want 1", got)
}
record := u.recentRemotes[0]
if record.LocalVaultPath != cachePath {
t.Fatalf("recentRemotes[0].LocalVaultPath = %q, want %q", record.LocalVaultPath, cachePath)
}
if record.RemoteProfileID == "" || record.CredentialEntryID == "" {
t.Fatalf("recentRemotes[0] = %#v, want binding ids populated", record)
}
var reopened session.Manager
if err := reopened.Open(cachePath, key); err != nil {
t.Fatalf("Open(cachePath) error = %v", err)
}
model, err := reopened.Current()
if err != nil {
t.Fatalf("Current() error = %v", err)
}
if got := len(model.RemoteProfiles); got != 1 {
t.Fatalf("len(RemoteProfiles) = %d, want 1", got)
}
if got := model.RemoteProfiles[0].BaseURL; got != server.URL {
t.Fatalf("RemoteProfiles[0].BaseURL = %q, want %q", got, server.URL)
}
entry, err := model.EntryByID(record.CredentialEntryID)
if err != nil {
t.Fatalf("EntryByID(%q) error = %v", record.CredentialEntryID, err)
}
if entry.Username != "tjulian" || entry.Password != "token-1" {
t.Fatalf("credential entry = %#v, want tjulian/token-1", entry)
}
}
func TestUIStartOpenRemoteActionMaterializesLocalCacheAndBinding(t *testing.T) {
t.Parallel()
dir := t.TempDir()
cachePath := filepath.Join(dir, "remote-cache.kdbx")
paths := statePaths{
DefaultSaveAsPath: cachePath,
RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"),
RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"),
UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"),
}
key := vault.MasterKey{Password: "correct horse battery staple"}
var encoded bytes.Buffer
if err := vault.SaveKDBXWithKey(&encoded, vault.Model{
Entries: []vault.Entry{
{ID: "entry-1", Title: "Vault Console", Path: []string{"Root", "Internet"}},
},
}, key); err != nil {
t.Fatalf("SaveKDBXWithKey() error = %v", err)
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
username, password, ok := r.BasicAuth()
if !ok || username != "tjulian" || password != "token-1" {
t.Fatalf("BasicAuth() = (%q, %q, %t), want (tjulian, token-1, true)", username, password, ok)
}
if r.Method != http.MethodGet {
t.Fatalf("method = %s, want GET", r.Method)
}
w.Header().Set("ETag", "\"v1\"")
_, _ = w.Write(encoded.Bytes())
}))
defer server.Close()
u := newUIWithState("desktop", &session.Manager{}, paths)
u.lifecycleMode = "remote"
u.masterPassword.SetText(key.Password)
u.remoteBaseURL.SetText(server.URL)
u.remotePath.SetText("vault.kdbx")
u.remoteUsername.SetText("tjulian")
u.remotePassword.SetText("token-1")
u.startOpenRemoteAction()
result := waitForBackgroundResult(t, u)
u.applyBackgroundResult(result)
if got := u.state.ErrorMessage; got != "" {
t.Fatalf("ErrorMessage after apply = %q, want empty", got)
}
if got := u.vaultPath.Text(); got != cachePath {
t.Fatalf("vaultPath = %q, want %q", got, cachePath)
}
if _, err := os.Stat(cachePath); err != nil {
t.Fatalf("Stat(cachePath) error = %v", err)
}
if got := len(u.recentRemotes); got != 1 {
t.Fatalf("len(recentRemotes) = %d, want 1", got)
}
record := u.recentRemotes[0]
if record.LocalVaultPath != cachePath {
t.Fatalf("recentRemotes[0].LocalVaultPath = %q, want %q", record.LocalVaultPath, cachePath)
}
if record.RemoteProfileID == "" || record.CredentialEntryID == "" {
t.Fatalf("recentRemotes[0] = %#v, want binding ids populated", record)
}
}
func TestDefaultStatePathsUsesProvidedStateDir(t *testing.T) {
t.Parallel()