use super::*;
#[path = "ingestion_tests.rs"]
mod ingestion;
#[path = "receipt_tests.rs"]
mod receipts;
#[path = "snapshot_tests.rs"]
mod snapshots;
fn reconnect_fixture_after_runtime_change(
core: &mut AppCore,
previous_generation: u64,
links: Vec<crate::updates::FipsNearbyLinkSnapshot>,
) {
assert_ne!(core.fips_connection_generation, previous_generation);
assert!(
core.fips_nearby_links.is_empty(),
"retired links must be cleared"
);
core.handle_internal(InternalEvent::FipsNearbyPeersChanged {
generation: core.fips_connection_generation,
peers: links,
});
}
fn event_id() -> String {
"ab".repeat(32)
}
#[test]
fn private_nearby_replays_do_not_scan_known_devices() {
let local_hex = Keys::generate().public_key().to_hex();
let recipient = Keys::generate().public_key();
let recipient_hex = recipient.to_hex();
let stranger_hex = Keys::generate().public_key().to_hex();
for targets in [
BTreeSet::from([recipient.to_bech32().unwrap()]),
BTreeSet::new(),
] {
let inspected = std::cell::Cell::new(0);
let known_devices = std::iter::repeat_n(stranger_hex.as_str(), 256)
.chain(std::iter::once(recipient_hex.as_str()))
.inspect(|_| inspected.set(inspected.get() + 1));
let configured = configured_fips_nearby_peers(Some(&targets), &local_hex, known_devices);
assert_eq!(
inspected.get(),
0,
"private replay must not traverse the device roster, even for unresolved invites"
);
assert!(
configured.is_empty(),
"private targets are supplied exclusively by the authenticated recipient set"
);
}
}
#[test]
fn public_nearby_publications_keep_valid_configured_peers() {
let local_hex = Keys::generate().public_key().to_hex();
let peer = Keys::generate().public_key();
let peer_hex = peer.to_hex();
let configured = configured_fips_nearby_peers(
None,
&local_hex,
[local_hex.as_str(), "invalid-device", peer_hex.as_str()].into_iter(),
);
assert_eq!(
configured
.iter()
.map(|peer| peer.npub())
.collect::<Vec<_>>(),
vec![peer.to_bech32().unwrap()]
);
}
#[test]
fn event_and_receipt_round_trip() {
for packet in [
FipsNearbyPacket::event(event_id(), "{\"kind\":14}".to_string()).unwrap(),
FipsNearbyPacket::receipt(event_id()).unwrap(),
] {
assert_eq!(
FipsNearbyPacket::decode(&packet.encode().unwrap()),
Some(packet)
);
}
}
#[test]
fn discovery_demand_requires_unacknowledged_exact_private_target() {
let mut outbox = FipsNearbyOutbox::default();
outbox.insert_for_targets("public".into(), vec![], None);
outbox.insert_for_targets("unresolved".into(), vec![], Some(BTreeSet::new()));
outbox.insert_for_targets(
"another-peer".into(),
vec![],
Some(BTreeSet::from(["other".into()])),
);
assert!(!outbox.has_pending_private_for_peer("peer"));
outbox.insert_for_targets(
"private".into(),
vec![],
Some(BTreeSet::from(["peer".into()])),
);
assert!(outbox.has_pending_private_for_peer("peer"));
outbox.mark_sent_on_link("peer", 7, &["private".into()]);
assert!(outbox.has_pending_private_for_peer("peer"));
outbox.acknowledge("peer", "private");
assert!(!outbox.has_pending_private_for_peer("peer"));
assert!(outbox.has_pending_private_for_peer("other"));
}
#[test]
fn nearby_outbox_replays_unacknowledged_links_and_scopes_receipts() {
let mut outbox = FipsNearbyOutbox::default();
let event_id = event_id();
let payload = b"queued nearby event".to_vec();
outbox.insert(event_id.clone(), payload.clone());
assert_eq!(
outbox.pending_for_link("peer", 7),
vec![(event_id.clone(), payload.clone())]
);
outbox.mark_sent_on_link("peer", 7, std::slice::from_ref(&event_id));
assert!(outbox.pending_for_link("peer", 7).is_empty());
assert_eq!(
outbox.pending_for_link("peer", 8),
vec![(event_id.clone(), payload.clone())]
);
outbox.acknowledge("peer", &event_id);
assert!(outbox.pending_for_link("peer", 8).is_empty());
assert_eq!(
outbox.pending_for_link("later-peer", 9),
vec![(event_id.clone(), payload.clone())]
);
outbox.insert(event_id.clone(), payload.clone());
assert!(outbox.pending_for_link("peer", 10).is_empty());
assert_eq!(
outbox.pending_for_link("later-peer", 9),
vec![(event_id, payload)]
);
}
#[test]
fn rejects_wrong_version_invalid_id_and_oversized_packet() {
let wrong_version = serde_json::json!({
"type": "receipt",
"v": 2,
"eventId": event_id(),
});
assert!(FipsNearbyPacket::decode(&serde_json::to_vec(&wrong_version).unwrap()).is_none());
assert!(FipsNearbyPacket::receipt("not-an-event-id".to_string()).is_none());
assert!(FipsNearbyPacket::decode(&vec![b'x'; FIPS_NEARBY_MAX_PACKET_BYTES + 1]).is_none());
}
#[test]
fn host_ble_attachment_is_single_owner_and_can_be_replaced_after_detach() {
let temp_dir = tempfile::TempDir::new().unwrap();
let mut core = AppCore::new(
flume::unbounded().0,
flume::unbounded().0,
temp_dir.path().to_string_lossy().to_string(),
Arc::new(RwLock::new(AppState::empty())),
);
let (first_io, _first_adapter) = core
.runtime
.block_on(async { HostBleIo::channel("mobile", "first", 8) })
.unwrap();
let (duplicate_io, _duplicate_adapter) = core
.runtime
.block_on(async { HostBleIo::channel("mobile", "duplicate", 8) })
.unwrap();
assert!(core
.attach_host_ble(HostBleAttachment::new(first_io))
.is_ok());
assert!(core
.attach_host_ble(HostBleAttachment::new(duplicate_io))
.is_err());
core.detach_host_ble();
assert!(core.pending_host_ble.is_none());
assert!(!core.host_ble_attached);
let (replacement_io, _replacement_adapter) = core
.runtime
.block_on(async { HostBleIo::channel("mobile", "replacement", 8) })
.unwrap();
assert!(core
.attach_host_ble(HostBleAttachment::new(replacement_io))
.is_ok());
}
#[test]
fn host_ble_starts_without_configured_relays() {
let temp_dir = tempfile::TempDir::new().unwrap();
let mut core = AppCore::new(
flume::unbounded().0,
flume::unbounded().0,
temp_dir.path().to_string_lossy().to_string(),
Arc::new(RwLock::new(AppState::empty())),
);
core.create_account("Offline BLE");
core.preferences.nostr_relay_urls.clear();
core.logged_in.as_mut().unwrap().relay_urls.clear();
let (io, adapter) = core
.runtime
.block_on(async { HostBleIo::channel("mobile", "offline", 8) })
.unwrap();
core.attach_host_ble(HostBleAttachment::new(io).with_background(true))
.unwrap();
core.reconcile_device_sync();
let command = core.runtime.block_on(async {
tokio::time::timeout(std::time::Duration::from_secs(1), adapter.next_command())
.await
.ok()
.flatten()
});
assert!(
matches!(
command,
Some(fips_core::transport::ble::host::HostBleCommand::Listen { .. })
),
"command={command:?} log={:?}",
core.debug_log
);
assert!(core.host_ble_attached);
core.preferences.nearby_enabled = true;
core.preferences.nearby_bluetooth_enabled = true;
assert!(core.keep_background_ble());
let endpoint = core.device_sync_endpoint_for_test().unwrap();
core.handle_action(AppAction::AppBackgrounded);
core.prepare_for_suspend();
assert!(core.suspended);
assert!(core.host_ble_attached);
assert!(
Arc::ptr_eq(&endpoint, &core.device_sync_endpoint_for_test().unwrap()),
"suspension must retain the OS Bluetooth bridge and endpoint"
);
let retained_pubsub = core
.device_sync
.as_ref()
.unwrap()
.pubsub
.as_ref()
.unwrap()
.clone();
core.stop_device_sync_now();
assert_eq!(
retained_pubsub.active_subscription_count().unwrap(),
0,
"stopping Chat must close pubsub even when another owner retains it"
);
assert!(core
.runtime
.block_on(retained_pubsub.subscribe(vec![nostr::Filter::new()]))
.is_err());
}
#[test]
fn nearby_lan_changes_apply_when_host_ble_bridge_is_replaced() {
use fips_core::transport::ble::host::{HostBleCommand, HostBleEvent};
let directory = tempfile::TempDir::new().unwrap();
let mut core = AppCore::new(
flume::unbounded().0,
flume::unbounded().0,
directory.path().to_string_lossy().to_string(),
Arc::new(RwLock::new(AppState::empty())),
);
core.create_account("Nearby settings");
core.preferences.nostr_relay_urls.clear();
core.logged_in.as_mut().unwrap().relay_urls.clear();
core.preferences.nearby_enabled = true;
core.preferences.nearby_lan_enabled = false;
let mut previous_adapter = None;
for (generation, enabled) in [false, true, false].into_iter().enumerate() {
core.handle_action(AppAction::SetNearbyLanEnabled { enabled });
core.detach_host_ble();
drop(previous_adapter.take());
let (io, adapter) = core
.runtime
.block_on(async { HostBleIo::channel("mobile", format!("settings-{generation}"), 32) })
.unwrap();
core.attach_host_ble(HostBleAttachment::new(io)).unwrap();
core.reconcile_device_sync();
let endpoint = core.device_sync_endpoint_for_test().unwrap();
let addresses = core
.runtime
.block_on(endpoint.bound_udp_listen_addrs())
.unwrap();
assert_eq!(
!addresses.is_empty(),
enabled,
"LAN listener must match the requested setting after bridge replacement"
);
let command = core.runtime.block_on(async {
tokio::time::timeout(Duration::from_secs(2), adapter.next_command())
.await
.expect("fresh BLE listener command")
.expect("live BLE command channel")
});
let HostBleCommand::Listen { request_id, .. } = command else {
panic!("expected a fresh BLE listener, got {command:?}");
};
core.runtime
.block_on(adapter.emit(HostBleEvent::Listening {
request_id,
psm: 0x81,
}))
.unwrap();
assert!(
core.host_ble_attached,
"LAN changes must reattach Bluetooth"
);
previous_adapter = Some(adapter);
}
core.stop_device_sync_now();
}
#[test]
fn app_keys_event_received_over_fips_installs_peer_roster() {
let alice_dir = tempfile::TempDir::new().unwrap();
let mut alice = AppCore::new(
flume::unbounded().0,
flume::unbounded().0,
alice_dir.path().to_string_lossy().to_string(),
Arc::new(RwLock::new(AppState::empty())),
);
alice.create_account("Alice");
let alice_login = alice.logged_in.as_ref().unwrap();
let alice_owner = alice_login.owner_pubkey.to_hex();
let alice_device = alice_login.device_keys.public_key().to_hex();
let app_keys_event = alice
.pending_relay_publishes
.values()
.filter_map(|pending| serde_json::from_str::<Event>(&pending.event_json).ok())
.find(is_app_keys_event)
.expect("created account AppKeys event");
let payload = FipsNearbyPacket::event(
app_keys_event.id.to_string(),
serde_json::to_string(&app_keys_event).unwrap(),
)
.unwrap()
.encode()
.unwrap();
let bob_dir = tempfile::TempDir::new().unwrap();
let (bob_updates_tx, bob_updates_rx) = flume::unbounded();
let mut bob = AppCore::new(
bob_updates_tx,
flume::unbounded().0,
bob_dir.path().to_string_lossy().to_string(),
Arc::new(RwLock::new(AppState::empty())),
);
bob.create_account("Bob");
bob.fips_nearby_links = vec![crate::updates::FipsNearbyLinkSnapshot {
device_pubkey_hex: alice_device.clone(),
transport_type: "UDP".to_string(),
transport_addr: Some("192.168.1.25:7000".to_string()),
}];
while bob_updates_rx.try_recv().is_ok() {}
let previous_generation = bob.fips_connection_generation;
let links = bob.fips_nearby_links.clone();
bob.handle_fips_nearby_packet(&alice_device, FIPS_NEARBY_PORT, &payload);
reconnect_fixture_after_runtime_change(&mut bob, previous_generation, links);
assert_eq!(bob.debug_event_counters.app_keys_events, 1);
let roster = bob.app_keys.get(&alice_owner).expect("Alice roster");
assert!(roster
.devices
.iter()
.any(|device| device.identity_pubkey_hex == alice_device));
let peer_update = bob_updates_rx
.try_iter()
.filter_map(|update| match update {
AppUpdate::NearbyPeersChanged {
snapshot,
lan_peer_ids,
..
} => Some((snapshot, lan_peer_ids)),
_ => None,
})
.last()
.expect("FIPS peer projection update");
assert_eq!(peer_update.0.peers.len(), 1);
assert_eq!(
peer_update.0.peers[0].owner_pubkey_hex.as_deref(),
Some(alice_owner.as_str())
);
assert_eq!(peer_update.1, vec![alice_device]);
}
fn assert_fips_bootstrap_drains_queued_message(
alice_owner: PublicKey,
alice_device: &str,
bootstrap_payloads: Vec<Vec<u8>>,
) {
let bob_dir = tempfile::TempDir::new().unwrap();
let mut bob = AppCore::new(
flume::unbounded().0,
flume::unbounded().0,
bob_dir.path().to_string_lossy().to_string(),
Arc::new(RwLock::new(AppState::empty())),
);
bob.create_account("Bob");
let chat_id = alice_owner.to_hex();
bob.send_direct_message(&chat_id, "queued over FIPS", UnixSeconds(10), None);
let queued = bob
.threads
.get(&chat_id)
.and_then(|thread| thread.messages.first())
.expect("queued direct message");
assert_eq!(queued.delivery, DeliveryState::Queued);
assert!(queued.delivery_trace.outer_event_ids.is_empty());
for payload in bootstrap_payloads {
bob.handle_fips_nearby_packet(alice_device, FIPS_NEARBY_PORT, &payload);
}
assert_eq!(
bob.protocol_engine
.as_ref()
.expect("Bob protocol engine")
.direct_send_readiness(alice_owner),
DirectSendReadiness::Ready
);
let drained = bob
.threads
.get(&chat_id)
.and_then(|thread| thread.messages.first())
.expect("drained direct message");
assert_ne!(drained.delivery, DeliveryState::Queued);
assert!(!drained.delivery_trace.outer_event_ids.is_empty());
}
fn test_peer_bootstrap() -> (tempfile::TempDir, AppCore, PublicKey, String) {
let alice_dir = tempfile::TempDir::new().unwrap();
let mut alice = AppCore::new(
flume::unbounded().0,
flume::unbounded().0,
alice_dir.path().to_string_lossy().to_string(),
Arc::new(RwLock::new(AppState::empty())),
);
alice.create_account("Alice");
let alice_login = alice.logged_in.as_ref().unwrap();
let alice_owner = alice_login.owner_pubkey;
let alice_device = alice_login.device_keys.public_key().to_hex();
(alice_dir, alice, alice_owner, alice_device)
}
#[test]
fn linked_device_forwards_signed_identity_after_restart() {
let directory = tempfile::TempDir::new().unwrap();
let owner = Keys::generate();
let device = Keys::generate();
let roster = AppKeys::new(vec![DeviceEntry::new(device.public_key(), 1)])
.get_encrypted_event_at(&owner, 1)
.unwrap()
.sign_with_keys(&owner)
.unwrap();
let metadata = EventBuilder::new(
Kind::Metadata,
serde_json::json!({"name": "Linked Alice", "picture": "https://example.com/alice.png"})
.to_string(),
)
.custom_created_at(Timestamp::from_secs(1))
.sign_with_keys(&owner)
.unwrap();
let make_core = || {
AppCore::new(
flume::unbounded().0,
flume::unbounded().0,
directory.path().to_string_lossy().to_string(),
Arc::new(RwLock::new(AppState::empty())),
)
};
let mut linked = make_core();
linked.preferences.nostr_relay_urls.clear();
linked
.start_session(owner.public_key(), None, device.clone(), false, false)
.unwrap();
linked.handle_relay_event(roster.clone());
assert!(linked.apply_profile_metadata_event(&metadata));
linked.remember_event(metadata.id.to_hex());
let storage = linked.local_fips_identity_storage().unwrap();
storage.del("appcore/nearby-identity-v1/0").unwrap();
linked.handle_relay_event(metadata.clone());
let older_metadata = EventBuilder::new(Kind::Metadata, "{\"name\":\"Old Alice\"}")
.custom_created_at(Timestamp::from_secs(0))
.sign_with_keys(&owner)
.unwrap();
linked.handle_relay_event(older_metadata);
linked.pending_relay_publishes.clear();
linked.persist_best_effort();
drop(linked);
let mut linked = make_core();
linked.preferences.nostr_relay_urls.clear();
linked
.start_session(owner.public_key(), None, device.clone(), true, true)
.unwrap();
assert!(linked.logged_in.as_ref().unwrap().owner_keys.is_none());
let payloads = linked.local_fips_nearby_bootstrap_payloads();
let events = payloads
.iter()
.filter_map(|payload| match FipsNearbyPacket::decode(payload)? {
FipsNearbyPacket::Event { event_json, .. } => {
serde_json::from_str::<Event>(&event_json).ok()
}
_ => None,
})
.collect::<Vec<_>>();
assert!(
events.iter().any(|event| event.id == roster.id),
"linked device must forward the owner's signed device list"
);
assert!(
events.iter().any(|event| event.id == metadata.id),
"linked device must forward the owner's signed profile"
);
assert!(events.iter().all(|event| event.verify().is_ok()));
let receiver_dir = tempfile::TempDir::new().unwrap();
let (updates_tx, updates_rx) = flume::unbounded();
let mut receiver = AppCore::new(
updates_tx,
flume::unbounded().0,
receiver_dir.path().to_string_lossy().to_string(),
Arc::new(RwLock::new(AppState::empty())),
);
receiver.preferences.nostr_relay_urls.clear();
receiver.create_account("Receiver");
receiver.handle_internal(InternalEvent::FipsNearbyPeersChanged {
generation: receiver.fips_connection_generation,
peers: vec![crate::updates::FipsNearbyLinkSnapshot {
device_pubkey_hex: device.public_key().to_hex(),
transport_type: "UDP".to_string(),
transport_addr: Some("192.168.1.25:7000".to_string()),
}],
});
let previous_generation = receiver.fips_connection_generation;
let links = receiver.fips_nearby_links.clone();
for payload in &payloads {
receiver.handle_fips_nearby_packet(
&device.public_key().to_hex(),
FIPS_NEARBY_PORT,
payload,
);
}
reconnect_fixture_after_runtime_change(&mut receiver, previous_generation, links);
let snapshot = updates_rx
.try_iter()
.filter_map(|update| match update {
AppUpdate::NearbyPeersChanged { snapshot, .. } => Some(snapshot),
_ => None,
})
.last()
.unwrap();
assert_eq!(snapshot.peers[0].name, "Linked Alice");
assert_eq!(
snapshot.peers[0].picture_url.as_deref(),
Some("https://example.com/alice.png")
);
assert_eq!(
snapshot.peers[0].owner_pubkey_hex,
Some(owner.public_key().to_hex())
);
assert_fips_bootstrap_drains_queued_message(
owner.public_key(),
&device.public_key().to_hex(),
payloads,
);
}
#[test]
fn relay_identity_updates_refresh_an_existing_nearby_link() {
let (_alice_dir, alice, alice_owner, alice_device) = test_peer_bootstrap();
let bob_dir = tempfile::TempDir::new().unwrap();
let (updates_tx, updates_rx) = flume::unbounded();
let mut bob = AppCore::new(
updates_tx,
flume::unbounded().0,
bob_dir.path().to_string_lossy().to_string(),
Arc::new(RwLock::new(AppState::empty())),
);
bob.create_account("Bob");
bob.handle_internal(InternalEvent::FipsNearbyPeersChanged {
generation: bob.fips_connection_generation,
peers: vec![crate::updates::FipsNearbyLinkSnapshot {
device_pubkey_hex: alice_device.clone(),
transport_type: "BLE".to_string(),
transport_addr: Some("192.168.1.25:7000".to_string()),
}],
});
while updates_rx.try_recv().is_ok() {}
let app_keys = alice
.local_fips_nearby_bootstrap_payloads()
.into_iter()
.filter_map(|payload| FipsNearbyPacket::decode(&payload))
.filter_map(|packet| match packet {
FipsNearbyPacket::Event { event_json, .. } => {
serde_json::from_str::<Event>(&event_json).ok()
}
_ => None,
})
.find(is_app_keys_event)
.unwrap();
let previous_generation = bob.fips_connection_generation;
let links = bob.fips_nearby_links.clone();
bob.handle_relay_event(app_keys);
reconnect_fixture_after_runtime_change(&mut bob, previous_generation, links);
let identified = updates_rx
.try_iter()
.filter_map(|update| match update {
AppUpdate::NearbyPeersChanged { snapshot, .. } => Some(snapshot),
_ => None,
})
.last()
.expect("reconnected peer must resolve the relayed device list");
assert_eq!(
identified.peers[0].owner_pubkey_hex.as_deref(),
Some(alice_owner.to_hex().as_str())
);
let metadata = EventBuilder::new(
Kind::Metadata,
serde_json::json!({
"name": "Alice", "picture": "https://example.com/alice.jpg"
})
.to_string(),
)
.sign_with_keys(
alice
.logged_in
.as_ref()
.unwrap()
.owner_keys
.as_ref()
.unwrap(),
)
.unwrap();
bob.handle_relay_event(metadata);
let profiled = updates_rx
.try_iter()
.filter_map(|update| match update {
AppUpdate::NearbyPeersChanged { snapshot, .. } => Some(snapshot),
_ => None,
})
.last()
.expect("relay metadata must refresh the nearby name and photo");
assert_eq!(profiled.peers[0].id, alice_device);
assert_eq!(profiled.peers[0].name, "Alice");
assert_eq!(
profiled.peers[0].picture_url.as_deref(),
Some("https://example.com/alice.jpg")
);
}
#[test]
fn seen_peer_app_keys_recover_missing_protocol_roster() {
let (_alice_dir, alice, alice_owner, alice_device) = test_peer_bootstrap();
let bob_dir = tempfile::TempDir::new().unwrap();
let mut bob = AppCore::new(
flume::unbounded().0,
flume::unbounded().0,
bob_dir.path().to_string_lossy().to_string(),
Arc::new(RwLock::new(AppState::empty())),
);
bob.create_account("Bob");
let payloads = alice.local_fips_nearby_bootstrap_payloads();
for packet in payloads
.iter()
.filter_map(|payload| FipsNearbyPacket::decode(payload))
{
if let FipsNearbyPacket::Event {
event_id,
event_json,
..
} = packet
{
let event: Event = serde_json::from_str(&event_json).unwrap();
if is_app_keys_event(&event) {
bob.app_keys.insert(
alice_owner.to_hex(),
known_app_keys_from_ndr(
alice_owner,
&AppKeys::from_event(&event).unwrap(),
event.created_at.as_secs(),
),
);
bob.remember_event(event_id);
}
}
}
assert_eq!(
bob.protocol_engine
.as_ref()
.unwrap()
.direct_send_readiness(alice_owner),
DirectSendReadiness::MissingPeerAppKeys
);
bob.send_direct_message(
&alice_owner.to_hex(),
"waiting for identity",
unix_now(),
None,
);
assert_eq!(
bob.threads[&alice_owner.to_hex()].messages[0].delivery,
DeliveryState::Queued
);
for payload in &payloads {
bob.handle_fips_nearby_packet(&alice_device, FIPS_NEARBY_PORT, payload);
}
assert_eq!(
bob.protocol_engine
.as_ref()
.unwrap()
.direct_send_readiness(alice_owner),
DirectSendReadiness::Ready
);
assert!(!bob.threads[&alice_owner.to_hex()].messages[0]
.delivery_trace
.outer_event_ids
.is_empty());
let applied = bob.debug_event_counters.app_keys_events;
for payload in payloads {
bob.handle_fips_nearby_packet(&alice_device, FIPS_NEARBY_PORT, &payload);
}
assert_eq!(
bob.debug_event_counters.app_keys_events, applied,
"duplicates should be skipped again after the signed roster is recovered"
);
}
#[test]
fn exact_fips_bootstrap_makes_peer_ready_and_drains_queued_direct_message() {
let (_alice_dir, alice, alice_owner, alice_device) = test_peer_bootstrap();
let bootstrap_payloads = alice.local_fips_nearby_bootstrap_payloads();
assert_fips_bootstrap_drains_queued_message(alice_owner, &alice_device, bootstrap_payloads);
}
#[test]
fn unchanged_nearby_identity_does_not_restart_bootstrap_exchange() {
let (_alice_dir, mut alice, alice_owner, alice_device) = test_peer_bootstrap();
let (_bob_dir, mut bob, _, bob_device) = test_peer_bootstrap();
let alice_payloads = alice.local_fips_nearby_bootstrap_payloads();
let bob_payloads = bob.local_fips_nearby_bootstrap_payloads();
let mut settled_counts = None;
for _ in 0..6 {
for payload in &alice_payloads {
bob.handle_fips_nearby_packet(&alice_device, FIPS_NEARBY_PORT, payload);
}
for payload in &bob_payloads {
alice.handle_fips_nearby_packet(&bob_device, FIPS_NEARBY_PORT, payload);
}
assert!(
alice.local_fips_nearby_bootstrap_payloads() == alice_payloads,
"receiving a peer's identity must not create a fresh local announcement"
);
assert!(bob.local_fips_nearby_bootstrap_payloads() == bob_payloads);
let counts = (
alice.debug_event_counters.app_keys_events,
bob.debug_event_counters.app_keys_events,
);
if let Some(settled) = settled_counts {
assert_eq!(
counts, settled,
"idle peers must stop reapplying device lists"
);
}
settled_counts = Some(counts);
}
let profile = alice.owner_profiles.get_mut(&alice_owner.to_hex()).unwrap();
profile.name = Some("Updated Alice".into());
profile.updated_at_secs += 1;
let changed = alice.local_fips_nearby_bootstrap_payloads();
assert!(
changed != alice_payloads,
"real profile changes must propagate"
);
assert!(alice.local_fips_nearby_bootstrap_payloads() == changed);
let roster = alice.app_keys.get_mut(&alice_owner.to_hex()).unwrap();
roster.created_at_secs += 1;
roster.devices.push(KnownAppKeyDevice {
identity_pubkey_hex: Keys::generate().public_key().to_hex(),
created_at_secs: roster.created_at_secs,
device_label: None,
client_label: None,
label_updated_at_secs: 0,
});
let linked = alice.local_fips_nearby_bootstrap_payloads();
assert!(linked != changed, "new devices must propagate");
assert!(alice.local_fips_nearby_bootstrap_payloads() == linked);
alice.current_device_labels = Some(CurrentDeviceLabels {
device_label: Some("New phone".into()),
client_label: None,
});
alice.defer_owner_app_keys_publish = true;
let restored = alice.local_fips_nearby_bootstrap_payloads();
assert!(restored != linked);
assert!(
alice.local_fips_nearby_bootstrap_payloads() == restored,
"restoring offline must also reuse its encrypted announcement"
);
}
#[test]
fn reordered_fips_bootstrap_makes_peer_ready_and_drains_queued_direct_message() {
let (_alice_dir, alice, alice_owner, alice_device) = test_peer_bootstrap();
let mut bootstrap_payloads = alice.local_fips_nearby_bootstrap_payloads();
bootstrap_payloads.reverse();
assert_fips_bootstrap_drains_queued_message(alice_owner, &alice_device, bootstrap_payloads);
}
#[test]
fn restored_primary_offline_fips_bootstrap_makes_peer_ready_and_drains_queued_message() {
let alice_dir = tempfile::TempDir::new().unwrap();
let mut alice = AppCore::new(
flume::unbounded().0,
flume::unbounded().0,
alice_dir.path().to_string_lossy().to_string(),
Arc::new(RwLock::new(AppState::empty())),
);
let alice_owner_keys = Keys::generate();
let alice_owner = alice_owner_keys.public_key();
let alice_device_keys = Keys::generate();
let alice_device = alice_device_keys.public_key().to_hex();
alice.preferences.nostr_relay_urls.clear();
alice
.start_primary_session(alice_owner_keys, alice_device_keys, true, false)
.expect("restored primary session");
assert!(alice.defer_owner_app_keys_publish);
assert_fips_bootstrap_drains_queued_message(
alice_owner,
&alice_device,
alice.local_fips_nearby_bootstrap_payloads(),
);
}
#[test]
fn local_identity_bootstrap_survives_successful_relay_publish() {
let temp_dir = tempfile::TempDir::new().unwrap();
let mut core = AppCore::new(
flume::unbounded().0,
flume::unbounded().0,
temp_dir.path().to_string_lossy().to_string(),
Arc::new(RwLock::new(AppState::empty())),
);
core.create_account("Alice");
core.pending_relay_publishes.clear();
let events = core
.local_fips_nearby_bootstrap_payloads()
.into_iter()
.filter_map(|payload| FipsNearbyPacket::decode(&payload))
.filter_map(|packet| match packet {
FipsNearbyPacket::Event { event_json, .. } => {
serde_json::from_str::<Event>(&event_json).ok()
}
FipsNearbyPacket::Receipt { .. } => None,
})
.collect::<Vec<_>>();
assert!(events.iter().any(is_app_keys_event));
assert!(events
.iter()
.any(|event| event.kind.as_u16() as u32 == INVITE_EVENT_KIND));
assert!(events.iter().all(|event| event.verify().is_ok()));
}