1use anyhow::Result;
4use rand::rngs::StdRng;
5use std::sync::Arc;
6use std::sync::atomic::Ordering;
7use std::time::Duration;
8use wacore::libsignal::protocol::{
9 IdentityChange, PreKeyBundle, SignalProtocolError, UsePQRatchet, process_prekey_bundle,
10};
11use wacore::libsignal::store::SessionStore;
12use wacore::types::jid::JidExt;
13use wacore_binary::Jid;
14
15use super::Client;
16use crate::types::events::{Event, OfflineSyncCompleted};
17
18impl Client {
19 pub(crate) async fn install_prekey_bundle_cached(
24 &self,
25 jid: &Jid,
26 bundle: &PreKeyBundle,
27 adapter: &mut crate::store::signal_adapter::SignalProtocolStoreAdapter,
28 rng: &mut StdRng,
29 ) -> Result<IdentityChange, SignalProtocolError> {
30 let signal_address = jid.to_protocol_address();
31 let session_mutex = self.session_lock_for(signal_address.as_str()).await;
32 let session_guard = session_mutex.lock().await;
33
34 let identity_change = process_prekey_bundle(
35 &signal_address,
36 &mut adapter.session_store,
37 &mut adapter.identity_store,
38 bundle,
39 rng,
40 UsePQRatchet::No,
41 )
42 .await?;
43
44 drop(session_guard);
45 if identity_change == IdentityChange::ReplacedExisting {
46 self.react_to_local_identity_change(jid);
47 }
48 Ok(identity_change)
49 }
50
51 pub(crate) const DEFAULT_OFFLINE_SYNC_TIMEOUT: Duration = Duration::from_secs(60);
53
54 pub(crate) async fn complete_offline_sync(&self, count: i32) {
55 self.offline_sync_metrics
56 .active
57 .store(false, Ordering::Release);
58 match self.offline_sync_metrics.start_time.lock() {
59 Ok(mut guard) => *guard = None,
60 Err(poison) => *poison.into_inner() = None,
61 }
62
63 if self
69 .offline_sync_finish_started
70 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
71 .is_err()
72 {
73 return;
74 }
75
76 let Some(client) = self.self_weak.get().and_then(|w| w.upgrade()) else {
77 log::error!(
81 "complete_offline_sync: self_weak upgrade failed; dropping the drain tail and switching to live mode"
82 );
83 self.inbound_commit_batch.force_live_dropping_entries();
84 self.publish_offline_sync_live_state(count, None);
85 return;
86 };
87
88 let generation = self.connection_generation.load(Ordering::Acquire);
97 self.runtime
98 .spawn(Box::pin(async move {
99 client.finish_offline_sync(count, generation).await;
100 }))
101 .detach();
102 }
103
104 async fn finish_offline_sync(self: &Arc<Self>, count: i32, generation: u64) {
110 let durable = self.finish_inbound_commit_drain(generation).await;
116
117 if self.connection_generation.load(Ordering::Acquire) != generation {
118 log::debug!(
119 "finish_offline_sync: connection generation changed during the tail commit; leaving the new connection's state alone"
120 );
121 return;
122 }
123
124 self.publish_offline_sync_live_state(count, Some(durable));
125 }
126
127 fn publish_offline_sync_live_state(&self, count: i32, durable: Option<bool>) {
153 self.offline_sync_completed.store(true, Ordering::Release);
154 if durable != Some(false) {
155 self.swap_message_semaphore(64);
156 }
157 match durable {
158 Some(true) => self.flush_offline_receipts(),
159 Some(false) => {
160 log::warn!(
161 "finish_offline_sync: tail commit not durable; dropping buffered offline receipts so the server redelivers"
162 );
163 self.clear_offline_receipt_buffer();
164 }
165 None => {}
166 }
167 self.offline_sync_notifier.notify(usize::MAX);
168 self.core.event_bus.dispatch(Event::OfflineSyncCompleted(
169 OfflineSyncCompleted::builder().count(count).build(),
170 ));
171 }
172
173 pub(crate) async fn wait_for_offline_delivery_end(&self) {
175 self.wait_for_offline_delivery_end_with_timeout(Self::DEFAULT_OFFLINE_SYNC_TIMEOUT)
176 .await;
177 }
178
179 pub(crate) async fn wait_for_offline_delivery_end_with_timeout(&self, timeout: Duration) {
180 let wait_generation = self.connection_generation.load(Ordering::Acquire);
181 let offline_fut = self.offline_sync_notifier.listen();
182 if self.offline_sync_completed.load(Ordering::Relaxed) {
183 return;
184 }
185
186 if wacore::runtime::timeout(&*self.runtime, timeout, offline_fut)
187 .await
188 .is_err()
189 {
190 if self.connection_generation.load(Ordering::Acquire) != wait_generation
194 || self.expected_disconnect.load(Ordering::Relaxed)
195 {
196 log::debug!(
197 target: "Client/OfflineSync",
198 "Offline sync timeout ignored: connection generation changed or disconnected",
199 );
200 return;
201 }
202
203 let processed = self
204 .offline_sync_metrics
205 .processed_messages
206 .load(Ordering::Acquire);
207 let expected = self
208 .offline_sync_metrics
209 .total_messages
210 .load(Ordering::Acquire);
211 log::warn!(
212 target: "Client/OfflineSync",
213 "Offline sync timed out after {:?} (processed {} of {} items); marking sync complete",
214 timeout,
215 processed,
216 expected,
217 );
218 self.complete_offline_sync(i32::try_from(processed).unwrap_or(i32::MAX))
219 .await;
220 let deadline = wacore::time::Instant::now() + timeout;
230 loop {
231 let listener = self.offline_sync_notifier.listen();
232 if self.offline_sync_completed.load(Ordering::Acquire)
233 || self.connection_generation.load(Ordering::Acquire) != wait_generation
234 || self.expected_disconnect.load(Ordering::Relaxed)
235 {
236 return;
237 }
238 if wacore::time::Instant::now() >= deadline {
239 log::warn!(
240 target: "Client/OfflineSync",
241 "Drain finisher still running {:?} after the offline sync timeout; proceeding without it",
242 timeout,
243 );
244 return;
245 }
246 let _ = wacore::runtime::timeout(&*self.runtime, Duration::from_secs(1), listener)
247 .await;
248 }
249 }
250 }
251
252 pub(crate) fn begin_history_sync_task(
253 &self,
254 payload_bytes: usize,
255 ) -> crate::sync_task::HistorySyncTaskTracker {
256 self.history_sync_activity.begin(payload_bytes)
257 }
258
259 pub async fn wait_for_startup_sync(&self, timeout: Duration) -> Result<()> {
260 use anyhow::anyhow;
261 use wacore::time::Instant;
262
263 let deadline = Instant::now() + timeout;
264
265 let offline_fut = self.offline_sync_notifier.listen();
268 if !self.offline_sync_completed.load(Ordering::Relaxed) {
269 let remaining = deadline.saturating_duration_since(Instant::now());
270 wacore::runtime::timeout(&*self.runtime, remaining, offline_fut)
271 .await
272 .map_err(|_| anyhow!("Timeout waiting for offline sync completion"))?;
273 }
274
275 loop {
276 let history_fut = self.history_sync_activity.listen();
277 if self.history_sync_activity.tasks() == 0 {
278 return Ok(());
279 }
280
281 let remaining = deadline.saturating_duration_since(Instant::now());
282 wacore::runtime::timeout(&*self.runtime, remaining, history_fut)
283 .await
284 .map_err(|_| anyhow!("Timeout waiting for history sync tasks to become idle"))?;
285 }
286 }
287
288 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.session.ensure", level = "debug", skip_all, fields(count = device_jids.len()), err(Debug)))]
291 pub(crate) async fn ensure_e2e_sessions(&self, device_jids: &[Jid]) -> Result<()> {
292 if device_jids.is_empty() {
293 return Ok(());
294 }
295 self.wait_for_offline_delivery_end().await;
296 let resolved_jids = self.resolve_lid_mappings(device_jids).await;
297 self.ensure_sessions_inner(resolved_jids).await
298 }
299
300 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.session.ensure_resolved", level = "debug", skip_all, fields(count = jids.len()), err(Debug)))]
304 pub(crate) async fn ensure_e2e_sessions_resolved(&self, jids: &[Jid]) -> Result<()> {
305 if jids.is_empty() {
306 return Ok(());
307 }
308 self.wait_for_offline_delivery_end().await;
309 self.ensure_sessions_inner(jids.to_vec()).await
310 }
311}
312
313const UNREGISTERED_DEVICE_CODE: u16 = 406;
328
329fn is_device_unregistered(err: &anyhow::Error) -> bool {
330 use crate::error::ErrorChainExt;
331 err.server_rejection()
332 .is_some_and(|r| r.code == UNREGISTERED_DEVICE_CODE)
333}
334
335fn distinct_users(jids: &[Jid]) -> smallvec::SmallVec<[&str; 4]> {
344 let mut seen: smallvec::SmallVec<[&str; 4]> = smallvec::SmallVec::new();
345 for jid in jids {
346 if !seen.contains(&jid.user.as_str()) {
347 seen.push(jid.user.as_str());
348 }
349 }
350 seen
351}
352
353impl Client {
354 async fn invalidate_device_caches_for(&self, jids: &[Jid]) {
356 for user in distinct_users(jids) {
357 self.invalidate_device_cache(user).await;
358 }
359 }
360}
361
362impl Client {
363 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.session.ensure_inner", level = "debug", skip_all, fields(count = jids.len()), err(Debug)))]
365 async fn ensure_sessions_inner(&self, mut jids: Vec<Jid>) -> Result<()> {
366 use wacore::types::jid::JidExt;
367
368 let mut reusable_addr = wacore::types::jid::make_reusable_protocol_address();
376 jids.retain(|jid| {
377 jid.reset_protocol_address(&mut reusable_addr);
378 self.signal_cache.try_has_session(&reusable_addr) != Some(true)
379 });
380 if jids.is_empty() {
381 return Ok(());
382 }
383
384 let device_snapshot = self.persistence_manager.get_device_snapshot();
385
386 use futures::StreamExt;
390 const SESSION_PROBE_CONCURRENCY: usize = 16;
391 let backend = device_snapshot.backend.clone();
392 let jids_needing_sessions: Vec<Jid> = futures::stream::iter(jids)
393 .map(|jid| {
394 let backend = backend.clone();
395 async move {
396 let signal_addr = jid.to_protocol_address();
397 match self.signal_cache.has_session(&signal_addr, &*backend).await {
399 Ok(true) => None,
400 Ok(false) => Some(jid),
401 Err(e) => {
402 log::warn!("Failed to check session for {}: {}", jid.observe(), e);
403 None
404 }
405 }
406 }
407 })
408 .buffer_unordered(SESSION_PROBE_CONCURRENCY)
409 .filter_map(|needed| async move { needed })
410 .collect()
411 .await;
412
413 if jids_needing_sessions.is_empty() {
414 return Ok(());
415 }
416
417 for batch in jids_needing_sessions.chunks(crate::session::SESSION_CHECK_BATCH_SIZE) {
418 self.fetch_and_establish_sessions(batch).await?;
419 }
420
421 Ok(())
422 }
423
424 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.session.fetch_establish", level = "debug", skip_all, fields(count = jids.len()), err(Debug)))]
427 async fn fetch_and_establish_sessions(&self, jids: &[Jid]) -> Result<usize, anyhow::Error> {
428 if jids.is_empty() {
429 return Ok(0);
430 }
431
432 let prekey_bundles = match self
433 .fetch_pre_keys(jids, Some(wacore::iq::prekeys::PreKeyFetchReason::Identity))
434 .await
435 {
436 Ok(bundles) => bundles,
437 Err(e) if is_device_unregistered(&e) => {
453 log::debug!(
454 "Prekey fetch returned 406 for {} device(s); \
455 refreshing their device lists before failing the send",
456 jids.len()
457 );
458 self.invalidate_device_caches_for(jids).await;
459 return Err(e);
460 }
461 Err(e) => return Err(e),
462 };
463
464 if !prekey_bundles.rejected.is_empty() {
471 let rejected: Vec<Jid> = prekey_bundles
472 .rejected
473 .iter()
474 .filter(|device| device.code == UNREGISTERED_DEVICE_CODE)
475 .map(|device| device.jid.clone())
476 .collect();
477 if !rejected.is_empty() {
478 log::debug!(
479 "prekey fetch rejected {} of {} device(s) as unregistered; \
480 refreshing their device lists",
481 rejected.len(),
482 jids.len()
483 );
484 self.invalidate_device_caches_for(&rejected).await;
485 }
486 }
487
488 let mut adapter = self.signal_adapter().await;
489 let mut rng = rand::make_rng::<StdRng>();
490
491 let mut success_count = 0;
492 let mut missing_count = 0;
493 let mut failed_count = 0;
494
495 for jid in jids {
496 if let Some(bundle) = prekey_bundles.bundles.get(jid) {
497 match self
498 .install_prekey_bundle_cached(jid, bundle, &mut adapter, &mut rng)
499 .await
500 {
501 Ok(_) => {
502 success_count += 1;
503 log::debug!("Successfully established session with {}", jid.observe());
504 }
505 Err(e) => {
506 failed_count += 1;
507 log::warn!("Failed to establish session with {}: {}", jid.observe(), e);
508 }
509 }
510 } else {
511 missing_count += 1;
512 if jid.device == 0 {
513 log::warn!(
514 "Server did not return prekeys for primary phone {}",
515 jid.observe()
516 );
517 } else {
518 log::debug!("Server did not return prekeys for {}", jid.observe());
519 }
520 }
521 }
522
523 if missing_count > 0 || failed_count > 0 {
524 log::debug!(
525 "Session establishment: {} succeeded, {} missing prekeys, {} failed (of {} requested)",
526 success_count,
527 missing_count,
528 failed_count,
529 jids.len()
530 );
531 }
532
533 if success_count > 0 {
538 self.flush_signal_cache_batch_safe().await?;
539 }
540
541 Ok(success_count)
542 }
543
544 #[cfg_attr(
547 feature = "tracing",
548 tracing::instrument(
549 name = "wa.session.primary_phone_check",
550 level = "debug",
551 skip_all,
552 err(Debug)
553 )
554 )]
555 pub(crate) async fn establish_primary_phone_session_immediate(&self) -> Result<()> {
556 let device_snapshot = self.persistence_manager.get_device_snapshot();
557
558 let own_pn = device_snapshot
559 .pn
560 .clone()
561 .ok_or_else(|| anyhow::Error::from(crate::client::ClientError::NotLoggedIn))?;
562
563 let Some(ref own_lid) = device_snapshot.lid else {
564 log::debug!("No own LID yet, skipping primary phone session check");
565 return Ok(());
566 };
567
568 let primary_phone_lid = own_lid.with_device(0);
569 let primary_phone_pn = own_pn.with_device(0);
570
571 let lid_exists = self
572 .check_session_exists(&primary_phone_lid)
573 .await
574 .unwrap_or(false);
575 let pn_exists = self
576 .check_session_exists(&primary_phone_pn)
577 .await
578 .unwrap_or(false);
579
580 match (lid_exists, pn_exists) {
581 (true, _) => log::debug!("LID session with {} exists", primary_phone_lid.observe()),
582 (false, true) => {
583 log::debug!("PN-only session for own device 0 — will migrate on first message")
584 }
585 (false, false) => {
586 log::debug!("No session with own device 0 — will establish on first message")
587 }
588 }
589
590 Ok(())
591 }
592
593 #[cfg(feature = "voip-runtime")]
598 pub(crate) async fn would_emit_pkmsg(&self, jid: &Jid) -> Result<bool, anyhow::Error> {
599 let device_store = self.persistence_manager.get_device_arc().await;
600 let mut adapter = self.signal_adapter_from(device_store);
601 let signal_addr = jid.to_protocol_address();
602 wacore::send::pkmsg_would_be_emitted(&mut adapter.session_store, &signal_addr).await
603 }
604
605 pub(crate) async fn check_session_exists(&self, jid: &Jid) -> Result<bool, anyhow::Error> {
607 let device_snapshot = self.persistence_manager.get_device_snapshot();
608 let signal_addr = jid.to_protocol_address();
609
610 device_snapshot
611 .contains_session(&signal_addr)
612 .await
613 .map_err(|e| anyhow::anyhow!("Failed to check session for {}: {}", jid.observe(), e))
614 }
615}
616
617#[cfg(test)]
618mod tests {
619 use super::*;
620 use wacore_binary::{JidExt, Server};
621
622 #[test]
627 fn only_a_406_counts_as_an_unregistered_device() {
628 let as_iq_error = |code| {
632 anyhow::Error::new(crate::request::IqError::ServerError {
633 code,
634 text: "not-acceptable".to_string(),
635 error_type: None,
636 backoff: None,
637 })
638 };
639 let as_shared = |code| {
640 anyhow::Error::new(wacore::request::ServerErrorCode {
641 code,
642 text: "not-acceptable".to_string(),
643 error_type: None,
644 backoff: None,
645 })
646 };
647
648 assert!(
649 is_device_unregistered(&as_iq_error(406)),
650 "the error this preflight actually receives must be recognised"
651 );
652 assert!(is_device_unregistered(&as_shared(406)));
653
654 for code in [400, 401, 403, 404, 429, 500, 503] {
655 assert!(
656 !is_device_unregistered(&as_iq_error(code)),
657 "a {code} must not be treated as an unregistered device"
658 );
659 assert!(!is_device_unregistered(&as_shared(code)));
660 }
661
662 assert!(!is_device_unregistered(&anyhow::anyhow!("socket closed")));
664 }
665
666 #[test]
674 fn a_batch_names_each_user_once_in_order() {
675 let a = Jid::pn("5511900000050");
676 let b = Jid::pn("5511900000051");
677 let jids = vec![
678 a.with_device(0),
679 a.with_device(1),
680 b.with_device(0),
681 a.with_device(2),
682 b.with_device(3),
683 ];
684
685 assert_eq!(
686 distinct_users(&jids).as_slice(),
687 [a.user.as_str(), b.user.as_str()],
688 "each user once, in the order the batch first names them"
689 );
690
691 assert!(distinct_users(&[]).is_empty());
692 assert_eq!(
693 distinct_users(std::slice::from_ref(&a.with_device(7))).as_slice(),
694 [a.user.as_str()]
695 );
696 }
697
698 #[test]
699 fn test_primary_phone_jid_creation_from_pn() {
700 let own_pn = Jid::pn("559999999999");
701 let primary_phone_jid = own_pn.with_device(0);
702
703 assert_eq!(primary_phone_jid.user, "559999999999");
704 assert_eq!(primary_phone_jid.server, Server::Pn);
705 assert_eq!(primary_phone_jid.device, 0);
706 assert_eq!(primary_phone_jid.agent, 0);
707 assert_eq!(primary_phone_jid.to_string(), "559999999999@s.whatsapp.net");
708 }
709
710 #[test]
711 fn test_primary_phone_jid_overwrites_existing_device() {
712 let own_pn = Jid::pn_device("559999999999", 33);
714 let primary_phone_jid = own_pn.with_device(0);
715
716 assert_eq!(primary_phone_jid.user, "559999999999");
717 assert_eq!(primary_phone_jid.server, Server::Pn);
718 assert_eq!(primary_phone_jid.device, 0);
719 }
720
721 #[test]
722 fn test_primary_phone_jid_is_not_ad() {
723 let primary_phone_jid = Jid::pn("559999999999").with_device(0);
724 assert!(!primary_phone_jid.is_ad()); }
726
727 #[test]
728 fn test_linked_device_is_ad() {
729 let linked_device_jid = Jid::pn_device("559999999999", 33);
730 assert!(linked_device_jid.is_ad()); }
732
733 #[test]
734 fn test_primary_phone_jid_from_lid() {
735 let own_lid = Jid::lid("100000000000001");
736 let primary_phone_jid = own_lid.with_device(0);
737
738 assert_eq!(primary_phone_jid.user, "100000000000001");
739 assert_eq!(primary_phone_jid.server, Server::Lid);
740 assert_eq!(primary_phone_jid.device, 0);
741 assert!(!primary_phone_jid.is_ad());
742 }
743
744 #[test]
745 fn test_primary_phone_jid_roundtrip() {
746 let own_pn = Jid::pn("559999999999");
747 let primary_phone_jid = own_pn.with_device(0);
748
749 let jid_string = primary_phone_jid.to_string();
750 assert_eq!(jid_string, "559999999999@s.whatsapp.net");
751
752 let parsed: Jid = jid_string.parse().expect("JID should be parseable");
753 assert_eq!(parsed.user, "559999999999");
754 assert_eq!(parsed.server, Server::Pn);
755 assert_eq!(parsed.device, 0);
756 }
757
758 #[test]
759 fn test_with_device_preserves_identity() {
760 let pn = Jid::pn("1234567890");
761 let pn_device_0 = pn.with_device(0);
762 let pn_device_5 = pn.with_device(5);
763
764 assert_eq!(pn_device_0.user, pn_device_5.user);
765 assert_eq!(pn_device_0.server, pn_device_5.server);
766 assert_eq!(pn_device_0.device, 0);
767 assert_eq!(pn_device_5.device, 5);
768
769 let lid = Jid::lid("100000012345678");
770 let lid_device_0 = lid.with_device(0);
771 let lid_device_33 = lid.with_device(33);
772
773 assert_eq!(lid_device_0.user, lid_device_33.user);
774 assert_eq!(lid_device_0.server, lid_device_33.server);
775 assert_eq!(lid_device_0.device, 0);
776 assert_eq!(lid_device_33.device, 33);
777 }
778
779 #[test]
780 fn test_primary_phone_vs_companion_devices() {
781 let user = "559999999999";
782 let primary = Jid::pn(user).with_device(0);
783 let companion_web = Jid::pn_device(user, 33);
784 let companion_desktop = Jid::pn_device(user, 34);
785
786 assert_eq!(primary.user, companion_web.user);
788 assert_eq!(primary.user, companion_desktop.user);
789
790 assert_eq!(primary.device, 0);
792 assert_eq!(companion_web.device, 33);
793 assert_eq!(companion_desktop.device, 34);
794
795 assert!(!primary.is_ad());
797 assert!(companion_web.is_ad());
798 assert!(companion_desktop.is_ad());
799 }
800
801 #[test]
803 fn test_session_check_behavior_documentation() {
804 enum SessionCheckResult {
806 Exists,
807 NotExists,
808 CheckFailed,
809 }
810
811 fn should_establish_session(
812 check_result: SessionCheckResult,
813 ) -> Result<bool, &'static str> {
814 match check_result {
815 SessionCheckResult::Exists => Ok(false), SessionCheckResult::NotExists => Ok(true), SessionCheckResult::CheckFailed => Err("Cannot verify - fail safe"),
818 }
819 }
820
821 assert_eq!(
823 should_establish_session(SessionCheckResult::Exists),
824 Ok(false)
825 );
826 assert_eq!(
827 should_establish_session(SessionCheckResult::NotExists),
828 Ok(true)
829 );
830 assert!(should_establish_session(SessionCheckResult::CheckFailed).is_err());
831 }
832
833 #[test]
835 fn test_protocol_address_format_for_session_lookup() {
836 use wacore::types::jid::JidExt;
837
838 let pn = Jid::pn("559999999999").with_device(0);
839 let addr = pn.to_protocol_address();
840 assert_eq!(addr.name(), "559999999999@c.us");
841 assert_eq!(u32::from(addr.device_id()), 0);
842 assert_eq!(addr.to_string(), "559999999999@c.us.0");
843
844 let companion = Jid::pn_device("559999999999", 33);
845 let companion_addr = companion.to_protocol_address();
846 assert_eq!(companion_addr.name(), "559999999999:33@c.us");
847 assert_eq!(companion_addr.to_string(), "559999999999:33@c.us.0");
848
849 let lid = Jid::lid("100000000000001").with_device(0);
850 let lid_addr = lid.to_protocol_address();
851 assert_eq!(lid_addr.name(), "100000000000001@lid");
852 assert_eq!(u32::from(lid_addr.device_id()), 0);
853 assert_eq!(lid_addr.to_string(), "100000000000001@lid.0");
854
855 let lid_device = Jid::lid_device("100000000000001", 33);
856 let lid_device_addr = lid_device.to_protocol_address();
857 assert_eq!(lid_device_addr.name(), "100000000000001:33@lid");
858 assert_eq!(lid_device_addr.to_string(), "100000000000001:33@lid.0");
859 }
860
861 #[test]
862 fn test_filter_logic_for_session_establishment() {
863 let jids = vec![
864 Jid::pn_device("111", 0),
865 Jid::pn_device("222", 0),
866 Jid::pn_device("333", 0),
867 ];
868
869 let session_exists = |jid: &Jid| -> Result<bool, &'static str> {
871 match jid.user.as_str() {
872 "111" => Ok(true), "222" => Ok(false), "333" => Err("DB error"), _ => Ok(false),
876 }
877 };
878
879 let mut jids_needing_sessions = Vec::with_capacity(jids.len());
881 for jid in &jids {
882 match session_exists(jid) {
883 Ok(true) => {} Ok(false) => jids_needing_sessions.push(jid.clone()), Err(e) => eprintln!("Warning: failed to check {}: {}", jid, e), }
887 }
888
889 assert_eq!(jids_needing_sessions.len(), 1);
891 assert_eq!(jids_needing_sessions[0].user, "222");
892 }
893
894 #[test]
897 fn test_dual_addressing_pn_and_lid_are_independent() {
898 let pn_address = Jid::pn("551199887766").with_device(0);
899 let lid_address = Jid::lid("236395184570386").with_device(0);
900
901 assert_ne!(pn_address.user, lid_address.user);
902 assert_ne!(pn_address.server, lid_address.server);
903
904 use wacore::types::jid::JidExt;
905 let pn_signal_addr = pn_address.to_protocol_address();
906 let lid_signal_addr = lid_address.to_protocol_address();
907
908 assert_ne!(pn_signal_addr.name(), lid_signal_addr.name());
909 assert_eq!(pn_signal_addr.name(), "551199887766@c.us");
910 assert_eq!(lid_signal_addr.name(), "236395184570386@lid");
911 assert_eq!(pn_address.device, 0);
912 assert_eq!(lid_address.device, 0);
913 }
914
915 #[test]
916 fn test_lid_extraction_from_own_device() {
917 let own_lid_with_device = Jid::lid_device("236395184570386", 61);
918 let primary_lid = own_lid_with_device.with_device(0);
919
920 assert_eq!(primary_lid.user, "236395184570386");
921 assert_eq!(primary_lid.device, 0);
922 assert!(!primary_lid.is_ad());
923 }
924
925 #[test]
927 fn test_stale_session_scenario_documentation() {
928 fn should_establish_pn_session(pn_exists: bool) -> bool {
929 !pn_exists
930 }
931
932 fn should_establish_lid_session(_lid_exists: bool) -> bool {
933 false }
935
936 assert!(!should_establish_pn_session(true));
938 assert!(should_establish_pn_session(false));
940 assert!(!should_establish_lid_session(true));
942 assert!(!should_establish_lid_session(false));
943 }
944
945 #[test]
947 fn test_retry_mechanism_for_stale_sessions() {
948 const RETRY_ERROR_NO_SESSION: u8 = 1;
949 const RETRY_ERROR_INVALID_MESSAGE: u8 = 4;
950
951 fn action_for_error(error_code: u8) -> &'static str {
952 match error_code {
953 RETRY_ERROR_NO_SESSION => "Establish new session via prekey",
954 RETRY_ERROR_INVALID_MESSAGE => "Delete stale session, resend message",
955 _ => "Unknown error",
956 }
957 }
958
959 assert_eq!(
960 action_for_error(RETRY_ERROR_NO_SESSION),
961 "Establish new session via prekey"
962 );
963 assert_eq!(
964 action_for_error(RETRY_ERROR_INVALID_MESSAGE),
965 "Delete stale session, resend message"
966 );
967 }
968
969 #[test]
970 fn test_session_establishment_lookup_normalization() {
971 use std::collections::HashMap;
972 use wacore_binary::Jid;
973
974 let mut prekey_bundles: HashMap<Jid, ()> = HashMap::new(); let normalized_jid = Jid::lid("123456789"); prekey_bundles.insert(normalized_jid.clone(), ());
980
981 let mut requested_jid = Jid::lid("123456789");
984 requested_jid.agent = 1;
985
986 assert!(
990 prekey_bundles.contains_key(&requested_jid),
991 "an inert agent must not hide the bundle"
992 );
993 assert_eq!(requested_jid, normalized_jid);
994 }
995}