Files
nexus/rust/core/tests/sync.rs
T
Andy Dixon 9805681c2c license: release Nexus Terminal under the GNU AGPL v3
- LICENSE: the GNU Affero General Public License, version 3 (verbatim
  from gnu.org).
- SPDX-License-Identifier: AGPL-3.0-only plus a copyright line on every
  first-party source file (Rust, shell, Python, PHP); vendored crates keep
  their own licenses.
- Cargo manifests carry license/authors/repository; the macOS bundle
  copyright and the About page state the license and link the source.
- Release zips (and the macOS app bundle) include LICENSE.
- README: License section.
2026-10-13 09:46:31 +01:00

60 lines
2.4 KiB
Rust

// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Andy Dixon
//! Profile sync between two "machines" (data directories) sharing one folder.
use nexus_core::config::AppConfig;
use nexus_core::models::{Profile, Protocol};
use nexus_core::service::Service;
fn machine(root: &std::path::Path, name: &str, share: &std::path::Path) -> Service {
let cfg = AppConfig::in_dir(root.join(name));
std::fs::create_dir_all(&cfg.vault_dir).unwrap();
let svc = Service::open(cfg).unwrap();
svc.secrets.vault.initialise("master").unwrap();
let mut prefs = svc.preferences();
prefs.sync_folder = share.to_string_lossy().into_owned();
svc.save_preferences(&prefs).unwrap();
svc.set_sync_password("shared-secret").unwrap();
svc
}
#[test]
fn profiles_follow_edits_and_deletes_across_machines() {
let root = std::env::temp_dir().join(format!("nexus-sync-e2e-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let share = root.join("share");
std::fs::create_dir_all(&share).unwrap();
let (a, b) = (machine(&root, "a", &share), machine(&root, "b", &share));
let mut p = Profile::new(Protocol::Ssh);
p.display_name = "web".into();
p.host = "web.example".into();
p.username = "me".into();
let p = a.create_profile(p).unwrap();
let pw_key = nexus_core::secrets::SecretKind::Password.key(&p.id);
a.secrets.set_checked(&pw_key, "hunter2").unwrap();
assert!(a.sync_profiles().unwrap().pushed);
assert_eq!(b.sync_profiles().unwrap().pulled, 1);
assert_eq!(b.profiles()[0].host, "web.example");
std::thread::sleep(std::time::Duration::from_millis(1100)); // updated_at has 1 s resolution
let mut edited = b.profiles()[0].clone();
edited.host = "web2.example".into();
b.update_profile(edited).unwrap();
b.sync_profiles().unwrap();
assert_eq!(a.sync_profiles().unwrap().pulled, 1);
assert_eq!(a.profiles()[0].host, "web2.example");
// A's saved password was for web.example; the synced copy points elsewhere, so it's forgotten
// rather than sent to the new host without asking.
assert!(a.secrets.get(&pw_key).is_none());
a.delete_profile(&p.id).unwrap();
a.sync_profiles().unwrap();
assert_eq!(b.sync_profiles().unwrap().removed, 1);
assert!(b.profiles().is_empty());
// Nothing left to do on either side.
assert!(!a.sync_profiles().unwrap().pushed && !b.sync_profiles().unwrap().pushed);
let _ = std::fs::remove_dir_all(root);
}