From 059398ff58bedf692093ba8bb9bbe856a54dca1e Mon Sep 17 00:00:00 2001 From: samialabri Date: Tue, 20 Oct 2026 22:40:13 +0900 Subject: [PATCH] kv-ffi live test: a removed device re-enrols against the real server Ignored like the other live test: a phone denied while pending keeps its identity over a reinstall and a fresh Add-a-device QR signs it straight in as a new device. Passed against kestrove-server reenroll 62fe35d. Co-Authored-By: Claude Opus 5.5 --- crates/kv-ffi/tests/live_server.rs | 66 +++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/crates/kv-ffi/tests/live_server.rs b/crates/kv-ffi/tests/live_server.rs index 0665672..ac8c95c 100644 --- a/crates/kv-ffi/tests/live_server.rs +++ b/crates/kv-ffi/tests/live_server.rs @@ -8,8 +8,9 @@ //! KESTROVE_SERVER_URL=http://127.0.0.1:8080 KESTROVE_SETUP_TOKEN=live-test-setup-code \ //! cargo test -p kv-ffi --test live_server -- --ignored --nocapture //! ``` -//! The setup code works once, so restart the server with an empty database for every run. The run takes about -//! four minutes because it waits out the server's rate limit on the login endpoints. +//! The setup code works once, so restart the server with an empty database for every run (one test per run: +//! `two_devices_against_local_server` or `removed_device_reenrols_against_local_server`). They take a few minutes +//! because they wait out the server's rate limit on the login endpoints. use std::sync::Arc; @@ -131,3 +132,64 @@ async fn two_devices_against_local_server() { b.logout().await.unwrap(); println!("live sync OK against {url}"); } + +/// A platform signer whose identity can be reset, like the iOS Keychain one (its items survive a reinstall). +struct Resettable(std::sync::Mutex); + +impl DeviceSigner for Resettable { + fn device_id(&self) -> String { + kv_sync::DeviceSigner::device_id(&*self.0.lock().unwrap()).to_string() + } + fn public_key(&self) -> Vec { + kv_sync::DeviceSigner::public_key(&*self.0.lock().unwrap()) + } + fn sign(&self, message: Vec) -> Result, CoreError> { + kv_sync::DeviceSigner::sign(&*self.0.lock().unwrap(), &message) + .map_err(|e| CoreError::Internal { msg: e.to_string() }) + } + fn reset(&self) -> Result<(), CoreError> { + *self.0.lock().unwrap() = SoftwareSigner::generate(Uuid::new_v4()); + Ok(()) + } +} + +/// docs/03 "Removed devices" against the real server: the phone waits for approval, is denied, keeps its identity +/// over a "reinstall", and a fresh "Add a device" QR gets it straight in as a new device. +#[tokio::test] +#[ignore = "needs a fresh local kestrove-server; see the module docs"] +async fn removed_device_reenrols_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 web = VaultCore::new_in_memory(); + web.create_account(pw.into()).unwrap(); + connect(&web, &url, "web vault"); + web.register_account(token).await.unwrap(); + let id = web.save_item(item("Bank", "pw-a")).unwrap(); + web.sync_now().await.unwrap(); + + let signer = Arc::new(Resettable(std::sync::Mutex::new(SoftwareSigner::generate(Uuid::new_v4())))); + let phone = VaultCore::new_in_memory(); + phone.set_device_signer(signer.clone(), "iPhone".into(), PlatformDto::Ios).unwrap(); + let kit = kv_ffi::parse_setup_payload(web.setup_payload_for_kit().unwrap()).unwrap(); + assert_eq!(phone.login_with_setup(kit, pw.into()).await.unwrap(), LoginResult::PendingApproval); + let old_id = signer.device_id(); + web.revoke_device(old_id.clone()).await.unwrap(); + + tokio::time::sleep(std::time::Duration::from_secs(61)).await; // rate limit + // Reinstall: a new core, the same identity, a fresh unbound ticket. + let phone = VaultCore::new_in_memory(); + phone.set_device_signer(signer.clone(), "iPhone".into(), PlatformDto::Ios).unwrap(); + let setup = kv_ffi::parse_setup_payload(web.add_device_payload().await.unwrap().payload).unwrap(); + assert_eq!(phone.login_with_setup(setup, pw.into()).await.unwrap(), LoginResult::Ok); + assert_ne!(signer.device_id(), old_id); + phone.sync_now().await.unwrap(); + assert_eq!(phone.get_item(id).unwrap().password.as_deref(), Some("pw-a")); + let devices = web.list_devices().await.unwrap(); + assert!(devices.iter().any(|d| d.device_id == old_id && d.revoked && !d.approved)); + assert!(devices.iter().any(|d| d.device_id == signer.device_id() && d.approved)); + assert!(matches!(web.approve_device(old_id).await, Err(CoreError::NotFound))); + println!("removed device re-enrolled OK against {url}"); +}