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