twonly-app/rust/tests/api/recovery.rs
2026-08-29 00:27:58 +02:00

110 lines
3.7 KiB
Rust

use super::Tester;
use prost::Message as _;
use rust_lib_twonly::api::messages::incoming::client2client::recovery::perform_heartbeat;
use rust_lib_twonly::api::messages::outgoing::send_c2c_message_to_contact;
use rust_lib_twonly::api::proto::client::{self as proto, encrypted_content};
use rust_lib_twonly::bridge::api::ApiConnectionState;
use rust_lib_twonly::services::contacts::ContactService;
async fn create_authenticated_tester() -> anyhow::Result<Tester> {
let mut tester = Tester::new().await?;
tester.wait_until(ApiConnectionState::Connected).await?;
tester.register_and_authenticate().await?;
tester.wait_until(ApiConnectionState::Authenticated).await?;
Ok(tester)
}
#[tokio::test]
async fn test_passwordless_recovery_share_heartbeat_and_delete() -> anyhow::Result<()> {
let _ = tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.with_ansi(true)
.event_format(rust_lib_twonly::log::ShortEventFormatter::ansi())
.try_init();
let tester_a = create_authenticated_tester().await?;
let tester_b = create_authenticated_tester().await?;
// Connect contacts A and B
ContactService::new(&tester_a.context)
.request_by_username(tester_b.username.clone(), true)
.await?;
tester_b
.wait_for_contact_state(tester_a.user_id, false, true)
.await?;
ContactService::new(&tester_b.context)
.accept_request(tester_a.user_id, true)
.await?;
tester_a
.wait_for_contact_state(tester_b.user_id, true, false)
.await?;
// 1. Tester A sends recovery share to Tester B
let secret_share = vec![10, 20, 30, 40];
let recovery_content = proto::EncryptedContent {
passwordless_recovery: Some(encrypted_content::PasswordLessRecovery {
recovery_secret_share: Some(secret_share.clone()),
threshold: 2,
delete: false,
}),
..Default::default()
};
send_c2c_message_to_contact()
.ctx(&tester_a.context)
.contact_id(tester_b.user_id)
.encrypted_content(recovery_content.encode_to_vec())
.call()
.await?;
// Verify Tester B stores the share
tester_b
.wait_for_recovery_contacts_share(tester_a.user_id)
.await?;
// Configure Tester A's contact record for Tester B to track expected share for heartbeat verification
{
let db_a = tester_a.context.app_db.read().await.clone();
sqlx::query!(
"UPDATE contacts SET recovery_secret_share = ?, recovery_is_trusted_friend = 1 WHERE user_id = ?",
secret_share,
tester_b.user_id,
)
.execute(&db_a.pool)
.await?;
}
// 2. Tester B performs heartbeat
perform_heartbeat(&tester_b.context).await?;
// Verify Tester A receives heartbeat and updates recovery_last_heartbeat
tester_a
.wait_for_recovery_last_heartbeat(tester_b.user_id)
.await?;
// 3. Tester A sends delete request for the recovery share
let delete_content = proto::EncryptedContent {
passwordless_recovery: Some(encrypted_content::PasswordLessRecovery {
recovery_secret_share: None,
threshold: 0,
delete: true,
}),
..Default::default()
};
send_c2c_message_to_contact()
.ctx(&tester_a.context)
.contact_id(tester_b.user_id)
.encrypted_content(delete_content.encode_to_vec())
.call()
.await?;
tester_b
.wait_for_recovery_share_deleted(tester_a.user_id)
.await?;
Ok(())
}