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