//! Two devices against a real, freshly started kestrove-server. Ignored by default. //! //! ```sh //! # in kestrove-server (empty database, so it prints SETUP_TOKEN=… on boot): //! KV_DATABASE=sqlite::memory: KV_BIND=127.0.0.1:8080 KV_PUBLIC_URL=http://127.0.0.1:8080 cargo run //! # here: //! KESTROVE_SERVER_URL=http://127.0.0.1:8080 KESTROVE_SETUP_TOKEN= \ //! cargo test -p kv-ffi --test live_server -- --ignored --nocapture //! ``` //! The setup token works once, so restart the server with an empty database for every run. use std::sync::Arc; use kv_ffi::{CoreError, DeviceSigner, ItemDto, ItemKindDto, LoginResult, PlatformDto, VaultCore}; use kv_sync::signing::SoftwareSigner; use uuid::Uuid; struct Signer(SoftwareSigner); impl DeviceSigner for Signer { fn device_id(&self) -> String { kv_sync::DeviceSigner::device_id(&self.0).to_string() } fn public_key(&self) -> Vec { kv_sync::DeviceSigner::public_key(&self.0) } fn sign(&self, message: Vec) -> Result, CoreError> { kv_sync::DeviceSigner::sign(&self.0, &message).map_err(|e| CoreError::Internal { msg: e.to_string() }) } } fn connect(core: &VaultCore, url: &str, name: &str) { core.configure_server(url.into(), vec![]).unwrap(); core.set_device_signer(Arc::new(Signer(SoftwareSigner::generate(Uuid::new_v4()))), name.into(), PlatformDto::Linux) .unwrap(); } fn item(title: &str, password: &str) -> ItemDto { ItemDto { id: String::new(), kind: ItemKindDto::Login, title: title.into(), username: Some("me".into()), password: Some(password.into()), urls: vec!["https://example.com".into()], totp: None, notes: None, favorite: false, tags: vec![], modified_at: 0, } } #[tokio::test] #[ignore = "needs a fresh local kestrove-server; see the module docs"] async fn two_devices_against_local_server() { let url = std::env::var("KESTROVE_SERVER_URL").unwrap_or_else(|_| "http://127.0.0.1:8080".into()); let token = std::env::var("KESTROVE_SETUP_TOKEN").expect("set KESTROVE_SETUP_TOKEN to the token the server printed"); let pw = "a long master password for the live test"; let a = VaultCore::new_in_memory(); let sk = a.create_account(pw.into()).unwrap(); connect(&a, &url, "device A"); assert!(a.server_info().await.unwrap().setup_open, "server must be fresh (setup open)"); a.register_account(token).await.unwrap(); let id = a.save_item(item("Bank", "pw-a")).unwrap(); let r = a.sync_now().await.unwrap(); assert_eq!(r.pushed, 1); // The server allows 5 unauthenticated auth calls per minute per IP and both devices share one. tokio::time::sleep(std::time::Duration::from_secs(61)).await; let b = VaultCore::new_in_memory(); connect(&b, &url, "device B"); if b.login(pw.into(), sk.clone(), None).await.unwrap() == LoginResult::PendingApproval { let devices = a.list_devices().await.unwrap(); let pending = devices.iter().find(|d| !d.approved).expect("B listed as pending"); assert_eq!(pending.key_fingerprint, b.device_key_fingerprint().unwrap()); a.approve_device(pending.device_id.clone()).await.unwrap(); tokio::time::sleep(std::time::Duration::from_secs(61)).await; // rate limit again assert_eq!(b.login(pw.into(), sk.clone(), None).await.unwrap(), LoginResult::Ok); } b.sync_now().await.unwrap(); assert_eq!(b.get_item(id.clone()).unwrap().password.as_deref(), Some("pw-a")); // Conflicting edits merge. let mut on_a = a.get_item(id.clone()).unwrap(); on_a.notes = Some("A".into()); a.save_item(on_a).unwrap(); let mut on_b = b.get_item(id.clone()).unwrap(); on_b.username = Some("b".into()); b.save_item(on_b).unwrap(); a.sync_now().await.unwrap(); assert_eq!(b.sync_now().await.unwrap().merged, 1); a.sync_now().await.unwrap(); let (ia, ib) = (a.get_item(id.clone()).unwrap(), b.get_item(id).unwrap()); assert_eq!((ia.notes.as_deref(), ia.username.as_deref()), (Some("A"), Some("b"))); assert_eq!((ib.notes, ib.username), (ia.notes, ia.username)); b.logout().await.unwrap(); println!("live sync OK against {url}"); }