package appstate import ( "encoding/json" "errors" "strings" "testing" "git.julianfamily.org/keepassgo/vault" ) func TestRemoteBindingResolveUsesVaultProfileAndCredentialEntry(t *testing.T) { t.Parallel() model := vault.Model{ Entries: []vault.Entry{ { ID: "linuscaldwell-webdav", Title: "Bellagio WebDAV Sign-In", Username: "linuscaldwell", Password: "bellagio-pass-1", Path: []string{"Crew", "Internet"}, }, }, RemoteProfiles: []vault.RemoteProfile{ { ID: "bellagio-webdav", Name: "Bellagio Vault", Backend: vault.RemoteBackendWebDAV, BaseURL: "https://dav.example.invalid/remote.php/dav", Path: "files/bellagio/keepass.kdbx", }, }, } binding := RemoteBinding{ LocalVaultPath: "/tmp/bellagio.kdbx", RemoteProfileID: "bellagio-webdav", CredentialEntryID: "linuscaldwell-webdav", SyncMode: SyncModeAutomaticOnOpenSave, } resolved, err := binding.Resolve(model) if err != nil { t.Fatalf("Resolve() error = %v", err) } if got := resolved.Profile.BaseURL; got != "https://dav.example.invalid/remote.php/dav" { t.Fatalf("resolved profile base URL = %q, want remote.php/dav URL", got) } if got := resolved.Profile.Path; got != "files/bellagio/keepass.kdbx" { t.Fatalf("resolved profile path = %q, want files/bellagio/keepass.kdbx", got) } if got := resolved.Credentials.Username; got != "linuscaldwell" { t.Fatalf("resolved credentials username = %q, want linuscaldwell", got) } if got := resolved.Credentials.Password; got != "bellagio-pass-1" { t.Fatalf("resolved credentials password = %q, want bellagio-pass-1", got) } } func TestRemoteBindingResolveFailsWhenVaultReferenceIsMissing(t *testing.T) { t.Parallel() model := vault.Model{ Entries: []vault.Entry{ {ID: "linuscaldwell-webdav", Title: "Bellagio WebDAV Sign-In"}, }, } _, err := (RemoteBinding{ LocalVaultPath: "/tmp/bellagio.kdbx", RemoteProfileID: "bellagio-webdav", CredentialEntryID: "missing-creds", }).Resolve(model) if !errors.Is(err, vault.ErrRemoteProfileNotFound) { t.Fatalf("Resolve() error = %v, want ErrRemoteProfileNotFound first", err) } model.RemoteProfiles = []vault.RemoteProfile{{ ID: "bellagio-webdav", Name: "Bellagio Vault", Backend: vault.RemoteBackendWebDAV, BaseURL: "https://dav.example.invalid/remote.php/dav", Path: "files/bellagio/keepass.kdbx", }} _, err = (RemoteBinding{ LocalVaultPath: "/tmp/bellagio.kdbx", RemoteProfileID: "bellagio-webdav", CredentialEntryID: "missing-creds", }).Resolve(model) if !errors.Is(err, vault.ErrEntryNotFound) { t.Fatalf("Resolve() error = %v, want ErrEntryNotFound", err) } } func TestRemoteBindingJSONStoresOnlyNonSecretReferences(t *testing.T) { t.Parallel() content, err := json.Marshal(RemoteBinding{ LocalVaultPath: "/tmp/bellagio.kdbx", RemoteProfileID: "bellagio-webdav", CredentialEntryID: "remote-creds-1", SyncMode: SyncModeAutomaticOnOpenSave, }) if err != nil { t.Fatalf("json.Marshal(RemoteBinding) error = %v", err) } text := string(content) for _, disallowed := range []string{"bellagio-pass-1", "password", "username", "baseUrl"} { if strings.Contains(text, disallowed) { t.Fatalf("binding JSON %q unexpectedly contains %q", text, disallowed) } } } func TestConfigureRemoteBindingStoresProfileAndCredentialsInVault(t *testing.T) { t.Parallel() var model vault.Model binding, err := ConfigureRemoteBinding(&model, RemoteBindingInput{ LocalVaultPath: "/tmp/bellagio.kdbx", RemoteProfileID: "bellagio-webdav", RemoteProfileName: "Bellagio Vault", BaseURL: "https://dav.example.invalid/remote.php/dav", RemotePath: "files/bellagio/keepass.kdbx", CredentialEntryID: "remote-creds-1", CredentialTitle: "Bellagio WebDAV Sign-In", Username: "linuscaldwell", Password: "bellagio-pass-1", CredentialPath: []string{"Crew", "Internet"}, SyncMode: SyncModeAutomaticOnOpenSave, }) if err != nil { t.Fatalf("ConfigureRemoteBinding() error = %v", err) } if len(model.RemoteProfiles) != 1 { t.Fatalf("len(RemoteProfiles) = %d, want 1", len(model.RemoteProfiles)) } if got := model.RemoteProfiles[0].BaseURL; got != "https://dav.example.invalid/remote.php/dav" { t.Fatalf("stored remote profile base URL = %q, want remote.php/dav URL", got) } credentials, err := model.EntryByID("remote-creds-1") if err != nil { t.Fatalf("EntryByID(remote-creds-1) error = %v", err) } if credentials.Username != "linuscaldwell" || credentials.Password != "bellagio-pass-1" { t.Fatalf("stored credential entry = %#v, want linuscaldwell/bellagio-pass-1", credentials) } if credentials.URL != "https://dav.example.invalid/remote.php/dav" { t.Fatalf("stored credential entry URL = %q, want remote.php/dav URL", credentials.URL) } if binding.LocalVaultPath != "/tmp/bellagio.kdbx" { t.Fatalf("binding LocalVaultPath = %q, want /tmp/bellagio.kdbx", binding.LocalVaultPath) } if binding.RemoteProfileID != "bellagio-webdav" || binding.CredentialEntryID != "remote-creds-1" { t.Fatalf("binding = %#v, want only vault references", binding) } } func TestConfigureRemoteBindingRejectsIncompleteInput(t *testing.T) { t.Parallel() for _, tc := range []struct { name string input RemoteBindingInput }{ { name: "missing_local_vault_path", input: RemoteBindingInput{ RemoteProfileID: "bellagio-webdav", BaseURL: "https://dav.example.invalid/remote.php/dav", RemotePath: "files/bellagio/keepass.kdbx", CredentialEntryID: "remote-creds-1", Password: "bellagio-pass-1", }, }, { name: "missing_remote_base_url", input: RemoteBindingInput{ LocalVaultPath: "/tmp/bellagio.kdbx", RemoteProfileID: "bellagio-webdav", RemotePath: "files/bellagio/keepass.kdbx", CredentialEntryID: "remote-creds-1", Password: "bellagio-pass-1", }, }, { name: "missing_credential_entry_id", input: RemoteBindingInput{ LocalVaultPath: "/tmp/bellagio.kdbx", RemoteProfileID: "bellagio-webdav", BaseURL: "https://dav.example.invalid/remote.php/dav", RemotePath: "files/bellagio/keepass.kdbx", Password: "bellagio-pass-1", }, }, } { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() var model vault.Model if _, err := ConfigureRemoteBinding(&model, tc.input); err == nil { t.Fatalf("ConfigureRemoteBinding(%#v) error = nil, want validation error", tc.input) } }) } } func TestRemoveRemoteBindingRemovesProfileAndCredentialsFromVault(t *testing.T) { t.Parallel() model := vault.Model{ Entries: []vault.Entry{{ ID: "remote-creds-1", Title: "Bellagio WebDAV Sign-In", Username: "linuscaldwell", Password: "bellagio-pass-1", }}, RemoteProfiles: []vault.RemoteProfile{{ ID: "bellagio-webdav", Name: "Bellagio Vault", Backend: vault.RemoteBackendWebDAV, BaseURL: "https://dav.example.invalid/remote.php/dav", Path: "files/bellagio/keepass.kdbx", }}, } err := RemoveRemoteBinding(&model, RemoteBinding{ LocalVaultPath: "/tmp/bellagio.kdbx", RemoteProfileID: "bellagio-webdav", CredentialEntryID: "remote-creds-1", }) if err != nil { t.Fatalf("RemoveRemoteBinding() error = %v", err) } if got := len(model.RemoteProfiles); got != 0 { t.Fatalf("len(RemoteProfiles) = %d, want 0", got) } if _, err := model.EntryByID("remote-creds-1"); !errors.Is(err, vault.ErrEntryNotFound) { t.Fatalf("EntryByID(remote-creds-1) error = %v, want ErrEntryNotFound", err) } }