1use mls_crypto_provider::MlsCryptoProvider;
18use openmls::prelude::{
19 group_info::VerifiableGroupInfo, CredentialType, MlsGroup, MlsMessageOut, Proposal, Sender, StagedCommit,
20};
21use openmls_traits::OpenMlsCryptoProvider;
22use tls_codec::Serialize;
23
24use core_crypto_keystore::{
25 connection::FetchFromDatabase,
26 entities::{MlsPendingMessage, PersistedMlsPendingGroup},
27 CryptoKeystoreMls,
28};
29
30use crate::{
31 e2e_identity::{conversation_state::compute_state, init_certificates::NewCrlDistributionPoint},
32 group_store::GroupStoreValue,
33 mls::credential::crl::{extract_crl_uris_from_group, get_new_crl_distribution_points},
34 prelude::{
35 decrypt::MlsBufferedConversationDecryptMessage, id::ClientId, ConversationId, CoreCryptoCallbacks, CryptoError,
36 CryptoResult, E2eiConversationState, MlsCiphersuite, MlsConversation, MlsConversationConfiguration,
37 MlsCredentialType, MlsCustomConfiguration, MlsError, MlsGroupInfoBundle,
38 },
39};
40
41use crate::context::CentralContext;
42
43#[derive(Debug)]
45pub struct MlsConversationInitBundle {
46 pub conversation_id: ConversationId,
48 pub commit: MlsMessageOut,
50 pub group_info: MlsGroupInfoBundle,
52 pub crl_new_distribution_points: NewCrlDistributionPoint,
54}
55
56impl MlsConversationInitBundle {
57 #[allow(clippy::type_complexity)]
61 pub fn to_bytes(self) -> CryptoResult<(Vec<u8>, MlsGroupInfoBundle, NewCrlDistributionPoint)> {
62 let commit = self.commit.tls_serialize_detached().map_err(MlsError::from)?;
63 Ok((commit, self.group_info, self.crl_new_distribution_points))
64 }
65}
66
67impl CentralContext {
68 pub async fn join_by_external_commit(
95 &self,
96 group_info: VerifiableGroupInfo,
97 custom_cfg: MlsCustomConfiguration,
98 credential_type: MlsCredentialType,
99 ) -> CryptoResult<MlsConversationInitBundle> {
100 let client = &self.mls_client().await?;
101
102 let cs: MlsCiphersuite = group_info.ciphersuite().into();
103 let mls_provider = self.mls_provider().await?;
104 let cb = client
105 .get_most_recent_or_create_credential_bundle(&mls_provider, cs.signature_algorithm(), credential_type)
106 .await?;
107
108 let serialized_cfg = serde_json::to_vec(&custom_cfg).map_err(MlsError::MlsKeystoreSerializationError)?;
109
110 let configuration = MlsConversationConfiguration {
111 ciphersuite: cs,
112 custom: custom_cfg,
113 ..Default::default()
114 };
115
116 let (group, commit, group_info) = MlsGroup::join_by_external_commit(
117 &mls_provider,
118 &cb.signature_key,
119 None,
120 group_info,
121 &configuration.as_openmls_default_configuration()?,
122 &[],
123 cb.to_mls_credential_with_key(),
124 )
125 .await
126 .map_err(MlsError::from)?;
127
128 let group_info = group_info.ok_or(CryptoError::ImplementationError)?;
130 let group_info = MlsGroupInfoBundle::try_new_full_plaintext(group_info)?;
131
132 let crl_new_distribution_points =
133 get_new_crl_distribution_points(&mls_provider, extract_crl_uris_from_group(&group)?).await?;
134
135 mls_provider
136 .key_store()
137 .mls_pending_groups_save(
138 group.group_id().as_slice(),
139 &core_crypto_keystore::ser(&group)?,
140 &serialized_cfg,
141 None,
142 )
143 .await?;
144
145 Ok(MlsConversationInitBundle {
146 conversation_id: group.group_id().to_vec(),
147 commit,
148 group_info,
149 crl_new_distribution_points,
150 })
151 }
152
153 #[cfg_attr(test, crate::dispotent)]
162 pub async fn merge_pending_group_from_external_commit(
163 &self,
164 id: &ConversationId,
165 ) -> CryptoResult<Option<Vec<MlsBufferedConversationDecryptMessage>>> {
166 let mls_provider = self.mls_provider().await?;
168 let (group, cfg) = mls_provider.key_store().mls_pending_groups_load(id).await?;
169
170 let mut mls_group = core_crypto_keystore::deser::<MlsGroup>(&group)?;
171
172 mls_group
174 .merge_pending_commit(&mls_provider)
175 .await
176 .map_err(MlsError::from)?;
177
178 let custom_cfg = serde_json::from_slice(&cfg).map_err(MlsError::MlsKeystoreSerializationError)?;
180 let configuration = MlsConversationConfiguration {
181 ciphersuite: mls_group.ciphersuite().into(),
182 custom: custom_cfg,
183 ..Default::default()
184 };
185
186 let is_rejoin = mls_provider.key_store().mls_group_exists(id.as_slice()).await;
187
188 let mut conversation = MlsConversation::from_mls_group(mls_group, configuration, &mls_provider).await?;
191
192 let pending_messages = self.restore_pending_messages(&mut conversation, is_rejoin).await?;
193
194 self.mls_groups().await?.insert(id.clone(), conversation);
195
196 mls_provider.key_store().mls_pending_groups_delete(id).await?;
198
199 if pending_messages.is_some() {
200 mls_provider.key_store().remove::<MlsPendingMessage, _>(id).await?;
201 }
202
203 Ok(pending_messages)
204 }
205
206 #[cfg_attr(test, crate::dispotent)]
216 pub async fn clear_pending_group_from_external_commit(&self, id: &ConversationId) -> CryptoResult<()> {
217 Ok(self.keystore().await?.mls_pending_groups_delete(id).await?)
218 }
219
220 pub(crate) async fn pending_group_exists(&self, id: &ConversationId) -> CryptoResult<bool> {
221 Ok(self
222 .keystore()
223 .await?
224 .find::<PersistedMlsPendingGroup>(id.as_slice())
225 .await
226 .ok()
227 .flatten()
228 .is_some())
229 }
230}
231
232impl MlsConversation {
233 pub(crate) async fn validate_external_commit(
234 &self,
235 commit: &StagedCommit,
236 sender: ClientId,
237 parent_conversation: Option<&GroupStoreValue<MlsConversation>>,
238 backend: &MlsCryptoProvider,
239 callbacks: Option<&dyn CoreCryptoCallbacks>,
240 ) -> CryptoResult<()> {
241 let is_external_init = commit.queued_proposals().any(|p| {
243 matches!(p.sender(), Sender::NewMemberCommit) && matches!(p.proposal(), Proposal::ExternalInit(_))
244 });
245
246 if is_external_init {
247 let callbacks = callbacks.ok_or(CryptoError::CallbacksNotSet)?;
248 let existing_clients = self.members_in_next_epoch();
250 let parent_clients = if let Some(parent_conv) = parent_conversation {
251 Some(
252 parent_conv
253 .read()
254 .await
255 .group
256 .members()
257 .map(|kp| kp.credential.identity().to_vec().into())
258 .collect(),
259 )
260 } else {
261 None
262 };
263 if !callbacks
264 .client_is_existing_group_user(
265 self.id.clone(),
266 sender.clone(),
267 existing_clients.clone(),
268 parent_clients,
269 )
270 .await
271 {
272 return Err(CryptoError::UnauthorizedExternalCommit);
273 }
274 if !callbacks
277 .user_authorize(self.id.clone(), sender, existing_clients)
278 .await
279 {
280 return Err(CryptoError::UnauthorizedExternalCommit);
281 }
282 }
283
284 if backend.authentication_service().is_env_setup().await {
285 let credentials: Vec<_> = commit
286 .add_proposals()
287 .filter_map(|add_proposal| {
288 let credential = add_proposal.add_proposal().key_package().leaf_node().credential();
289
290 matches!(credential.credential_type(), CredentialType::X509).then(|| credential.clone())
291 })
292 .collect();
293 let state = compute_state(
294 self.ciphersuite(),
295 credentials.iter(),
296 MlsCredentialType::X509,
297 backend.authentication_service().borrow().await.as_ref(),
298 )
299 .await;
300 if state != E2eiConversationState::Verified {
301 }
304 }
305
306 Ok(())
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use openmls::prelude::*;
313 use std::sync::Arc;
314 use wasm_bindgen_test::*;
315
316 use core_crypto_keystore::{CryptoKeystoreError, CryptoKeystoreMls, MissingKeyErrorKind};
317
318 use crate::prelude::MlsConversationConfiguration;
319 use crate::{prelude::MlsConversationInitBundle, test_utils::*, CryptoError};
320
321 wasm_bindgen_test_configure!(run_in_browser);
322
323 #[apply(all_cred_cipher)]
324 #[wasm_bindgen_test]
325 async fn join_by_external_commit_should_succeed(case: TestCase) {
326 run_test_with_client_ids(
327 case.clone(),
328 ["alice", "bob"],
329 move |[alice_central, mut bob_central]| {
330 Box::pin(async move {
331 let id = conversation_id();
332 alice_central
333 .context
334 .new_conversation(&id, case.credential_type, case.cfg.clone())
335 .await
336 .unwrap();
337
338 let group_info = alice_central.get_group_info(&id).await;
340
341 let MlsConversationInitBundle {
343 conversation_id: group_id,
344 commit: external_commit,
345 ..
346 } = bob_central
347 .context
348 .join_by_external_commit(group_info, case.custom_cfg(), case.credential_type)
349 .await
350 .unwrap();
351 assert_eq!(group_id.as_slice(), &id);
352
353 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
355 let decrypted = alice_central
356 .context
357 .decrypt_message(&id, &external_commit.to_bytes().unwrap())
358 .await
359 .unwrap();
360 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
361
362 bob_central.verify_sender_identity(&case, &decrypted).await;
364
365 assert!(bob_central.context.get_conversation(&id).await.is_err());
368 bob_central
369 .context
370 .merge_pending_group_from_external_commit(&id)
371 .await
372 .unwrap();
373 assert!(bob_central.context.get_conversation(&id).await.is_ok());
374 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 2);
375 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
376
377 let error = alice_central
379 .context
380 .keystore()
381 .await
382 .unwrap()
383 .mls_pending_groups_load(&id)
384 .await;
385 assert!(matches!(
386 error.unwrap_err(),
387 CryptoKeystoreError::MissingKeyInStore(MissingKeyErrorKind::MlsPendingGroup)
388 ));
389
390 bob_central.context.drop_and_restore(&group_id).await;
392 assert!(bob_central.try_talk_to(&id, &alice_central).await.is_ok());
393 })
394 },
395 )
396 .await
397 }
398
399 #[apply(all_cred_cipher)]
400 #[wasm_bindgen_test]
401 async fn join_by_external_commit_should_be_retriable(case: TestCase) {
402 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
403 Box::pin(async move {
404 let id = conversation_id();
405 alice_central
406 .context
407 .new_conversation(&id, case.credential_type, case.cfg.clone())
408 .await
409 .unwrap();
410
411 let group_info = alice_central.get_group_info(&id).await;
413
414 bob_central
416 .context
417 .join_by_external_commit(group_info.clone(), case.custom_cfg(), case.credential_type)
418 .await
419 .unwrap();
420 let MlsConversationInitBundle {
425 conversation_id,
426 commit: external_commit,
427 ..
428 } = bob_central
429 .context
430 .join_by_external_commit(group_info, case.custom_cfg(), case.credential_type)
431 .await
432 .unwrap();
433 assert_eq!(conversation_id.as_slice(), &id);
434
435 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
437 alice_central
438 .context
439 .decrypt_message(&id, &external_commit.to_bytes().unwrap())
440 .await
441 .unwrap();
442 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
443
444 bob_central
446 .context
447 .merge_pending_group_from_external_commit(&id)
448 .await
449 .unwrap();
450 assert!(bob_central.context.get_conversation(&id).await.is_ok());
451 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 2);
452 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
453 })
454 })
455 .await
456 }
457
458 #[apply(all_cred_cipher)]
459 #[wasm_bindgen_test]
460 async fn should_fail_when_bad_epoch(case: TestCase) {
461 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
462 Box::pin(async move {
463 let id = conversation_id();
464 alice_central
465 .context
466 .new_conversation(&id, case.credential_type, case.cfg.clone())
467 .await
468 .unwrap();
469
470 let group_info = alice_central.get_group_info(&id).await;
471 let MlsConversationInitBundle {
473 commit: external_commit,
474 ..
475 } = bob_central
476 .context
477 .join_by_external_commit(group_info, case.custom_cfg(), case.credential_type)
478 .await
479 .unwrap();
480
481 alice_central.context.update_keying_material(&id).await.unwrap();
483 alice_central.context.commit_accepted(&id).await.unwrap();
484
485 let result = alice_central
488 .context
489 .decrypt_message(&id, &external_commit.to_bytes().unwrap())
490 .await;
491 assert!(matches!(result.unwrap_err(), crate::CryptoError::StaleCommit));
492 })
493 })
494 .await
495 }
496
497 #[apply(all_cred_cipher)]
498 #[wasm_bindgen_test]
499 async fn existing_clients_can_join(case: TestCase) {
500 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
501 Box::pin(async move {
502 let id = conversation_id();
503 alice_central
504 .context
505 .new_conversation(&id, case.credential_type, case.cfg.clone())
506 .await
507 .unwrap();
508 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
509 let group_info = alice_central.get_group_info(&id).await;
510 alice_central
512 .context
513 .join_by_external_commit(group_info.clone(), case.custom_cfg(), case.credential_type)
514 .await
515 .unwrap();
516 alice_central
517 .context
518 .merge_pending_group_from_external_commit(&id)
519 .await
520 .unwrap();
521 })
522 })
523 .await
524 }
525
526 #[apply(all_cred_cipher)]
527 #[wasm_bindgen_test]
528 async fn should_fail_when_no_pending_external_commit(case: TestCase) {
529 run_test_with_central(case.clone(), move |[central]| {
530 Box::pin(async move {
531 let id = conversation_id();
532 let merge_unknown = central.context.merge_pending_group_from_external_commit(&id).await;
534
535 assert!(matches!(
536 merge_unknown.unwrap_err(),
537 crate::CryptoError::KeyStoreError(CryptoKeystoreError::MissingKeyInStore(
538 MissingKeyErrorKind::MlsPendingGroup
539 ))
540 ));
541 })
542 })
543 .await
544 }
545
546 #[apply(all_cred_cipher)]
547 #[wasm_bindgen_test]
548 async fn should_return_valid_group_info(case: TestCase) {
549 run_test_with_client_ids(
550 case.clone(),
551 ["alice", "bob", "charlie"],
552 move |[alice_central, bob_central, charlie_central]| {
553 Box::pin(async move {
554 let id = conversation_id();
555 alice_central
556 .context
557 .new_conversation(&id, case.credential_type, case.cfg.clone())
558 .await
559 .unwrap();
560
561 let group_info = alice_central.get_group_info(&id).await;
563
564 let MlsConversationInitBundle {
566 commit: bob_external_commit,
567 group_info,
568 ..
569 } = bob_central
570 .context
571 .join_by_external_commit(group_info, case.custom_cfg(), case.credential_type)
572 .await
573 .unwrap();
574
575 alice_central
577 .context
578 .decrypt_message(&id, &bob_external_commit.to_bytes().unwrap())
579 .await
580 .unwrap();
581 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
582
583 bob_central
585 .context
586 .merge_pending_group_from_external_commit(&id)
587 .await
588 .unwrap();
589 assert!(bob_central.context.get_conversation(&id).await.is_ok());
590 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 2);
591 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
592
593 let bob_gi = group_info.get_group_info();
595 let MlsConversationInitBundle {
596 commit: charlie_external_commit,
597 ..
598 } = charlie_central
599 .context
600 .join_by_external_commit(bob_gi, case.custom_cfg(), case.credential_type)
601 .await
602 .unwrap();
603
604 alice_central
606 .context
607 .decrypt_message(&id, charlie_external_commit.to_bytes().unwrap())
608 .await
609 .unwrap();
610 bob_central
611 .context
612 .decrypt_message(&id, charlie_external_commit.to_bytes().unwrap())
613 .await
614 .unwrap();
615 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 3);
616 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 3);
617
618 charlie_central
620 .context
621 .merge_pending_group_from_external_commit(&id)
622 .await
623 .unwrap();
624 assert!(charlie_central.context.get_conversation(&id).await.is_ok());
625 assert_eq!(charlie_central.get_conversation_unchecked(&id).await.members().len(), 3);
626 assert!(charlie_central.try_talk_to(&id, &alice_central).await.is_ok());
627 assert!(charlie_central.try_talk_to(&id, &bob_central).await.is_ok());
628 })
629 },
630 )
631 .await
632 }
633
634 #[apply(all_cred_cipher)]
635 #[wasm_bindgen_test]
636 async fn should_fail_when_sender_user_not_in_group(case: TestCase) {
637 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
638 Box::pin(async move {
639 let id = conversation_id();
640
641 alice_central
642 .context
643 .set_callbacks(Some(Arc::new(ValidationCallbacks {
644 client_is_existing_group_user: false,
645 ..Default::default()
646 })))
647 .await
648 .unwrap();
649
650 alice_central
651 .context
652 .new_conversation(&id, case.credential_type, case.cfg.clone())
653 .await
654 .unwrap();
655
656 let group_info = alice_central.get_group_info(&id).await;
658
659 let MlsConversationInitBundle { commit, .. } = bob_central
661 .context
662 .join_by_external_commit(group_info, case.custom_cfg(), case.credential_type)
663 .await
664 .unwrap();
665 let alice_accepts_ext_commit = alice_central
666 .context
667 .decrypt_message(&id, &commit.to_bytes().unwrap())
668 .await;
669 assert!(matches!(
670 alice_accepts_ext_commit.unwrap_err(),
671 CryptoError::UnauthorizedExternalCommit
672 ))
673 })
674 })
675 .await
676 }
677
678 #[apply(all_cred_cipher)]
679 #[wasm_bindgen_test]
680 async fn should_fail_when_sender_lacks_role(case: TestCase) {
681 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
682 Box::pin(async move {
683 let id = conversation_id();
684
685 alice_central
686 .context
687 .set_callbacks(Some(Arc::new(ValidationCallbacks {
688 user_authorize: false,
689 ..Default::default()
690 })))
691 .await
692 .unwrap();
693
694 alice_central
695 .context
696 .new_conversation(&id, case.credential_type, case.cfg.clone())
697 .await
698 .unwrap();
699
700 let group_info = alice_central.get_group_info(&id).await;
702
703 let MlsConversationInitBundle { commit, .. } = bob_central
705 .context
706 .join_by_external_commit(group_info, case.custom_cfg(), case.credential_type)
707 .await
708 .unwrap();
709 let alice_accepts_ext_commit = alice_central
710 .context
711 .decrypt_message(&id, &commit.to_bytes().unwrap())
712 .await;
713 assert!(matches!(
714 alice_accepts_ext_commit.unwrap_err(),
715 CryptoError::UnauthorizedExternalCommit
716 ))
717 })
718 })
719 .await
720 }
721
722 #[apply(all_cred_cipher)]
723 #[wasm_bindgen_test]
724 async fn clear_pending_group_should_succeed(case: TestCase) {
725 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
726 Box::pin(async move {
727 let id = conversation_id();
728 alice_central
729 .context
730 .new_conversation(&id, case.credential_type, case.cfg.clone())
731 .await
732 .unwrap();
733
734 let initial_count = alice_central.context.count_entities().await;
735
736 let group_info = alice_central.get_group_info(&id).await;
738
739 bob_central
741 .context
742 .join_by_external_commit(group_info, case.custom_cfg(), case.credential_type)
743 .await
744 .unwrap();
745
746 bob_central
748 .context
749 .clear_pending_group_from_external_commit(&id)
750 .await
751 .unwrap();
752
753 let final_count = alice_central.context.count_entities().await;
754 assert_eq!(initial_count, final_count);
755
756 let result = bob_central.context.merge_pending_group_from_external_commit(&id).await;
758 assert!(matches!(
759 result.unwrap_err(),
760 CryptoError::KeyStoreError(CryptoKeystoreError::MissingKeyInStore(
761 MissingKeyErrorKind::MlsPendingGroup
762 ))
763 ))
764 })
765 })
766 .await
767 }
768
769 #[apply(all_cred_cipher)]
770 #[wasm_bindgen_test]
771 async fn new_with_inflight_join_should_fail_when_already_exists(case: TestCase) {
772 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
773 Box::pin(async move {
774 let id = conversation_id();
775 alice_central
776 .context
777 .new_conversation(&id, case.credential_type, case.cfg.clone())
778 .await
779 .unwrap();
780 let gi = alice_central.get_group_info(&id).await;
781
782 bob_central
785 .context
786 .join_by_external_commit(gi, case.custom_cfg(), case.credential_type)
787 .await
788 .unwrap();
789 let conflict_join = bob_central
791 .context
792 .new_conversation(&id, case.credential_type, case.cfg.clone())
793 .await;
794 assert!(matches!(conflict_join.unwrap_err(), CryptoError::ConversationAlreadyExists(i) if i == id));
795 })
796 })
797 .await
798 }
799
800 #[apply(all_cred_cipher)]
801 #[wasm_bindgen_test]
802 async fn new_with_inflight_welcome_should_fail_when_already_exists(case: TestCase) {
803 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
804 Box::pin(async move {
805 let id = conversation_id();
806 alice_central
807 .context
808 .new_conversation(&id, case.credential_type, case.cfg.clone())
809 .await
810 .unwrap();
811 let gi = alice_central.get_group_info(&id).await;
812
813 bob_central
816 .context
817 .join_by_external_commit(gi, case.custom_cfg(), case.credential_type)
818 .await
819 .unwrap();
820
821 let bob = bob_central.rand_key_package(&case).await;
822 let welcome = alice_central
823 .context
824 .add_members_to_conversation(&id, vec![bob])
825 .await
826 .unwrap()
827 .welcome;
828
829 let conflict_welcome = bob_central
831 .context
832 .process_welcome_message(welcome.into(), case.custom_cfg())
833 .await;
834
835 assert!(matches!(conflict_welcome.unwrap_err(), CryptoError::ConversationAlreadyExists(i) if i == id));
836 })
837 })
838 .await
839 }
840
841 #[apply(all_cred_cipher)]
842 #[wasm_bindgen_test]
843 async fn should_fail_when_invalid_group_info(case: TestCase) {
844 run_test_with_client_ids(
845 case.clone(),
846 ["alice", "bob", "guest"],
847 move |[alice_central, bob_central, guest_central]| {
848 Box::pin(async move {
849 let expiration_time = 14;
850 let start = fluvio_wasm_timer::Instant::now();
851 let id = conversation_id();
852 alice_central
853 .context
854 .new_conversation(&id, case.credential_type, case.cfg.clone())
855 .await
856 .unwrap();
857
858 let invalid_kp = bob_central.new_keypackage(&case, Lifetime::new(expiration_time)).await;
859 alice_central
860 .context
861 .add_members_to_conversation(&id, vec![invalid_kp.into()])
862 .await
863 .unwrap();
864 alice_central.context.commit_accepted(&id).await.unwrap();
865
866 let elapsed = start.elapsed();
867 let expiration_time = core::time::Duration::from_secs(expiration_time);
869 if expiration_time > elapsed {
870 async_std::task::sleep(expiration_time - elapsed + core::time::Duration::from_secs(1)).await;
871 }
872
873 let group_info = alice_central.get_group_info(&id).await;
874
875 let join_ext_commit = guest_central
876 .context
877 .join_by_external_commit(group_info, case.custom_cfg(), case.credential_type)
878 .await;
879
880 join_ext_commit.unwrap();
882 })
891 },
892 )
893 .await
894 }
895
896 #[apply(all_cred_cipher)]
897 #[wasm_bindgen_test]
898 async fn group_should_have_right_config(case: TestCase) {
899 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
900 Box::pin(async move {
901 let id = conversation_id();
902 alice_central
903 .context
904 .new_conversation(&id, case.credential_type, case.cfg.clone())
905 .await
906 .unwrap();
907
908 let gi = alice_central.get_group_info(&id).await;
909 bob_central
910 .context
911 .join_by_external_commit(gi, case.custom_cfg(), case.credential_type)
912 .await
913 .unwrap();
914 bob_central
915 .context
916 .merge_pending_group_from_external_commit(&id)
917 .await
918 .unwrap();
919 let group = bob_central.get_conversation_unchecked(&id).await;
920
921 let capabilities = group.group.group_context_extensions().required_capabilities().unwrap();
922
923 assert!(capabilities.extension_types().is_empty());
925 assert!(capabilities.proposal_types().is_empty());
926 assert_eq!(
927 capabilities.credential_types(),
928 MlsConversationConfiguration::DEFAULT_SUPPORTED_CREDENTIALS
929 );
930 })
931 })
932 .await
933 }
934}