1use crate::client::Client;
6use crate::request::IqError;
7use log::{debug, warn};
8use wacore::iq::usync::{DeviceListResponse, DeviceListSpec};
9use wacore_binary::Jid;
10
11const DEVICE_REFRESH_MAX_ATTEMPTS: usize = 3;
15
16#[inline]
17fn device_response_contains_user(response: &DeviceListResponse, user: &str) -> bool {
18 response
19 .device_lists
20 .iter()
21 .any(|device_list| device_list.user.user == user)
22 || response
23 .lid_mappings
24 .iter()
25 .any(|mapping| mapping.phone_number == user || mapping.lid == user)
26}
27
28pub use wacore::iq::usync::{
29 UsyncAddressingMode, UsyncBotCommand, UsyncBotProfessionalType, UsyncBotProfileResult,
30 UsyncBotPrompt, UsyncBusinessResult, UsyncContactResult, UsyncContext, UsyncDeviceListResult,
31 UsyncDeviceResult, UsyncDeviceSyncHint, UsyncDevicesResult, UsyncDisappearingModeResult,
32 UsyncFeature, UsyncFeatureResult, UsyncKeyIndexResult, UsyncMode, UsyncOutcome, UsyncProtocol,
33 UsyncProtocolKind, UsyncProtocolResult, UsyncProtocolState, UsyncQuery, UsyncResponse,
34 UsyncStatusResult, UsyncSubprotocolError, UsyncTextStatusResult, UsyncUser, UsyncUserResult,
35 UsyncValidationError,
36};
37
38impl Client {
39 pub async fn query_usync(&self, query: UsyncQuery) -> Result<UsyncResponse, IqError> {
45 let sid = self.generate_request_id();
46 let spec = wacore::iq::usync::UsyncQuerySpec::new(query, sid)
47 .map_err(|error| IqError::EncodeError(error.into()))?;
48 self.execute(spec).await
49 }
50
51 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.usync.get_user_devices", level = "debug", skip_all, fields(users = jids.len()), err(Debug)))]
52 pub(crate) async fn get_user_devices(&self, jids: &[Jid]) -> Result<Vec<Jid>, anyhow::Error> {
53 let mut owned = Vec::with_capacity(jids.len());
54 owned.extend(jids.iter().map(Jid::to_non_ad));
55 self.get_user_devices_owned(owned).await
56 }
57
58 pub(crate) async fn get_user_devices_owned(
59 &self,
60 jids: Vec<Jid>,
61 ) -> Result<Vec<Jid>, anyhow::Error> {
62 let input_len = jids.len();
63 let mut jids_to_fetch: Vec<Jid> = Vec::with_capacity(input_len);
64 let mut all_devices = Vec::with_capacity(input_len * 2);
65
66 use futures::StreamExt;
74 const DEVICE_LIST_RESOLVE_CONCURRENCY: usize = 16;
76 let mut resolved = futures::stream::iter(jids.into_iter().map(Jid::into_non_ad))
77 .map(|jid| async move {
78 let devices = self.get_devices_from_registry(&jid).await;
79 (jid, devices)
80 })
81 .buffer_unordered(DEVICE_LIST_RESOLVE_CONCURRENCY);
82
83 while let Some((jid, devices)) = resolved.next().await {
84 match devices {
85 Some(devices) => all_devices.extend(devices),
86 None => {
87 jids_to_fetch.push(jid);
88 }
89 }
90 }
91
92 if !jids_to_fetch.is_empty() {
93 wacore::types::jid::sort_dedup_by_user(&mut jids_to_fetch);
94 debug!(
95 "get_user_devices: Cache miss, fetching from network for {} unique users",
96 jids_to_fetch.len()
97 );
98 all_devices.extend(self.fetch_user_devices(jids_to_fetch).await?);
99 }
100
101 Ok(all_devices)
102 }
103
104 pub(crate) async fn refresh_user_devices(
105 &self,
106 mut jids: Vec<Jid>,
107 ) -> Result<Vec<Jid>, anyhow::Error> {
108 for jid in &mut jids {
109 jid.agent = 0;
110 jid.device = 0;
111 }
112 wacore::types::jid::sort_dedup_by_user(&mut jids);
113 self.fetch_user_devices_with_freshness(jids, crate::cache::Freshness::Refresh)
114 .await
115 }
116
117 async fn fetch_user_devices(&self, jids: Vec<Jid>) -> Result<Vec<Jid>, anyhow::Error> {
118 self.fetch_user_devices_with_freshness(jids, crate::cache::Freshness::CachePreferred)
119 .await
120 }
121
122 async fn fetch_user_devices_with_freshness(
123 &self,
124 mut jids: Vec<Jid>,
125 freshness: crate::cache::Freshness,
126 ) -> Result<Vec<Jid>, anyhow::Error> {
127 if jids.is_empty() {
128 return Ok(Vec::new());
129 }
130 if freshness == crate::cache::Freshness::CachePreferred {
131 let sid = self.generate_request_id();
132 let response = self.execute(DeviceListSpec::new(jids, sid)).await?;
133 return self
134 .process_device_list_response(&response, freshness)
135 .await;
136 }
137
138 for attempt in 0..DEVICE_REFRESH_MAX_ATTEMPTS {
139 let topology_generation = self.device_topology.current();
140 let sid = self.generate_request_id();
141 let response = self
142 .execute(DeviceListSpec::new(jids, sid).require_complete_response())
143 .await?;
144
145 if let Some(devices) = self
146 .try_process_refreshed_device_list_response(
147 &response,
148 freshness,
149 topology_generation,
150 )
151 .await?
152 {
153 return Ok(devices);
154 }
155
156 if attempt + 1 == DEVICE_REFRESH_MAX_ATTEMPTS {
157 anyhow::bail!(
158 "device registry kept changing while an authoritative refresh was in flight"
159 );
160 }
161
162 jids = response
166 .device_lists
167 .into_iter()
168 .map(|user| user.user)
169 .collect();
170 }
171
172 unreachable!("bounded device refresh loop always returns")
173 }
174
175 async fn learn_device_list_mappings_guarded(
176 &self,
177 response: &DeviceListResponse,
178 guard: &crate::lid_pn_cache::LidPnMutationGuard<'_>,
179 ) -> Result<(), anyhow::Error> {
180 if response.lid_mappings.is_empty() {
194 return Ok(());
195 }
196 let client = self
197 .self_weak
198 .get()
199 .and_then(|weak| weak.upgrade())
200 .ok_or_else(|| anyhow::anyhow!("client ownership unavailable during device sync"))?;
201 let mappings: Vec<(String, String)> = response
202 .lid_mappings
203 .iter()
204 .map(|mapping| (mapping.lid.to_string(), mapping.phone_number.to_string()))
205 .collect();
206 client
207 .learn_lid_pn_mappings_batch_guarded(
208 mappings,
209 crate::lid_pn_cache::LearningSource::Usync,
210 false,
211 guard,
212 )
213 .await;
214 Ok(())
215 }
216
217 #[cfg_attr(feature = "tracing", tracing::instrument(name = "wa.usync.process_device_list", level = "debug", skip_all, fields(users = response.device_lists.len())))]
226 async fn process_device_list_response(
227 &self,
228 response: &DeviceListResponse,
229 freshness: crate::cache::Freshness,
230 ) -> Result<Vec<Jid>, anyhow::Error> {
231 let mapping_guard = self.lid_pn_cache.lock_mutation().await;
234 let registry_guard = self.device_topology.lock_registry().await;
235 self.learn_device_list_mappings_guarded(response, &mapping_guard)
236 .await?;
237 self.process_device_list_response_guarded(response, freshness, ®istry_guard)
238 .await
239 }
240
241 async fn try_process_refreshed_device_list_response(
242 &self,
243 response: &DeviceListResponse,
244 freshness: crate::cache::Freshness,
245 topology_generation: u64,
246 ) -> Result<Option<Vec<Jid>>, anyhow::Error> {
247 let mapping_guard = self.lid_pn_cache.lock_mutation().await;
250 let registry_guard = self.device_topology.lock_registry().await;
251 if !self
252 .device_topology
253 .unchanged_for(topology_generation, |user| {
254 device_response_contains_user(response, user)
255 })
256 {
257 return Ok(None);
258 }
259 self.learn_device_list_mappings_guarded(response, &mapping_guard)
260 .await?;
261 self.process_device_list_response_guarded(response, freshness, ®istry_guard)
262 .await
263 .map(Some)
264 }
265
266 async fn process_device_list_response_guarded(
267 &self,
268 response: &DeviceListResponse,
269 freshness: crate::cache::Freshness,
270 guard: &crate::client::device_topology::DeviceRegistryMutationGuard<'_>,
271 ) -> Result<Vec<Jid>, anyhow::Error> {
272 let mut fetched_devices = Vec::with_capacity(response.device_lists.len());
273 let mut device_records: Vec<wacore::store::traits::DeviceListRecord> =
274 Vec::with_capacity(response.device_lists.len());
275 struct PendingIdentityReset<'a> {
276 user: &'a Jid,
277 previous: wacore::store::traits::DeviceListRecord,
278 invalidate_registry: bool,
279 }
280 let mut pending_identity_resets = Vec::new();
285
286 for user_list in &response.device_lists {
287 let mut existing_record = self.load_device_record(&user_list.user.user).await;
292
293 let decoded_key_index = user_list
295 .key_index_bytes
296 .as_deref()
297 .and_then(wacore::adv::decode_key_index_list);
298
299 let mut raw_id = decoded_key_index.as_ref().map(|d| d.raw_id);
302 let pending_identity_reset = if let Some(ref decoded) = decoded_key_index
303 && let Some(ref existing) = existing_record
304 && let Some(stored_raw_id) = existing.raw_id
305 && stored_raw_id != decoded.raw_id
306 {
307 log::info!(
308 "raw_id mismatch for user {} in usync: stored={stored_raw_id}, received={}. Scheduling record reset.",
309 user_list.user.user,
310 decoded.raw_id
311 );
312 existing_record.take()
313 } else {
314 None
315 };
316
317 if raw_id.is_none() {
321 raw_id = existing_record
322 .as_ref()
323 .filter(|record| !record.devices.is_empty())
324 .and_then(|record| record.raw_id);
325 }
326
327 let mut devices: Vec<wacore::store::traits::DeviceInfo> = user_list
328 .devices
329 .iter()
330 .map(|d| {
331 let key_index = d.key_index.or_else(|| {
333 existing_record.as_ref().and_then(|record| {
337 record
338 .devices
339 .iter()
340 .find(|cached| cached.device_id == d.device as u32)
341 .and_then(|cached| cached.key_index)
342 })
343 });
344 wacore::store::traits::DeviceInfo::new(d.device as u32, key_index)
345 .with_hosting(d.is_hosted)
346 })
347 .collect();
348
349 if let Some(ref decoded) = decoded_key_index {
351 wacore::adv::retain_devices_by_key_index(&mut devices, decoded);
352 }
353
354 if devices.is_empty() {
359 if freshness == crate::cache::Freshness::Refresh {
360 anyhow::bail!(
361 "device-list refresh left no valid devices for {}",
362 user_list.user
363 );
364 }
365 if let Some(previous) = pending_identity_reset {
366 pending_identity_resets.push(PendingIdentityReset {
367 user: &user_list.user,
368 previous,
369 invalidate_registry: true,
373 });
374 }
375 continue;
376 }
377
378 if let Some(previous) = pending_identity_reset {
379 pending_identity_resets.push(PendingIdentityReset {
380 user: &user_list.user,
381 previous,
382 invalidate_registry: false,
383 });
384 }
385
386 let user_jid = &user_list.user;
388 for d in &devices {
389 fetched_devices.push(user_jid.with_device_hosting(d.device_id as u16, d.is_hosted));
390 }
391
392 device_records.push(wacore::store::traits::DeviceListRecord {
393 user: user_list.user.user.to_string(),
394 devices,
395 timestamp: wacore::time::now_secs(),
396 phash: user_list.phash.clone(),
397 raw_id,
398 });
399 }
400
401 for reset in pending_identity_resets {
405 self.clear_device_record(
406 &reset.user.user,
407 reset.user.server.as_str(),
408 &reset.previous,
409 )
410 .await;
411 if reset.invalidate_registry {
412 self.invalidate_device_cache_guarded(&reset.user.user, guard)
413 .await;
414 }
415 }
416
417 if let Err(e) = self
421 .update_device_lists_guarded(device_records, guard)
422 .await
423 {
424 warn!("Failed to update device registry batch: {e}");
425 }
426
427 Ok(fetched_devices)
428 }
429
430 #[cfg_attr(
436 feature = "tracing",
437 tracing::instrument(
438 name = "wa.usync.sync_own_device_list",
439 level = "debug",
440 skip_all,
441 err(Debug)
442 )
443 )]
444 pub(crate) async fn sync_own_device_list(&self) -> Result<(), anyhow::Error> {
445 let device_snapshot = self.persistence_manager.get_device_snapshot();
446
447 let mut jids = Vec::with_capacity(2);
448 let mut hashes: std::collections::HashMap<Jid, (String, i64)> =
449 std::collections::HashMap::new();
450 for own in device_snapshot.pn.iter().chain(device_snapshot.lid.iter()) {
451 let bare = own.to_non_ad();
452 if let Some(record) = self.load_device_record(&bare.user).await
454 && let Some(phash) = record.phash
455 {
456 hashes.insert(bare.clone(), (phash, record.timestamp));
457 }
458 jids.push(bare);
459 }
460
461 if jids.is_empty() {
462 return Ok(());
463 }
464
465 let sid = self.generate_request_id();
466 let spec = DeviceListSpec::with_hashes(jids, sid, hashes);
467 let response = self.execute(spec).await?;
468 let devices = self
471 .process_device_list_response(&response, crate::cache::Freshness::CachePreferred)
472 .await?;
473 log::info!(
474 "Re-synced own device list: {} device(s) updated",
475 devices.len()
476 );
477 Ok(())
478 }
479
480 #[cfg_attr(
482 feature = "tracing",
483 tracing::instrument(
484 name = "wa.usync.flush_pending_device_sync",
485 level = "debug",
486 skip_all
487 )
488 )]
489 pub(crate) async fn flush_pending_device_sync(&self) {
490 let pending = self.pending_device_sync.take_all().await;
491 if pending.is_empty() {
492 return;
493 }
494
495 debug!("Flushing pending device sync for {} users", pending.len());
496
497 for jid in &pending {
499 self.invalidate_device_cache(&jid.user).await;
500 }
501
502 match self.get_user_devices(&pending).await {
503 Ok(devices) => {
504 debug!(
505 "Pending device sync completed: {} devices across {} users",
506 devices.len(),
507 pending.len()
508 );
509 }
510 Err(e) => {
511 warn!(
512 "Pending device sync failed, re-enqueueing {} users: {e:?}",
513 pending.len()
514 );
515 for jid in &pending {
516 self.pending_device_sync.add(jid).await;
517 }
518 }
519 }
520 }
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526 use crate::cache::Freshness;
527 use crate::test_utils::create_test_client;
528 use wacore::libsignal::protocol::{ProtocolAddress, SessionRecord};
529 use wacore::store::traits::{DeviceInfo, DeviceListRecord};
530 use wacore::types::jid::JidExt;
531
532 fn signed_key_index_bytes(valid_indexes: Vec<u32>, current_index: u32) -> Vec<u8> {
533 let key_index = waproto::whatsapp::ADVKeyIndexList {
534 raw_id: Some(1),
535 timestamp: Some(1_700_000_000),
536 current_index: Some(current_index),
537 valid_indexes,
538 ..Default::default()
539 };
540 let signed = waproto::whatsapp::ADVSignedKeyIndexList {
541 details: Some(waproto::codec::adv_key_index_list_to_vec(&key_index)),
542 ..Default::default()
543 };
544 waproto::codec::adv_signed_key_index_list_to_vec(&signed)
545 }
546
547 async fn seed_fresh_session(client: &Client, jid: &Jid) -> ProtocolAddress {
548 let address = jid.to_protocol_address();
549 client
550 .signal_cache
551 .put_session(&address, SessionRecord::new_fresh())
552 .await;
553 address
554 }
555
556 async fn has_session(client: &Client, address: &ProtocolAddress) -> bool {
557 let snapshot = client.persistence_manager.get_device_snapshot();
558 client
559 .signal_cache
560 .has_session(address, &*snapshot.backend)
561 .await
562 .unwrap()
563 }
564
565 #[tokio::test]
566 async fn test_device_registry_hit_resolves_devices() {
567 let client = create_test_client().await;
568
569 let user_jid: Jid = "1234567890@s.whatsapp.net".parse().unwrap();
570
571 let record = DeviceListRecord {
573 user: "1234567890".into(),
574 devices: vec![DeviceInfo::new(0, None), DeviceInfo::new(3, Some(10))],
575 timestamp: wacore::time::now_secs(),
576 phash: None,
577 raw_id: None,
578 };
579 client.update_device_list(record).await.unwrap();
580
581 let devices = client.get_user_devices(&[user_jid]).await.unwrap();
583 assert_eq!(devices.len(), 2);
584 assert!(devices.iter().any(|d| d.device == 0));
585 assert!(devices.iter().any(|d| d.device == 3));
586 assert!(devices.iter().all(|d| d.is_pn()));
587 }
588
589 #[tokio::test]
590 async fn refresh_bypasses_a_warm_registry_without_clearing_it_first() {
591 let client = create_test_client().await;
592 let user: Jid = "12025550102@s.whatsapp.net".parse().unwrap();
593 client
594 .update_device_list(DeviceListRecord {
595 user: "12025550102".into(),
596 devices: vec![DeviceInfo::new(0, None), DeviceInfo::new(8, None)],
597 timestamp: wacore::time::now_secs(),
598 phash: None,
599 raw_id: None,
600 })
601 .await
602 .unwrap();
603
604 let cached = client
605 .get_user_devices(std::slice::from_ref(&user))
606 .await
607 .unwrap();
608 assert_eq!(
609 cached.len(),
610 2,
611 "cache-preferred must use the warm snapshot"
612 );
613
614 let refresh = client.refresh_user_devices(vec![user.clone()]).await;
615 assert!(
616 refresh.is_err(),
617 "the offline fixture proves refresh consulted the source"
618 );
619
620 let preserved = client
621 .get_devices_from_registry(&user)
622 .await
623 .expect("a failed refresh must leave the previous snapshot readable");
624 assert_eq!(preserved.len(), 2);
625 assert!(preserved.iter().any(|device| device.device == 8));
626 }
627
628 #[tokio::test]
629 async fn test_device_registry_hit_for_lid_jid() {
630 let client = create_test_client().await;
631
632 let lid_jid: Jid = "100000012345678@lid".parse().unwrap();
633
634 let record = DeviceListRecord {
635 user: "100000012345678".into(),
636 devices: vec![DeviceInfo::new(0, None), DeviceInfo::new(39, Some(25))],
637 timestamp: wacore::time::now_secs(),
638 phash: None,
639 raw_id: None,
640 };
641 client.update_device_list(record).await.unwrap();
642
643 let devices = client.get_user_devices(&[lid_jid]).await.unwrap();
644 assert_eq!(devices.len(), 2);
645 assert!(devices.iter().any(|d| d.device == 0));
646 assert!(devices.iter().any(|d| d.device == 39));
647 assert!(devices.iter().all(|d| d.is_lid()));
648 }
649
650 #[tokio::test]
651 async fn test_device_registry_db_fallback() {
652 let client = create_test_client().await;
653
654 let user_jid: Jid = "9876543210@s.whatsapp.net".parse().unwrap();
655
656 let record = DeviceListRecord {
658 user: "9876543210".into(),
659 devices: vec![DeviceInfo::new(5, None)],
660 timestamp: wacore::time::now_secs(),
661 phash: None,
662 raw_id: None,
663 };
664 client.update_device_list(record).await.unwrap();
665
666 client
668 .device_registry_cache
669 .raw_invalidate_for_tests("9876543210")
670 .await;
671 client.device_registry_cache.run_pending_tasks().await;
672
673 let devices = client.get_user_devices(&[user_jid]).await.unwrap();
675 assert_eq!(devices.len(), 1);
676 assert_eq!(devices[0].device, 5);
677 }
678
679 #[tokio::test]
684 async fn test_empty_device_record_falls_through_to_network() {
685 let client = create_test_client().await;
686
687 let user_jid: Jid = "5551230000@s.whatsapp.net".parse().unwrap();
688
689 let record = DeviceListRecord {
690 user: "5551230000".into(),
691 devices: vec![],
692 timestamp: wacore::time::now_secs(),
693 phash: None,
694 raw_id: None,
695 };
696 client.update_device_list(record).await.unwrap();
697
698 let result = client.get_user_devices(&[user_jid]).await;
699 assert!(
700 result.is_err(),
701 "empty record must fall through to the network, got {result:?}"
702 );
703 }
704
705 #[tokio::test]
706 async fn test_cache_size_eviction() {
707 use crate::cache::Cache;
708
709 let cache: Cache<i32, String> = Cache::builder().max_capacity(2).build();
710
711 cache.insert(1, "one".to_string()).await;
712 cache.insert(2, "two".to_string()).await;
713 cache.insert(3, "three".to_string()).await;
714
715 cache.run_pending_tasks().await;
716
717 let count = cache.entry_count();
718 assert!(
719 count <= 2,
720 "Cache should have at most 2 items, has {}",
721 count
722 );
723 }
724
725 #[tokio::test]
729 async fn process_response_preserves_omitted_users() {
730 use wacore::iq::usync::DeviceListResponse;
731 use wacore::usync::{UserDeviceList, UsyncDevice};
732
733 let client = create_test_client().await;
734
735 client
737 .update_device_list(DeviceListRecord {
738 user: "2222222222".into(),
739 devices: vec![DeviceInfo::new(0, None), DeviceInfo::new(7, None)],
740 timestamp: wacore::time::now_secs(),
741 phash: Some("2:oldB".to_string()),
742 raw_id: None,
743 })
744 .await
745 .unwrap();
746
747 let response = DeviceListResponse {
749 device_lists: vec![UserDeviceList {
750 user: "1111111111@s.whatsapp.net".parse().unwrap(),
751 devices: vec![UsyncDevice::new(0, None)],
752 phash: Some("2:a".to_string()),
753 key_index_bytes: None,
754 }],
755 lid_mappings: vec![],
756 };
757
758 let fetched = client
759 .process_device_list_response(&response, Freshness::CachePreferred)
760 .await
761 .unwrap();
762 assert!(
763 fetched.iter().any(|j| j.user == "1111111111"),
764 "returned user A must be resolved"
765 );
766
767 let b_jid: Jid = "2222222222@s.whatsapp.net".parse().unwrap();
769 let b_devices = client
770 .get_devices_from_registry(&b_jid)
771 .await
772 .expect("omitted user B must keep its cached record");
773 assert_eq!(
774 b_devices.len(),
775 2,
776 "omitted user's devices must be preserved"
777 );
778 }
779
780 #[tokio::test]
781 async fn process_response_preserves_hosted_device_addressing() {
782 use wacore::iq::usync::DeviceListResponse;
783 use wacore::usync::{UserDeviceList, UsyncDevice};
784
785 let client = create_test_client().await;
786 let user = Jid::pn("1111111111");
787 let response = DeviceListResponse {
788 device_lists: vec![UserDeviceList {
789 user: user.clone(),
790 devices: vec![
791 UsyncDevice::new(0, None),
792 UsyncDevice::new(7, Some(3)).with_hosting(true),
793 ],
794 phash: Some("2:hosted".to_string()),
795 key_index_bytes: None,
796 }],
797 lid_mappings: Vec::new(),
798 };
799
800 let fetched = client
801 .process_device_list_response(&response, Freshness::CachePreferred)
802 .await
803 .unwrap();
804 assert!(
805 fetched
806 .iter()
807 .any(|jid| jid.device == 7 && jid.server == wacore_binary::Server::Hosted)
808 );
809
810 let cached = client
811 .get_devices_from_registry(&user)
812 .await
813 .expect("processed devices should be cached");
814 assert!(
815 cached
816 .iter()
817 .any(|jid| jid.device == 7 && jid.server == wacore_binary::Server::Hosted)
818 );
819 }
820
821 #[tokio::test]
822 async fn stale_refresh_does_not_overwrite_a_newer_device_notification() {
823 use wacore::usync::{UserDeviceList, UsyncDevice};
824
825 let client = create_test_client().await;
826 let user = Jid::pn("12025550102");
827 client
828 .update_device_list(DeviceListRecord {
829 user: user.user.to_string(),
830 devices: vec![DeviceInfo::new(0, None)],
831 timestamp: wacore::time::now_secs(),
832 phash: Some("1:before".to_string()),
833 raw_id: None,
834 })
835 .await
836 .unwrap();
837
838 let refresh_started_at = client.device_topology.current();
839 let stale_response = DeviceListResponse {
840 device_lists: vec![UserDeviceList {
841 user: user.clone(),
842 devices: vec![UsyncDevice::new(0, None)],
843 phash: Some("1:stale".to_string()),
844 key_index_bytes: None,
845 }],
846 lid_mappings: Vec::new(),
847 };
848
849 client
850 .patch_device_add(
851 &user.user,
852 &wacore::stanza::devices::DeviceElement {
853 jid: user.with_device(7),
854 key_index: None,
855 lid: None,
856 },
857 None,
858 )
859 .await;
860
861 let published = client
862 .try_process_refreshed_device_list_response(
863 &stale_response,
864 Freshness::Refresh,
865 refresh_started_at,
866 )
867 .await
868 .unwrap();
869 assert!(published.is_none(), "the stale response must be retried");
870
871 let retained = client
872 .get_devices_from_registry(&user)
873 .await
874 .expect("notification snapshot must remain available");
875 assert!(retained.iter().any(|device| device.device == 7));
876 }
877
878 #[tokio::test]
879 async fn stale_refresh_does_not_overwrite_a_newer_lid_mapping() {
880 use wacore::usync::UsyncLidMapping;
881
882 let client = create_test_client().await;
883 let phone = "12025550110";
884 let current_lid = "100000000000110";
885 let stale_lid = "100000000000111";
886 let refresh_started_at = client.device_topology.current();
887
888 client
889 .add_lid_pn_mapping(
890 current_lid,
891 phone,
892 crate::lid_pn_cache::LearningSource::PeerPnMessage,
893 )
894 .await
895 .unwrap();
896
897 let stale_response = DeviceListResponse {
898 device_lists: Vec::new(),
899 lid_mappings: vec![UsyncLidMapping {
900 phone_number: phone.into(),
901 lid: stale_lid.into(),
902 }],
903 };
904 let published = client
905 .try_process_refreshed_device_list_response(
906 &stale_response,
907 Freshness::Refresh,
908 refresh_started_at,
909 )
910 .await
911 .unwrap();
912
913 assert!(published.is_none(), "the stale response must be retried");
914 assert_eq!(
915 client.lid_pn_cache.get_current_lid(phone).await.as_deref(),
916 Some(current_lid),
917 "the mapping learned after the refresh started must survive"
918 );
919 }
920
921 #[tokio::test]
922 async fn refresh_commits_its_own_lid_mapping_after_the_cas() {
923 use wacore::usync::UsyncLidMapping;
924
925 let client = create_test_client().await;
926 let phone = "12025550112";
927 let lid = "100000000000112";
928 let refresh_started_at = client.device_topology.current();
929 let response = DeviceListResponse {
930 device_lists: Vec::new(),
931 lid_mappings: vec![UsyncLidMapping {
932 phone_number: phone.into(),
933 lid: lid.into(),
934 }],
935 };
936
937 let published = client
938 .try_process_refreshed_device_list_response(
939 &response,
940 Freshness::Refresh,
941 refresh_started_at,
942 )
943 .await
944 .unwrap();
945
946 assert!(
947 published.is_some(),
948 "the response must not conflict with itself"
949 );
950 assert_eq!(
951 client.lid_pn_cache.get_current_lid(phone).await.as_deref(),
952 Some(lid)
953 );
954 }
955
956 #[tokio::test]
957 async fn unrelated_registry_change_does_not_restart_a_refresh() {
958 use wacore::usync::{UserDeviceList, UsyncDevice};
959
960 let client = create_test_client().await;
961 let refreshed = Jid::pn("12025550104");
962 let refresh_started_at = client.device_topology.current();
963 client
964 .update_device_list(DeviceListRecord {
965 user: "12025550105".to_string(),
966 devices: vec![DeviceInfo::new(0, None)],
967 timestamp: wacore::time::now_secs(),
968 phash: None,
969 raw_id: None,
970 })
971 .await
972 .unwrap();
973
974 let response = DeviceListResponse {
975 device_lists: vec![UserDeviceList {
976 user: refreshed,
977 devices: vec![UsyncDevice::new(0, None)],
978 phash: None,
979 key_index_bytes: None,
980 }],
981 lid_mappings: Vec::new(),
982 };
983 let published = client
984 .try_process_refreshed_device_list_response(
985 &response,
986 Freshness::Refresh,
987 refresh_started_at,
988 )
989 .await
990 .unwrap();
991 assert!(published.is_some());
992 }
993
994 #[tokio::test]
995 async fn unrelated_mapping_change_does_not_restart_a_refresh() {
996 use wacore::usync::{UserDeviceList, UsyncDevice};
997
998 let client = create_test_client().await;
999 let refreshed = Jid::pn("12025550113");
1000 let refresh_started_at = client.device_topology.current();
1001 client
1002 .add_lid_pn_mapping(
1003 "100000000000114",
1004 "12025550114",
1005 crate::lid_pn_cache::LearningSource::PeerPnMessage,
1006 )
1007 .await
1008 .unwrap();
1009
1010 let response = DeviceListResponse {
1011 device_lists: vec![UserDeviceList {
1012 user: refreshed,
1013 devices: vec![UsyncDevice::new(0, None)],
1014 phash: None,
1015 key_index_bytes: None,
1016 }],
1017 lid_mappings: Vec::new(),
1018 };
1019 let published = client
1020 .try_process_refreshed_device_list_response(
1021 &response,
1022 Freshness::Refresh,
1023 refresh_started_at,
1024 )
1025 .await
1026 .unwrap();
1027
1028 assert!(published.is_some());
1029 }
1030
1031 #[tokio::test]
1037 async fn process_response_skips_empty_device_list() {
1038 use wacore::usync::UserDeviceList;
1039
1040 let client = create_test_client().await;
1041
1042 client
1043 .update_device_list(DeviceListRecord {
1044 user: "3333333333".into(),
1045 devices: vec![DeviceInfo::new(0, None), DeviceInfo::new(4, None)],
1046 timestamp: wacore::time::now_secs(),
1047 phash: Some("3:old".to_string()),
1048 raw_id: None,
1049 })
1050 .await
1051 .unwrap();
1052
1053 let response = DeviceListResponse {
1055 device_lists: vec![UserDeviceList {
1056 user: "3333333333@s.whatsapp.net".parse().unwrap(),
1057 devices: vec![],
1058 phash: Some("3:empty".to_string()),
1059 key_index_bytes: None,
1060 }],
1061 lid_mappings: vec![],
1062 };
1063
1064 let fetched = client
1065 .process_device_list_response(&response, Freshness::CachePreferred)
1066 .await
1067 .unwrap();
1068 assert!(
1069 !fetched.iter().any(|j| j.user == "3333333333"),
1070 "an empty returned list contributes no devices"
1071 );
1072
1073 let jid: Jid = "3333333333@s.whatsapp.net".parse().unwrap();
1075 let devices = client
1076 .get_devices_from_registry(&jid)
1077 .await
1078 .expect("the good record must survive an empty usync response");
1079 assert_eq!(
1080 devices.len(),
1081 2,
1082 "empty response must not clobber the record"
1083 );
1084 }
1085
1086 #[tokio::test]
1087 async fn refresh_rejects_a_device_list_emptied_by_key_index_filtering() {
1088 use wacore::usync::{UserDeviceList, UsyncDevice};
1089
1090 let client = create_test_client().await;
1091 let user = Jid::pn("4444444444");
1092 client
1093 .update_device_list(DeviceListRecord {
1094 user: user.user.to_string(),
1095 devices: vec![DeviceInfo::new(0, None), DeviceInfo::new(7, Some(3))],
1096 timestamp: wacore::time::now_secs(),
1097 phash: Some("2:previous".to_string()),
1098 raw_id: Some(1),
1099 })
1100 .await
1101 .unwrap();
1102
1103 let response = DeviceListResponse {
1107 device_lists: vec![UserDeviceList {
1108 user: user.clone(),
1109 devices: vec![UsyncDevice::new(7, Some(3))],
1110 phash: Some("2:incomplete".to_string()),
1111 key_index_bytes: Some(signed_key_index_bytes(Vec::new(), 10)),
1112 }],
1113 lid_mappings: Vec::new(),
1114 };
1115
1116 let error = client
1117 .process_device_list_response(&response, Freshness::Refresh)
1118 .await
1119 .expect_err("an authoritative refresh must not return an empty projection");
1120 assert!(error.to_string().contains("no valid devices"));
1121
1122 let preserved = client
1123 .get_devices_from_registry(&user)
1124 .await
1125 .expect("a rejected refresh must preserve the previous snapshot");
1126 assert_eq!(preserved.len(), 2);
1127 assert!(preserved.iter().any(|device| device.device == 7));
1128 }
1129
1130 #[tokio::test]
1131 async fn rejected_refresh_defers_identity_cleanup_for_every_user() {
1132 use wacore::usync::{UserDeviceList, UsyncDevice};
1133
1134 let client = create_test_client().await;
1135 let identity_changed = Jid::pn("4444444451");
1136 let invalid = Jid::pn("4444444452");
1137
1138 for (user, raw_id) in [(&identity_changed, 2), (&invalid, 1)] {
1139 client
1140 .update_device_list(DeviceListRecord {
1141 user: user.user.to_string(),
1142 devices: vec![DeviceInfo::new(0, None), DeviceInfo::new(7, Some(3))],
1143 timestamp: wacore::time::now_secs(),
1144 phash: Some("2:previous".to_string()),
1145 raw_id: Some(raw_id),
1146 })
1147 .await
1148 .unwrap();
1149 }
1150
1151 let previous_session = seed_fresh_session(&client, &identity_changed.with_device(7)).await;
1152 let response = DeviceListResponse {
1153 device_lists: vec![
1154 UserDeviceList {
1155 user: identity_changed.clone(),
1156 devices: vec![UsyncDevice::new(0, None)],
1159 phash: Some("1:changed".to_string()),
1160 key_index_bytes: Some(signed_key_index_bytes(Vec::new(), 10)),
1161 },
1162 UserDeviceList {
1163 user: invalid,
1164 devices: vec![UsyncDevice::new(7, Some(3))],
1167 phash: Some("1:invalid".to_string()),
1168 key_index_bytes: Some(signed_key_index_bytes(Vec::new(), 10)),
1169 },
1170 ],
1171 lid_mappings: Vec::new(),
1172 };
1173
1174 client
1175 .process_device_list_response(&response, Freshness::Refresh)
1176 .await
1177 .expect_err("the second user must reject the whole authoritative refresh");
1178
1179 assert!(
1180 has_session(&client, &previous_session).await,
1181 "validation failure must not partially clear an earlier user's sessions"
1182 );
1183 let preserved = client
1184 .get_devices_from_registry(&identity_changed)
1185 .await
1186 .expect("validation failure must preserve the earlier registry snapshot");
1187 assert!(preserved.iter().any(|device| device.device == 7));
1188 }
1189
1190 #[tokio::test]
1191 async fn filtered_identity_change_invalidates_the_stale_registry() {
1192 use wacore::usync::{UserDeviceList, UsyncDevice};
1193
1194 let client = create_test_client().await;
1195 let user = Jid::pn("4444444453");
1196 client
1197 .update_device_list(DeviceListRecord {
1198 user: user.user.to_string(),
1199 devices: vec![DeviceInfo::new(0, None), DeviceInfo::new(7, Some(3))],
1200 timestamp: wacore::time::now_secs(),
1201 phash: Some("2:previous".to_string()),
1202 raw_id: Some(2),
1203 })
1204 .await
1205 .unwrap();
1206 let previous_session = seed_fresh_session(&client, &user.with_device(7)).await;
1207
1208 let response = DeviceListResponse {
1209 device_lists: vec![UserDeviceList {
1210 user: user.clone(),
1211 devices: vec![UsyncDevice::new(7, Some(3))],
1212 phash: Some("1:changed".to_string()),
1213 key_index_bytes: Some(signed_key_index_bytes(Vec::new(), 10)),
1214 }],
1215 lid_mappings: Vec::new(),
1216 };
1217
1218 let fetched = client
1219 .process_device_list_response(&response, Freshness::CachePreferred)
1220 .await
1221 .unwrap();
1222 assert!(fetched.is_empty());
1223 assert!(
1224 !has_session(&client, &previous_session).await,
1225 "an accepted identity change must clear sessions from the old identity"
1226 );
1227 assert!(
1228 client.get_devices_from_registry(&user).await.is_none(),
1229 "without a replacement snapshot, the stale registry must be invalidated"
1230 );
1231 }
1232
1233 #[tokio::test]
1238 async fn process_response_warms_lid_pn_cache_synchronously() {
1239 use wacore::usync::UsyncLidMapping;
1240
1241 let client = create_test_client().await;
1242
1243 assert!(
1249 client.self_weak.get().and_then(|w| w.upgrade()).is_some(),
1250 "fixture must populate self_weak so the batched learner is exercised"
1251 );
1252
1253 let response = DeviceListResponse {
1254 device_lists: vec![],
1255 lid_mappings: vec![UsyncLidMapping {
1256 phone_number: "559980000123".into(),
1257 lid: "100000000000123".into(),
1258 }],
1259 };
1260
1261 assert!(
1262 client
1263 .lid_pn_cache
1264 .get_current_lid("559980000123")
1265 .await
1266 .is_none()
1267 );
1268
1269 client
1270 .process_device_list_response(&response, Freshness::CachePreferred)
1271 .await
1272 .unwrap();
1273
1274 assert_eq!(
1275 client
1276 .lid_pn_cache
1277 .get_current_lid("559980000123")
1278 .await
1279 .as_deref(),
1280 Some("100000000000123"),
1281 "usync LID mapping must be in the cache synchronously after the call"
1282 );
1283 }
1284}