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 <[email protected]>
This commit is contained in:
2026-10-20 22:40:13 +09:00
co-authored by Claude Opus 5.5
parent 83d56d43e2
commit 059398ff58
+64 -2
View File
@@ -8,8 +8,9 @@
//! KESTROVE_SERVER_URL=http://127.0.0.1:8080 KESTROVE_SETUP_TOKEN=live-test-setup-code \ //! 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 //! 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 //! The setup code works once, so restart the server with an empty database for every run (one test per run:
//! four minutes because it waits out the server's rate limit on the login endpoints. //! `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; use std::sync::Arc;
@@ -131,3 +132,64 @@ async fn two_devices_against_local_server() {
b.logout().await.unwrap(); b.logout().await.unwrap();
println!("live sync OK against {url}"); 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<SoftwareSigner>);
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<u8> {
kv_sync::DeviceSigner::public_key(&*self.0.lock().unwrap())
}
fn sign(&self, message: Vec<u8>) -> Result<Vec<u8>, 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}");
}