1use std::sync::Arc;
13
14use anyhow::Result;
15use log::debug;
16use wacore::iq::usync::LidQuerySpec;
17use wacore::store::traits::{LidPnMappingEntry, SignalStore};
18use wacore_binary::Jid;
19
20use super::Client;
21use crate::lid_pn_cache::{LearningSource, LidPnEntry};
22
23const MIGRATION_DEVICE_RANGE: u16 = 100;
28
29fn mapping_to_entry(m: LidPnMappingEntry) -> LidPnEntry {
31 LidPnEntry::with_timestamp(
32 m.lid,
33 m.phone_number,
34 m.created_at,
35 LearningSource::parse(&m.learning_source),
36 )
37}
38
39fn lid_pn_write_policy(source: LearningSource, lid_unseen: bool, exact: bool) -> (bool, bool) {
56 let lid_known_mismatch = !lid_unseen && !exact;
57 match source {
58 LearningSource::Usync => (lid_unseen || lid_known_mismatch, false),
61 LearningSource::PeerPnMessage
63 | LearningSource::PeerLidMessage
64 | LearningSource::RecipientLatestLid
65 | LearningSource::MigrationSyncLatest
66 | LearningSource::MigrationSyncOld
67 | LearningSource::BlocklistActive
68 | LearningSource::BlocklistInactive => (!exact, false),
69 LearningSource::Other | LearningSource::Pairing | LearningSource::DeviceNotification => {
75 (lid_unseen, lid_known_mismatch)
76 }
77 }
78}
79
80fn is_stale_source(source: LearningSource) -> bool {
86 matches!(
87 source,
88 LearningSource::MigrationSyncOld | LearningSource::BlocklistInactive
89 )
90}
91
92enum RecordOutcome {
94 Skipped,
96 Written {
100 entry: LidPnEntry,
101 needs_migration: bool,
102 },
103 NeedsUsync,
106}
107
108struct BatchRecordOutcome {
111 entries: Vec<LidPnEntry>,
112 migration_flags: Vec<bool>,
113 usync_phones: Vec<String>,
114}
115
116impl Client {
117 #[cfg_attr(
121 feature = "tracing",
122 tracing::instrument(
123 name = "wa.session.warm_up_lid_pn_cache",
124 level = "debug",
125 skip_all,
126 err(Debug)
127 )
128 )]
129 pub(crate) async fn warm_up_lid_pn_cache(&self) -> Result<(), anyhow::Error> {
130 let backend = self.persistence_manager.backend();
131 let entries = backend.get_all_lid_mappings().await?;
132
133 if entries.is_empty() {
134 debug!("LID-PN cache warm-up: no entries found in storage");
135 return Ok(());
136 }
137
138 self.lid_pn_cache
139 .warm_up(entries.into_iter().map(mapping_to_entry))
140 .await;
141 Ok(())
142 }
143
144 #[cfg_attr(
157 feature = "tracing",
158 tracing::instrument(
159 name = "wa.session.add_lid_pn_mapping",
160 level = "debug",
161 skip_all,
162 err(Debug)
163 )
164 )]
165 pub async fn add_lid_pn_mapping(
166 &self,
167 lid: &str,
168 phone_number: &str,
169 source: LearningSource,
170 ) -> Result<()> {
171 match self
172 .record_lid_pn_in_memory(lid, phone_number, source)
173 .await
174 {
175 RecordOutcome::Skipped => Ok(()),
176 RecordOutcome::NeedsUsync => {
177 self.spawn_lid_usync_reconcile(vec![phone_number.to_string()]);
178 Ok(())
179 }
180 RecordOutcome::Written {
181 entry,
182 needs_migration,
183 } => {
184 self.persist_and_migrate_lid_pn(entry, needs_migration)
185 .await
186 }
187 }
188 }
189
190 pub async fn add_lid_pn_mappings(
193 &self,
194 mappings: Vec<(String, String)>,
195 source: LearningSource,
196 ) -> Result<usize> {
197 let BatchRecordOutcome {
198 entries,
199 migration_flags,
200 usync_phones,
201 } = self.record_lid_pn_batch_in_memory(mappings, source).await;
202 self.spawn_lid_usync_reconcile(usync_phones);
203
204 let count = entries.len();
205 if !entries.is_empty() {
206 self.persist_and_migrate_lid_pn_batch(entries, migration_flags)
207 .await?;
208 }
209 Ok(count)
210 }
211
212 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.session.learn_lid_pn_fast", level = "trace", skip_all, fields(is_offline = is_offline)))]
235 pub(crate) async fn learn_lid_pn_mapping_fast(
236 self: &Arc<Self>,
237 lid: &str,
238 phone_number: &str,
239 source: LearningSource,
240 is_offline: bool,
241 ) {
242 let (entry, needs_migration) = match self
243 .record_lid_pn_in_memory(lid, phone_number, source)
244 .await
245 {
246 RecordOutcome::Skipped => return,
247 RecordOutcome::NeedsUsync => {
248 self.spawn_lid_usync_reconcile(vec![phone_number.to_string()]);
249 return;
250 }
251 RecordOutcome::Written {
252 entry,
253 needs_migration,
254 } => (entry, needs_migration),
255 };
256 if is_offline {
257 return;
258 }
259 let client = Arc::clone(self);
260 self.runtime
261 .spawn(Box::pin(async move {
262 if let Err(err) = client
263 .persist_and_migrate_lid_pn(entry, needs_migration)
264 .await
265 {
266 log::warn!("Background LID-PN persist failed: {err}");
267 }
268 }))
269 .detach();
270 }
271
272 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.session.learn_lid_pn_batch", level = "debug", skip_all, fields(count = mappings.len(), is_offline = is_offline)))]
290 pub(crate) async fn learn_lid_pn_mappings_batch(
291 self: &Arc<Self>,
292 mappings: Vec<(String, String)>,
293 source: LearningSource,
294 is_offline: bool,
295 ) {
296 let outcome = self.record_lid_pn_batch_in_memory(mappings, source).await;
297 self.finish_lid_pn_batch_learning(outcome, is_offline);
298 }
299
300 pub(crate) async fn learn_lid_pn_mappings_batch_guarded(
301 self: &Arc<Self>,
302 mappings: Vec<(String, String)>,
303 source: LearningSource,
304 is_offline: bool,
305 guard: &crate::lid_pn_cache::LidPnMutationGuard<'_>,
306 ) {
307 let outcome = self
308 .record_lid_pn_batch_in_memory_guarded(mappings, source, guard)
309 .await;
310 self.finish_lid_pn_batch_learning(outcome, is_offline);
311 }
312
313 fn finish_lid_pn_batch_learning(
314 self: &Arc<Self>,
315 outcome: BatchRecordOutcome,
316 is_offline: bool,
317 ) {
318 let BatchRecordOutcome {
319 entries,
320 migration_flags,
321 usync_phones,
322 } = outcome;
323
324 self.spawn_lid_usync_reconcile(usync_phones);
328
329 if is_offline || entries.is_empty() {
331 return;
332 }
333
334 let client = Arc::clone(self);
335 self.runtime
336 .spawn(Box::pin(async move {
337 if let Err(err) = client
338 .persist_and_migrate_lid_pn_batch(entries, migration_flags)
339 .await
340 {
341 log::warn!("Background LID-PN batch persist failed: {err}");
342 }
343 }))
344 .detach();
345 }
346
347 fn spawn_lid_usync_reconcile(&self, phones: Vec<String>) {
354 if phones.is_empty() {
355 return;
356 }
357 let Some(client) = self.self_weak.get().and_then(|w| w.upgrade()) else {
358 return;
359 };
360 let runtime = client.runtime.clone();
361 runtime
362 .spawn(Box::pin(async move {
363 client.reconcile_lid_mappings_via_usync(phones).await;
364 }))
365 .detach();
366 }
367
368 async fn reconcile_lid_mappings_via_usync(&self, phones: Vec<String>) {
369 let jids: Vec<Jid> = phones.iter().map(|p| Jid::pn(p.as_str())).collect();
370 let sid = self.generate_request_id();
371 match self.execute(LidQuerySpec::new(jids, sid)).await {
372 Ok(resp) => {
373 for mapping in &resp.lid_mappings {
374 if let Err(err) = self
375 .add_lid_pn_mapping(
376 &mapping.lid,
377 &mapping.phone_number,
378 LearningSource::Usync,
379 )
380 .await
381 {
382 log::warn!(
383 "LID reconcile persist failed for {} -> {}: {err}",
384 mapping.phone_number,
385 mapping.lid
386 );
387 }
388 }
389 }
390 Err(err) => debug!("LID reconcile usync query failed: {err}"),
391 }
392 }
393
394 async fn record_lid_pn_batch_in_memory(
404 &self,
405 mappings: Vec<(String, String)>,
406 source: LearningSource,
407 ) -> BatchRecordOutcome {
408 let guard = self.lid_pn_cache.lock_mutation().await;
409 self.record_lid_pn_batch_in_memory_guarded(mappings, source, &guard)
410 .await
411 }
412
413 async fn record_lid_pn_batch_in_memory_guarded(
414 &self,
415 mappings: Vec<(String, String)>,
416 source: LearningSource,
417 guard: &crate::lid_pn_cache::LidPnMutationGuard<'_>,
418 ) -> BatchRecordOutcome {
419 let cap = mappings.len();
420 let mut deduped: std::collections::HashMap<String, String> =
421 std::collections::HashMap::with_capacity(cap);
422 for (lid, phone_number) in mappings {
423 deduped.insert(phone_number, lid);
424 }
425
426 let mut entries: Vec<LidPnEntry> = Vec::with_capacity(deduped.len());
427 let mut migration_flags: Vec<bool> = Vec::with_capacity(deduped.len());
428 let mut usync_phones: Vec<String> = Vec::new();
429 for (phone_number, lid) in deduped {
430 match self
431 .record_lid_pn_in_memory_guarded(&lid, &phone_number, source, guard)
432 .await
433 {
434 RecordOutcome::Skipped => {}
435 RecordOutcome::Written {
436 entry,
437 needs_migration,
438 } => {
439 entries.push(entry);
440 migration_flags.push(needs_migration);
441 }
442 RecordOutcome::NeedsUsync => usync_phones.push(phone_number),
443 }
444 }
445 BatchRecordOutcome {
446 entries,
447 migration_flags,
448 usync_phones,
449 }
450 }
451
452 async fn record_lid_pn_in_memory(
456 &self,
457 lid: &str,
458 phone_number: &str,
459 source: LearningSource,
460 ) -> RecordOutcome {
461 let guard = self.lid_pn_cache.lock_mutation().await;
462 self.record_lid_pn_in_memory_guarded(lid, phone_number, source, &guard)
463 .await
464 }
465
466 async fn record_lid_pn_in_memory_guarded(
467 &self,
468 lid: &str,
469 phone_number: &str,
470 source: LearningSource,
471 guard: &crate::lid_pn_cache::LidPnMutationGuard<'_>,
472 ) -> RecordOutcome {
473 if self.lid_pn_cache.can_skip_relearn(phone_number, lid).await {
475 return RecordOutcome::Skipped;
476 }
477
478 let current_lid = self.lid_pn_cache.get_current_lid(phone_number).await;
479 let reverse_pn = self.lid_pn_cache.get_phone_number(lid).await;
480 let exact = current_lid.as_deref() == Some(lid);
481
482 let same_pair_forward_evicted =
488 current_lid.is_none() && reverse_pn.as_deref() == Some(phone_number);
489 if exact || same_pair_forward_evicted {
490 let needs_migration = !self.lid_pn_cache.is_persisted(phone_number, lid).await;
493 let existing = match self.lid_pn_cache.get_entry_by_phone(phone_number).await {
494 Some(entry) => Some(entry),
495 None => self.lid_pn_cache.get_entry_by_lid(lid).await,
496 };
497 return match existing {
498 Some(entry) => {
499 self.lid_pn_cache.add_guarded(&entry, guard).await;
500 RecordOutcome::Written {
501 entry,
502 needs_migration,
503 }
504 }
505 None => RecordOutcome::Skipped,
506 };
507 }
508
509 let lid_unseen = reverse_pn.is_none();
511 let (write, needs_usync) = lid_pn_write_policy(source, lid_unseen, exact);
512
513 if write {
514 let created_at = if is_stale_source(source) {
515 0
516 } else {
517 wacore::time::now_secs()
518 };
519 let entry = LidPnEntry::with_timestamp(lid, phone_number, created_at, source);
520 self.lid_pn_cache.add_guarded(&entry, guard).await;
521 return RecordOutcome::Written {
522 entry,
523 needs_migration: current_lid.is_none(),
524 };
525 }
526
527 if needs_usync {
530 return RecordOutcome::NeedsUsync;
531 }
532 RecordOutcome::Skipped
533 }
534
535 #[cfg_attr(
536 feature = "tracing",
537 tracing::instrument(
538 name = "wa.session.persist_migrate_lid_pn",
539 level = "debug",
540 skip_all,
541 fields(needs_migration),
542 err(Debug)
543 )
544 )]
545 async fn persist_and_migrate_lid_pn(
546 &self,
547 entry: LidPnEntry,
548 needs_migration: bool,
549 ) -> Result<()> {
550 use anyhow::anyhow;
551
552 let storage_entry = LidPnMappingEntry {
553 lid: entry.lid.to_string(),
554 phone_number: entry.phone_number.to_string(),
555 created_at: entry.created_at,
556 updated_at: entry.created_at,
557 learning_source: entry.learning_source.as_str().to_string(),
558 };
559
560 self.persistence_manager
561 .backend()
562 .put_lid_mapping(&storage_entry)
563 .await
564 .map_err(|e| anyhow!("persisting LID-PN mapping: {e}"))?;
565
566 self.lid_pn_cache
569 .mark_persisted(&storage_entry.phone_number, &storage_entry.lid)
570 .await;
571
572 if needs_migration {
573 self.migrate_device_registry_on_lid_discovery(
574 &storage_entry.phone_number,
575 &storage_entry.lid,
576 )
577 .await;
578 self.migrate_signal_sessions_on_lid_discovery(
579 &storage_entry.phone_number,
580 &storage_entry.lid,
581 )
582 .await;
583 }
584
585 Ok(())
586 }
587
588 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.session.persist_migrate_lid_pn_batch", level = "debug", skip_all, fields(count = entries.len()), err(Debug)))]
589 async fn persist_and_migrate_lid_pn_batch(
590 &self,
591 entries: Vec<LidPnEntry>,
592 migration_flags: Vec<bool>,
593 ) -> Result<()> {
594 let storage = self.persist_lid_pn_batch(entries).await?;
595 self.migrate_lid_pn_batch(storage, migration_flags).await;
596 Ok(())
597 }
598
599 async fn persist_lid_pn_batch(
602 &self,
603 entries: Vec<LidPnEntry>,
604 ) -> Result<Vec<LidPnMappingEntry>> {
605 use anyhow::anyhow;
606
607 let storage: Vec<LidPnMappingEntry> = entries
611 .into_iter()
612 .map(|entry| LidPnMappingEntry {
613 lid: entry.lid.to_string(),
614 phone_number: entry.phone_number.to_string(),
615 created_at: entry.created_at,
616 updated_at: entry.created_at,
617 learning_source: entry.learning_source.as_str().to_string(),
618 })
619 .collect();
620
621 self.persistence_manager
622 .backend()
623 .put_lid_mappings(&storage)
624 .await
625 .map_err(|e| anyhow!("persisting LID-PN mapping batch: {e}"))?;
626
627 for entry in &storage {
628 self.lid_pn_cache
629 .mark_persisted(&entry.phone_number, &entry.lid)
630 .await;
631 }
632 Ok(storage)
633 }
634
635 async fn migrate_lid_pn_batch(
640 &self,
641 storage: Vec<LidPnMappingEntry>,
642 migration_flags: Vec<bool>,
643 ) {
644 for (entry, needs_migration) in storage.iter().zip(migration_flags.iter()) {
645 if *needs_migration {
646 self.migrate_device_registry_on_lid_discovery(&entry.phone_number, &entry.lid)
647 .await;
648 self.migrate_signal_sessions_on_lid_discovery(&entry.phone_number, &entry.lid)
649 .await;
650 }
651 }
652 }
653
654 pub(crate) async fn resolve_lid_mappings(&self, jids: &[Jid]) -> Vec<Jid> {
661 let mut resolved = Vec::with_capacity(jids.len());
662
663 for jid in jids {
664 if !jid.is_pn() && !jid.is_lid() {
666 resolved.push(jid.clone());
667 continue;
668 }
669
670 if jid.is_lid() {
672 resolved.push(jid.clone());
673 continue;
674 }
675
676 if let Some(lid_user) = self.lid_pn_cache.get_current_lid(&jid.user).await {
678 resolved.push(Jid::lid_device(lid_user, jid.device));
679 } else {
680 resolved.push(jid.clone());
683 }
684 }
685
686 resolved
687 }
688
689 pub(crate) async fn resolve_encryption_jid(&self, target: &Jid) -> Jid {
698 use wacore_binary::Server;
699 let lid_server = match target.server {
700 Server::Pn => Server::Lid,
701 Server::Hosted => Server::HostedLid,
702 _ => return target.clone(),
703 };
704 match self.lid_pn_cache.get_current_lid(&target.user).await {
705 Some(lid_user) => Jid {
706 user: lid_user,
707 server: lid_server,
708 device: target.device,
709 agent: target.agent,
710 integrator: target.integrator,
711 },
712 None => target.clone(),
713 }
714 }
715
716 pub async fn is_lid_migrated(&self) -> bool {
721 if self.persistence_manager.get_device_snapshot().lid_migrated {
722 return true;
723 }
724 self.ab_props()
725 .is_enabled(wacore::iq::abprops::web::LID_ONE_ON_ONE_MIGRATION_ENABLED)
726 .await
727 }
728
729 pub(crate) async fn latch_lid_migrated_from_props(&self) {
735 if !self.persistence_manager.get_device_snapshot().lid_migrated
736 && self
737 .ab_props()
738 .is_enabled(wacore::iq::abprops::web::LID_ONE_ON_ONE_MIGRATION_ENABLED)
739 .await
740 {
741 log::info!("Account is 1:1-LID-migrated (ab prop observation)");
742 self.persistence_manager
743 .process_command(crate::store::commands::DeviceCommand::SetLidMigrated(true))
744 .await;
745 }
746 }
747
748 pub(crate) async fn resolve_dm_wire_jid(&self, to: &Jid) -> Jid {
760 if self.is_lid_migrated().await {
761 return self.resolve_encryption_jid(to).await.into_non_ad();
762 }
763 let bare = to.to_non_ad();
764 if bare.is_lid() {
765 self.swap_pn_lid_namespace(&bare).await.unwrap_or(bare)
766 } else {
767 bare
768 }
769 }
770
771 pub(crate) async fn handle_lid_migration_mapping_sync(
777 self: &Arc<Self>,
778 sync: &waproto::whatsapp::LIDMigrationMappingSyncMessage,
779 ) {
780 let Some(payload_bytes) = sync.encoded_mapping_payload.as_deref() else {
781 log::warn!("lid_migration_mapping_sync without payload");
782 return;
783 };
784 let payload = match waproto::codec::lid_migration_mapping_sync_payload_decode(payload_bytes)
785 {
786 Ok(p) => p,
787 Err(e) => {
788 log::warn!("Failed to decode LID migration mapping payload: {e}");
789 return;
790 }
791 };
792
793 let mappings: Vec<(String, String)> = payload
794 .pn_to_lid_mappings
795 .iter()
796 .filter_map(|mapping| {
797 let lid = mapping
802 .latest_lid
803 .filter(|&l| l != 0)
804 .unwrap_or(mapping.assigned_lid);
805 if mapping.pn == 0 || lid == 0 {
806 log::warn!("Skipping migration mapping with zero pn/lid");
807 return None;
808 }
809 Some((lid.to_string(), mapping.pn.to_string()))
810 })
811 .collect();
812 let BatchRecordOutcome {
820 entries,
821 migration_flags,
822 usync_phones,
823 } = self
824 .record_lid_pn_batch_in_memory(mappings, LearningSource::MigrationSyncLatest)
825 .await;
826 self.spawn_lid_usync_reconcile(usync_phones);
827 if !entries.is_empty() {
828 match self.persist_lid_pn_batch(entries).await {
829 Ok(storage) => {
830 let client = Arc::clone(self);
836 self.runtime
837 .spawn(Box::pin(async move {
838 client.migrate_lid_pn_batch(storage, migration_flags).await;
839 }))
840 .detach();
841 }
842 Err(e) => {
843 log::warn!("Failed to persist migration mappings: {e:?}");
848 return;
849 }
850 }
851 }
852
853 if !self.persistence_manager.get_device_snapshot().lid_migrated
854 && self
855 .ab_props()
856 .is_enabled(wacore::iq::abprops::web::LID_ONE_ON_ONE_MIGRATION_ENABLED)
857 .await
858 {
859 log::info!("Account is 1:1-LID-migrated (primary mapping sync)");
860 self.persistence_manager
861 .process_command(crate::store::commands::DeviceCommand::SetLidMigrated(true))
862 .await;
863 }
864 }
865
866 pub(crate) async fn swap_pn_lid_namespace(&self, jid: &Jid) -> Option<Jid> {
869 if jid.is_lid() {
870 let pn_user = self.lid_pn_cache.get_phone_number(&jid.user).await?;
871 Some(Jid {
872 user: pn_user.into(),
873 server: wacore_binary::Server::Pn,
874 device: jid.device,
875 agent: jid.agent,
876 integrator: jid.integrator,
877 })
878 } else if jid.is_pn() {
879 let lid_user = self.lid_pn_cache.get_current_lid(&jid.user).await?;
880 Some(Jid {
881 user: lid_user,
882 server: wacore_binary::Server::Lid,
883 device: jid.device,
884 agent: jid.agent,
885 integrator: jid.integrator,
886 })
887 } else {
888 None
889 }
890 }
891
892 #[cfg_attr(
909 feature = "tracing",
910 tracing::instrument(
911 name = "wa.session.migrate_signal_sessions",
912 level = "debug",
913 skip_all
914 )
915 )]
916 pub(crate) async fn migrate_signal_sessions_on_lid_discovery(
917 &self,
918 pn: &str,
919 lid: &str,
920 ) -> bool {
921 use log::warn;
922
923 let backend = self.persistence_manager.backend();
924 if let Ok(false) = self
925 .signal_cache
926 .has_state_for_user(pn, backend.as_ref())
927 .await
928 {
929 return false;
930 }
931
932 let standard = self
933 .migrate_signal_sessions_with_backend(&Jid::pn(pn), &Jid::lid(lid), backend.as_ref())
934 .await;
935 let hosted = self
936 .migrate_signal_sessions_with_backend(
937 &Jid::new(pn, wacore_binary::Server::Hosted),
938 &Jid::new(lid, wacore_binary::Server::HostedLid),
939 backend.as_ref(),
940 )
941 .await;
942 let migrated_sessions = standard.migrated != 0 || hosted.migrated != 0;
943 if (standard.has_state_changes()
944 || hosted.has_state_changes()
945 || self
946 .signal_cache
947 .has_pending_pairwise_writes_for_user(pn)
948 .await)
949 && let Err(error) = self.signal_cache.flush(backend.as_ref()).await
950 {
951 warn!("Failed to flush signal cache after migration: {error:?}");
952 }
953 migrated_sessions
954 }
955
956 pub(crate) async fn migrate_signal_sessions(
957 &self,
958 from: &Jid,
959 to: &Jid,
960 ) -> crate::features::SignalSessionMigration {
961 let backend = self.persistence_manager.backend();
962
963 if let Ok(false) = self
968 .signal_cache
969 .has_state_for_user(&from.user, backend.as_ref())
970 .await
971 {
972 return crate::features::SignalSessionMigration::default();
973 }
974
975 self.migrate_signal_sessions_with_backend(from, to, backend.as_ref())
976 .await
977 }
978
979 async fn migrate_signal_sessions_with_backend(
984 &self,
985 from: &Jid,
986 to: &Jid,
987 backend: &dyn SignalStore,
988 ) -> crate::features::SignalSessionMigration {
989 use log::{info, warn};
990 use wacore::types::jid::JidExt;
991
992 let mut outcome = crate::features::SignalSessionMigration::default();
993
994 for device_id in 0..MIGRATION_DEVICE_RANGE {
995 let pn_jid = from.with_device(device_id);
996 let lid_jid = to.with_device(device_id);
997
998 let pn_proto = pn_jid.to_protocol_address();
999 let lid_proto = lid_jid.to_protocol_address();
1000
1001 let pn_lock = self.session_lock_for(pn_proto.as_str()).await;
1005 let lid_lock = self.session_lock_for(lid_proto.as_str()).await;
1006 let (_first_guard, _second_guard) = if pn_proto.as_str() <= lid_proto.as_str() {
1007 let pn_g = pn_lock.lock_arc().await;
1008 let lid_g = lid_lock.lock_arc().await;
1009 (pn_g, lid_g)
1010 } else {
1011 let lid_g = lid_lock.lock_arc().await;
1012 let pn_g = pn_lock.lock_arc().await;
1013 (lid_g, pn_g)
1014 };
1015
1016 match self.signal_cache.get_session(&pn_proto, backend).await {
1019 Ok(Some(session)) => {
1020 outcome.total += 1;
1021 self.signal_cache.put_session(&lid_proto, session).await;
1022 self.signal_cache.delete_session(&pn_proto).await;
1023 outcome.migrated += 1;
1024 info!(
1025 "Migrated session {} -> {} (PN wins on conflict)",
1026 pn_proto, lid_proto
1027 );
1028 }
1029 Ok(None) => {}
1030 Err(error) => {
1031 outcome.total += 1;
1032 outcome.skipped += 1;
1033 warn!("Skipping session migration for {}: {error:?}", pn_proto);
1034 }
1035 }
1036
1037 match self.signal_cache.get_identity(&pn_proto, backend).await {
1049 Ok(Some(identity_data)) => {
1050 match self.signal_cache.get_identity(&lid_proto, backend).await {
1051 Ok(None) => {
1052 self.signal_cache
1053 .put_identity(&lid_proto, &identity_data)
1054 .await;
1055 self.signal_cache.delete_identity(&pn_proto).await;
1056 outcome.migrated_identities += 1;
1057 info!("Migrated identity {} -> {}", pn_proto, lid_proto);
1058 }
1059 Ok(Some(_)) => {
1060 self.signal_cache.delete_identity(&pn_proto).await;
1062 outcome.discarded_identities += 1;
1063 }
1064 Err(e) => {
1065 outcome.skipped_identities += 1;
1066 warn!(
1067 "Skipping identity migration {} -> {}: \
1068 failed to read LID identity: {e:?}",
1069 pn_proto, lid_proto
1070 );
1071 }
1072 }
1073 }
1074 Ok(None) => {}
1075 Err(error) => {
1076 outcome.skipped_identities += 1;
1077 warn!("Skipping identity migration for {}: {error:?}", pn_proto);
1078 }
1079 }
1080 }
1081
1082 outcome
1083 }
1084
1085 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.session.get_lid_pn_entry", level = "trace", skip_all, fields(peer = %jid.observe()), err(Debug)))]
1092 pub async fn get_lid_pn_entry(&self, jid: &Jid) -> Result<Option<LidPnEntry>> {
1093 let is_lid = if jid.is_lid() {
1094 true
1095 } else if jid.is_pn() {
1096 false
1097 } else {
1098 return Ok(None);
1099 };
1100
1101 self.get_lid_pn_entry_by_user(&jid.user, is_lid).await
1102 }
1103
1104 async fn get_lid_pn_entry_by_user(
1105 &self,
1106 user: &str,
1107 is_lid: bool,
1108 ) -> Result<Option<LidPnEntry>> {
1109 let hit = if is_lid {
1110 self.lid_pn_cache.get_entry_by_lid(user).await
1111 } else {
1112 self.lid_pn_cache.get_entry_by_phone(user).await
1113 };
1114
1115 if let Some(entry) = hit {
1116 return Ok(Some(entry));
1117 }
1118
1119 let backend = self.persistence_manager.backend();
1120 let mapping = if is_lid {
1121 backend.get_lid_mapping(user).await?
1122 } else {
1123 backend.get_pn_mapping(user).await?
1124 };
1125
1126 let Some(mapping) = mapping else {
1127 return Ok(None);
1128 };
1129
1130 let entry = mapping_to_entry(mapping);
1131 self.lid_pn_cache.add(&entry).await;
1132 Ok(Some(entry))
1133 }
1134
1135 pub(crate) async fn jids_share_user_identity(&self, left: &Jid, right: &Jid) -> Result<bool> {
1140 if left.is_same_chat_as(right) {
1141 return Ok(true);
1142 }
1143
1144 let same_user_and_integrator =
1145 left.user == right.user && left.integrator == right.integrator;
1146 if same_user_and_integrator
1147 && ((left.server.is_pn_family() && right.server.is_pn_family())
1148 || (left.server.is_lid_family() && right.server.is_lid_family()))
1149 {
1150 return Ok(true);
1151 }
1152
1153 let (lid, pn) = if left.server.is_lid_family() && right.server.is_pn_family() {
1154 (left, right)
1155 } else if right.server.is_lid_family() && left.server.is_pn_family() {
1156 (right, left)
1157 } else {
1158 return Ok(false);
1159 };
1160 if lid.integrator != pn.integrator {
1161 return Ok(false);
1162 }
1163
1164 Ok(self
1165 .get_lid_pn_entry_by_user(&lid.user, true)
1166 .await?
1167 .is_some_and(|mapping| {
1168 &*mapping.lid == lid.user.as_str() && &*mapping.phone_number == pn.user.as_str()
1169 }))
1170 }
1171
1172 pub(crate) async fn resolve_recipient_to_lid(&self, jid: &Jid) -> Option<Jid> {
1180 if jid.is_lid() {
1181 return Some(jid.to_non_ad());
1182 }
1183 if !jid.is_pn() {
1184 return None;
1185 }
1186 match self.get_lid_pn_entry(jid).await {
1187 Ok(Some(entry)) => Some(Jid::new(&*entry.lid, wacore_binary::Server::Lid)),
1188 Ok(None) => None,
1189 Err(e) => {
1190 log::warn!(
1191 "resolve_recipient_to_lid: LID lookup for {} failed: {:?}",
1192 jid.observe(),
1193 e
1194 );
1195 None
1196 }
1197 }
1198 }
1199}
1200
1201#[cfg(test)]
1202#[allow(clippy::disallowed_methods)]
1203mod tests {
1204 use super::*;
1205 use crate::lid_pn_cache::LearningSource;
1206 use crate::test_utils::{create_test_client, create_test_client_with_backend};
1207 use std::sync::Arc;
1208 use wacore::store::in_memory::InMemoryBackend;
1209 use wacore::store::traits::SignalStore;
1210 use wacore_binary::Server;
1211
1212 async fn client_with_peer_mapping() -> (Arc<Client>, &'static str, &'static str) {
1214 let client = create_test_client().await;
1215 let pn = "5511987650001";
1216 let lid = "111000011112222";
1217 client
1218 .add_lid_pn_mapping(lid, pn, LearningSource::PeerPnMessage)
1219 .await
1220 .unwrap();
1221 (client, pn, lid)
1222 }
1223
1224 const ALL_SOURCES: [LearningSource; 11] = [
1228 LearningSource::Usync,
1229 LearningSource::PeerPnMessage,
1230 LearningSource::PeerLidMessage,
1231 LearningSource::RecipientLatestLid,
1232 LearningSource::MigrationSyncLatest,
1233 LearningSource::MigrationSyncOld,
1234 LearningSource::BlocklistActive,
1235 LearningSource::BlocklistInactive,
1236 LearningSource::Pairing,
1237 LearningSource::DeviceNotification,
1238 LearningSource::Other,
1239 ];
1240
1241 #[test]
1246 fn test_lid_pn_write_policy_switch_matrix() {
1247 for src in ALL_SOURCES {
1249 assert_eq!(
1250 lid_pn_write_policy(src, true, false),
1251 (true, false),
1252 "a brand-new LID must always be written, never re-queried ({src:?})"
1253 );
1254 }
1255
1256 for src in ALL_SOURCES {
1258 assert_eq!(
1259 lid_pn_write_policy(src, false, true),
1260 (false, false),
1261 "an exact match is a no-op ({src:?})"
1262 );
1263 }
1264
1265 let directed = [
1268 LearningSource::Usync,
1269 LearningSource::PeerPnMessage,
1270 LearningSource::PeerLidMessage,
1271 LearningSource::RecipientLatestLid,
1272 LearningSource::MigrationSyncLatest,
1273 LearningSource::MigrationSyncOld,
1274 LearningSource::BlocklistActive,
1275 LearningSource::BlocklistInactive,
1276 ];
1277 for src in directed {
1278 assert_eq!(
1279 lid_pn_write_policy(src, false, false),
1280 (true, false),
1281 "a directed source overwrites a conflicting known LID ({src:?})"
1282 );
1283 }
1284 for src in [
1285 LearningSource::Other,
1286 LearningSource::Pairing,
1287 LearningSource::DeviceNotification,
1288 ] {
1289 assert_eq!(
1290 lid_pn_write_policy(src, false, false),
1291 (false, true),
1292 "an observational source must not clobber; it re-queries ({src:?})"
1293 );
1294 }
1295 }
1296
1297 #[test]
1298 fn test_is_stale_source() {
1299 assert!(is_stale_source(LearningSource::MigrationSyncOld));
1300 assert!(is_stale_source(LearningSource::BlocklistInactive));
1301 for src in ALL_SOURCES {
1304 if matches!(
1305 src,
1306 LearningSource::MigrationSyncOld | LearningSource::BlocklistInactive
1307 ) {
1308 continue;
1309 }
1310 assert!(!is_stale_source(src), "{src:?} is not stale");
1311 }
1312 }
1313
1314 #[test]
1319 fn all_sources_is_exhaustive() {
1320 fn arm_count(s: LearningSource) -> usize {
1321 match s {
1323 LearningSource::Usync
1324 | LearningSource::PeerPnMessage
1325 | LearningSource::PeerLidMessage
1326 | LearningSource::RecipientLatestLid
1327 | LearningSource::MigrationSyncLatest
1328 | LearningSource::MigrationSyncOld
1329 | LearningSource::BlocklistActive
1330 | LearningSource::BlocklistInactive
1331 | LearningSource::Pairing
1332 | LearningSource::DeviceNotification
1333 | LearningSource::Other => 11,
1334 }
1335 }
1336 assert_eq!(
1337 ALL_SOURCES.len(),
1338 arm_count(LearningSource::Other),
1339 "add the new LearningSource variant to ALL_SOURCES (and the match above)"
1340 );
1341 for (idx, src) in ALL_SOURCES.iter().enumerate() {
1344 assert!(
1345 !ALL_SOURCES[..idx].contains(src),
1346 "ALL_SOURCES contains duplicate {src:?}"
1347 );
1348 }
1349 }
1350
1351 #[tokio::test]
1355 async fn test_record_observational_preserves_conflicting_known_lid() {
1356 let client = create_test_client().await;
1357 let phone = "5511900000001";
1358 let lid_live = "200000000000001";
1359 let lid_other = "200000000000002";
1360 client
1361 .add_lid_pn_mapping(lid_live, phone, LearningSource::Usync)
1362 .await
1363 .unwrap();
1364 client
1366 .add_lid_pn_mapping(lid_other, "5511900000099", LearningSource::Usync)
1367 .await
1368 .unwrap();
1369
1370 let outcome = client
1371 .record_lid_pn_in_memory(lid_other, phone, LearningSource::Other)
1372 .await;
1373
1374 assert!(
1375 matches!(outcome, RecordOutcome::NeedsUsync),
1376 "observational conflict must request a usync, not clobber"
1377 );
1378 assert_eq!(
1379 client.lid_pn_cache.get_current_lid(phone).await.as_deref(),
1380 Some(lid_live),
1381 "the live mapping must survive an observational conflict"
1382 );
1383 }
1384
1385 #[tokio::test]
1388 async fn test_record_directed_overwrites_conflicting_known_lid() {
1389 let client = create_test_client().await;
1390 let phone = "5511900000010";
1391 let lid_old = "200000000000010";
1392 let lid_new = "200000000000020";
1393 client
1394 .add_lid_pn_mapping(lid_old, phone, LearningSource::Usync)
1395 .await
1396 .unwrap();
1397 client
1398 .add_lid_pn_mapping(lid_new, "5511900000098", LearningSource::Usync)
1399 .await
1400 .unwrap();
1401
1402 let outcome = client
1403 .record_lid_pn_in_memory(lid_new, phone, LearningSource::PeerPnMessage)
1404 .await;
1405
1406 assert!(matches!(
1407 outcome,
1408 RecordOutcome::Written {
1409 needs_migration: false,
1410 ..
1411 }
1412 ));
1413 assert_eq!(
1414 client.lid_pn_cache.get_current_lid(phone).await.as_deref(),
1415 Some(lid_new),
1416 "a directed source must overwrite a conflicting known LID"
1417 );
1418 }
1419
1420 #[tokio::test]
1423 async fn test_record_observational_seeds_new_lid_over_existing() {
1424 let client = create_test_client().await;
1425 let phone = "5511900000030";
1426 let lid_old = "200000000000030";
1427 let lid_brand_new = "200000000000031";
1428 client
1429 .add_lid_pn_mapping(lid_old, phone, LearningSource::Usync)
1430 .await
1431 .unwrap();
1432
1433 let outcome = client
1434 .record_lid_pn_in_memory(lid_brand_new, phone, LearningSource::Other)
1435 .await;
1436
1437 assert!(matches!(outcome, RecordOutcome::Written { .. }));
1438 assert_eq!(
1439 client.lid_pn_cache.get_current_lid(phone).await.as_deref(),
1440 Some(lid_brand_new),
1441 "a brand-new LID is seeded even by an observational source"
1442 );
1443 }
1444
1445 #[tokio::test]
1448 async fn test_record_exact_match_preserves_pending_migration() {
1449 let client = create_test_client().await;
1450 let phone = "5511900000040";
1451 let lid = "200000000000040";
1452 let _ = client
1454 .record_lid_pn_in_memory(lid, phone, LearningSource::Other)
1455 .await;
1456
1457 let outcome = client
1458 .record_lid_pn_in_memory(lid, phone, LearningSource::Other)
1459 .await;
1460
1461 assert!(
1462 matches!(
1463 outcome,
1464 RecordOutcome::Written {
1465 needs_migration: true,
1466 ..
1467 }
1468 ),
1469 "an exact re-learn must retain its pending migration"
1470 );
1471 assert_eq!(
1472 client.lid_pn_cache.get_current_lid(phone).await.as_deref(),
1473 Some(lid)
1474 );
1475 }
1476
1477 #[tokio::test]
1481 async fn test_record_batch_retry_preserves_pending_migration() {
1482 let client = create_test_client().await;
1483 let phone = "5511900000041";
1484 let lid = "200000000000041";
1485 let mapping = || vec![(lid.to_string(), phone.to_string())];
1486
1487 let first = client
1488 .record_lid_pn_batch_in_memory(mapping(), LearningSource::Other)
1489 .await;
1490 assert_eq!(first.migration_flags, vec![true]);
1491
1492 let retry = client
1494 .record_lid_pn_batch_in_memory(mapping(), LearningSource::Other)
1495 .await;
1496 assert_eq!(retry.migration_flags, vec![true]);
1497
1498 client.lid_pn_cache.mark_persisted(phone, lid).await;
1499 let durable = client
1500 .record_lid_pn_batch_in_memory(mapping(), LearningSource::Other)
1501 .await;
1502 assert!(durable.entries.is_empty());
1503 assert!(durable.migration_flags.is_empty());
1504 }
1505
1506 #[tokio::test]
1509 async fn test_stale_source_does_not_outrank_fresh_mapping() {
1510 let client = create_test_client().await;
1511 let phone = "5511900000050";
1512 let lid_fresh = "200000000000050";
1513 let lid_stale = "200000000000051";
1514 client
1515 .add_lid_pn_mapping(lid_fresh, phone, LearningSource::Usync)
1516 .await
1517 .unwrap();
1518
1519 let outcome = client
1520 .record_lid_pn_in_memory(lid_stale, phone, LearningSource::MigrationSyncOld)
1521 .await;
1522
1523 assert!(matches!(outcome, RecordOutcome::Written { .. }));
1524 assert_eq!(
1525 client.lid_pn_cache.get_current_lid(phone).await.as_deref(),
1526 Some(lid_fresh),
1527 "a created_at=0 stale mapping must not outrank a fresh one"
1528 );
1529 assert_eq!(
1531 client
1532 .lid_pn_cache
1533 .get_phone_number(lid_stale)
1534 .await
1535 .as_deref(),
1536 Some(phone)
1537 );
1538 }
1539
1540 #[tokio::test]
1543 async fn test_stale_source_seeds_empty_cache() {
1544 let client = create_test_client().await;
1545 let phone = "5511900000060";
1546 let lid = "200000000000060";
1547
1548 let outcome = client
1549 .record_lid_pn_in_memory(lid, phone, LearningSource::MigrationSyncOld)
1550 .await;
1551
1552 assert!(matches!(
1553 outcome,
1554 RecordOutcome::Written {
1555 needs_migration: true,
1556 ..
1557 }
1558 ));
1559 assert_eq!(
1560 client.lid_pn_cache.get_current_lid(phone).await.as_deref(),
1561 Some(lid)
1562 );
1563 }
1564
1565 #[tokio::test]
1568 async fn test_record_batch_splits_written_and_usync() {
1569 let client = create_test_client().await;
1570 let phone_conflict = "5511900000070";
1571 let lid_live = "200000000000070";
1572 let lid_known = "200000000000071";
1573 let phone_fresh = "5511900000072";
1574 let lid_fresh = "200000000000073";
1575 client
1576 .add_lid_pn_mapping(lid_live, phone_conflict, LearningSource::Usync)
1577 .await
1578 .unwrap();
1579 client
1580 .add_lid_pn_mapping(lid_known, "5511900000079", LearningSource::Usync)
1581 .await
1582 .unwrap();
1583
1584 let outcome = client
1585 .record_lid_pn_batch_in_memory(
1586 vec![
1587 (lid_known.to_string(), phone_conflict.to_string()),
1588 (lid_fresh.to_string(), phone_fresh.to_string()),
1589 ],
1590 LearningSource::Other,
1591 )
1592 .await;
1593
1594 assert_eq!(outcome.usync_phones, vec![phone_conflict.to_string()]);
1595 assert_eq!(outcome.entries.len(), 1);
1596 assert_eq!(&*outcome.entries[0].phone_number, phone_fresh);
1597 assert_eq!(
1598 client
1599 .lid_pn_cache
1600 .get_current_lid(phone_conflict)
1601 .await
1602 .as_deref(),
1603 Some(lid_live),
1604 "the conflicting phone must keep its live LID"
1605 );
1606 }
1607
1608 #[tokio::test]
1611 async fn test_learn_batch_other_preserves_live_mapping() {
1612 let client = create_test_client().await;
1613 let phone = "5511900000080";
1614 let lid_live = "200000000000080";
1615 let lid_hist = "200000000000081";
1616 client
1617 .add_lid_pn_mapping(lid_live, phone, LearningSource::Usync)
1618 .await
1619 .unwrap();
1620 client
1621 .add_lid_pn_mapping(lid_hist, "5511900000089", LearningSource::Usync)
1622 .await
1623 .unwrap();
1624
1625 client
1626 .learn_lid_pn_mappings_batch(
1627 vec![(lid_hist.to_string(), phone.to_string())],
1628 LearningSource::Other,
1629 false,
1630 )
1631 .await;
1632
1633 assert_eq!(
1634 client.lid_pn_cache.get_current_lid(phone).await.as_deref(),
1635 Some(lid_live),
1636 "a history-sync seed must not clobber the live mapping"
1637 );
1638 }
1639
1640 #[tokio::test]
1641 async fn test_latch_lid_migrated_from_props() {
1642 let client: Arc<Client> = create_test_client().await;
1643
1644 client.latch_lid_migrated_from_props().await;
1646 assert!(
1647 !client
1648 .persistence_manager
1649 .get_device_snapshot()
1650 .lid_migrated
1651 );
1652
1653 client
1656 .ab_props()
1657 .apply_props(
1658 false,
1659 std::iter::once((
1660 wacore::iq::abprops::web::LID_ONE_ON_ONE_MIGRATION_ENABLED.code,
1661 "1".into(),
1662 )),
1663 )
1664 .await;
1665 client.latch_lid_migrated_from_props().await;
1666 client
1667 .ab_props()
1668 .apply_props(false, std::iter::empty())
1669 .await;
1670 assert!(
1671 client
1672 .persistence_manager
1673 .get_device_snapshot()
1674 .lid_migrated
1675 );
1676 assert!(client.is_lid_migrated().await);
1677 }
1678
1679 #[tokio::test]
1680 async fn test_resolve_encryption_jid_pn_to_lid() {
1681 let client: Arc<Client> = create_test_client().await;
1682 let pn = "55999999999";
1683 let lid = "100000012345678";
1684
1685 client
1687 .add_lid_pn_mapping(lid, pn, LearningSource::PeerPnMessage)
1688 .await
1689 .unwrap();
1690
1691 let pn_jid = Jid::pn(pn);
1692 let resolved = client.resolve_encryption_jid(&pn_jid).await;
1693
1694 assert_eq!(resolved.user, lid);
1695 assert_eq!(resolved.server, Server::Lid);
1696 }
1697
1698 #[tokio::test]
1699 async fn test_resolve_encryption_jid_preserves_lid() {
1700 let client: Arc<Client> = create_test_client().await;
1701 let lid = "100000012345678";
1702 let lid_jid = Jid::lid(lid);
1703
1704 let resolved = client.resolve_encryption_jid(&lid_jid).await;
1705
1706 assert_eq!(resolved, lid_jid);
1707 }
1708
1709 #[tokio::test]
1710 async fn test_resolve_encryption_jid_no_mapping_returns_pn() {
1711 let client: Arc<Client> = create_test_client().await;
1712 let pn = "55999999999";
1713 let pn_jid = Jid::pn(pn);
1714
1715 let resolved = client.resolve_encryption_jid(&pn_jid).await;
1716
1717 assert_eq!(resolved, pn_jid);
1718 }
1719
1720 #[tokio::test]
1721 async fn test_resolve_dm_wire_jid_unmigrated_keeps_pn() {
1722 let (client, pn, lid) = client_with_peer_mapping().await;
1723
1724 assert_eq!(client.resolve_dm_wire_jid(&Jid::pn(pn)).await, Jid::pn(pn));
1726 assert_eq!(
1729 client.resolve_dm_wire_jid(&Jid::lid(lid)).await,
1730 Jid::pn(pn)
1731 );
1732 assert_eq!(client.resolve_encryption_jid(&Jid::pn(pn)).await.user, lid);
1734 }
1735
1736 #[tokio::test]
1737 async fn test_resolve_dm_wire_jid_unmigrated_unmapped_lid_stays_lid() {
1738 let client: Arc<Client> = create_test_client().await;
1739 let lid_jid = Jid::lid("111000011112222");
1740 assert_eq!(client.resolve_dm_wire_jid(&lid_jid).await, lid_jid);
1741 }
1742
1743 #[tokio::test]
1744 async fn test_resolve_dm_wire_jid_migrated_flag_upgrades_to_lid() {
1745 let (client, pn, lid) = client_with_peer_mapping().await;
1746
1747 client
1748 .persistence_manager
1749 .process_command(crate::store::commands::DeviceCommand::SetLidMigrated(true))
1750 .await;
1751
1752 assert!(client.is_lid_migrated().await);
1753 assert_eq!(
1754 client.resolve_dm_wire_jid(&Jid::pn(pn)).await,
1755 Jid::lid(lid)
1756 );
1757 }
1758
1759 #[tokio::test]
1760 async fn test_resolve_dm_wire_jid_migration_prop_upgrades_to_lid() {
1761 let (client, pn, lid) = client_with_peer_mapping().await;
1762
1763 assert!(!client.is_lid_migrated().await);
1764 client
1765 .ab_props()
1766 .apply_props(
1767 false,
1768 std::iter::once((
1769 wacore::iq::abprops::web::LID_ONE_ON_ONE_MIGRATION_ENABLED.code,
1770 "1".into(),
1771 )),
1772 )
1773 .await;
1774
1775 assert!(client.is_lid_migrated().await);
1776 assert_eq!(
1777 client.resolve_dm_wire_jid(&Jid::pn(pn)).await,
1778 Jid::lid(lid)
1779 );
1780 }
1781
1782 #[tokio::test]
1783 async fn test_lid_migration_mapping_sync_learns_and_migrates_with_prop() {
1784 use buffa::Message as _;
1785 use waproto::whatsapp as wa;
1786
1787 let client: Arc<Client> = create_test_client().await;
1788 let payload = wa::LIDMigrationMappingSyncPayload {
1789 pn_to_lid_mappings: vec![wa::LIDMigrationMapping {
1790 pn: 5511987650001,
1791 assigned_lid: 111000011112222,
1792 latest_lid: None,
1793 }],
1794 chat_db_migration_timestamp: None,
1795 };
1796 let sync = wa::LIDMigrationMappingSyncMessage {
1797 encoded_mapping_payload: Some(payload.encode_to_vec()),
1798 };
1799
1800 client.handle_lid_migration_mapping_sync(&sync).await;
1803 assert_eq!(
1804 client
1805 .resolve_encryption_jid(&Jid::pn("5511987650001"))
1806 .await
1807 .user,
1808 "111000011112222"
1809 );
1810 assert!(!client.is_lid_migrated().await);
1811
1812 client
1814 .ab_props()
1815 .apply_props(
1816 false,
1817 std::iter::once((
1818 wacore::iq::abprops::web::LID_ONE_ON_ONE_MIGRATION_ENABLED.code,
1819 "1".into(),
1820 )),
1821 )
1822 .await;
1823 client.handle_lid_migration_mapping_sync(&sync).await;
1824
1825 client
1827 .ab_props()
1828 .apply_props(false, std::iter::empty())
1829 .await;
1830 assert!(client.is_lid_migrated().await);
1831 }
1832
1833 #[tokio::test]
1834 async fn test_is_lid_migrated_prop_zero_or_absent_is_false() {
1835 let client: Arc<Client> = create_test_client().await;
1836 assert!(!client.is_lid_migrated().await);
1837
1838 client
1839 .ab_props()
1840 .apply_props(
1841 false,
1842 std::iter::once((
1843 wacore::iq::abprops::web::LID_ONE_ON_ONE_MIGRATION_ENABLED.code,
1844 "0".into(),
1845 )),
1846 )
1847 .await;
1848 assert!(!client.is_lid_migrated().await);
1849 }
1850
1851 #[tokio::test]
1852 async fn test_lid_migration_mapping_sync_missing_or_malformed_payload_is_ignored() {
1853 use waproto::whatsapp as wa;
1854
1855 let client: Arc<Client> = create_test_client().await;
1856 client
1857 .ab_props()
1858 .apply_props(
1859 false,
1860 std::iter::once((
1861 wacore::iq::abprops::web::LID_ONE_ON_ONE_MIGRATION_ENABLED.code,
1862 "1".into(),
1863 )),
1864 )
1865 .await;
1866
1867 let missing = wa::LIDMigrationMappingSyncMessage {
1870 encoded_mapping_payload: None,
1871 };
1872 client.handle_lid_migration_mapping_sync(&missing).await;
1873 assert!(
1874 !client
1875 .persistence_manager
1876 .get_device_snapshot()
1877 .lid_migrated
1878 );
1879
1880 let malformed = wa::LIDMigrationMappingSyncMessage {
1881 encoded_mapping_payload: Some(vec![0xFF, 0xFF, 0xFF]),
1882 };
1883 client.handle_lid_migration_mapping_sync(&malformed).await;
1884 assert!(
1885 !client
1886 .persistence_manager
1887 .get_device_snapshot()
1888 .lid_migrated
1889 );
1890 }
1891
1892 #[tokio::test]
1893 async fn test_lid_migration_mapping_sync_prefers_latest_lid() {
1894 use buffa::Message as _;
1895 use waproto::whatsapp as wa;
1896
1897 let client: Arc<Client> = create_test_client().await;
1898 let payload = wa::LIDMigrationMappingSyncPayload {
1899 pn_to_lid_mappings: vec![wa::LIDMigrationMapping {
1900 pn: 5511987650001,
1901 assigned_lid: 111000011112222,
1902 latest_lid: Some(999000099990000),
1903 }],
1904 chat_db_migration_timestamp: None,
1905 };
1906 let sync = wa::LIDMigrationMappingSyncMessage {
1907 encoded_mapping_payload: Some(payload.encode_to_vec()),
1908 };
1909
1910 client.handle_lid_migration_mapping_sync(&sync).await;
1911 assert_eq!(
1912 client
1913 .resolve_encryption_jid(&Jid::pn("5511987650001"))
1914 .await
1915 .user,
1916 "999000099990000"
1917 );
1918 }
1919
1920 #[tokio::test]
1921 async fn test_resolve_encryption_jid_hosted_with_lid_upgrades_to_hosted_lid() {
1922 let client: Arc<Client> = create_test_client().await;
1923 let user = "55999999999";
1924 let lid = "100000012345678";
1925
1926 client
1927 .add_lid_pn_mapping(lid, user, LearningSource::PeerPnMessage)
1928 .await
1929 .unwrap();
1930
1931 for device in [99u16, 7] {
1932 let mut hosted = Jid::new(user, Server::Hosted);
1933 hosted.device = device;
1934 hosted.agent = 0xAB;
1935 hosted.integrator = 0xBEEF;
1936 let resolved = client.resolve_encryption_jid(&hosted).await;
1937
1938 assert_eq!(resolved.user, lid);
1939 assert_eq!(resolved.server, Server::HostedLid);
1940 assert_eq!(
1941 resolved.device, device,
1942 "device must round-trip, not be coerced to 99"
1943 );
1944 assert_eq!(resolved.agent, hosted.agent);
1945 assert_eq!(resolved.integrator, hosted.integrator);
1946 }
1947 }
1948
1949 #[tokio::test]
1950 async fn test_resolve_encryption_jid_hosted_no_mapping_keeps_hosted() {
1951 let client: Arc<Client> = create_test_client().await;
1952 let mut hosted = Jid::new("55999999999", Server::Hosted);
1953 hosted.device = 99;
1954
1955 let resolved = client.resolve_encryption_jid(&hosted).await;
1956
1957 assert_eq!(resolved, hosted);
1958 }
1959
1960 #[tokio::test]
1961 async fn test_resolve_encryption_jid_preserves_hosted_lid() {
1962 let client: Arc<Client> = create_test_client().await;
1963 let mut hosted_lid = Jid::new("100000012345678", Server::HostedLid);
1964 hosted_lid.device = 99;
1965
1966 let resolved = client.resolve_encryption_jid(&hosted_lid).await;
1967
1968 assert_eq!(resolved, hosted_lid);
1969 }
1970
1971 #[tokio::test]
1972 async fn test_get_lid_pn_entry_from_pn() {
1973 let client: Arc<Client> = create_test_client().await;
1974 let pn = "55999999999";
1975 let lid = "100000012345678";
1976
1977 assert!(
1978 client
1979 .get_lid_pn_entry(&Jid::pn(pn))
1980 .await
1981 .unwrap()
1982 .is_none()
1983 );
1984
1985 client
1986 .add_lid_pn_mapping(lid, pn, LearningSource::Usync)
1987 .await
1988 .unwrap();
1989
1990 let entry = client
1991 .get_lid_pn_entry(&Jid::pn(pn))
1992 .await
1993 .unwrap()
1994 .unwrap();
1995 assert_eq!(&*entry.lid, lid);
1996 assert_eq!(&*entry.phone_number, pn);
1997 }
1998
1999 #[tokio::test]
2000 async fn test_get_lid_pn_entry_from_lid() {
2001 let client: Arc<Client> = create_test_client().await;
2002 let pn = "55999999999";
2003 let lid = "100000012345678";
2004
2005 assert!(
2006 client
2007 .get_lid_pn_entry(&Jid::lid(lid))
2008 .await
2009 .unwrap()
2010 .is_none()
2011 );
2012
2013 client
2014 .add_lid_pn_mapping(lid, pn, LearningSource::Usync)
2015 .await
2016 .unwrap();
2017
2018 let entry = client
2019 .get_lid_pn_entry(&Jid::lid(lid))
2020 .await
2021 .unwrap()
2022 .unwrap();
2023 assert_eq!(&*entry.lid, lid);
2024 assert_eq!(&*entry.phone_number, pn);
2025 }
2026
2027 #[tokio::test]
2030 async fn test_get_lid_pn_entry_falls_back_to_backend() {
2031 use wacore::store::traits::LidPnMappingEntry;
2032
2033 let client: Arc<Client> = create_test_client().await;
2034 let pn = "15555550123";
2035 let lid = "100000000000123";
2036
2037 let backend = client.persistence_manager.backend();
2038 backend
2039 .put_lid_mapping(&LidPnMappingEntry {
2040 lid: lid.into(),
2041 phone_number: pn.into(),
2042 created_at: 1,
2043 updated_at: 1,
2044 learning_source: "usync".into(),
2045 })
2046 .await
2047 .unwrap();
2048
2049 let entry = client
2051 .get_lid_pn_entry(&Jid::lid(lid))
2052 .await
2053 .unwrap()
2054 .unwrap();
2055 assert_eq!(&*entry.lid, lid);
2056 assert_eq!(&*entry.phone_number, pn);
2057
2058 let entry = client
2060 .get_lid_pn_entry(&Jid::pn(pn))
2061 .await
2062 .unwrap()
2063 .unwrap();
2064 assert_eq!(&*entry.lid, lid);
2065 }
2066
2067 #[tokio::test]
2071 async fn test_learn_lid_pn_mapping_fast_populates_cache_synchronously() {
2072 let client: Arc<Client> = create_test_client().await;
2073 let pn = "5511999998877";
2074 let lid = "200000000007788";
2075
2076 client
2077 .learn_lid_pn_mapping_fast(lid, pn, LearningSource::PeerPnMessage, false)
2078 .await;
2079
2080 let resolved = client.resolve_encryption_jid(&Jid::pn(pn)).await;
2081 assert_eq!(resolved.user, lid, "cache must have the mapping on return");
2082 assert_eq!(resolved.server, Server::Lid);
2083 }
2084
2085 #[tokio::test]
2089 async fn learn_fast_offline_then_live_persists() {
2090 let client: Arc<Client> = create_test_client().await;
2091 let lid = "200000000012345";
2092 let pn = "5511988887777";
2093 let backend = client.persistence_manager.backend();
2094
2095 client
2096 .learn_lid_pn_mapping_fast(lid, pn, LearningSource::PeerPnMessage, true)
2097 .await;
2098 assert_eq!(client.resolve_encryption_jid(&Jid::pn(pn)).await.user, lid);
2099 assert!(
2100 backend.get_lid_mapping(lid).await.unwrap().is_none(),
2101 "offline learn must not persist"
2102 );
2103
2104 client
2105 .learn_lid_pn_mapping_fast(lid, pn, LearningSource::PeerPnMessage, false)
2106 .await;
2107 let start = wacore::time::Instant::now();
2110 while !matches!(backend.get_lid_mapping(lid).await, Ok(Some(_))) {
2111 assert!(
2112 start.elapsed() < std::time::Duration::from_secs(5),
2113 "live learn after an offline-only learn must persist"
2114 );
2115 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
2116 }
2117 }
2118
2119 #[tokio::test]
2122 async fn test_learn_lid_pn_mappings_batch_populates_cache_synchronously() {
2123 let client: Arc<Client> = create_test_client().await;
2124 let pairs = [
2125 ("200000000000001", "5511911111111"),
2126 ("200000000000002", "5511922222222"),
2127 ("200000000000003", "5511933333333"),
2128 ];
2129
2130 let batch: Vec<(String, String)> = pairs
2131 .iter()
2132 .map(|(lid, pn)| ((*lid).to_string(), (*pn).to_string()))
2133 .collect();
2134 client
2135 .learn_lid_pn_mappings_batch(batch, LearningSource::Other, false)
2136 .await;
2137
2138 for (lid, pn) in &pairs {
2139 let resolved = client.resolve_encryption_jid(&Jid::pn(*pn)).await;
2140 assert_eq!(resolved.user, *lid, "batch entry {pn} missing from cache");
2141 assert_eq!(resolved.server, Server::Lid);
2142 }
2143 }
2144
2145 #[tokio::test]
2147 async fn test_learn_lid_pn_mappings_batch_empty_is_noop() {
2148 let client: Arc<Client> = create_test_client().await;
2149 client
2150 .learn_lid_pn_mappings_batch(Vec::new(), LearningSource::Other, false)
2151 .await;
2152 assert_eq!(client.lid_pn_cache.lid_count().await, 0);
2153 }
2154
2155 #[tokio::test]
2156 async fn test_add_lid_pn_mappings_deduplicates_and_is_durable_on_return() {
2157 let client: Arc<Client> = create_test_client().await;
2158 let phone = "5511900012345";
2159 let stale_lid = "200000000001234";
2160 let current_lid = "200000000001235";
2161
2162 let written = client
2163 .add_lid_pn_mappings(
2164 vec![
2165 (stale_lid.to_owned(), phone.to_owned()),
2166 (current_lid.to_owned(), phone.to_owned()),
2167 ],
2168 LearningSource::Other,
2169 )
2170 .await
2171 .unwrap();
2172
2173 assert_eq!(written, 1);
2174 let persisted = client
2175 .persistence_manager
2176 .backend()
2177 .get_lid_mapping(current_lid)
2178 .await
2179 .unwrap()
2180 .expect("mapping must be durable when the call returns");
2181 assert_eq!(persisted.phone_number, phone);
2182 assert_eq!(
2183 client.resolve_encryption_jid(&Jid::pn(phone)).await.user,
2184 current_lid
2185 );
2186 }
2187
2188 #[tokio::test]
2192 async fn test_learn_lid_pn_mappings_batch_online_persists_and_migrates() {
2193 use wacore::store::traits::{DeviceInfo, DeviceListRecord};
2194 use wacore_binary::Jid;
2195
2196 let client: Arc<Client> = create_test_client().await;
2197 let lid = "200000000077777";
2198 let pn = "5511955550000";
2199 let backend = client.persistence_manager.backend();
2200
2201 backend
2206 .update_device_list(DeviceListRecord {
2207 user: pn.to_string(),
2208 devices: vec![DeviceInfo::new(3, None)],
2209 timestamp: wacore::time::now_secs(),
2210 phash: None,
2211 raw_id: None,
2212 })
2213 .await
2214 .unwrap();
2215
2216 client
2217 .learn_lid_pn_mappings_batch(
2218 vec![(lid.to_string(), pn.to_string())],
2219 LearningSource::Other,
2220 false,
2221 )
2222 .await;
2223
2224 let start = wacore::time::Instant::now();
2229 let deadline = std::time::Duration::from_secs(5);
2230 loop {
2231 if backend.get_devices(lid).await.unwrap().is_some() {
2232 break;
2233 }
2234 assert!(
2235 start.elapsed() < deadline,
2236 "timed out waiting for batch persist + migration"
2237 );
2238 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
2239 }
2240
2241 assert!(
2242 backend.get_lid_mapping(lid).await.unwrap().is_some(),
2243 "mapping must be persisted"
2244 );
2245 assert!(
2246 backend.get_devices(pn).await.unwrap().is_none(),
2247 "migration must delete the old PN-keyed device row"
2248 );
2249 let lid_row = backend.get_devices(lid).await.unwrap().unwrap();
2250 assert_eq!(lid_row.devices[0].device_id, 3);
2251 assert_eq!(
2253 client
2254 .get_lid_pn_entry(&Jid::pn(pn))
2255 .await
2256 .unwrap()
2257 .unwrap()
2258 .lid,
2259 lid.into()
2260 );
2261 }
2262
2263 #[tokio::test]
2266 async fn test_learn_lid_pn_mappings_batch_offline_skips_persist() {
2267 use wacore_binary::Jid;
2268
2269 let client: Arc<Client> = create_test_client().await;
2270 let lid = "200000000009999";
2271 let pn = "5511900009999";
2272
2273 client
2274 .learn_lid_pn_mappings_batch(
2275 vec![(lid.to_string(), pn.to_string())],
2276 LearningSource::Other,
2277 true,
2278 )
2279 .await;
2280
2281 let resolved = client.resolve_encryption_jid(&Jid::pn(pn)).await;
2282 assert_eq!(resolved.user, lid);
2283
2284 assert!(
2285 client
2286 .persistence_manager
2287 .backend()
2288 .get_lid_mapping(lid)
2289 .await
2290 .unwrap()
2291 .is_none(),
2292 "offline batch must not persist to DB"
2293 );
2294 }
2295
2296 #[tokio::test]
2301 async fn test_learn_lid_pn_mappings_batch_dedups_duplicate_phones() {
2302 use wacore_binary::Jid;
2303
2304 let client: Arc<Client> = create_test_client().await;
2305 let pn = "5511900000007";
2306 let lid_stale = "200000000007777";
2307 let lid_fresh = "200000000007999";
2308
2309 client
2310 .learn_lid_pn_mappings_batch(
2311 vec![
2312 (lid_stale.to_string(), pn.to_string()),
2313 (lid_fresh.to_string(), pn.to_string()),
2314 ],
2315 LearningSource::Other,
2316 true, )
2318 .await;
2319
2320 let resolved = client.resolve_encryption_jid(&Jid::pn(pn)).await;
2322 assert_eq!(
2323 resolved.user, lid_fresh,
2324 "dedup must keep the last lid for a repeated phone_number"
2325 );
2326 }
2327
2328 fn tagged_session_blob(remote_regid: u32) -> Vec<u8> {
2332 use wacore::libsignal::protocol::{SessionRecord, SessionState};
2333 use waproto::whatsapp::SessionStructure;
2334
2335 let state = SessionState::from_session_structure(SessionStructure {
2336 session_version: Some(3),
2337 local_identity_public: None,
2338 remote_identity_public: None,
2339 root_key: None,
2340 previous_counter: Some(0),
2341 sender_chain: buffa::MessageField::none(),
2342 receiver_chains: vec![],
2343 pending_pre_key: buffa::MessageField::none(),
2344 remote_registration_id: Some(remote_regid),
2345 local_registration_id: Some(0),
2346 alice_base_key: Some(vec![]),
2347 needs_refresh: None,
2348 pending_key_exchange: buffa::MessageField::none(),
2349 });
2350 SessionRecord::new(state)
2351 .serialize()
2352 .expect("serialize session record")
2353 }
2354
2355 #[tokio::test]
2362 async fn migration_preserves_working_session_when_both_namespaces_present() {
2363 use wacore::libsignal::protocol::SessionRecord;
2364 use wacore::types::jid::JidExt as _;
2365
2366 let client: Arc<Client> = create_test_client().await;
2367 let pn = "5500000000000";
2368 let lid = "111111111111111";
2369
2370 client
2371 .add_lid_pn_mapping(lid, pn, LearningSource::PeerPnMessage)
2372 .await
2373 .unwrap();
2374
2375 let pn_addr = Jid::pn_device(pn.to_string(), 0).to_protocol_address();
2376 let lid_addr = Jid::lid_device(lid.to_string(), 0).to_protocol_address();
2377
2378 const WORKING_REGID: u32 = 0xDEAD_BEEF;
2383 const FRESH_REGID: u32 = 0x0BAD_F00D;
2384
2385 let backend = client.persistence_manager.backend();
2386
2387 client
2392 .signal_cache
2393 .put_session(
2394 &pn_addr,
2395 SessionRecord::deserialize(&tagged_session_blob(WORKING_REGID))
2396 .expect("seed PN blob deserializes"),
2397 )
2398 .await;
2399 client
2400 .signal_cache
2401 .put_session(
2402 &lid_addr,
2403 SessionRecord::deserialize(&tagged_session_blob(FRESH_REGID))
2404 .expect("seed LID blob deserializes"),
2405 )
2406 .await;
2407 client.signal_cache.flush(backend.as_ref()).await.unwrap();
2408
2409 client
2410 .migrate_signal_sessions_on_lid_discovery(pn, lid)
2411 .await;
2412
2413 assert!(
2416 backend
2417 .get_session(pn_addr.as_str())
2418 .await
2419 .unwrap()
2420 .is_none(),
2421 "PN address must be cleared post-migration"
2422 );
2423
2424 let surviving_bytes = backend
2425 .get_session(lid_addr.as_str())
2426 .await
2427 .unwrap()
2428 .expect("LID slot must have a session after migration");
2429 let record = SessionRecord::deserialize(&surviving_bytes)
2430 .expect("surviving session blob must parse");
2431 let surviving_regid = record
2432 .remote_registration_id()
2433 .expect("surviving session must expose its remote reg id");
2434
2435 assert_eq!(
2436 surviving_regid, WORKING_REGID,
2437 "LID slot held the FRESH (regid={:#x}) blob — that's the prod \
2438 deadlock: the working PN session ({:#x}) got discarded by the \
2439 'both exist' branch, leaving us pinned to a session that has no \
2440 link to the peer's outbound chain.",
2441 surviving_regid, WORKING_REGID
2442 );
2443 }
2444
2445 #[tokio::test]
2446 async fn lid_discovery_migrates_standard_and_hosted_signal_namespaces() {
2447 use wacore::libsignal::protocol::SessionRecord;
2448 use wacore::types::jid::JidExt as _;
2449
2450 let client: Arc<Client> = create_test_client().await;
2451 let pn = "13135550100";
2452 let lid = "100000000000100";
2453 let backend = client.persistence_manager.backend();
2454 let pairs = [
2455 (Server::Pn, Server::Lid, 11),
2456 (Server::Hosted, Server::HostedLid, 12),
2457 ];
2458
2459 for (from_server, _, registration_id) in pairs {
2460 let source = Jid::new(pn, from_server).to_protocol_address();
2461 client
2462 .signal_cache
2463 .put_session(
2464 &source,
2465 SessionRecord::deserialize(&tagged_session_blob(registration_id)).unwrap(),
2466 )
2467 .await;
2468 }
2469 client.signal_cache.flush(backend.as_ref()).await.unwrap();
2470
2471 assert!(
2472 client
2473 .migrate_signal_sessions_on_lid_discovery(pn, lid)
2474 .await
2475 );
2476 for (from_server, to_server, _) in pairs {
2477 let source = Jid::new(pn, from_server).to_protocol_address();
2478 let destination = Jid::new(lid, to_server).to_protocol_address();
2479 assert!(
2480 backend
2481 .get_session(source.as_str())
2482 .await
2483 .unwrap()
2484 .is_none()
2485 );
2486 assert!(
2487 backend
2488 .get_session(destination.as_str())
2489 .await
2490 .unwrap()
2491 .is_some()
2492 );
2493 }
2494 }
2495
2496 #[tokio::test]
2500 async fn migrate_skips_when_no_pn_signal_state() {
2501 use wacore::types::jid::JidExt as _;
2502
2503 let client: Arc<Client> = create_test_client().await;
2504 let pn = "5500000000777";
2505 let lid = "222222222222222";
2506 client
2507 .add_lid_pn_mapping(lid, pn, LearningSource::PeerPnMessage)
2508 .await
2509 .unwrap();
2510 let backend = client.persistence_manager.backend();
2511
2512 assert!(
2514 !client
2515 .signal_cache
2516 .has_state_for_user(pn, backend.as_ref())
2517 .await
2518 .unwrap(),
2519 "fresh peer should have no PN Signal state"
2520 );
2521
2522 client
2523 .migrate_signal_sessions_on_lid_discovery(pn, lid)
2524 .await;
2525
2526 let lid_addr = Jid::lid_device(lid.to_string(), 0).to_protocol_address();
2528 assert!(
2529 client
2530 .signal_cache
2531 .get_session(&lid_addr, backend.as_ref())
2532 .await
2533 .unwrap()
2534 .is_none(),
2535 "migration of a stateless peer must not create a LID session"
2536 );
2537 }
2538
2539 #[tokio::test]
2545 async fn migration_blocks_on_per_address_session_lock() {
2546 use std::time::Duration;
2547 use wacore::types::jid::JidExt as _;
2548
2549 let client: Arc<Client> = create_test_client().await;
2550 let pn = "5500000000000";
2551 let lid = "111111111111111";
2552 client
2553 .add_lid_pn_mapping(lid, pn, LearningSource::PeerPnMessage)
2554 .await
2555 .unwrap();
2556
2557 let pn_addr = Jid::pn_device(pn.to_string(), 0).to_protocol_address();
2561 client
2562 .signal_cache
2563 .put_session(
2564 &pn_addr,
2565 wacore::libsignal::protocol::SessionRecord::deserialize(&tagged_session_blob(
2566 0xDEAD_BEEF,
2567 ))
2568 .expect("seed PN blob deserializes"),
2569 )
2570 .await;
2571
2572 let lid_addr = Jid::lid_device(lid.to_string(), 0).to_protocol_address();
2573 let lid_lock = client.session_lock_for(lid_addr.as_str()).await;
2574 let held = lid_lock.lock().await;
2575
2576 let migrate_client = client.clone();
2577 let pn_s = pn.to_string();
2578 let lid_s = lid.to_string();
2579 let mut handle = tokio::spawn(async move {
2580 migrate_client
2581 .migrate_signal_sessions_on_lid_discovery(&pn_s, &lid_s)
2582 .await;
2583 });
2584
2585 let blocked = tokio::time::timeout(Duration::from_millis(200), &mut handle).await;
2586 assert!(
2587 blocked.is_err(),
2588 "migration must block while another holder owns the LID address \
2589 session lock — otherwise concurrent encrypt/decrypt races"
2590 );
2591
2592 drop(held);
2595 tokio::time::timeout(Duration::from_secs(5), handle)
2596 .await
2597 .expect("migration must complete once the lock is released")
2598 .expect("migration task must not panic");
2599 }
2600
2601 #[tokio::test]
2608 async fn migration_lock_dance_completes_when_caller_drops_guard() {
2609 use std::time::Duration;
2610 use wacore::types::jid::JidExt as _;
2611
2612 let client: Arc<Client> = create_test_client().await;
2613 let pn = "5500000000000";
2614 let lid = "111111111111111";
2615 client
2616 .add_lid_pn_mapping(lid, pn, LearningSource::PeerPnMessage)
2617 .await
2618 .unwrap();
2619
2620 let lid_addr = Jid::lid_device(lid.to_string(), 0).to_protocol_address();
2621 let session_mutex = client.session_lock_for(lid_addr.as_str()).await;
2622 let mut session_guard: Option<async_lock::MutexGuardArc<()>> =
2623 Some(session_mutex.lock_arc().await);
2624
2625 let dance = async {
2629 session_guard = None;
2630 client
2631 .migrate_signal_sessions_on_lid_discovery(pn, lid)
2632 .await;
2633 session_guard = Some(session_mutex.lock_arc().await);
2634 };
2635 tokio::time::timeout(Duration::from_secs(5), dance)
2636 .await
2637 .expect("drop → migrate → reacquire must not deadlock");
2638
2639 assert!(
2640 session_guard.is_some(),
2641 "guard must be re-held after the dance so the next batch payload \
2642 stays serialized on the address lock"
2643 );
2644 }
2645
2646 #[tokio::test]
2651 async fn migration_reports_whether_anything_moved() {
2652 use wacore::libsignal::protocol::SessionRecord;
2653 use wacore::types::jid::JidExt as _;
2654
2655 let client: Arc<Client> = create_test_client().await;
2656 let pn = "5500000001111";
2657 let lid = "122222222222222";
2658
2659 client
2660 .add_lid_pn_mapping(lid, pn, LearningSource::PeerPnMessage)
2661 .await
2662 .unwrap();
2663
2664 assert!(
2665 !client
2666 .migrate_signal_sessions_on_lid_discovery(pn, lid)
2667 .await,
2668 "no PN signal state, so nothing can move"
2669 );
2670
2671 let pn_addr = Jid::pn_device(pn.to_string(), 0).to_protocol_address();
2672 client
2673 .signal_cache
2674 .put_session(
2675 &pn_addr,
2676 SessionRecord::deserialize(&tagged_session_blob(7)).expect("blob deserializes"),
2677 )
2678 .await;
2679 let backend = client.persistence_manager.backend();
2680 client.signal_cache.flush(backend.as_ref()).await.unwrap();
2681
2682 assert!(
2683 client
2684 .migrate_signal_sessions_on_lid_discovery(pn, lid)
2685 .await,
2686 "a PN session moved into the LID slot"
2687 );
2688 assert!(
2689 !client
2690 .migrate_signal_sessions_on_lid_discovery(pn, lid)
2691 .await,
2692 "second call finds the PN side already drained"
2693 );
2694 }
2695
2696 #[tokio::test]
2699 async fn identity_only_migration_flushes_without_requesting_decrypt_retry() {
2700 use wacore::types::jid::JidExt as _;
2701
2702 let client: Arc<Client> = create_test_client().await;
2703 let pn = "5500000002222";
2704 let lid = "133333333333333";
2705 let pn_addr = Jid::pn_device(pn.to_string(), 0).to_protocol_address();
2706 let lid_addr = Jid::lid_device(lid.to_string(), 0).to_protocol_address();
2707 let backend = client.persistence_manager.backend();
2708
2709 client.signal_cache.put_identity(&pn_addr, &[7; 32]).await;
2710 client.signal_cache.put_identity(&lid_addr, &[8; 32]).await;
2711 client.signal_cache.flush(backend.as_ref()).await.unwrap();
2712
2713 assert!(
2714 !client
2715 .migrate_signal_sessions_on_lid_discovery(pn, lid)
2716 .await,
2717 "discarding only the stale PN identity cannot help a decrypt retry"
2718 );
2719 assert_eq!(backend.load_identity(pn_addr.as_str()).await.unwrap(), None);
2720 assert_eq!(
2721 backend.load_identity(lid_addr.as_str()).await.unwrap(),
2722 Some([8; 32]),
2723 "the destination identity must win and the cleanup must be durable"
2724 );
2725 }
2726
2727 #[tokio::test]
2728 async fn lid_discovery_retries_pending_migration_flush() {
2729 use wacore::libsignal::protocol::SessionRecord;
2730 use wacore::types::jid::JidExt as _;
2731
2732 let backend = Arc::new(InMemoryBackend::new());
2733 let client = create_test_client_with_backend(backend.clone()).await;
2734 let pn = "5500000003333";
2735 let lid = "144444444444444";
2736 let pn_addr = Jid::pn_device(pn, 0).to_protocol_address();
2737 let lid_addr = Jid::lid_device(lid, 0).to_protocol_address();
2738 client
2739 .signal_cache
2740 .put_session(
2741 &pn_addr,
2742 SessionRecord::deserialize(&tagged_session_blob(9)).unwrap(),
2743 )
2744 .await;
2745 client.signal_cache.flush(backend.as_ref()).await.unwrap();
2746
2747 backend.set_fail_session_writes(true);
2748 assert!(
2749 client
2750 .migrate_signal_sessions_on_lid_discovery(pn, lid)
2751 .await,
2752 "the first pass moved a session in memory"
2753 );
2754 backend.set_fail_session_writes(false);
2755 let attempts_before_retry = backend.session_batch_write_count();
2756
2757 assert!(
2758 !client
2759 .migrate_signal_sessions_on_lid_discovery(pn, lid)
2760 .await,
2761 "a durability retry must not request another decrypt attempt"
2762 );
2763 assert!(backend.session_batch_write_count() > attempts_before_retry);
2764 assert!(
2765 backend
2766 .get_session(pn_addr.as_str())
2767 .await
2768 .unwrap()
2769 .is_none()
2770 );
2771 assert!(
2772 backend
2773 .get_session(lid_addr.as_str())
2774 .await
2775 .unwrap()
2776 .is_some()
2777 );
2778 }
2779}