1use std::collections::HashMap;
2use std::str::FromStr;
3
4use itertools::Itertools;
5use openmls_traits::OpenMlsCryptoProvider;
6use x509_cert::der::pem::LineEnding;
7
8use crate::context::CentralContext;
9use crate::e2e_identity::id::WireQualifiedClientId;
10use crate::mls::credential::ext::CredentialExt;
11use crate::prelude::MlsCredentialType;
12use crate::{
13 e2e_identity::device_status::DeviceStatus,
14 prelude::{user_id::UserId, ClientId, ConversationId, CryptoError, CryptoResult, MlsCentral, MlsConversation},
15};
16
17#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
20pub struct WireIdentity {
21 pub client_id: String,
23 pub thumbprint: String,
25 pub status: DeviceStatus,
27 pub credential_type: MlsCredentialType,
29 pub x509_identity: Option<X509Identity>,
31}
32
33#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
38pub struct X509Identity {
39 pub handle: String,
41 pub display_name: String,
43 pub domain: String,
45 pub certificate: String,
47 pub serial_number: String,
49 pub not_before: u64,
51 pub not_after: u64,
53}
54
55impl<'a> TryFrom<(wire_e2e_identity::prelude::WireIdentity, &'a [u8])> for WireIdentity {
56 type Error = CryptoError;
57
58 fn try_from((i, cert): (wire_e2e_identity::prelude::WireIdentity, &'a [u8])) -> CryptoResult<Self> {
59 use x509_cert::der::Decode as _;
60 let document = x509_cert::der::Document::from_der(cert)?;
61 let certificate = document.to_pem("CERTIFICATE", LineEnding::LF)?;
62
63 let client_id = WireQualifiedClientId::from_str(&i.client_id)?;
64
65 Ok(Self {
66 client_id: client_id.try_into()?,
67 status: i.status.into(),
68 thumbprint: i.thumbprint,
69 credential_type: MlsCredentialType::X509,
70 x509_identity: Some(X509Identity {
71 handle: i.handle.to_string(),
72 display_name: i.display_name,
73 domain: i.domain,
74 certificate,
75 serial_number: i.serial_number,
76 not_before: i.not_before,
77 not_after: i.not_after,
78 }),
79 })
80 }
81}
82
83impl CentralContext {
84 pub async fn get_device_identities(
86 &self,
87 conversation_id: &ConversationId,
88 client_ids: &[ClientId],
89 ) -> CryptoResult<Vec<WireIdentity>> {
90 let mls_provider = self.mls_provider().await?;
91 let auth_service = mls_provider.authentication_service();
92 auth_service.refresh_time_of_interest().await;
93 let auth_service = auth_service.borrow().await;
94 let conversation = self.get_conversation(conversation_id).await?;
95 let conversation_guard = conversation.read().await;
96 conversation_guard.get_device_identities(client_ids, auth_service.as_ref())
97 }
98
99 pub async fn get_user_identities(
101 &self,
102 conversation_id: &ConversationId,
103 user_ids: &[String],
104 ) -> CryptoResult<HashMap<String, Vec<WireIdentity>>> {
105 let mls_provider = self.mls_provider().await?;
106 let auth_service = mls_provider.authentication_service();
107 auth_service.refresh_time_of_interest().await;
108 let auth_service = auth_service.borrow().await;
109 let conversation = self.get_conversation(conversation_id).await?;
110 let conversation_guard = conversation.read().await;
111 conversation_guard.get_user_identities(user_ids, auth_service.as_ref())
112 }
113}
114
115impl MlsCentral {
116 pub async fn get_device_identities(
120 &self,
121 conversation_id: &ConversationId,
122 client_ids: &[ClientId],
123 ) -> CryptoResult<Vec<WireIdentity>> {
124 self.mls_backend
125 .authentication_service()
126 .refresh_time_of_interest()
127 .await;
128 let conversation = self.get_conversation(conversation_id).await?;
129 let Some(conversation) = conversation else {
130 return Err(CryptoError::ConversationNotFound(conversation_id.clone()));
131 };
132 conversation.get_device_identities(
133 client_ids,
134 self.mls_backend.authentication_service().borrow().await.as_ref(),
135 )
136 }
137
138 pub async fn get_user_identities(
145 &self,
146 conversation_id: &ConversationId,
147 user_ids: &[String],
148 ) -> CryptoResult<HashMap<String, Vec<WireIdentity>>> {
149 self.mls_backend
150 .authentication_service()
151 .refresh_time_of_interest()
152 .await;
153 let Some(conversation) = self.get_conversation(conversation_id).await? else {
154 return Err(CryptoError::ConversationNotFound(conversation_id.clone()));
155 };
156 conversation.get_user_identities(
157 user_ids,
158 self.mls_backend.authentication_service().borrow().await.as_ref(),
159 )
160 }
161}
162
163impl MlsConversation {
164 fn get_device_identities(
165 &self,
166 device_ids: &[ClientId],
167 env: Option<&wire_e2e_identity::prelude::x509::revocation::PkiEnvironment>,
168 ) -> CryptoResult<Vec<WireIdentity>> {
169 if device_ids.is_empty() {
170 return Err(CryptoError::ConsumerError);
171 }
172 self.members_with_key()
173 .into_iter()
174 .filter(|(id, _)| device_ids.contains(&ClientId::from(id.as_slice())))
175 .map(|(_, c)| c.extract_identity(self.ciphersuite(), env))
176 .collect::<CryptoResult<Vec<_>>>()
177 }
178
179 fn get_user_identities(
180 &self,
181 user_ids: &[String],
182 env: Option<&wire_e2e_identity::prelude::x509::revocation::PkiEnvironment>,
183 ) -> CryptoResult<HashMap<String, Vec<WireIdentity>>> {
184 if user_ids.is_empty() {
185 return Err(CryptoError::ConsumerError);
186 }
187 let user_ids = user_ids.iter().map(|uid| uid.as_bytes()).collect::<Vec<_>>();
188
189 self.members_with_key()
190 .iter()
191 .filter_map(|(id, c)| UserId::try_from(id.as_slice()).ok().zip(Some(c)))
192 .filter(|(uid, _)| user_ids.contains(uid))
193 .map(|(uid, c)| (uid, c.extract_identity(self.ciphersuite(), env)))
194 .map(|(uid, identity)| {
195 let uid = String::try_from(uid);
196 uid.and_then(|uid| identity.map(|id| (uid, id)))
198 })
199 .process_results(|iter| iter.into_group_map())
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use wasm_bindgen_test::*;
206
207 use crate::context::CentralContext;
208 use crate::prelude::{ClientId, ConversationId, MlsCredentialType};
209 use crate::{
210 prelude::{DeviceStatus, E2eiConversationState},
211 test_utils::*,
212 CryptoError,
213 };
214
215 wasm_bindgen_test_configure!(run_in_browser);
216
217 async fn all_identities_check<const N: usize>(
218 central: &CentralContext,
219 id: &ConversationId,
220 user_ids: &[String; N],
221 expected_sizes: [usize; N],
222 ) {
223 let all_identities = central.get_user_identities(id, user_ids).await.unwrap();
224 assert_eq!(all_identities.len(), N);
225 for (expected_size, user_id) in expected_sizes.into_iter().zip(user_ids.iter()) {
226 let alice_identities = all_identities.get(user_id).unwrap();
227 assert_eq!(alice_identities.len(), expected_size);
228 }
229 let not_found = central
231 .get_user_identities(id, &["aaaaaaaaaaaaa".to_string()])
232 .await
233 .unwrap();
234 assert!(not_found.is_empty());
235
236 let invalid = central.get_user_identities(id, &[]).await;
238 assert!(matches!(invalid.unwrap_err(), CryptoError::ConsumerError));
239 }
240
241 async fn check_identities_device_status<const N: usize>(
242 central: &CentralContext,
243 id: &ConversationId,
244 client_ids: &[ClientId; N],
245 name_status: &[(&'static str, DeviceStatus); N],
246 ) {
247 let mut identities = central.get_device_identities(id, client_ids).await.unwrap();
248
249 for j in 0..N {
250 let client_identity = identities.remove(
251 identities
252 .iter()
253 .position(|i| i.x509_identity.as_ref().unwrap().display_name == name_status[j].0)
254 .unwrap(),
255 );
256 assert_eq!(client_identity.status, name_status[j].1);
257 }
258 assert!(identities.is_empty());
259
260 assert_eq!(
261 central.e2ei_conversation_state(id).await.unwrap(),
262 E2eiConversationState::NotVerified
263 );
264 }
265
266 #[async_std::test]
267 #[wasm_bindgen_test]
268 async fn should_read_device_identities() {
269 let case = TestCase::default_x509();
270 run_test_with_client_ids(
271 case.clone(),
272 ["alice_android", "alice_ios"],
273 move |[alice_android_central, alice_ios_central]| {
274 Box::pin(async move {
275 let id = conversation_id();
276 alice_android_central
277 .context
278 .new_conversation(&id, case.credential_type, case.cfg.clone())
279 .await
280 .unwrap();
281 alice_android_central
282 .invite_all(&case, &id, [&alice_ios_central])
283 .await
284 .unwrap();
285
286 let (android_id, ios_id) = (
287 alice_android_central.get_client_id().await,
288 alice_ios_central.get_client_id().await,
289 );
290
291 let mut android_ids = alice_android_central
292 .context
293 .get_device_identities(&id, &[android_id.clone(), ios_id.clone()])
294 .await
295 .unwrap();
296 android_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
297 assert_eq!(android_ids.len(), 2);
298 let mut ios_ids = alice_ios_central
299 .context
300 .get_device_identities(&id, &[android_id.clone(), ios_id.clone()])
301 .await
302 .unwrap();
303 ios_ids.sort_by(|a, b| a.client_id.cmp(&b.client_id));
304 assert_eq!(ios_ids.len(), 2);
305
306 assert_eq!(android_ids, ios_ids);
307
308 let android_identities = alice_android_central
309 .context
310 .get_device_identities(&id, &[android_id])
311 .await
312 .unwrap();
313 let android_id = android_identities.first().unwrap();
314 assert_eq!(
315 android_id.client_id.as_bytes(),
316 alice_android_central.context.client_id().await.unwrap().0.as_slice()
317 );
318
319 let ios_identities = alice_android_central
320 .context
321 .get_device_identities(&id, &[ios_id])
322 .await
323 .unwrap();
324 let ios_id = ios_identities.first().unwrap();
325 assert_eq!(
326 ios_id.client_id.as_bytes(),
327 alice_ios_central.context.client_id().await.unwrap().0.as_slice()
328 );
329
330 let invalid = alice_android_central.context.get_device_identities(&id, &[]).await;
331 assert!(matches!(invalid.unwrap_err(), CryptoError::ConsumerError));
332 })
333 },
334 )
335 .await
336 }
337
338 #[async_std::test]
339 #[wasm_bindgen_test]
340 async fn should_read_revoked_device_cross_signed() {
341 let case = TestCase::default_x509();
342 run_test_with_client_ids_and_revocation(
343 case.clone(),
344 ["alice", "bob", "rupert"],
345 ["john", "dilbert"],
346 &["rupert", "dilbert"],
347 move |[mut alice, mut bob, mut rupert], [mut john, mut dilbert]| {
348 Box::pin(async move {
349 let id = conversation_id();
350 alice
351 .context
352 .new_conversation(&id, case.credential_type, case.cfg.clone())
353 .await
354 .unwrap();
355 alice
356 .invite_all(&case, &id, [&bob, &rupert, &dilbert, &john])
357 .await
358 .unwrap();
359
360 let (alice_id, bob_id, rupert_id, dilbert_id, john_id) = (
361 alice.get_client_id().await,
362 bob.get_client_id().await,
363 rupert.get_client_id().await,
364 dilbert.get_client_id().await,
365 john.get_client_id().await,
366 );
367
368 let client_ids = [alice_id, bob_id, rupert_id, dilbert_id, john_id];
369 let name_status = [
370 ("alice", DeviceStatus::Valid),
371 ("bob", DeviceStatus::Valid),
372 ("rupert", DeviceStatus::Revoked),
373 ("john", DeviceStatus::Valid),
374 ("dilbert", DeviceStatus::Revoked),
375 ];
376 for _ in 0..2 {
378 check_identities_device_status(&mut alice.context, &id, &client_ids, &name_status).await;
379 check_identities_device_status(&mut bob.context, &id, &client_ids, &name_status).await;
380 check_identities_device_status(&mut rupert.context, &id, &client_ids, &name_status).await;
381 check_identities_device_status(&mut john.context, &id, &client_ids, &name_status).await;
382 check_identities_device_status(&mut dilbert.context, &id, &client_ids, &name_status).await;
383 }
384 })
385 },
386 )
387 .await
388 }
389
390 #[async_std::test]
391 #[wasm_bindgen_test]
392 async fn should_read_revoked_device() {
393 let case = TestCase::default_x509();
394 run_test_with_client_ids_and_revocation(
395 case.clone(),
396 ["alice", "bob", "rupert"],
397 [],
398 &["rupert"],
399 move |[mut alice, mut bob, mut rupert], []| {
400 Box::pin(async move {
401 let id = conversation_id();
402 alice
403 .context
404 .new_conversation(&id, case.credential_type, case.cfg.clone())
405 .await
406 .unwrap();
407 alice.invite_all(&case, &id, [&bob, &rupert]).await.unwrap();
408
409 let (alice_id, bob_id, rupert_id) = (
410 alice.get_client_id().await,
411 bob.get_client_id().await,
412 rupert.get_client_id().await,
413 );
414
415 let client_ids = [alice_id, bob_id, rupert_id];
416 let name_status = [
417 ("alice", DeviceStatus::Valid),
418 ("bob", DeviceStatus::Valid),
419 ("rupert", DeviceStatus::Revoked),
420 ];
421
422 for _ in 0..2 {
424 check_identities_device_status(&mut alice.context, &id, &client_ids, &name_status).await;
425 check_identities_device_status(&mut bob.context, &id, &client_ids, &name_status).await;
426 check_identities_device_status(&mut rupert.context, &id, &client_ids, &name_status).await;
427 }
428 })
429 },
430 )
431 .await
432 }
433
434 #[async_std::test]
435 #[wasm_bindgen_test]
436 async fn should_not_fail_when_basic() {
437 let case = TestCase::default();
438 run_test_with_client_ids(
439 case.clone(),
440 ["alice_android", "alice_ios"],
441 move |[alice_android_central, alice_ios_central]| {
442 Box::pin(async move {
443 let id = conversation_id();
444 alice_android_central
445 .context
446 .new_conversation(&id, case.credential_type, case.cfg.clone())
447 .await
448 .unwrap();
449 alice_android_central
450 .invite_all(&case, &id, [&alice_ios_central])
451 .await
452 .unwrap();
453
454 let (android_id, ios_id) = (
455 alice_android_central.get_client_id().await,
456 alice_ios_central.get_client_id().await,
457 );
458
459 let mut android_ids = alice_android_central
460 .context
461 .get_device_identities(&id, &[android_id.clone(), ios_id.clone()])
462 .await
463 .unwrap();
464 android_ids.sort();
465
466 let mut ios_ids = alice_ios_central
467 .context
468 .get_device_identities(&id, &[android_id, ios_id])
469 .await
470 .unwrap();
471 ios_ids.sort();
472
473 assert_eq!(ios_ids.len(), 2);
474 assert_eq!(ios_ids, android_ids);
475
476 assert!(ios_ids.iter().all(|i| {
477 matches!(i.credential_type, MlsCredentialType::Basic)
478 && matches!(i.status, DeviceStatus::Valid)
479 && i.x509_identity.is_none()
480 && !i.thumbprint.is_empty()
481 && !i.client_id.is_empty()
482 }));
483 })
484 },
485 )
486 .await
487 }
488
489 #[async_std::test]
492 #[wasm_bindgen_test]
493 async fn should_read_users_cross_signed() {
494 let case = TestCase::default_x509();
495
496 let (alice_android, alice_ios) = (
497 "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@world.com",
498 "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@world.com",
499 );
500 let (alicem_android, alicem_ios) = (
501 "8h2PRVj_Qyi7p1XLGmdulw:a7c5ac4446bf@world.com",
502 "8h2PRVj_Qyi7p1XLGmdulw:10c6f7a0b5ed@world.com",
503 );
504 let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@world.com";
505 let bobt_android = "HSLU78bpQCOYwh4FWCac5g:68db8bac6a65d@world.com";
506
507 run_test_with_deterministic_client_ids_and_revocation(
508 case.clone(),
509 [
510 [alice_android, "alice_wire", "Alice Smith"],
511 [alice_ios, "alice_wire", "Alice Smith"],
512 [bob_android, "bob_wire", "Bob Doe"],
513 ],
514 [
515 [alicem_android, "alice_zeta", "Alice Muller"],
516 [alicem_ios, "alice_zeta", "Alice Muller"],
517 [bobt_android, "bob_zeta", "Bob Tables"],
518 ],
519 &[],
520 move |[alice_android_central, alice_ios_central, bob_android_central],
521 [alicem_android_central, alicem_ios_central, bobt_android_central]| {
522 Box::pin(async move {
523 let id = conversation_id();
524 alice_android_central
525 .context
526 .new_conversation(&id, case.credential_type, case.cfg.clone())
527 .await
528 .unwrap();
529 alice_android_central
530 .invite_all(
531 &case,
532 &id,
533 [
534 &alice_ios_central,
535 &bob_android_central,
536 &bobt_android_central,
537 &alicem_ios_central,
538 &alicem_android_central,
539 ],
540 )
541 .await
542 .unwrap();
543
544 let nb_members = alice_android_central
545 .get_conversation_unchecked(&id)
546 .await
547 .members()
548 .len();
549 assert_eq!(nb_members, 6);
550
551 assert_eq!(
552 alice_android_central.get_user_id().await,
553 alice_ios_central.get_user_id().await
554 );
555
556 let alicem_user_id = alicem_ios_central.get_user_id().await;
557 let bobt_user_id = bobt_android_central.get_user_id().await;
558
559 let alice_user_id = alice_android_central.get_user_id().await;
561 let alice_identities = alice_android_central
562 .context
563 .get_user_identities(&id, &[alice_user_id.clone()])
564 .await
565 .unwrap();
566 assert_eq!(alice_identities.len(), 1);
567 let identities = alice_identities.get(&alice_user_id).unwrap();
568 assert_eq!(identities.len(), 2);
569
570 let bob_user_id = bob_android_central.get_user_id().await;
572 let bob_identities = alice_android_central
573 .context
574 .get_user_identities(&id, &[bob_user_id.clone()])
575 .await
576 .unwrap();
577 assert_eq!(bob_identities.len(), 1);
578 let identities = bob_identities.get(&bob_user_id).unwrap();
579 assert_eq!(identities.len(), 1);
580
581 let user_ids = [alice_user_id, bob_user_id, alicem_user_id, bobt_user_id];
583 let expected_sizes = [2, 1, 2, 1];
584
585 all_identities_check(&alice_android_central.context, &id, &user_ids, expected_sizes).await;
586 all_identities_check(&alicem_android_central.context, &id, &user_ids, expected_sizes).await;
587 all_identities_check(&alice_ios_central.context, &id, &user_ids, expected_sizes).await;
588 all_identities_check(&alicem_ios_central.context, &id, &user_ids, expected_sizes).await;
589 all_identities_check(&bob_android_central.context, &id, &user_ids, expected_sizes).await;
590 all_identities_check(&bobt_android_central.context, &id, &user_ids, expected_sizes).await;
591 })
592 },
593 )
594 .await
595 }
596
597 #[async_std::test]
598 #[wasm_bindgen_test]
599 async fn should_read_users() {
600 let case = TestCase::default_x509();
601
602 let (alice_android, alice_ios) = (
603 "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@world.com",
604 "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@world.com",
605 );
606 let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@world.com";
607
608 run_test_with_deterministic_client_ids(
609 case.clone(),
610 [
611 [alice_android, "alice_wire", "Alice Smith"],
612 [alice_ios, "alice_wire", "Alice Smith"],
613 [bob_android, "bob_wire", "Bob Doe"],
614 ],
615 move |[mut alice_android_central, mut alice_ios_central, mut bob_android_central]| {
616 Box::pin(async move {
617 let id = conversation_id();
618 alice_android_central
619 .context
620 .new_conversation(&id, case.credential_type, case.cfg.clone())
621 .await
622 .unwrap();
623 alice_android_central
624 .invite_all(&case, &id, [&alice_ios_central, &bob_android_central])
625 .await
626 .unwrap();
627
628 let nb_members = alice_android_central
629 .get_conversation_unchecked(&id)
630 .await
631 .members()
632 .len();
633 assert_eq!(nb_members, 3);
634
635 assert_eq!(
636 alice_android_central.get_user_id().await,
637 alice_ios_central.get_user_id().await
638 );
639
640 let alice_user_id = alice_android_central.get_user_id().await;
642 let alice_identities = alice_android_central
643 .context
644 .get_user_identities(&id, &[alice_user_id.clone()])
645 .await
646 .unwrap();
647 assert_eq!(alice_identities.len(), 1);
648 let identities = alice_identities.get(&alice_user_id).unwrap();
649 assert_eq!(identities.len(), 2);
650
651 let bob_user_id = bob_android_central.get_user_id().await;
653 let bob_identities = alice_android_central
654 .context
655 .get_user_identities(&id, &[bob_user_id.clone()])
656 .await
657 .unwrap();
658 assert_eq!(bob_identities.len(), 1);
659 let identities = bob_identities.get(&bob_user_id).unwrap();
660 assert_eq!(identities.len(), 1);
661
662 let user_ids = [alice_user_id, bob_user_id];
663 let expected_sizes = [2, 1];
664
665 all_identities_check(&mut alice_android_central.context, &id, &user_ids, expected_sizes).await;
666 all_identities_check(&mut alice_ios_central.context, &id, &user_ids, expected_sizes).await;
667 all_identities_check(&mut bob_android_central.context, &id, &user_ids, expected_sizes).await;
668 })
669 },
670 )
671 .await
672 }
673
674 #[async_std::test]
675 #[wasm_bindgen_test]
676 async fn should_exchange_messages_cross_signed() {
677 let (alice_android, alice_ios) = (
678 "satICT30SbiIpjj1n-XQtA:7684f3f95a5e6848@wire.com",
679 "satICT30SbiIpjj1n-XQtA:7dfd976fc672c899@wire.com",
680 );
681 let (alicem_android, alicem_ios) = (
682 "8h2PRVj_Qyi7p1XLGmdulw:a7c5ac4446bf@zeta.com",
683 "8h2PRVj_Qyi7p1XLGmdulw:10c6f7a0b5ed@zeta.com",
684 );
685 let bob_android = "I_7X5oRAToKy9z_kvhDKKQ:8b1fd601510d102a@wire.com";
686 let bobt_android = "HSLU78bpQCOYwh4FWCac5g:68db8bac6a65d@zeta.com";
687
688 let case = TestCase::default_x509();
689
690 run_cross_signed_tests_with_client_ids(
691 case.clone(),
692 [
693 [alice_android, "alice_wire", "Alice Smith"],
694 [alice_ios, "alice_wire", "Alice Smith"],
695 [bob_android, "bob_wire", "Bob Doe"],
696 ],
697 [
698 [alicem_android, "alice_zeta", "Alice Muller"],
699 [alicem_ios, "alice_zeta", "Alice Muller"],
700 [bobt_android, "bob_zeta", "Bob Tables"],
701 ],
702 ("wire.com", "zeta.com"),
703 move |[mut alices_android_central, mut alices_ios_central, mut bob_android_central],
704 [mut alicem_android_central, mut alicem_ios_central, mut bobt_android_central]| {
705 Box::pin(async move {
706 let id = conversation_id();
707 alices_ios_central
708 .context
709 .new_conversation(&id, case.credential_type, case.cfg.clone())
710 .await
711 .unwrap();
712
713 alices_ios_central
714 .invite_all(
715 &case,
716 &id,
717 [
718 &mut alices_android_central,
719 &mut bob_android_central,
720 &mut alicem_android_central,
721 &mut alicem_ios_central,
722 &mut bobt_android_central,
723 ],
724 )
725 .await
726 .unwrap();
727
728 let nb_members = alices_android_central
729 .get_conversation_unchecked(&id)
730 .await
731 .members()
732 .len();
733 assert_eq!(nb_members, 6);
734
735 assert_eq!(
736 alicem_android_central.get_user_id().await,
737 alicem_ios_central.get_user_id().await
738 );
739
740 bobt_android_central
742 .try_talk_to(&id, &mut alices_ios_central)
743 .await
744 .unwrap();
745
746 bob_android_central
748 .try_talk_to(&id, &mut alices_ios_central)
749 .await
750 .unwrap();
751 })
752 },
753 )
754 .await;
755 }
756}