#[test]
fn private_invite_delete_failure_blocks_sibling_imports_in_same_retry_pass() {
let inviter_owner = Keys::generate();
let inviter_device = Keys::generate();
let peer_owner = Keys::generate();
let first_peer_device = Keys::generate();
let second_peer_device = Keys::generate();
let mut inviter = logged_in_test_core(
"private-response-delete-blocks-siblings",
&inviter_owner,
&inviter_device,
);
let invite_url = create_private_invite_for_test(&mut inviter);
inviter.handle_action(AppAction::PushScreen { screen: Screen::NewChat });
let invite_key = inviter
.private_chat_invites
.keys()
.next()
.cloned()
.expect("private invite key");
for peer_device in [&first_peer_device, &second_peer_device] {
inviter.handle_relay_event(crafted_private_invite_response(
&invite_url,
peer_device,
Some(peer_device.public_key().to_hex()),
Some(peer_owner.public_key()),
));
}
assert_eq!(inviter.pending_private_invite_responses.len(), 2);
{
let shared = inviter.app_store.shared();
let conn = shared.lock().expect("storage connection");
conn.execute_batch(&format!(
"CREATE TEMP TRIGGER fail_private_invite_delete_siblings
BEFORE DELETE ON ndr_kv
WHEN OLD.key = '{invite_key}'
BEGIN SELECT RAISE(ABORT, 'injected invite delete failure'); END;"
))
.expect("install delete failure trigger");
}
let roster = AppKeys::new(vec![
DeviceEntry::new(first_peer_device.public_key(), 10),
DeviceEntry::new(second_peer_device.public_key(), 10),
])
.get_event_at(peer_owner.public_key(), 10)
.sign_with_keys(&peer_owner)
.expect("signed sibling roster");
ingest_invite_owner_app_keys(
&mut inviter,
peer_owner.public_key(),
vec![roster.clone()],
);
assert_eq!(
stored_session_count(&inviter, peer_owner.public_key()),
1,
"one failed cleanup must stop sibling responses from reusing the invite"
);
assert_eq!(inviter.pending_private_invite_responses.len(), 2);
assert!(inviter.private_chat_invites.contains_key(&invite_key));
assert!(matches!(inviter.screen_stack.last(), Some(Screen::NewChat)),
"Do not navigate before one-use invite consumption is durable");
{
let shared = inviter.app_store.shared();
let conn = shared.lock().expect("storage connection");
conn.execute_batch("DROP TRIGGER fail_private_invite_delete_siblings;")
.expect("remove delete failure trigger");
}
ingest_invite_owner_app_keys(
&mut inviter,
peer_owner.public_key(),
vec![roster],
);
assert_eq!(stored_session_count(&inviter, peer_owner.public_key()), 1);
assert!(inviter.pending_private_invite_responses.is_empty());
assert!(inviter.private_chat_invites.is_empty());
assert!(inviter.threads.contains_key(&peer_owner.public_key().to_hex()));
assert!(matches!(inviter.screen_stack.last(), Some(Screen::NewChat)),
"A legacy link that is not displayed must not redirect the current code screen");
}
#[test]
fn unchanged_roster_replay_finishes_invite_cleanup_visibly_and_durably() {
let owner = Keys::generate();
let device = Keys::generate();
let peer = Keys::generate();
let peer_device = Keys::generate();
let dir = tempfile::tempdir().unwrap();
let data_dir = dir.path().to_string_lossy().into_owned();
let mut core = logged_in_test_core_at_data_dir(&owner, &device, data_dir.clone());
let invite_url = create_private_invite_for_test(&mut core);
let invite_key = core.private_chat_invites.keys().next().unwrap().clone();
core.screen_stack = vec![Screen::Settings];
core.handle_relay_event(crafted_private_invite_response(
&invite_url,
&peer_device,
Some(peer_device.public_key().to_hex()),
Some(peer.public_key()),
));
assert_eq!(core.pending_private_invite_responses.len(), 1);
{
let shared = core.app_store.shared();
shared.lock().unwrap().execute_batch(&format!(
"CREATE TEMP TRIGGER fail_replayed_invite_cleanup BEFORE DELETE ON ndr_kv
WHEN OLD.key = '{invite_key}'
BEGIN SELECT RAISE(ABORT, 'injected invite cleanup failure'); END;"
)).unwrap();
}
let roster = signed_app_keys_authorization_event(&peer, peer_device.public_key(), 10);
core.handle_relay_event(roster.clone());
assert_eq!(stored_session_count(&core, peer.public_key()), 1);
assert_eq!(core.pending_private_invite_responses.len(), 1);
let chat_id = peer.public_key().to_hex();
assert!(!core.threads.contains_key(&chat_id));
{
let shared = core.app_store.shared();
shared.lock().unwrap().execute_batch("DROP TRIGGER fail_replayed_invite_cleanup;").unwrap();
}
for index in 0..MAX_SEEN_EVENT_IDS {
core.remember_event(format!("{index:064x}"));
}
assert!(!core.has_seen_event(&roster.id.to_hex()));
core.enter_batch();
core.handle_relay_event(roster);
core.exit_batch();
assert!(core.pending_private_invite_responses.is_empty());
assert_eq!(stored_session_count(&core, peer.public_key()), 1);
assert_eq!(core.screen_stack, vec![Screen::Settings]);
assert!(core.state.chat_list.iter().any(|chat| chat.chat_id == chat_id));
drop(core);
let mut restarted = logged_in_test_core_at_data_dir(&owner, &device, data_dir);
let saved = restarted.load_persisted().unwrap().unwrap();
assert!(saved.threads.iter().any(|chat| chat.chat_id == chat_id));
}
#[test]
fn local_relay_normal_app_keys_subscription_releases_staged_response_end_to_end() {
let relay = crate::local_relay::TestRelay::start();
let relay_urls = relay_urls_from_strings(&[relay.url().to_string()]);
let inviter_owner = Keys::generate();
let inviter_device = Keys::generate();
let peer_owner = Keys::generate();
let peer_device = Keys::generate();
let (core_tx, core_rx) = flume::unbounded();
let temp_dir = tempfile::TempDir::new().expect("temp dir");
let mut inviter = AppCore::new(
flume::unbounded().0,
core_tx,
temp_dir.path().to_string_lossy().to_string(),
Arc::new(RwLock::new(AppState::empty())),
);
inviter.preferences.nostr_relay_urls = vec![relay.url().to_string()];
inviter
.start_primary_session(inviter_owner.clone(), inviter_device, false, false)
.expect("start inviter");
while let Ok(message) = core_rx.try_recv() {
inviter.handle_message(message);
}
let inviter_client = inviter.logged_in.as_ref().unwrap().client.clone();
inviter.runtime.block_on(async {
ensure_session_relays_configured(&inviter_client, &relay_urls).await;
connect_client_with_timeout(&inviter_client, Duration::from_secs(2)).await;
});
let roster = signed_app_keys_authorization_event(
&peer_owner,
peer_device.public_key(),
unix_now().get(),
);
let publisher = Client::new(peer_owner.clone());
inviter.runtime.block_on(async {
ensure_session_relays_configured(&publisher, &relay_urls).await;
connect_client_with_timeout(&publisher, Duration::from_secs(2)).await;
publisher
.send_event(&roster)
.await
.expect("publish owner roster");
});
let invite_url = create_private_invite_for_test(&mut inviter);
let response = crafted_private_invite_response(
&invite_url,
&peer_device,
Some(peer_device.public_key().to_hex()),
Some(peer_owner.public_key()),
);
inviter.handle_relay_event(response);
assert_eq!(inviter.pending_private_invite_responses.len(), 1);
let deadline = std::time::Instant::now() + Duration::from_secs(8);
while std::time::Instant::now() < deadline {
while let Ok(message) = core_rx.try_recv() {
inviter.handle_message(message);
}
if inviter.pending_private_invite_responses.is_empty() {
break;
}
std::thread::sleep(Duration::from_millis(10));
}
assert!(inviter.pending_private_invite_responses.is_empty());
assert!(inviter.private_chat_invites.is_empty());
assert!(inviter
.threads
.contains_key(&peer_owner.public_key().to_hex()));
assert_eq!(
active_session_device_pubkeys(&inviter, peer_owner.public_key()),
vec![peer_device.public_key()]
);
}