1use super::{lock_shared_state, OfflineProtocol};
5use crate::protocol::types::MAX_BLOCKED_USERS;
6use crate::{Error, Event, Result};
7use offline_protocol_core::UserId;
8use tracing::{debug, info};
9
10impl OfflineProtocol {
11 pub fn block_user(&mut self, user_id: &str) -> Result<()> {
19 let _ = UserId::new(user_id)
21 .map_err(|_| Error::InvalidConfiguration(format!("Invalid user ID: {}", user_id)))?;
22
23 if user_id == self.local_id {
25 return Err(Error::InvalidConfiguration(
26 "Cannot block own user ID".to_string(),
27 ));
28 }
29
30 if self.blocked_users.len() >= MAX_BLOCKED_USERS && !self.blocked_users.contains(user_id) {
32 return Err(Error::InvalidConfiguration(format!(
33 "Blocked users limit reached ({})",
34 MAX_BLOCKED_USERS
35 )));
36 }
37
38 if !self.blocked_users.insert(user_id.to_string()) {
39 debug!(user_id = %user_id, "User already blocked");
41 return Ok(());
42 }
43
44 self.persist_blocked_user(user_id);
45
46 let discarded = self.discard_pending_decryption_for_peer(user_id);
55 if discarded > 0 {
56 debug!(
57 user_id = %user_id,
58 count = discarded,
59 "Discarded pending decryption queue for blocked user"
60 );
61 }
62
63 info!(user_id = %user_id, "User blocked");
64
65 if let Ok(state) = lock_shared_state(&self.shared_state) {
66 state.emit_event(Event::user_blocked(user_id.to_string()));
67 }
68
69 Ok(())
70 }
71
72 pub fn unblock_user(&mut self, user_id: &str) -> Result<()> {
80 let _ = UserId::new(user_id)
82 .map_err(|_| Error::InvalidConfiguration(format!("Invalid user ID: {}", user_id)))?;
83
84 if !self.blocked_users.remove(user_id) {
85 debug!(user_id = %user_id, "User was not blocked");
87 return Ok(());
88 }
89
90 self.delete_blocked_user(user_id);
91 self.cleanup_peer_session_state(user_id);
92
93 if self.known_peers.contains_key(user_id)
97 && self.config.encryption.enabled
98 && self.config.encryption.auto_key_exchange
99 && self.mls_manager.is_some()
100 {
101 if let Err(e) = self.send_key_package_to(user_id, true) {
102 debug!(user_id = %user_id, error = %e, "Failed to send key package after unblock (peer may reconnect later)");
103 }
104 }
105
106 info!(user_id = %user_id, "User unblocked");
107
108 if let Ok(state) = lock_shared_state(&self.shared_state) {
109 state.emit_event(Event::user_unblocked(user_id.to_string()));
110 }
111
112 Ok(())
113 }
114
115 fn cleanup_peer_session_state(&mut self, user_id: &str) {
121 if self.mls_manager.is_some() {
126 if let Err(e) = self.manual_mls_delete_session(user_id) {
127 debug!(user_id = %user_id, error = %e, "No MLS session to clean up for unblocked user");
129 }
130 }
131
132 if self.pending_key_packages.remove(user_id).is_some() {
136 debug!(user_id = %user_id, "Cleared pending key package for unblocked user");
137 }
138 self.delete_peer_key_package_from_storage(user_id);
139 self.peer_compact_envelope.remove(user_id);
140 self.peer_ctrl_freshness.remove(user_id);
147 self.peer_rich_payload.remove(user_id);
148 self.peer_rich_attested.remove(user_id);
149 self.forget_data_sync_peer(user_id);
150 self.transport_manager.clear_peer_nostr_pubkey(user_id);
158 self.delete_peer_capabilities_from_storage(user_id);
159
160 if self.drop_pending_queue_for_peer(user_id, "Peer session state reset on unblock") > 0 {
163 debug!(user_id = %user_id, "Cleared pending encrypted messages for unblocked user");
164 }
165
166 let drained = self.discard_pending_decryption_for_peer(user_id);
169 if drained > 0 {
170 debug!(
171 user_id = %user_id,
172 count = drained,
173 "Drained pending decryption queue for unblocked user"
174 );
175 }
176
177 let file_ids_to_cancel: Vec<String> = self
180 .pending_media_metadata
181 .iter()
182 .filter(|(_, entry)| entry.sender == user_id)
183 .map(|(file_id, _)| file_id.clone())
184 .collect();
185 for file_id in &file_ids_to_cancel {
186 self.file_transfer_manager.cancel_transfer(file_id);
187 if let Some(purpose) = self
198 .pending_media_metadata
199 .remove(file_id)
200 .and_then(|entry| entry.data_purpose)
201 {
202 self.report_data_media_transfer_failure(user_id, &purpose, "cancelled");
203 }
204 }
205 if !file_ids_to_cancel.is_empty() {
206 debug!(
207 user_id = %user_id,
208 count = file_ids_to_cancel.len(),
209 "Cancelled in-progress file transfers for unblocked user"
210 );
211 }
212
213 self.key_package_sent_to.remove(user_id);
215 }
216
217 pub fn get_blocked_users(&self) -> Vec<String> {
219 let mut users: Vec<String> = self.blocked_users.iter().cloned().collect();
220 users.sort();
221 users
222 }
223
224 pub fn is_user_blocked(&self, user_id: &str) -> bool {
226 self.blocked_users.contains(user_id)
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use crate::mls::InMemoryStorage;
233 use crate::test_identity::id;
234 use crate::ProtocolConfig;
235 use offline_protocol_transport::Transport;
236 use std::sync::Arc;
237
238 fn make_protocol(user_id: &str) -> crate::OfflineProtocol {
239 let mut config = ProtocolConfig::new("test-app", user_id);
240 config.encryption.require_encryption = false;
244 crate::OfflineProtocol::new(config).unwrap()
245 }
246
247 #[test]
248 fn test_block_unblock_roundtrip() {
249 let mut proto = make_protocol("alice");
250 assert!(!proto.is_user_blocked("bob"));
251 assert!(proto.get_blocked_users().is_empty());
252
253 proto.block_user("bob").unwrap();
254 assert!(proto.is_user_blocked("bob"));
255 let blocked = proto.get_blocked_users();
256 assert_eq!(blocked.len(), 1);
257 assert!(blocked.contains(&"bob".to_string()));
258
259 proto.unblock_user("bob").unwrap();
260 assert!(!proto.is_user_blocked("bob"));
261 assert!(proto.get_blocked_users().is_empty());
262 }
263
264 #[test]
265 fn test_block_self_rejected() {
266 let mut proto = make_protocol("alice");
267 let result = proto.block_user("alice");
268 assert!(result.is_err());
269 assert!(result.unwrap_err().to_string().contains("Cannot block own"));
270 }
271
272 #[test]
273 fn test_block_invalid_user_id() {
274 let mut proto = make_protocol("alice");
275 let result = proto.block_user("");
276 assert!(result.is_err());
277 }
278
279 #[test]
280 fn test_block_idempotent() {
281 let mut proto = make_protocol("alice");
282 proto.block_user("bob").unwrap();
283 proto.block_user("bob").unwrap(); assert!(proto.is_user_blocked("bob"));
285 }
286
287 #[test]
288 fn test_unblock_not_blocked() {
289 let mut proto = make_protocol("alice");
290 proto.unblock_user("bob").unwrap(); }
292
293 #[test]
294 fn test_blocked_user_persistence() {
295 let mut proto = make_protocol("alice");
296 let storage = Arc::new(InMemoryStorage::new());
297 proto.enable_message_persistence_for_test(storage).unwrap();
298
299 proto.block_user("bob").unwrap();
300 assert!(proto.is_user_blocked("bob"));
301
302 proto.blocked_users.clear();
304 assert!(!proto.is_user_blocked("bob"));
305
306 proto.restore_blocked_users().unwrap();
307 assert!(proto.is_user_blocked("bob"));
308 }
309
310 #[test]
311 fn test_receive_drops_blocked_sender() {
312 use offline_protocol_core::{AppId, Message, UserId};
313 use offline_protocol_transport::{mock::MockTransport, TransportType};
314
315 let mut proto = make_protocol("alice");
316 proto.block_user("mallory").unwrap();
317
318 let mock = MockTransport::new(TransportType::BLE);
319 mock.start().unwrap();
320
321 let msg = Message::new(
323 UserId::new("mallory").unwrap(),
324 UserId::new("alice").unwrap(),
325 AppId::new("test-app").unwrap(),
326 "you shouldn't see this",
327 );
328 mock.queue_message(msg);
329
330 proto
331 .transport_manager_mut()
332 .add_transport(TransportType::BLE, Box::new(mock));
333 proto.start().unwrap();
334
335 assert!(proto.receive_message().is_none());
337 }
338
339 #[test]
340 fn test_relay_continues_for_blocked_user() {
341 use crate::events::Event;
342 use offline_protocol_core::{AppId, Message, UserId};
343 use offline_protocol_transport::{mock::MockTransport, TransportType};
344 use std::sync::{Arc, Mutex};
345
346 let mut config = ProtocolConfig::new("test-app", "alice");
347 config.encryption.require_encryption = false;
348 config.mesh_relay.jitter_min = std::time::Duration::from_millis(0);
350 config.mesh_relay.jitter_max = std::time::Duration::from_millis(0);
351 let mut proto = crate::OfflineProtocol::new(config).unwrap();
352 proto.block_user("mallory").unwrap();
353
354 let relay_events = Arc::new(Mutex::new(Vec::<Event>::new()));
355 let relay_events_clone = relay_events.clone();
356 proto.on_event(move |event| {
357 if matches!(event, Event::MessageRelayed { .. }) {
358 relay_events_clone.lock().unwrap().push(event);
359 }
360 });
361
362 let mock = MockTransport::new(TransportType::BLE);
363 mock.start().unwrap();
364 mock.add_connected_peer("dave", -55);
368
369 let msg = Message::new(
372 UserId::new("mallory").unwrap(),
373 UserId::new("charlie").unwrap(),
374 AppId::new("test-app").unwrap(),
375 "relay this",
376 );
377 mock.queue_message(msg);
378
379 proto
380 .transport_manager_mut()
381 .add_transport(TransportType::BLE, Box::new(mock));
382 proto.start().unwrap();
383
384 let received = proto.receive_message();
388 assert!(
389 received.is_none(),
390 "Relay messages for third parties should be forwarded, not returned"
391 );
392 proto.process().unwrap();
393
394 let events = relay_events.lock().unwrap();
397 assert_eq!(
398 events.len(),
399 1,
400 "Blocked-user relay to third party must emit MessageRelayed"
401 );
402 match &events[0] {
403 Event::MessageRelayed {
404 sender, recipient, ..
405 } => {
406 assert_eq!(sender, "mallory");
407 assert_eq!(recipient, "charlie");
408 }
409 _ => panic!("Expected MessageRelayed event"),
410 }
411 }
412
413 #[test]
414 fn test_unblock_clears_session_state() {
415 use crate::mls::InMemoryStorage as MlsInMemoryStorage;
416 use offline_protocol_core::{AppId, Message, UserId};
417
418 let mut proto = make_protocol("alice");
419 let storage = Arc::new(MlsInMemoryStorage::new());
420 proto.initialize_mls_for_test(storage).unwrap();
421
422 use crate::protocol::types::ReceivedKeyPackage;
423
424 proto.pending_key_packages.insert(
426 id("bob"),
427 ReceivedKeyPackage {
428 key_package_data: vec![0x01, 0x02],
429 local_expires_at_ms: u64::MAX,
430 },
431 );
432 proto.key_package_sent_to.insert(id("bob"));
433
434 let pending_msg = Message::new(
436 UserId::new(&id("alice")).unwrap(),
437 UserId::new(&id("bob")).unwrap(),
438 AppId::new("test-app").unwrap(),
439 "queued",
440 );
441 proto.enqueue_pending_decryption(&id("bob"), &pending_msg);
442
443 proto.block_user(&id("bob")).unwrap();
445 proto.unblock_user(&id("bob")).unwrap();
446
447 assert!(proto.pending_key_packages.get(&id("bob")).is_none());
448 assert!(!proto.key_package_sent_to.contains(&id("bob")));
449 assert!(!proto.confirmed_sessions.contains(&id("bob")));
450 assert!(proto
451 .pending_queue
452 .drain_for_peer(&proto.config.encryption.pending_queue, &id("bob"))
453 .is_empty());
454 }
455
456 #[test]
457 fn test_unblock_then_receive() {
458 use offline_protocol_core::{AppId, Message, UserId};
459 use offline_protocol_transport::{mock::MockTransport, TransportType};
460
461 let mut proto = make_protocol("alice");
462 proto.block_user("bob").unwrap();
463 proto.unblock_user("bob").unwrap();
464
465 let mock = MockTransport::new(TransportType::BLE);
466 mock.start().unwrap();
467
468 let msg = Message::new(
469 UserId::new("bob").unwrap(),
470 UserId::new("alice").unwrap(),
471 AppId::new("test-app").unwrap(),
472 "hello again",
473 );
474 let msg_id = msg.id.clone();
475 mock.queue_message(msg);
476
477 proto
478 .transport_manager_mut()
479 .add_transport(TransportType::BLE, Box::new(mock));
480 proto.start().unwrap();
481
482 let received = proto.receive_message();
484 assert!(
485 received.is_some(),
486 "Messages should be received after unblocking"
487 );
488 assert_eq!(received.unwrap().id, msg_id);
489 }
490
491 #[test]
492 fn test_send_to_blocked_user_rejected() {
493 use offline_protocol_transport::{mock::MockTransport, TransportType};
494
495 let mut proto = make_protocol("alice");
496
497 let mock = MockTransport::new(TransportType::BLE);
498 mock.start().unwrap();
499 proto
500 .transport_manager_mut()
501 .add_transport(TransportType::BLE, Box::new(mock));
502 proto.start().unwrap();
503
504 proto.block_user("mallory").unwrap();
505
506 let result = proto.send_message("mallory", "hello", None, None::<String>);
507 assert!(matches!(
508 result,
509 Err(crate::Error::UserBlocked(ref id)) if id == "mallory"
510 ));
511 }
512
513 #[test]
514 fn test_send_via_transport_to_blocked_user_rejected() {
515 use offline_protocol_transport::{mock::MockTransport, TransportType};
516
517 let mut proto = make_protocol("alice");
518
519 let mock = MockTransport::new(TransportType::BLE);
520 mock.start().unwrap();
521 proto
522 .transport_manager_mut()
523 .add_transport(TransportType::BLE, Box::new(mock));
524 proto.start().unwrap();
525
526 proto.block_user("mallory").unwrap();
527
528 let result = proto.send_message_via_transport(
529 "mallory",
530 "hello",
531 None,
532 TransportType::BLE,
533 None::<String>,
534 );
535 assert!(matches!(
536 result,
537 Err(crate::Error::UserBlocked(ref id)) if id == "mallory"
538 ));
539 }
540
541 #[test]
542 fn test_send_media_to_blocked_user_rejected() {
543 use offline_protocol_core::ContentType;
544 use offline_protocol_transport::{mock::MockTransport, TransportType};
545
546 let mut proto = make_protocol("alice");
547
548 let mock = MockTransport::new(TransportType::BLE);
549 mock.start().unwrap();
550 proto
551 .transport_manager_mut()
552 .add_transport(TransportType::BLE, Box::new(mock));
553 proto.start().unwrap();
554
555 proto.block_user("mallory").unwrap();
556
557 let result = proto.send_media(
558 "mallory",
559 vec![0u8; 100],
560 "photo.jpg",
561 ContentType::Image,
562 None,
563 );
564 assert!(matches!(
565 result,
566 Err(crate::Error::UserBlocked(ref id)) if id == "mallory"
567 ));
568 }
569
570 #[test]
571 fn test_pending_decryption_drops_blocked_sender() {
572 use offline_protocol_core::{AppId, Message, UserId};
573
574 let mut proto = make_protocol("alice");
575
576 let msg = Message::new(
578 UserId::new("bob").unwrap(),
579 UserId::new("alice").unwrap(),
580 AppId::new("test-app").unwrap(),
581 "secret",
582 );
583 proto.enqueue_pending_decryption("bob", &msg);
584
585 proto.block_user("bob").unwrap();
587
588 proto.process_pending_decryption("bob");
590
591 let state = crate::protocol::lock_shared_state(&proto.shared_state).unwrap();
593 assert!(
594 state.received_messages.is_empty(),
595 "Pending messages from blocked user should not be surfaced"
596 );
597 }
598
599 #[test]
600 fn test_duplicate_from_blocked_user_no_ack() {
601 use offline_protocol_core::{AppId, Message, UserId};
602 use offline_protocol_transport::{mock::MockTransport, TransportType};
603
604 let mut proto = make_protocol("alice");
605
606 let mock = MockTransport::new(TransportType::BLE);
607 mock.start().unwrap();
608
609 let msg = Message::new(
611 UserId::new("mallory").unwrap(),
612 UserId::new("alice").unwrap(),
613 AppId::new("test-app").unwrap(),
614 "first message",
615 );
616 let msg_clone = msg.clone();
617 mock.queue_message(msg);
618
619 proto
620 .transport_manager_mut()
621 .add_transport(TransportType::BLE, Box::new(mock));
622 proto.start().unwrap();
623
624 let received = proto.receive_message();
626 assert!(received.is_some());
627
628 proto.block_user("mallory").unwrap();
630
631 let mock2 = MockTransport::new(TransportType::BLE);
634 mock2.start().unwrap();
635 mock2.queue_message(msg_clone);
636 proto
637 .transport_manager_mut()
638 .add_transport(TransportType::BLE, Box::new(mock2));
639
640 assert!(proto.receive_message().is_none());
642 }
643
644 #[test]
645 fn test_get_blocked_users_sorted() {
646 let mut proto = make_protocol("alice");
647 proto.block_user("charlie").unwrap();
648 proto.block_user("bob").unwrap();
649 proto.block_user("dave").unwrap();
650
651 let blocked = proto.get_blocked_users();
652 assert_eq!(blocked, vec!["bob", "charlie", "dave"]);
653 }
654
655 #[test]
656 fn test_presence_to_blocked_user_rejected() {
657 use crate::events::PresenceStatus;
658 use offline_protocol_transport::{mock::MockTransport, TransportType};
659
660 let mut proto = make_protocol("alice");
661
662 let mock = MockTransport::new(TransportType::BLE);
663 mock.start().unwrap();
664 proto
665 .transport_manager_mut()
666 .add_transport(TransportType::BLE, Box::new(mock));
667 proto.start().unwrap();
668
669 proto.block_user("mallory").unwrap();
670
671 let result = proto.send_presence_update("mallory", PresenceStatus::Online);
672 assert!(matches!(
673 result,
674 Err(crate::Error::UserBlocked(ref id)) if id == "mallory"
675 ));
676 }
677
678 #[test]
679 fn test_typing_indicator_to_blocked_user_rejected() {
680 use offline_protocol_transport::{mock::MockTransport, TransportType};
681
682 let mut proto = make_protocol("alice");
683
684 let mock = MockTransport::new(TransportType::BLE);
685 mock.start().unwrap();
686 proto
687 .transport_manager_mut()
688 .add_transport(TransportType::BLE, Box::new(mock));
689 proto.start().unwrap();
690
691 proto.block_user("mallory").unwrap();
692
693 let result = proto.send_typing_indicator("mallory", "conv-1", true);
694 assert!(matches!(
695 result,
696 Err(crate::Error::UserBlocked(ref id)) if id == "mallory"
697 ));
698 }
699
700 #[test]
701 fn test_read_receipt_to_blocked_user_rejected() {
702 use offline_protocol_transport::{mock::MockTransport, TransportType};
703
704 let mut proto = make_protocol("alice");
705
706 let mock = MockTransport::new(TransportType::BLE);
707 mock.start().unwrap();
708 proto
709 .transport_manager_mut()
710 .add_transport(TransportType::BLE, Box::new(mock));
711 proto.start().unwrap();
712
713 proto.block_user("mallory").unwrap();
714
715 let result = proto.send_read_receipt("mallory", vec!["msg-1".to_string()]);
716 assert!(matches!(
717 result,
718 Err(crate::Error::UserBlocked(ref id)) if id == "mallory"
719 ));
720 }
721
722 #[test]
723 fn test_block_cap_enforced() {
724 use crate::protocol::types::MAX_BLOCKED_USERS;
725
726 let mut proto = make_protocol("alice");
727 for i in 0..MAX_BLOCKED_USERS {
728 proto.block_user(&format!("user-{i}")).unwrap();
729 }
730 assert_eq!(proto.blocked_users.len(), MAX_BLOCKED_USERS);
731
732 let result = proto.block_user("one-too-many");
734 assert!(result.is_err());
735 assert!(result
736 .unwrap_err()
737 .to_string()
738 .contains("Blocked users limit reached"));
739
740 proto.block_user("user-0").unwrap();
742 }
743
744 #[test]
745 fn test_on_neighbor_discovered_skips_blocked_user() {
746 let mut proto = make_protocol("alice");
747 proto.block_user("mallory").unwrap();
748
749 proto.on_neighbor_discovered("mallory");
752 assert!(
753 !proto.known_peers.contains_key("mallory"),
754 "Blocked user should not be added to known_peers"
755 );
756 assert!(
757 !proto.key_package_sent_to.contains("mallory"),
758 "Blocked user should not trigger key package exchange"
759 );
760
761 proto.on_neighbor_discovered("bob");
763 assert!(
764 proto.known_peers.contains_key("bob"),
765 "Non-blocked peer should be tracked"
766 );
767 }
768
769 #[test]
770 fn test_cleanup_cancels_inbound_file_transfers() {
771 use crate::file_transfer::FileChunk;
772 use offline_protocol_core::ContentType;
773
774 let mut proto = make_protocol("alice");
775
776 let chunk = FileChunk {
779 file_id: "file-from-bob".to_string(),
780 file_name: "photo.jpg".to_string(),
781 file_size: 1000,
782 total_chunks: 10,
783 chunk_index: 0,
784 chunk_data: vec![0u8; 100],
785 file_checksum: "abc123".to_string(),
786 };
787 proto
788 .file_transfer_manager
789 .process_chunk("bob", chunk)
790 .unwrap();
791
792 use crate::protocol::types::PendingMediaMetadataEntry;
793 use std::time::Instant;
794 proto.pending_media_metadata.insert(
795 "file-from-bob".to_string(),
796 PendingMediaMetadataEntry {
797 data_purpose: None,
798 content_type: ContentType::Image,
799 media_metadata: None,
800 last_updated_at: Instant::now(),
801 sender: "bob".to_string(),
802 rich_extras: None,
803 timestamp_ms: 0,
804 },
805 );
806
807 proto.block_user("bob").unwrap();
809 proto.unblock_user("bob").unwrap();
810
811 assert!(
812 proto
813 .file_transfer_manager
814 .get_progress("file-from-bob")
815 .is_none(),
816 "Inbound file transfer should be cancelled on unblock cleanup"
817 );
818 assert!(
819 !proto.pending_media_metadata.contains_key("file-from-bob"),
820 "Pending media metadata should be removed on unblock cleanup"
821 );
822 }
823
824 #[test]
825 fn test_rejected_file_chunk_leaves_no_state() {
826 use crate::file_transfer::FileChunk;
827 use offline_protocol_core::{AppId, Message, UserId};
828
829 let mut proto = make_protocol("alice");
830
831 let chunk_message = |chunk: &FileChunk| {
832 Message::new(
833 UserId::new("bob").unwrap(),
834 UserId::new("alice").unwrap(),
835 AppId::new("test-app").unwrap(),
836 chunk.to_json().unwrap(),
837 )
838 };
839
840 let good = FileChunk {
842 file_id: "good-file".to_string(),
843 file_name: "photo.jpg".to_string(),
844 file_size: 11,
845 total_chunks: 2,
846 chunk_index: 0,
847 chunk_data: vec![1u8; 6],
848 file_checksum: "abc".to_string(),
849 };
850 let _ = proto.handle_incoming_file_chunk(&chunk_message(&good));
851 assert!(proto.pending_media_metadata.contains_key("good-file"));
852 assert_eq!(proto.file_transfer_manager.active_transfer_count(), 1);
853
854 let evil = FileChunk {
857 file_id: "evil-file".to_string(),
858 file_name: "evil.bin".to_string(),
859 file_size: u64::MAX,
860 total_chunks: 1,
861 chunk_index: 0,
862 chunk_data: vec![0u8; 16],
863 file_checksum: "def".to_string(),
864 };
865 let _ = proto.handle_incoming_file_chunk(&chunk_message(&evil));
866 assert!(!proto.pending_media_metadata.contains_key("evil-file"));
867 assert_eq!(proto.file_transfer_manager.active_transfer_count(), 1);
868 }
869
870 #[test]
871 fn test_resource_rejected_file_chunk_emits_receive_failed_event() {
872 use crate::events::Event;
873 use crate::file_transfer::FileChunk;
874 use offline_protocol_core::{AppId, Message, UserId};
875 use std::sync::{Arc, Mutex};
876
877 let mut proto = make_protocol("alice");
878 proto.file_transfer_manager = crate::file_transfer::FileTransferManager::with_config(
879 crate::file_transfer::FileTransferConfig {
880 max_concurrent_assemblies: 1,
881 ..crate::file_transfer::FileTransferConfig::default()
882 },
883 );
884
885 let failed_events = Arc::new(Mutex::new(Vec::<Event>::new()));
886 let failed_events_clone = failed_events.clone();
887 proto.on_event(move |event| {
888 if matches!(event, Event::FileReceiveFailed { .. }) {
889 failed_events_clone.lock().unwrap().push(event);
890 }
891 });
892
893 let chunk_message = |chunk: &FileChunk| {
894 Message::new(
895 UserId::new("bob").unwrap(),
896 UserId::new("alice").unwrap(),
897 AppId::new("test-app").unwrap(),
898 chunk.to_json().unwrap(),
899 )
900 };
901 let make_chunk = |file_id: &str| FileChunk {
902 file_id: file_id.to_string(),
903 file_name: "photo.jpg".to_string(),
904 file_size: 1000,
905 total_chunks: 10,
906 chunk_index: 0,
907 chunk_data: vec![0u8; 100],
908 file_checksum: "abc".to_string(),
909 };
910
911 let _ = proto.handle_incoming_file_chunk(&chunk_message(&make_chunk("file-1")));
913 assert!(failed_events.lock().unwrap().is_empty());
914
915 let _ = proto.handle_incoming_file_chunk(&chunk_message(&make_chunk("file-2")));
919 let events = failed_events.lock().unwrap();
920 assert_eq!(events.len(), 1);
921 match &events[0] {
922 Event::FileReceiveFailed {
923 file_id,
924 file_name,
925 sender,
926 reason,
927 } => {
928 assert_eq!(file_id, "file-2");
929 assert_eq!(file_name, "photo.jpg");
930 assert_eq!(sender, "bob");
931 assert_eq!(reason, "too_many_transfers");
932 }
933 other => panic!("unexpected event: {:?}", other),
934 }
935 }
936
937 #[test]
938 fn test_resource_rejected_transfer_emits_single_failed_event() {
939 use crate::events::Event;
940 use crate::file_transfer::FileChunk;
941 use offline_protocol_core::{AppId, Message, UserId};
942 use std::sync::{Arc, Mutex};
943
944 let mut proto = make_protocol("alice");
945 proto.file_transfer_manager = crate::file_transfer::FileTransferManager::with_config(
946 crate::file_transfer::FileTransferConfig {
947 max_concurrent_assemblies: 1,
948 ..crate::file_transfer::FileTransferConfig::default()
949 },
950 );
951
952 let failed_events = Arc::new(Mutex::new(Vec::<Event>::new()));
953 let failed_events_clone = failed_events.clone();
954 proto.on_event(move |event| {
955 if matches!(event, Event::FileReceiveFailed { .. }) {
956 failed_events_clone.lock().unwrap().push(event);
957 }
958 });
959
960 let chunk_message = |chunk: &FileChunk| {
961 Message::new(
962 UserId::new("bob").unwrap(),
963 UserId::new("alice").unwrap(),
964 AppId::new("test-app").unwrap(),
965 chunk.to_json().unwrap(),
966 )
967 };
968 let make_chunk = |file_id: &str, index: u32| FileChunk {
969 file_id: file_id.to_string(),
970 file_name: "photo.jpg".to_string(),
971 file_size: 1000,
972 total_chunks: 10,
973 chunk_index: index,
974 chunk_data: vec![0u8; 100],
975 file_checksum: "abc".to_string(),
976 };
977
978 let _ = proto.handle_incoming_file_chunk(&chunk_message(&make_chunk("file-1", 0)));
980
981 for index in 0..6u32 {
985 let _ = proto.handle_incoming_file_chunk(&chunk_message(&make_chunk("file-2", index)));
986 }
987
988 let events = failed_events.lock().unwrap();
989 assert_eq!(events.len(), 1);
990 match &events[0] {
991 Event::FileReceiveFailed {
992 file_id, reason, ..
993 } => {
994 assert_eq!(file_id, "file-2");
995 assert_eq!(reason, "too_many_transfers");
996 }
997 other => panic!("unexpected event: {:?}", other),
998 }
999 assert_eq!(proto.file_transfer_manager.active_transfer_count(), 1);
1001 assert!(proto.file_transfer_manager.get_progress("file-2").is_none());
1002 }
1003
1004 #[test]
1005 fn test_budget_dropped_transfer_emits_no_second_stale_event() {
1006 use crate::events::Event;
1007 use crate::file_transfer::FileChunk;
1008 use crate::protocol::MEDIA_TRANSFER_STALE_TIMEOUT_SECS;
1009 use offline_protocol_core::{AppId, Message, UserId};
1010 use std::sync::{Arc, Mutex};
1011 use std::time::Duration;
1012
1013 let mut proto = make_protocol("alice");
1014 proto.file_transfer_manager = crate::file_transfer::FileTransferManager::with_config(
1015 crate::file_transfer::FileTransferConfig {
1016 max_file_size: 1_000_000,
1017 max_total_buffered_bytes: 1_065_536,
1018 ..crate::file_transfer::FileTransferConfig::default()
1019 },
1020 );
1021
1022 let failed_events = Arc::new(Mutex::new(Vec::<Event>::new()));
1023 let failed_events_clone = failed_events.clone();
1024 proto.on_event(move |event| {
1025 if matches!(event, Event::FileReceiveFailed { .. }) {
1026 failed_events_clone.lock().unwrap().push(event);
1027 }
1028 });
1029
1030 let chunk_message = |chunk: &FileChunk| {
1031 Message::new(
1032 UserId::new("bob").unwrap(),
1033 UserId::new("alice").unwrap(),
1034 AppId::new("test-app").unwrap(),
1035 chunk.to_json().unwrap(),
1036 )
1037 };
1038 let make_chunk =
1039 |file_id: &str, file_size: u64, total: u32, index: u32, len: usize| FileChunk {
1040 file_id: file_id.to_string(),
1041 file_name: "big.bin".to_string(),
1042 file_size,
1043 total_chunks: total,
1044 chunk_index: index,
1045 chunk_data: vec![0u8; len],
1046 file_checksum: "abc".to_string(),
1047 };
1048
1049 let _ = proto.handle_incoming_file_chunk(&chunk_message(&make_chunk(
1052 "f1", 1_000_000, 3, 0, 600_000,
1053 )));
1054 let _ = proto
1055 .handle_incoming_file_chunk(&chunk_message(&make_chunk("f2", 400_000, 2, 0, 300_000)));
1056 let _ = proto.handle_incoming_file_chunk(&chunk_message(&make_chunk(
1057 "f1", 1_000_000, 3, 1, 300_000,
1058 )));
1059 assert_eq!(failed_events.lock().unwrap().len(), 1);
1060
1061 let _ = proto.handle_incoming_file_chunk(&chunk_message(&make_chunk(
1064 "f1", 1_000_000, 3, 2, 100_000,
1065 )));
1066 assert_eq!(proto.file_transfer_manager.active_transfer_count(), 1);
1067 assert!(proto.file_transfer_manager.get_progress("f1").is_none());
1068
1069 proto
1072 .file_transfer_manager
1073 .backdate_transfers(Duration::from_secs(MEDIA_TRANSFER_STALE_TIMEOUT_SECS + 1));
1074 proto.cleanup_expired_entries();
1075
1076 let events = failed_events.lock().unwrap();
1077 let f1_reasons: Vec<&str> = events
1078 .iter()
1079 .filter_map(|e| match e {
1080 Event::FileReceiveFailed {
1081 file_id, reason, ..
1082 } if file_id == "f1" => Some(reason.as_str()),
1083 _ => None,
1084 })
1085 .collect();
1086 assert_eq!(f1_reasons, vec!["buffer_budget_exhausted"]);
1087 let f2_reasons: Vec<&str> = events
1088 .iter()
1089 .filter_map(|e| match e {
1090 Event::FileReceiveFailed {
1091 file_id, reason, ..
1092 } if file_id == "f2" => Some(reason.as_str()),
1093 _ => None,
1094 })
1095 .collect();
1096 assert_eq!(f2_reasons, vec!["stale_timeout"]);
1097 }
1098
1099 #[test]
1100 fn test_malformed_chunk_emits_no_failed_event() {
1101 use crate::events::Event;
1102 use crate::file_transfer::FileChunk;
1103 use offline_protocol_core::{AppId, Message, UserId};
1104 use std::sync::{Arc, Mutex};
1105
1106 let mut proto = make_protocol("alice");
1107
1108 let failed_events = Arc::new(Mutex::new(Vec::<Event>::new()));
1109 let failed_events_clone = failed_events.clone();
1110 proto.on_event(move |event| {
1111 if matches!(event, Event::FileReceiveFailed { .. }) {
1112 failed_events_clone.lock().unwrap().push(event);
1113 }
1114 });
1115
1116 let chunk = FileChunk {
1121 file_id: "fine-chunked".to_string(),
1122 file_name: "old.bin".to_string(),
1123 file_size: 100 * 1024 * 1024,
1124 total_chunks: 200_000,
1125 chunk_index: 0,
1126 chunk_data: vec![0u8; 512],
1127 file_checksum: "abc".to_string(),
1128 };
1129 let message = Message::new(
1130 UserId::new("bob").unwrap(),
1131 UserId::new("alice").unwrap(),
1132 AppId::new("test-app").unwrap(),
1133 chunk.to_json().unwrap(),
1134 );
1135 let _ = proto.handle_incoming_file_chunk(&message);
1136
1137 assert!(failed_events.lock().unwrap().is_empty());
1138 assert_eq!(proto.file_transfer_manager.active_transfer_count(), 0);
1139 assert!(!proto.pending_media_metadata.contains_key("fine-chunked"));
1140 }
1141
1142 #[test]
1143 fn test_finalize_failure_clears_metadata_and_emits_event() {
1144 use crate::events::Event;
1145 use crate::file_transfer::FileChunk;
1146 use offline_protocol_core::{AppId, Message, UserId};
1147 use sha2::{Digest, Sha256};
1148 use std::sync::{Arc, Mutex};
1149
1150 let mut proto = make_protocol("alice");
1151
1152 let failed_events = Arc::new(Mutex::new(Vec::<Event>::new()));
1153 let failed_events_clone = failed_events.clone();
1154 proto.on_event(move |event| {
1155 if matches!(event, Event::FileReceiveFailed { .. }) {
1156 failed_events_clone.lock().unwrap().push(event);
1157 }
1158 });
1159
1160 let data = vec![3u8; 100];
1164 let checksum = format!("{:x}", Sha256::digest(&data));
1165 let chunk = FileChunk {
1166 file_id: "bad-file".to_string(),
1167 file_name: "evil.bin".to_string(),
1168 file_size: 2048,
1169 total_chunks: 1,
1170 chunk_index: 0,
1171 chunk_data: data,
1172 file_checksum: checksum,
1173 };
1174 let message = Message::new(
1175 UserId::new("bob").unwrap(),
1176 UserId::new("alice").unwrap(),
1177 AppId::new("test-app").unwrap(),
1178 chunk.to_json().unwrap(),
1179 );
1180 let _ = proto.handle_incoming_file_chunk(&message);
1181
1182 assert_eq!(proto.file_transfer_manager.active_transfer_count(), 0);
1184 assert!(!proto.pending_media_metadata.contains_key("bad-file"));
1185
1186 let events = failed_events.lock().unwrap();
1187 assert_eq!(events.len(), 1);
1188 match &events[0] {
1189 Event::FileReceiveFailed {
1190 file_id, reason, ..
1191 } => {
1192 assert_eq!(file_id, "bad-file");
1193 assert_eq!(reason, "integrity_check_failed");
1194 }
1195 other => panic!("unexpected event: {:?}", other),
1196 }
1197 }
1198
1199 #[test]
1200 fn test_stale_transfer_sweep_emits_receive_failed_event() {
1201 use crate::events::Event;
1202 use crate::file_transfer::FileChunk;
1203 use crate::protocol::MEDIA_TRANSFER_STALE_TIMEOUT_SECS;
1204 use offline_protocol_core::{AppId, Message, UserId};
1205 use std::sync::{Arc, Mutex};
1206 use std::time::Duration;
1207
1208 let mut proto = make_protocol("alice");
1209
1210 let failed_events = Arc::new(Mutex::new(Vec::<Event>::new()));
1211 let failed_events_clone = failed_events.clone();
1212 proto.on_event(move |event| {
1213 if matches!(event, Event::FileReceiveFailed { .. }) {
1214 failed_events_clone.lock().unwrap().push(event);
1215 }
1216 });
1217
1218 let chunk = FileChunk {
1220 file_id: "stale-file".to_string(),
1221 file_name: "photo.jpg".to_string(),
1222 file_size: 200,
1223 total_chunks: 2,
1224 chunk_index: 0,
1225 chunk_data: vec![0u8; 100],
1226 file_checksum: "abc".to_string(),
1227 };
1228 let message = Message::new(
1229 UserId::new("bob").unwrap(),
1230 UserId::new("alice").unwrap(),
1231 AppId::new("test-app").unwrap(),
1232 chunk.to_json().unwrap(),
1233 );
1234 let _ = proto.handle_incoming_file_chunk(&message);
1235 assert_eq!(proto.file_transfer_manager.active_transfer_count(), 1);
1236 assert!(proto.pending_media_metadata.contains_key("stale-file"));
1237
1238 proto
1242 .file_transfer_manager
1243 .backdate_transfers(Duration::from_secs(MEDIA_TRANSFER_STALE_TIMEOUT_SECS + 1));
1244 proto.cleanup_expired_entries();
1245
1246 assert_eq!(proto.file_transfer_manager.active_transfer_count(), 0);
1247 assert!(!proto.pending_media_metadata.contains_key("stale-file"));
1248 let events = failed_events.lock().unwrap();
1249 assert_eq!(events.len(), 1);
1250 match &events[0] {
1251 Event::FileReceiveFailed {
1252 file_id,
1253 file_name,
1254 sender,
1255 reason,
1256 } => {
1257 assert_eq!(file_id, "stale-file");
1258 assert_eq!(file_name, "photo.jpg");
1259 assert_eq!(sender, "bob");
1260 assert_eq!(reason, "stale_timeout");
1261 }
1262 other => panic!("unexpected event: {:?}", other),
1263 }
1264 }
1265
1266 #[test]
1267 fn test_connection_request_to_blocked_user_rejected() {
1268 use offline_protocol_transport::{mock::MockTransport, TransportType};
1269
1270 let mut proto = make_protocol("alice");
1271
1272 let mock = MockTransport::new(TransportType::BLE);
1273 mock.start().unwrap();
1274 proto
1275 .transport_manager_mut()
1276 .add_transport(TransportType::BLE, Box::new(mock));
1277 proto.start().unwrap();
1278
1279 proto.block_user("mallory").unwrap();
1280
1281 let result = proto.send_connection_request("mallory", "Alice", None, None);
1282 assert!(matches!(
1283 result,
1284 Err(crate::Error::UserBlocked(ref id)) if id == "mallory"
1285 ));
1286 }
1287
1288 #[test]
1289 fn test_accept_connection_request_to_blocked_user_rejected() {
1290 use offline_protocol_transport::{mock::MockTransport, TransportType};
1291
1292 let mut proto = make_protocol("alice");
1293
1294 let mock = MockTransport::new(TransportType::BLE);
1295 mock.start().unwrap();
1296 proto
1297 .transport_manager_mut()
1298 .add_transport(TransportType::BLE, Box::new(mock));
1299 proto.start().unwrap();
1300
1301 proto.block_user("mallory").unwrap();
1302
1303 let result = proto.accept_connection_request("mallory", "Alice", None);
1304 assert!(matches!(
1305 result,
1306 Err(crate::Error::UserBlocked(ref id)) if id == "mallory"
1307 ));
1308 }
1309
1310 #[test]
1311 fn test_reject_connection_request_to_blocked_user_rejected() {
1312 use offline_protocol_transport::{mock::MockTransport, TransportType};
1313
1314 let mut proto = make_protocol("alice");
1315
1316 let mock = MockTransport::new(TransportType::BLE);
1317 mock.start().unwrap();
1318 proto
1319 .transport_manager_mut()
1320 .add_transport(TransportType::BLE, Box::new(mock));
1321 proto.start().unwrap();
1322
1323 proto.block_user("mallory").unwrap();
1324
1325 let result = proto.reject_connection_request("mallory");
1326 assert!(matches!(
1327 result,
1328 Err(crate::Error::UserBlocked(ref id)) if id == "mallory"
1329 ));
1330 }
1331
1332 #[test]
1333 fn test_restore_blocked_users_skips_invalid_entries() {
1334 use crate::mls::InMemoryStorage;
1335 use offline_protocol_mls::MlsStorage;
1336
1337 let mut proto = make_protocol("alice");
1338 let storage = Arc::new(InMemoryStorage::new());
1339 proto
1340 .enable_message_persistence_for_test(storage.clone())
1341 .unwrap();
1342
1343 storage.store("blocked_users", "bob", &[]).unwrap();
1345 storage.store("blocked_users", "", &[]).unwrap(); proto.blocked_users.clear();
1348 proto.restore_blocked_users().unwrap();
1349
1350 assert!(proto.is_user_blocked("bob"));
1352 assert!(!proto.is_user_blocked(""));
1353 assert_eq!(proto.blocked_users.len(), 1);
1354 }
1355
1356 #[test]
1357 fn test_session_reset_key_package_clears_stale_session() {
1358 use crate::mls::InMemoryStorage as MlsInMemoryStorage;
1359 use crate::protocol::types::KeyPackagePayload;
1360
1361 let mut alice = make_protocol("alice");
1363 let alice_storage = Arc::new(MlsInMemoryStorage::new());
1364 alice.initialize_mls_for_test(alice_storage).unwrap();
1365
1366 let mut bob = make_protocol("bob");
1368 let bob_storage = Arc::new(MlsInMemoryStorage::new());
1369 bob.initialize_mls_for_test(bob_storage).unwrap();
1370
1371 let alice_key_pkg = {
1373 let mls = alice.mls_manager.as_ref().unwrap();
1374 let manager = mls.read().unwrap();
1375 manager.get_or_create_key_package().unwrap()
1376 };
1377 {
1379 let mls = bob.mls_manager.as_ref().unwrap();
1380 let manager = mls.read().unwrap();
1381 manager
1382 .import_key_package(&id("alice"), &alice_key_pkg.key_package_data)
1383 .unwrap();
1384 let welcome = manager.create_session(&id("alice")).unwrap();
1385 let alice_mls = alice.mls_manager.as_ref().unwrap();
1387 let alice_manager = alice_mls.read().unwrap();
1388 alice_manager.join_session(&welcome).unwrap();
1389 }
1390
1391 {
1393 let mls = alice.mls_manager.as_ref().unwrap();
1394 assert!(mls.read().unwrap().has_session(&id("bob")).unwrap());
1395 }
1396 {
1397 let mls = bob.mls_manager.as_ref().unwrap();
1398 assert!(mls.read().unwrap().has_session(&id("alice")).unwrap());
1399 }
1400
1401 alice.block_user(&id("bob")).unwrap();
1404 alice.unblock_user(&id("bob")).unwrap();
1405
1406 {
1408 let mls = alice.mls_manager.as_ref().unwrap();
1409 assert!(
1410 !mls.read().unwrap().has_session(&id("bob")).unwrap(),
1411 "Alice should have no session after unblock cleanup"
1412 );
1413 }
1414
1415 {
1417 let mls = bob.mls_manager.as_ref().unwrap();
1418 assert!(
1419 mls.read().unwrap().has_session(&id("alice")).unwrap(),
1420 "Bob still has the old session before receiving reset"
1421 );
1422 }
1423
1424 let alice_fresh_key_pkg = {
1426 let mls = alice.mls_manager.as_ref().unwrap();
1427 let manager = mls.read().unwrap();
1428 manager.get_or_create_key_package().unwrap()
1429 };
1430 let reset_payload = KeyPackagePayload {
1431 user_id: id("alice"),
1432 key_package_data: alice_fresh_key_pkg.key_package_data.clone(),
1433 remaining_lifetime_ms: alice_fresh_key_pkg.remaining_lifetime_ms(),
1434 timestamp_ms: 0,
1435 session_reset: true,
1436 wire_versions: Vec::new(),
1437 env_versions: Vec::new(),
1438 rich_versions: Vec::new(),
1439 data_versions: Vec::new(),
1440 ctrl_versions: Vec::new(),
1441 nostr_pubkey: None,
1442 };
1443 let content = serde_json::to_string(&reset_payload).unwrap();
1444
1445 bob.handle_key_package_message(
1449 &id("alice"),
1450 &content,
1451 true,
1452 Some(chrono::Utc::now().timestamp_millis()),
1453 );
1454
1455 {
1458 let mls = bob.mls_manager.as_ref().unwrap();
1459 let manager = mls.read().unwrap();
1460 assert!(
1461 manager.has_session(&id("alice")).unwrap(),
1462 "Bob should have a fresh session after session_reset + auto-establish"
1463 );
1464 }
1465
1466 assert!(
1469 !bob.confirmed_sessions.contains(&id("alice")),
1470 "Old confirmed session entry should be cleared"
1471 );
1472
1473 assert!(
1475 !bob.pending_key_packages.contains_key(&id("alice")),
1476 "Key package should be consumed after auto-establish"
1477 );
1478 }
1479}