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::{MlsConversation, MlsProposalBundle, Session},
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: &Session,
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: &Session,
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: TestContext) {
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 .transaction
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.transaction.new_update_proposal(&id).await.unwrap();
195 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
196
197 bob_central
199 .transaction
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 .transaction
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 .transaction
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 .transaction
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: TestContext) {
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 .transaction
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
268 .transaction
269 .new_update_proposal(&id)
270 .await
271 .unwrap()
272 .proposal;
273 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
274
275 bob_central
277 .transaction
278 .conversation(&id)
279 .await
280 .unwrap()
281 .decrypt_message(proposal.to_bytes().unwrap())
282 .await
283 .unwrap();
284
285 bob_central
286 .transaction
287 .conversation(&id)
288 .await
289 .unwrap()
290 .update_key_material()
291 .await
292 .unwrap();
293 let commit = bob_central.mls_transport.latest_commit().await;
294
295 let proposals = alice_central
297 .transaction
298 .conversation(&id)
299 .await
300 .unwrap()
301 .decrypt_message(commit.to_bytes().unwrap())
302 .await
303 .unwrap()
304 .proposals;
305 assert!(alice_central.pending_proposals(&id).await.is_empty());
307 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
308
309 let proposal = alice_central
310 .transaction
311 .new_update_proposal(&id)
312 .await
313 .unwrap()
314 .proposal;
315 bob_central
316 .transaction
317 .conversation(&id)
318 .await
319 .unwrap()
320 .decrypt_message(proposal.to_bytes().unwrap())
321 .await
322 .unwrap();
323 bob_central
324 .transaction
325 .conversation(&id)
326 .await
327 .unwrap()
328 .update_key_material()
329 .await
330 .unwrap();
331 let commit = bob_central.mls_transport.latest_commit().await;
332 let proposals = alice_central
333 .transaction
334 .conversation(&id)
335 .await
336 .unwrap()
337 .decrypt_message(commit.to_bytes().unwrap())
338 .await
339 .unwrap()
340 .proposals;
341 assert!(alice_central.pending_proposals(&id).await.is_empty());
343 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
344 })
345 },
346 )
347 .await
348 }
349
350 #[apply(all_cred_cipher)]
351 #[wasm_bindgen_test]
352 pub async fn not_renewable_by_ref(case: TestContext) {
353 run_test_with_client_ids(
354 case.clone(),
355 ["alice", "bob", "charlie"],
356 move |[mut alice_central, bob_central, charlie_central]| {
357 Box::pin(async move {
358 let id = conversation_id();
359 alice_central
360 .transaction
361 .new_conversation(&id, case.credential_type, case.cfg.clone())
362 .await
363 .unwrap();
364 alice_central
365 .invite_all(&case, &id, [&bob_central, &charlie_central])
366 .await
367 .unwrap();
368
369 let proposal = bob_central.transaction.new_update_proposal(&id).await.unwrap().proposal;
370 assert!(alice_central.pending_proposals(&id).await.is_empty());
371 alice_central
372 .transaction
373 .conversation(&id)
374 .await
375 .unwrap()
376 .decrypt_message(proposal.to_bytes().unwrap())
377 .await
378 .unwrap();
379 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
380
381 charlie_central
383 .transaction
384 .conversation(&id)
385 .await
386 .unwrap()
387 .update_key_material()
388 .await
389 .unwrap();
390 let commit = charlie_central.mls_transport.latest_commit().await;
391 let proposals = alice_central
392 .transaction
393 .conversation(&id)
394 .await
395 .unwrap()
396 .decrypt_message(commit.to_bytes().unwrap())
397 .await
398 .unwrap()
399 .proposals;
400 assert!(alice_central.pending_proposals(&id).await.is_empty());
402 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
403 })
404 },
405 )
406 .await
407 }
408 }
409
410 mod add {
411 use super::*;
412
413 #[apply(all_cred_cipher)]
414 #[wasm_bindgen_test]
415 pub async fn not_renewable_when_valid_commit_adds_same(case: TestContext) {
416 run_test_with_client_ids(
417 case.clone(),
418 ["alice", "bob", "charlie"],
419 move |[mut alice_central, bob_central, charlie_central]| {
420 Box::pin(async move {
421 let id = conversation_id();
422 alice_central
423 .transaction
424 .new_conversation(&id, case.credential_type, case.cfg.clone())
425 .await
426 .unwrap();
427 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
428
429 let charlie_kp = charlie_central.get_one_key_package(&case).await;
430 assert!(alice_central.pending_proposals(&id).await.is_empty());
431 alice_central
432 .transaction
433 .new_add_proposal(&id, charlie_kp)
434 .await
435 .unwrap();
436 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
437
438 let charlie = charlie_central.rand_key_package(&case).await;
439 bob_central
440 .transaction
441 .conversation(&id)
442 .await
443 .unwrap()
444 .add_members(vec![charlie])
445 .await
446 .unwrap();
447 let commit = bob_central.mls_transport.latest_commit().await;
448 let proposals = alice_central
449 .transaction
450 .conversation(&id)
451 .await
452 .unwrap()
453 .decrypt_message(commit.to_bytes().unwrap())
454 .await
455 .unwrap()
456 .proposals;
457 assert!(alice_central.pending_proposals(&id).await.is_empty());
459 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
460 })
461 },
462 )
463 .await
464 }
465
466 #[apply(all_cred_cipher)]
467 #[wasm_bindgen_test]
468 pub async fn not_renewable_in_pending_commit_when_valid_commit_adds_same(case: TestContext) {
469 run_test_with_client_ids(
470 case.clone(),
471 ["alice", "bob", "charlie"],
472 move |[mut alice_central, bob_central, charlie_central]| {
473 Box::pin(async move {
474 let id = conversation_id();
475 alice_central
476 .transaction
477 .new_conversation(&id, case.credential_type, case.cfg.clone())
478 .await
479 .unwrap();
480 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
481
482 let charlie_kp = charlie_central.get_one_key_package(&case).await;
483 assert!(alice_central.pending_proposals(&id).await.is_empty());
484 alice_central
485 .transaction
486 .new_add_proposal(&id, charlie_kp)
487 .await
488 .unwrap();
489 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
490
491 alice_central.commit_pending_proposals_unmerged(&id).await;
493 assert!(alice_central.pending_commit(&id).await.is_some());
494
495 let charlie = charlie_central.rand_key_package(&case).await;
496 bob_central
497 .transaction
498 .conversation(&id)
499 .await
500 .unwrap()
501 .add_members(vec![charlie])
502 .await
503 .unwrap();
504 let commit = bob_central.mls_transport.latest_commit().await;
505 let proposals = alice_central
506 .transaction
507 .conversation(&id)
508 .await
509 .unwrap()
510 .decrypt_message(commit.to_bytes().unwrap())
511 .await
512 .unwrap()
513 .proposals;
514 assert!(alice_central.pending_proposals(&id).await.is_empty());
516 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
517 })
518 },
519 )
520 .await
521 }
522
523 #[apply(all_cred_cipher)]
524 #[wasm_bindgen_test]
525 pub async fn not_renewable_by_ref(case: TestContext) {
526 run_test_with_client_ids(
527 case.clone(),
528 ["alice", "bob", "charlie", "debbie"],
529 move |[mut alice_central, bob_central, charlie_central, debbie_central]| {
530 Box::pin(async move {
531 let id = conversation_id();
532 alice_central
533 .transaction
534 .new_conversation(&id, case.credential_type, case.cfg.clone())
535 .await
536 .unwrap();
537 alice_central
538 .invite_all(&case, &id, [&bob_central, &charlie_central])
539 .await
540 .unwrap();
541
542 let debbie_kp = debbie_central.get_one_key_package(&case).await;
544 let proposal = bob_central
545 .transaction
546 .new_add_proposal(&id, debbie_kp)
547 .await
548 .unwrap()
549 .proposal;
550 alice_central
551 .transaction
552 .conversation(&id)
553 .await
554 .unwrap()
555 .decrypt_message(proposal.to_bytes().unwrap())
556 .await
557 .unwrap();
558 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
559
560 charlie_central
562 .transaction
563 .conversation(&id)
564 .await
565 .unwrap()
566 .update_key_material()
567 .await
568 .unwrap();
569 let commit = charlie_central.mls_transport.latest_commit().await;
570 let proposals = alice_central
571 .transaction
572 .conversation(&id)
573 .await
574 .unwrap()
575 .decrypt_message(commit.to_bytes().unwrap())
576 .await
577 .unwrap()
578 .proposals;
579 assert!(alice_central.pending_proposals(&id).await.is_empty());
581 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
582 })
583 },
584 )
585 .await
586 }
587
588 #[apply(all_cred_cipher)]
589 #[wasm_bindgen_test]
590 pub async fn renewable_when_valid_commit_doesnt_adds_same(case: TestContext) {
591 run_test_with_client_ids(
592 case.clone(),
593 ["alice", "bob", "charlie"],
594 move |[mut alice_central, bob_central, charlie_central]| {
595 Box::pin(async move {
596 let id = conversation_id();
597 alice_central
598 .transaction
599 .new_conversation(&id, case.credential_type, case.cfg.clone())
600 .await
601 .unwrap();
602 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
603
604 let charlie_kp = charlie_central.get_one_key_package(&case).await;
606 assert!(alice_central.pending_proposals(&id).await.is_empty());
607 alice_central
608 .transaction
609 .new_add_proposal(&id, charlie_kp)
610 .await
611 .unwrap();
612 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
613
614 bob_central
616 .transaction
617 .conversation(&id)
618 .await
619 .unwrap()
620 .update_key_material()
621 .await
622 .unwrap();
623 let commit = bob_central.mls_transport.latest_commit().await;
624 let proposals = alice_central
625 .transaction
626 .conversation(&id)
627 .await
628 .unwrap()
629 .decrypt_message(commit.to_bytes().unwrap())
630 .await
631 .unwrap()
632 .proposals;
633 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
635 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
636
637 alice_central.commit_pending_proposals_unmerged(&id).await;
639 assert!(alice_central.pending_commit(&id).await.is_some());
640 bob_central
641 .transaction
642 .conversation(&id)
643 .await
644 .unwrap()
645 .update_key_material()
646 .await
647 .unwrap();
648 let commit = bob_central.mls_transport.latest_commit().await;
649 let proposals = alice_central
650 .transaction
651 .conversation(&id)
652 .await
653 .unwrap()
654 .decrypt_message(commit.to_bytes().unwrap())
655 .await
656 .unwrap()
657 .proposals;
658 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
661 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
662 })
663 },
664 )
665 .await
666 }
667
668 #[apply(all_cred_cipher)]
669 #[wasm_bindgen_test]
670 pub async fn renews_pending_commit_when_valid_commit_doesnt_add_same(case: TestContext) {
671 run_test_with_client_ids(
672 case.clone(),
673 ["alice", "bob"],
674 move |[mut alice_central, bob_central]| {
675 Box::pin(async move {
676 let id = conversation_id();
677 alice_central
678 .transaction
679 .new_conversation(&id, case.credential_type, case.cfg.clone())
680 .await
681 .unwrap();
682 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
683
684 alice_central.create_unmerged_commit(&id).await;
686 assert!(alice_central.pending_commit(&id).await.is_some());
687
688 bob_central
690 .transaction
691 .conversation(&id)
692 .await
693 .unwrap()
694 .update_key_material()
695 .await
696 .unwrap();
697 let commit = bob_central.mls_transport.latest_commit().await;
698 let proposals = alice_central
699 .transaction
700 .conversation(&id)
701 .await
702 .unwrap()
703 .decrypt_message(commit.to_bytes().unwrap())
704 .await
705 .unwrap()
706 .proposals;
707 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
709 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
710 })
711 },
712 )
713 .await
714 }
715 }
716
717 mod remove {
718 use super::*;
719
720 #[apply(all_cred_cipher)]
721 #[wasm_bindgen_test]
722 pub async fn not_renewable_when_valid_commit_removes_same(case: TestContext) {
723 run_test_with_client_ids(
724 case.clone(),
725 ["alice", "bob", "charlie"],
726 move |[mut alice_central, bob_central, charlie_central]| {
727 Box::pin(async move {
728 let id = conversation_id();
729 alice_central
730 .transaction
731 .new_conversation(&id, case.credential_type, case.cfg.clone())
732 .await
733 .unwrap();
734 alice_central
735 .invite_all(&case, &id, [&bob_central, &charlie_central])
736 .await
737 .unwrap();
738
739 assert!(alice_central.pending_proposals(&id).await.is_empty());
740 alice_central
741 .transaction
742 .new_remove_proposal(&id, charlie_central.get_client_id().await)
743 .await
744 .unwrap();
745 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
746
747 bob_central
748 .transaction
749 .conversation(&id)
750 .await
751 .unwrap()
752 .remove_members(&[charlie_central.get_client_id().await])
753 .await
754 .unwrap();
755 let commit = bob_central.mls_transport.latest_commit().await;
756 let proposals = alice_central
757 .transaction
758 .conversation(&id)
759 .await
760 .unwrap()
761 .decrypt_message(commit.to_bytes().unwrap())
762 .await
763 .unwrap()
764 .proposals;
765 assert!(alice_central.pending_proposals(&id).await.is_empty());
767 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
768 })
769 },
770 )
771 .await
772 }
773
774 #[apply(all_cred_cipher)]
775 #[wasm_bindgen_test]
776 pub async fn not_renewable_by_ref(case: TestContext) {
777 run_test_with_client_ids(
778 case.clone(),
779 ["alice", "bob", "charlie"],
780 move |[mut alice_central, bob_central, charlie_central]| {
781 Box::pin(async move {
782 let id = conversation_id();
783 alice_central
784 .transaction
785 .new_conversation(&id, case.credential_type, case.cfg.clone())
786 .await
787 .unwrap();
788 alice_central
789 .invite_all(&case, &id, [&bob_central, &charlie_central])
790 .await
791 .unwrap();
792
793 let proposal = bob_central
794 .transaction
795 .new_remove_proposal(&id, charlie_central.get_client_id().await)
796 .await
797 .unwrap()
798 .proposal;
799 assert!(alice_central.pending_proposals(&id).await.is_empty());
800 alice_central
801 .transaction
802 .conversation(&id)
803 .await
804 .unwrap()
805 .decrypt_message(proposal.to_bytes().unwrap())
806 .await
807 .unwrap();
808 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
809
810 charlie_central
811 .transaction
812 .conversation(&id)
813 .await
814 .unwrap()
815 .update_key_material()
816 .await
817 .unwrap();
818 let commit = charlie_central.mls_transport.latest_commit().await;
819 let proposals = alice_central
820 .transaction
821 .conversation(&id)
822 .await
823 .unwrap()
824 .decrypt_message(commit.to_bytes().unwrap())
825 .await
826 .unwrap()
827 .proposals;
828 assert!(alice_central.pending_proposals(&id).await.is_empty());
830 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
831 })
832 },
833 )
834 .await
835 }
836
837 #[apply(all_cred_cipher)]
838 #[wasm_bindgen_test]
839 pub async fn renewable_when_valid_commit_doesnt_remove_same(case: TestContext) {
840 run_test_with_client_ids(
841 case.clone(),
842 ["alice", "bob", "charlie", "debbie"],
843 move |[mut alice_central, bob_central, charlie_central, debbie_central]| {
844 Box::pin(async move {
845 let id = conversation_id();
846 alice_central
847 .transaction
848 .new_conversation(&id, case.credential_type, case.cfg.clone())
849 .await
850 .unwrap();
851 alice_central
852 .invite_all(&case, &id, [&bob_central, &charlie_central, &debbie_central])
853 .await
854 .unwrap();
855
856 assert!(alice_central.pending_proposals(&id).await.is_empty());
858 alice_central
859 .transaction
860 .new_remove_proposal(&id, charlie_central.get_client_id().await)
861 .await
862 .unwrap();
863 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
864
865 bob_central
867 .transaction
868 .conversation(&id)
869 .await
870 .unwrap()
871 .remove_members(&[debbie_central.get_client_id().await])
872 .await
873 .unwrap();
874 let commit = bob_central.mls_transport.latest_commit().await;
875 let proposals = alice_central
876 .transaction
877 .conversation(&id)
878 .await
879 .unwrap()
880 .decrypt_message(commit.to_bytes().unwrap())
881 .await
882 .unwrap()
883 .proposals;
884 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
886 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
887 })
888 },
889 )
890 .await
891 }
892
893 #[apply(all_cred_cipher)]
894 #[wasm_bindgen_test]
895 pub async fn renews_pending_commit_when_commit_doesnt_remove_same(case: TestContext) {
896 run_test_with_client_ids(
897 case.clone(),
898 ["alice", "bob", "charlie", "debbie"],
899 move |[mut alice_central, bob_central, charlie_central, debbie_central]| {
900 Box::pin(async move {
901 let id = conversation_id();
902 alice_central
903 .transaction
904 .new_conversation(&id, case.credential_type, case.cfg.clone())
905 .await
906 .unwrap();
907 alice_central
908 .invite_all(&case, &id, [&bob_central, &charlie_central, &debbie_central])
909 .await
910 .unwrap();
911
912 alice_central
914 .transaction
915 .new_remove_proposal(&id, charlie_central.get_client_id().await)
916 .await
917 .unwrap();
918 alice_central.commit_pending_proposals_unmerged(&id).await;
919 assert!(alice_central.pending_commit(&id).await.is_some());
920
921 bob_central
923 .transaction
924 .conversation(&id)
925 .await
926 .unwrap()
927 .remove_members(&[debbie_central.get_client_id().await])
928 .await
929 .unwrap();
930 let commit = bob_central.mls_transport.latest_commit().await;
931 let proposals = alice_central
932 .transaction
933 .conversation(&id)
934 .await
935 .unwrap()
936 .decrypt_message(commit.to_bytes().unwrap())
937 .await
938 .unwrap()
939 .proposals;
940 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
942 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
943 })
944 },
945 )
946 .await
947 }
948
949 #[apply(all_cred_cipher)]
950 #[wasm_bindgen_test]
951 pub async fn renews_pending_commit_from_proposal_when_commit_doesnt_remove_same(case: TestContext) {
952 run_test_with_client_ids(
953 case.clone(),
954 ["alice", "bob", "charlie", "debbie"],
955 move |[mut alice_central, bob_central, charlie_central, debbie_central]| {
956 Box::pin(async move {
957 let id = conversation_id();
958 alice_central
959 .transaction
960 .new_conversation(&id, case.credential_type, case.cfg.clone())
961 .await
962 .unwrap();
963 alice_central
964 .invite_all(&case, &id, [&bob_central, &charlie_central, &debbie_central])
965 .await
966 .unwrap();
967
968 alice_central
970 .transaction
971 .new_remove_proposal(&id, charlie_central.get_client_id().await)
972 .await
973 .unwrap();
974 alice_central.commit_pending_proposals_unmerged(&id).await;
975
976 bob_central
978 .transaction
979 .conversation(&id)
980 .await
981 .unwrap()
982 .remove_members(&[debbie_central.get_client_id().await])
983 .await
984 .unwrap();
985 let commit = bob_central.mls_transport.latest_commit().await;
986 let proposals = alice_central
987 .transaction
988 .conversation(&id)
989 .await
990 .unwrap()
991 .decrypt_message(commit.to_bytes().unwrap())
992 .await
993 .unwrap()
994 .proposals;
995 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
997 assert_eq!(proposals.len(), alice_central.pending_proposals(&id).await.len());
998 })
999 },
1000 )
1001 .await
1002 }
1003 }
1004}