core_crypto/mls/conversation/conversation_guard/
merge.rs1use openmls::prelude::MlsGroupStateError;
15
16use super::{ConversationGuard, Result};
17use crate::{
18 MlsError,
19 mls::conversation::{ConversationWithMls as _, Error},
20 prelude::MlsProposalRef,
21};
22
23impl ConversationGuard {
24 pub async fn clear_pending_proposal(&mut self, proposal_ref: MlsProposalRef) -> Result<()> {
39 let keystore = self.mls_provider().await?.keystore();
40 let mut conversation = self.conversation_mut().await;
41 conversation
42 .group
43 .remove_pending_proposal(&keystore, &proposal_ref)
44 .await
45 .map_err(|mls_group_state_error| match mls_group_state_error {
46 MlsGroupStateError::PendingProposalNotFound => Error::PendingProposalNotFound(proposal_ref),
47 _ => MlsError::wrap("removing pending proposal")(mls_group_state_error).into(),
48 })?;
49 conversation.persist_group_when_changed(&keystore, true).await?;
50 Ok(())
51 }
52
53 pub(crate) async fn clear_pending_commit(&mut self) -> Result<()> {
67 let keystore = self.mls_provider().await?.keystore();
68 let mut conversation = self.conversation_mut().await;
69 if conversation.group.pending_commit().is_some() {
70 conversation.group.clear_pending_commit();
71 conversation.persist_group_when_changed(&keystore, true).await?;
72 Ok(())
73 } else {
74 Err(Error::PendingCommitNotFound)
75 }
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use openmls::prelude::Proposal;
82 use wasm_bindgen_test::*;
83
84 use crate::test_utils::*;
85
86 use super::*;
87
88 wasm_bindgen_test_configure!(run_in_browser);
89
90 mod clear_pending_proposal {
91 use super::*;
92
93 #[apply(all_cred_cipher)]
94 #[wasm_bindgen_test]
95 pub async fn should_remove_proposal(case: TestCase) {
96 run_test_with_client_ids(
97 case.clone(),
98 ["alice", "bob", "charlie"],
99 move |[mut alice_central, bob_central, charlie_central]| {
100 Box::pin(async move {
101 let id = conversation_id();
102 alice_central
103 .context
104 .new_conversation(&id, case.credential_type, case.cfg.clone())
105 .await
106 .unwrap();
107 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
108 assert!(alice_central.pending_proposals(&id).await.is_empty());
109
110 let charlie_kp = charlie_central.get_one_key_package(&case).await;
111 let add_ref = alice_central
112 .context
113 .new_add_proposal(&id, charlie_kp)
114 .await
115 .unwrap()
116 .proposal_ref;
117
118 let remove_ref = alice_central
119 .context
120 .new_remove_proposal(&id, bob_central.get_client_id().await)
121 .await
122 .unwrap()
123 .proposal_ref;
124
125 let update_ref = alice_central
126 .context
127 .new_update_proposal(&id)
128 .await
129 .unwrap()
130 .proposal_ref;
131
132 assert_eq!(alice_central.pending_proposals(&id).await.len(), 3);
133 alice_central
134 .context
135 .conversation(&id)
136 .await
137 .unwrap()
138 .clear_pending_proposal(add_ref)
139 .await
140 .unwrap();
141 assert_eq!(alice_central.pending_proposals(&id).await.len(), 2);
142 assert!(
143 !alice_central
144 .pending_proposals(&id)
145 .await
146 .into_iter()
147 .any(|p| matches!(p.proposal(), Proposal::Add(_)))
148 );
149
150 alice_central
151 .context
152 .conversation(&id)
153 .await
154 .unwrap()
155 .clear_pending_proposal(remove_ref)
156 .await
157 .unwrap();
158 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
159 assert!(
160 !alice_central
161 .pending_proposals(&id)
162 .await
163 .into_iter()
164 .any(|p| matches!(p.proposal(), Proposal::Remove(_)))
165 );
166
167 alice_central
168 .context
169 .conversation(&id)
170 .await
171 .unwrap()
172 .clear_pending_proposal(update_ref)
173 .await
174 .unwrap();
175 assert!(alice_central.pending_proposals(&id).await.is_empty());
176 assert!(
177 !alice_central
178 .pending_proposals(&id)
179 .await
180 .into_iter()
181 .any(|p| matches!(p.proposal(), Proposal::Update(_)))
182 );
183 })
184 },
185 )
186 .await
187 }
188
189 #[apply(all_cred_cipher)]
190 #[wasm_bindgen_test]
191 pub async fn should_fail_when_proposal_ref_not_found(case: TestCase) {
192 run_test_with_client_ids(case.clone(), ["alice"], move |[mut alice_central]| {
193 Box::pin(async move {
194 let id = conversation_id();
195 alice_central
196 .context
197 .new_conversation(&id, case.credential_type, case.cfg.clone())
198 .await
199 .unwrap();
200 assert!(alice_central.pending_proposals(&id).await.is_empty());
201 let any_ref = MlsProposalRef::from(vec![0; case.ciphersuite().hash_length()]);
202 let clear = alice_central
203 .context
204 .conversation(&id)
205 .await
206 .unwrap()
207 .clear_pending_proposal(any_ref.clone())
208 .await;
209 assert!(
210 matches!(clear.unwrap_err(), Error::PendingProposalNotFound(prop_ref) if prop_ref == any_ref)
211 )
212 })
213 })
214 .await
215 }
216
217 #[apply(all_cred_cipher)]
218 #[wasm_bindgen_test]
219 pub async fn should_clean_associated_key_material(case: TestCase) {
220 run_test_with_client_ids(case.clone(), ["alice"], move |[mut cc]| {
221 Box::pin(async move {
222 let id = conversation_id();
223 cc.context
224 .new_conversation(&id, case.credential_type, case.cfg.clone())
225 .await
226 .unwrap();
227 assert!(cc.pending_proposals(&id).await.is_empty());
228
229 let init = cc.context.count_entities().await;
230
231 let proposal_ref = cc.context.new_update_proposal(&id).await.unwrap().proposal_ref;
232 assert_eq!(cc.pending_proposals(&id).await.len(), 1);
233
234 cc.context
235 .conversation(&id)
236 .await
237 .unwrap()
238 .clear_pending_proposal(proposal_ref)
239 .await
240 .unwrap();
241 assert!(cc.pending_proposals(&id).await.is_empty());
242
243 let after_clear_proposal = cc.context.count_entities().await;
247 assert_eq!(init, after_clear_proposal);
248 })
249 })
250 .await
251 }
252 }
253
254 mod clear_pending_commit {
255 use super::*;
256
257 #[apply(all_cred_cipher)]
258 #[wasm_bindgen_test]
259 pub async fn should_remove_commit(case: TestCase) {
260 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
261 Box::pin(async move {
262 let id = conversation_id();
263 alice_central
264 .context
265 .new_conversation(&id, case.credential_type, case.cfg.clone())
266 .await
267 .unwrap();
268 assert!(alice_central.pending_commit(&id).await.is_none());
269
270 alice_central.create_unmerged_commit(&id).await;
271 assert!(alice_central.pending_commit(&id).await.is_some());
272 alice_central
273 .context
274 .conversation(&id)
275 .await
276 .unwrap()
277 .clear_pending_commit()
278 .await
279 .unwrap();
280 assert!(alice_central.pending_commit(&id).await.is_none());
281 })
282 })
283 .await
284 }
285
286 #[apply(all_cred_cipher)]
287 #[wasm_bindgen_test]
288 pub async fn should_fail_when_pending_commit_absent(case: TestCase) {
289 run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
290 Box::pin(async move {
291 let id = conversation_id();
292 alice_central
293 .context
294 .new_conversation(&id, case.credential_type, case.cfg.clone())
295 .await
296 .unwrap();
297 assert!(alice_central.pending_commit(&id).await.is_none());
298 let clear = alice_central
299 .context
300 .conversation(&id)
301 .await
302 .unwrap()
303 .clear_pending_commit()
304 .await;
305 assert!(matches!(clear.unwrap_err(), Error::PendingCommitNotFound))
306 })
307 })
308 .await
309 }
310
311 #[apply(all_cred_cipher)]
312 #[wasm_bindgen_test]
313 pub async fn should_clean_associated_key_material(case: TestCase) {
314 run_test_with_client_ids(case.clone(), ["alice"], move |[cc]| {
315 Box::pin(async move {
316 let id = conversation_id();
317 cc.context
318 .new_conversation(&id, case.credential_type, case.cfg.clone())
319 .await
320 .unwrap();
321 assert!(cc.pending_commit(&id).await.is_none());
322
323 let init = cc.context.count_entities().await;
324
325 cc.create_unmerged_commit(&id).await;
326 assert!(cc.pending_commit(&id).await.is_some());
327
328 cc.context
329 .conversation(&id)
330 .await
331 .unwrap()
332 .clear_pending_commit()
333 .await
334 .unwrap();
335 assert!(cc.pending_commit(&id).await.is_none());
336
337 let after_clear_commit = cc.context.count_entities().await;
341 assert_eq!(init, after_clear_commit);
342 })
343 })
344 .await
345 }
346 }
347}