1use core_crypto_keystore::entities::MlsEncryptionKeyPair;
2use openmls::prelude::{LeafNode, LeafNodeIndex, Proposal, QueuedProposal, Sender, StagedCommit};
3use openmls_traits::OpenMlsCryptoProvider;
4
5use mls_crypto_provider::MlsCryptoProvider;
6
7use super::{Error, Result};
8use crate::{
9 KeystoreError, RecursiveError,
10 prelude::{Client, MlsConversation, MlsProposalBundle},
11};
12
13pub(crate) struct Renew;
16
17impl Renew {
18 pub(crate) fn renew<'a>(
30 self_index: &LeafNodeIndex,
31 pending_proposals: impl Iterator<Item = QueuedProposal> + 'a,
32 pending_commit: Option<&'a StagedCommit>,
33 valid_commit: &'a StagedCommit,
34 ) -> (Vec<QueuedProposal>, bool) {
35 let mut needs_update = false;
39
40 let renewed_pending_proposals = if let Some(pending_commit) = pending_commit {
41 let commit_proposals = pending_commit.queued_proposals().cloned().collect::<Vec<_>>();
43
44 let empty_commit = commit_proposals.is_empty();
46
47 let valid_commit_has_own_update_proposal = valid_commit.update_proposals().any(|p| match p.sender() {
49 Sender::Member(sender_index) => self_index == sender_index,
50 _ => false,
51 });
52
53 needs_update = !valid_commit_has_own_update_proposal && empty_commit;
55
56 commit_proposals
58 .into_iter()
59 .filter_map(|p| Self::is_proposal_renewable(p, Some(valid_commit)))
60 .collect::<Vec<_>>()
61 } else {
62 pending_proposals
64 .filter_map(|p| Self::is_proposal_renewable(p, Some(valid_commit)))
65 .collect::<Vec<_>>()
66 };
67 (renewed_pending_proposals, needs_update)
68 }
69
70 fn is_proposal_renewable(proposal: QueuedProposal, commit: Option<&StagedCommit>) -> Option<QueuedProposal> {
72 if let Some(commit) = commit {
73 let in_commit = match proposal.proposal() {
74 Proposal::Add(add) => commit.add_proposals().any(|p| {
75 let commits_identity = p.add_proposal().key_package().leaf_node().credential().identity();
76 let proposal_identity = add.key_package().leaf_node().credential().identity();
77 commits_identity == proposal_identity
78 }),
79 Proposal::Remove(remove) => commit
80 .remove_proposals()
81 .any(|p| p.remove_proposal().removed() == remove.removed()),
82 Proposal::Update(update) => commit
83 .update_proposals()
84 .any(|p| p.update_proposal().leaf_node() == update.leaf_node()),
85 _ => true,
86 };
87 if in_commit { None } else { Some(proposal) }
88 } else {
89 Some(proposal)
91 }
92 }
93}
94
95impl MlsConversation {
96 pub(crate) async fn renew_proposals_for_current_epoch(
99 &mut self,
100 client: &Client,
101 backend: &MlsCryptoProvider,
102 proposals: impl Iterator<Item = QueuedProposal>,
103 needs_update: bool,
104 ) -> Result<Vec<MlsProposalBundle>> {
105 let mut bundle = vec![];
106 let is_external = |p: &QueuedProposal| matches!(p.sender(), Sender::External(_) | Sender::NewMemberProposal);
107 let proposals = proposals.filter(|p| !is_external(p));
108 for proposal in proposals {
109 let msg = match proposal.proposal {
110 Proposal::Add(add) => self.propose_add_member(client, backend, add.key_package.into()).await?,
111 Proposal::Remove(remove) => self.propose_remove_member(client, backend, remove.removed()).await?,
112 Proposal::Update(update) => self.renew_update(client, backend, Some(update.leaf_node())).await?,
113 _ => return Err(Error::ProposalVariantCannotBeRenewed),
114 };
115 bundle.push(msg);
116 }
117 if needs_update {
118 let proposal = self.renew_update(client, backend, None).await?;
119 bundle.push(proposal);
120 }
121 Ok(bundle)
122 }
123
124 async fn renew_update(
128 &mut self,
129 client: &Client,
130 backend: &MlsCryptoProvider,
131 leaf_node: Option<&LeafNode>,
132 ) -> Result<MlsProposalBundle> {
133 if let Some(leaf_node) = leaf_node {
134 backend
137 .key_store()
138 .remove::<MlsEncryptionKeyPair, _>(leaf_node.encryption_key().as_slice())
139 .await
140 .map_err(KeystoreError::wrap("removing mls encryption keypair"))?;
141 }
142
143 let mut leaf_node = leaf_node
144 .or_else(|| self.group.own_leaf())
145 .cloned()
146 .ok_or(Error::MlsGroupInvalidState("own_leaf is None"))?;
147
148 let sc = self.signature_scheme();
149 let ct = self.own_credential_type()?;
150 let cb = client
151 .find_most_recent_credential_bundle(sc, ct)
152 .await
153 .map_err(RecursiveError::mls_client("finding most recent credential bundle"))?;
154
155 leaf_node.set_credential_with_key(cb.to_mls_credential_with_key());
156
157 self.propose_explicit_self_update(client, backend, Some(leaf_node))
158 .await
159 }
160
161 pub(crate) fn self_pending_proposals(&self) -> impl Iterator<Item = &QueuedProposal> {
162 self.group
163 .pending_proposals()
164 .filter(|&p| matches!(p.sender(), Sender::Member(i) if i == &self.group.own_leaf_index()))
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use wasm_bindgen_test::*;
171
172 use crate::test_utils::*;
173
174 mod update {
175 use super::*;
176
177 #[apply(all_cred_cipher)]
178 #[wasm_bindgen_test]
179 pub async fn renewable_when_created_by_self(case: TestCase) {
180 run_test_with_client_ids(
181 case.clone(),
182 ["alice", "bob"],
183 move |[mut alice_central, bob_central]| {
184 Box::pin(async move {
185 let id = conversation_id();
186 alice_central
187 .context
188 .new_conversation(&id, case.credential_type, case.cfg.clone())
189 .await
190 .unwrap();
191 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
192
193 assert!(alice_central.pending_proposals(&id).await.is_empty());
194 alice_central.context.new_update_proposal(&id).await.unwrap();
195 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
196
197 bob_central
199 .context
200 .conversation(&id)
201 .await
202 .unwrap()
203 .update_key_material()
204 .await
205 .unwrap();
206 let commit = bob_central.mls_transport.latest_commit().await;
207
208 let proposals = alice_central
209 .context
210 .conversation(&id)
211 .await
212 .unwrap()
213 .decrypt_message(commit.to_bytes().unwrap())
214 .await
215 .unwrap()
216 .proposals;
217 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
219 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
220
221 bob_central
223 .context
224 .conversation(&id)
225 .await
226 .unwrap()
227 .update_key_material()
228 .await
229 .unwrap();
230 let commit = bob_central.mls_transport.latest_commit().await;
231 let proposals = alice_central
232 .context
233 .conversation(&id)
234 .await
235 .unwrap()
236 .decrypt_message(commit.to_bytes().unwrap())
237 .await
238 .unwrap()
239 .proposals;
240 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
243 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
244 })
245 },
246 )
247 .await
248 }
249
250 #[apply(all_cred_cipher)]
251 #[wasm_bindgen_test]
252 pub async fn not_renewable_when_in_valid_commit(case: TestCase) {
253 run_test_with_client_ids(
254 case.clone(),
255 ["alice", "bob"],
256 move |[mut alice_central, bob_central]| {
257 Box::pin(async move {
258 let id = conversation_id();
259 alice_central
260 .context
261 .new_conversation(&id, case.credential_type, case.cfg.clone())
262 .await
263 .unwrap();
264 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
265
266 assert!(alice_central.pending_proposals(&id).await.is_empty());
267 let proposal = alice_central.context.new_update_proposal(&id).await.unwrap().proposal;
268 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
269
270 bob_central
272 .context
273 .conversation(&id)
274 .await
275 .unwrap()
276 .decrypt_message(proposal.to_bytes().unwrap())
277 .await
278 .unwrap();
279
280 bob_central
281 .context
282 .conversation(&id)
283 .await
284 .unwrap()
285 .update_key_material()
286 .await
287 .unwrap();
288 let commit = bob_central.mls_transport.latest_commit().await;
289
290 let proposals = alice_central
292 .context
293 .conversation(&id)
294 .await
295 .unwrap()
296 .decrypt_message(commit.to_bytes().unwrap())
297 .await
298 .unwrap()
299 .proposals;
300 assert!(alice_central.pending_proposals(&id).await.is_empty());
302 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
303
304 let proposal = alice_central.context.new_update_proposal(&id).await.unwrap().proposal;
305 bob_central
306 .context
307 .conversation(&id)
308 .await
309 .unwrap()
310 .decrypt_message(proposal.to_bytes().unwrap())
311 .await
312 .unwrap();
313 bob_central
314 .context
315 .conversation(&id)
316 .await
317 .unwrap()
318 .update_key_material()
319 .await
320 .unwrap();
321 let commit = bob_central.mls_transport.latest_commit().await;
322 let proposals = alice_central
323 .context
324 .conversation(&id)
325 .await
326 .unwrap()
327 .decrypt_message(commit.to_bytes().unwrap())
328 .await
329 .unwrap()
330 .proposals;
331 assert!(alice_central.pending_proposals(&id).await.is_empty());
333 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
334 })
335 },
336 )
337 .await
338 }
339
340 #[apply(all_cred_cipher)]
341 #[wasm_bindgen_test]
342 pub async fn not_renewable_by_ref(case: TestCase) {
343 run_test_with_client_ids(
344 case.clone(),
345 ["alice", "bob", "charlie"],
346 move |[mut alice_central, bob_central, charlie_central]| {
347 Box::pin(async move {
348 let id = conversation_id();
349 alice_central
350 .context
351 .new_conversation(&id, case.credential_type, case.cfg.clone())
352 .await
353 .unwrap();
354 alice_central
355 .invite_all(&case, &id, [&bob_central, &charlie_central])
356 .await
357 .unwrap();
358
359 let proposal = bob_central.context.new_update_proposal(&id).await.unwrap().proposal;
360 assert!(alice_central.pending_proposals(&id).await.is_empty());
361 alice_central
362 .context
363 .conversation(&id)
364 .await
365 .unwrap()
366 .decrypt_message(proposal.to_bytes().unwrap())
367 .await
368 .unwrap();
369 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
370
371 charlie_central
373 .context
374 .conversation(&id)
375 .await
376 .unwrap()
377 .update_key_material()
378 .await
379 .unwrap();
380 let commit = charlie_central.mls_transport.latest_commit().await;
381 let proposals = alice_central
382 .context
383 .conversation(&id)
384 .await
385 .unwrap()
386 .decrypt_message(commit.to_bytes().unwrap())
387 .await
388 .unwrap()
389 .proposals;
390 assert!(alice_central.pending_proposals(&id).await.is_empty());
392 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
393 })
394 },
395 )
396 .await
397 }
398 }
399
400 mod add {
401 use super::*;
402
403 #[apply(all_cred_cipher)]
404 #[wasm_bindgen_test]
405 pub async fn not_renewable_when_valid_commit_adds_same(case: TestCase) {
406 run_test_with_client_ids(
407 case.clone(),
408 ["alice", "bob", "charlie"],
409 move |[mut alice_central, bob_central, charlie_central]| {
410 Box::pin(async move {
411 let id = conversation_id();
412 alice_central
413 .context
414 .new_conversation(&id, case.credential_type, case.cfg.clone())
415 .await
416 .unwrap();
417 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
418
419 let charlie_kp = charlie_central.get_one_key_package(&case).await;
420 assert!(alice_central.pending_proposals(&id).await.is_empty());
421 alice_central.context.new_add_proposal(&id, charlie_kp).await.unwrap();
422 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
423
424 let charlie = charlie_central.rand_key_package(&case).await;
425 bob_central
426 .context
427 .conversation(&id)
428 .await
429 .unwrap()
430 .add_members(vec![charlie])
431 .await
432 .unwrap();
433 let commit = bob_central.mls_transport.latest_commit().await;
434 let proposals = alice_central
435 .context
436 .conversation(&id)
437 .await
438 .unwrap()
439 .decrypt_message(commit.to_bytes().unwrap())
440 .await
441 .unwrap()
442 .proposals;
443 assert!(alice_central.pending_proposals(&id).await.is_empty());
445 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
446 })
447 },
448 )
449 .await
450 }
451
452 #[apply(all_cred_cipher)]
453 #[wasm_bindgen_test]
454 pub async fn not_renewable_in_pending_commit_when_valid_commit_adds_same(case: TestCase) {
455 run_test_with_client_ids(
456 case.clone(),
457 ["alice", "bob", "charlie"],
458 move |[mut alice_central, bob_central, charlie_central]| {
459 Box::pin(async move {
460 let id = conversation_id();
461 alice_central
462 .context
463 .new_conversation(&id, case.credential_type, case.cfg.clone())
464 .await
465 .unwrap();
466 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
467
468 let charlie_kp = charlie_central.get_one_key_package(&case).await;
469 assert!(alice_central.pending_proposals(&id).await.is_empty());
470 alice_central.context.new_add_proposal(&id, charlie_kp).await.unwrap();
471 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
472
473 alice_central.commit_pending_proposals_unmerged(&id).await;
475 assert!(alice_central.pending_commit(&id).await.is_some());
476
477 let charlie = charlie_central.rand_key_package(&case).await;
478 bob_central
479 .context
480 .conversation(&id)
481 .await
482 .unwrap()
483 .add_members(vec![charlie])
484 .await
485 .unwrap();
486 let commit = bob_central.mls_transport.latest_commit().await;
487 let proposals = alice_central
488 .context
489 .conversation(&id)
490 .await
491 .unwrap()
492 .decrypt_message(commit.to_bytes().unwrap())
493 .await
494 .unwrap()
495 .proposals;
496 assert!(alice_central.pending_proposals(&id).await.is_empty());
498 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
499 })
500 },
501 )
502 .await
503 }
504
505 #[apply(all_cred_cipher)]
506 #[wasm_bindgen_test]
507 pub async fn not_renewable_by_ref(case: TestCase) {
508 run_test_with_client_ids(
509 case.clone(),
510 ["alice", "bob", "charlie", "debbie"],
511 move |[mut alice_central, bob_central, charlie_central, debbie_central]| {
512 Box::pin(async move {
513 let id = conversation_id();
514 alice_central
515 .context
516 .new_conversation(&id, case.credential_type, case.cfg.clone())
517 .await
518 .unwrap();
519 alice_central
520 .invite_all(&case, &id, [&bob_central, &charlie_central])
521 .await
522 .unwrap();
523
524 let debbie_kp = debbie_central.get_one_key_package(&case).await;
526 let proposal = bob_central
527 .context
528 .new_add_proposal(&id, debbie_kp)
529 .await
530 .unwrap()
531 .proposal;
532 alice_central
533 .context
534 .conversation(&id)
535 .await
536 .unwrap()
537 .decrypt_message(proposal.to_bytes().unwrap())
538 .await
539 .unwrap();
540 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
541
542 charlie_central
544 .context
545 .conversation(&id)
546 .await
547 .unwrap()
548 .update_key_material()
549 .await
550 .unwrap();
551 let commit = charlie_central.mls_transport.latest_commit().await;
552 let proposals = alice_central
553 .context
554 .conversation(&id)
555 .await
556 .unwrap()
557 .decrypt_message(commit.to_bytes().unwrap())
558 .await
559 .unwrap()
560 .proposals;
561 assert!(alice_central.pending_proposals(&id).await.is_empty());
563 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
564 })
565 },
566 )
567 .await
568 }
569
570 #[apply(all_cred_cipher)]
571 #[wasm_bindgen_test]
572 pub async fn renewable_when_valid_commit_doesnt_adds_same(case: TestCase) {
573 run_test_with_client_ids(
574 case.clone(),
575 ["alice", "bob", "charlie"],
576 move |[mut alice_central, bob_central, charlie_central]| {
577 Box::pin(async move {
578 let id = conversation_id();
579 alice_central
580 .context
581 .new_conversation(&id, case.credential_type, case.cfg.clone())
582 .await
583 .unwrap();
584 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
585
586 let charlie_kp = charlie_central.get_one_key_package(&case).await;
588 assert!(alice_central.pending_proposals(&id).await.is_empty());
589 alice_central.context.new_add_proposal(&id, charlie_kp).await.unwrap();
590 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
591
592 bob_central
594 .context
595 .conversation(&id)
596 .await
597 .unwrap()
598 .update_key_material()
599 .await
600 .unwrap();
601 let commit = bob_central.mls_transport.latest_commit().await;
602 let proposals = alice_central
603 .context
604 .conversation(&id)
605 .await
606 .unwrap()
607 .decrypt_message(commit.to_bytes().unwrap())
608 .await
609 .unwrap()
610 .proposals;
611 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
613 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
614
615 alice_central.commit_pending_proposals_unmerged(&id).await;
617 assert!(alice_central.pending_commit(&id).await.is_some());
618 bob_central
619 .context
620 .conversation(&id)
621 .await
622 .unwrap()
623 .update_key_material()
624 .await
625 .unwrap();
626 let commit = bob_central.mls_transport.latest_commit().await;
627 let proposals = alice_central
628 .context
629 .conversation(&id)
630 .await
631 .unwrap()
632 .decrypt_message(commit.to_bytes().unwrap())
633 .await
634 .unwrap()
635 .proposals;
636 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
639 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
640 })
641 },
642 )
643 .await
644 }
645
646 #[apply(all_cred_cipher)]
647 #[wasm_bindgen_test]
648 pub async fn renews_pending_commit_when_valid_commit_doesnt_add_same(case: TestCase) {
649 run_test_with_client_ids(
650 case.clone(),
651 ["alice", "bob"],
652 move |[mut alice_central, bob_central]| {
653 Box::pin(async move {
654 let id = conversation_id();
655 alice_central
656 .context
657 .new_conversation(&id, case.credential_type, case.cfg.clone())
658 .await
659 .unwrap();
660 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
661
662 alice_central.create_unmerged_commit(&id).await;
664 assert!(alice_central.pending_commit(&id).await.is_some());
665
666 bob_central
668 .context
669 .conversation(&id)
670 .await
671 .unwrap()
672 .update_key_material()
673 .await
674 .unwrap();
675 let commit = bob_central.mls_transport.latest_commit().await;
676 let proposals = alice_central
677 .context
678 .conversation(&id)
679 .await
680 .unwrap()
681 .decrypt_message(commit.to_bytes().unwrap())
682 .await
683 .unwrap()
684 .proposals;
685 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
687 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
688 })
689 },
690 )
691 .await
692 }
693 }
694
695 mod remove {
696 use super::*;
697
698 #[apply(all_cred_cipher)]
699 #[wasm_bindgen_test]
700 pub async fn not_renewable_when_valid_commit_removes_same(case: TestCase) {
701 run_test_with_client_ids(
702 case.clone(),
703 ["alice", "bob", "charlie"],
704 move |[mut alice_central, bob_central, charlie_central]| {
705 Box::pin(async move {
706 let id = conversation_id();
707 alice_central
708 .context
709 .new_conversation(&id, case.credential_type, case.cfg.clone())
710 .await
711 .unwrap();
712 alice_central
713 .invite_all(&case, &id, [&bob_central, &charlie_central])
714 .await
715 .unwrap();
716
717 assert!(alice_central.pending_proposals(&id).await.is_empty());
718 alice_central
719 .context
720 .new_remove_proposal(&id, charlie_central.get_client_id().await)
721 .await
722 .unwrap();
723 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
724
725 bob_central
726 .context
727 .conversation(&id)
728 .await
729 .unwrap()
730 .remove_members(&[charlie_central.get_client_id().await])
731 .await
732 .unwrap();
733 let commit = bob_central.mls_transport.latest_commit().await;
734 let proposals = alice_central
735 .context
736 .conversation(&id)
737 .await
738 .unwrap()
739 .decrypt_message(commit.to_bytes().unwrap())
740 .await
741 .unwrap()
742 .proposals;
743 assert!(alice_central.pending_proposals(&id).await.is_empty());
745 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
746 })
747 },
748 )
749 .await
750 }
751
752 #[apply(all_cred_cipher)]
753 #[wasm_bindgen_test]
754 pub async fn not_renewable_by_ref(case: TestCase) {
755 run_test_with_client_ids(
756 case.clone(),
757 ["alice", "bob", "charlie"],
758 move |[mut alice_central, bob_central, charlie_central]| {
759 Box::pin(async move {
760 let id = conversation_id();
761 alice_central
762 .context
763 .new_conversation(&id, case.credential_type, case.cfg.clone())
764 .await
765 .unwrap();
766 alice_central
767 .invite_all(&case, &id, [&bob_central, &charlie_central])
768 .await
769 .unwrap();
770
771 let proposal = bob_central
772 .context
773 .new_remove_proposal(&id, charlie_central.get_client_id().await)
774 .await
775 .unwrap()
776 .proposal;
777 assert!(alice_central.pending_proposals(&id).await.is_empty());
778 alice_central
779 .context
780 .conversation(&id)
781 .await
782 .unwrap()
783 .decrypt_message(proposal.to_bytes().unwrap())
784 .await
785 .unwrap();
786 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
787
788 charlie_central
789 .context
790 .conversation(&id)
791 .await
792 .unwrap()
793 .update_key_material()
794 .await
795 .unwrap();
796 let commit = charlie_central.mls_transport.latest_commit().await;
797 let proposals = alice_central
798 .context
799 .conversation(&id)
800 .await
801 .unwrap()
802 .decrypt_message(commit.to_bytes().unwrap())
803 .await
804 .unwrap()
805 .proposals;
806 assert!(alice_central.pending_proposals(&id).await.is_empty());
808 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
809 })
810 },
811 )
812 .await
813 }
814
815 #[apply(all_cred_cipher)]
816 #[wasm_bindgen_test]
817 pub async fn renewable_when_valid_commit_doesnt_remove_same(case: TestCase) {
818 run_test_with_client_ids(
819 case.clone(),
820 ["alice", "bob", "charlie", "debbie"],
821 move |[mut alice_central, bob_central, charlie_central, debbie_central]| {
822 Box::pin(async move {
823 let id = conversation_id();
824 alice_central
825 .context
826 .new_conversation(&id, case.credential_type, case.cfg.clone())
827 .await
828 .unwrap();
829 alice_central
830 .invite_all(&case, &id, [&bob_central, &charlie_central, &debbie_central])
831 .await
832 .unwrap();
833
834 assert!(alice_central.pending_proposals(&id).await.is_empty());
836 alice_central
837 .context
838 .new_remove_proposal(&id, charlie_central.get_client_id().await)
839 .await
840 .unwrap();
841 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
842
843 bob_central
845 .context
846 .conversation(&id)
847 .await
848 .unwrap()
849 .remove_members(&[debbie_central.get_client_id().await])
850 .await
851 .unwrap();
852 let commit = bob_central.mls_transport.latest_commit().await;
853 let proposals = alice_central
854 .context
855 .conversation(&id)
856 .await
857 .unwrap()
858 .decrypt_message(commit.to_bytes().unwrap())
859 .await
860 .unwrap()
861 .proposals;
862 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
864 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
865 })
866 },
867 )
868 .await
869 }
870
871 #[apply(all_cred_cipher)]
872 #[wasm_bindgen_test]
873 pub async fn renews_pending_commit_when_commit_doesnt_remove_same(case: TestCase) {
874 run_test_with_client_ids(
875 case.clone(),
876 ["alice", "bob", "charlie", "debbie"],
877 move |[mut alice_central, bob_central, charlie_central, debbie_central]| {
878 Box::pin(async move {
879 let id = conversation_id();
880 alice_central
881 .context
882 .new_conversation(&id, case.credential_type, case.cfg.clone())
883 .await
884 .unwrap();
885 alice_central
886 .invite_all(&case, &id, [&bob_central, &charlie_central, &debbie_central])
887 .await
888 .unwrap();
889
890 alice_central
892 .context
893 .new_remove_proposal(&id, charlie_central.get_client_id().await)
894 .await
895 .unwrap();
896 alice_central.commit_pending_proposals_unmerged(&id).await;
897 assert!(alice_central.pending_commit(&id).await.is_some());
898
899 bob_central
901 .context
902 .conversation(&id)
903 .await
904 .unwrap()
905 .remove_members(&[debbie_central.get_client_id().await])
906 .await
907 .unwrap();
908 let commit = bob_central.mls_transport.latest_commit().await;
909 let proposals = alice_central
910 .context
911 .conversation(&id)
912 .await
913 .unwrap()
914 .decrypt_message(commit.to_bytes().unwrap())
915 .await
916 .unwrap()
917 .proposals;
918 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
920 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
921 })
922 },
923 )
924 .await
925 }
926
927 #[apply(all_cred_cipher)]
928 #[wasm_bindgen_test]
929 pub async fn renews_pending_commit_from_proposal_when_commit_doesnt_remove_same(case: TestCase) {
930 run_test_with_client_ids(
931 case.clone(),
932 ["alice", "bob", "charlie", "debbie"],
933 move |[mut alice_central, bob_central, charlie_central, debbie_central]| {
934 Box::pin(async move {
935 let id = conversation_id();
936 alice_central
937 .context
938 .new_conversation(&id, case.credential_type, case.cfg.clone())
939 .await
940 .unwrap();
941 alice_central
942 .invite_all(&case, &id, [&bob_central, &charlie_central, &debbie_central])
943 .await
944 .unwrap();
945
946 alice_central
948 .context
949 .new_remove_proposal(&id, charlie_central.get_client_id().await)
950 .await
951 .unwrap();
952 alice_central.commit_pending_proposals_unmerged(&id).await;
953
954 bob_central
956 .context
957 .conversation(&id)
958 .await
959 .unwrap()
960 .remove_members(&[debbie_central.get_client_id().await])
961 .await
962 .unwrap();
963 let commit = bob_central.mls_transport.latest_commit().await;
964 let proposals = alice_central
965 .context
966 .conversation(&id)
967 .await
968 .unwrap()
969 .decrypt_message(commit.to_bytes().unwrap())
970 .await
971 .unwrap()
972 .proposals;
973 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
975 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
976 })
977 },
978 )
979 .await
980 }
981 }
982}