1use std::str::FromStr;
3
4use bitwarden_api_api::{
5 apis::ApiClient,
6 models::{EmergencyAccessStatusType, WebAuthnPrfStatus},
7};
8use bitwarden_core::key_management::account_cryptographic_state::WrappedAccountCryptographicState;
9use bitwarden_crypto::{EncString, Kdf, PublicKey, SpkiPublicKeyBytes, UnsignedSharedKey};
10use bitwarden_encoding::B64;
11use bitwarden_error::bitwarden_error;
12use bitwarden_vault::{Cipher, Folder};
13use thiserror::Error;
14use tokio::try_join;
15use tracing::{debug, debug_span, info};
16use uuid::Uuid;
17
18use crate::key_rotation::{
19 partial_rotateable_keyset::PartialRotateableKeyset,
20 unlock::{V1EmergencyAccessMembership, V1OrganizationMembership},
21};
22
23trait DebugMapErr<T, E: std::fmt::Debug> {
24 fn debug_map_err<E2>(self, target: E2) -> Result<T, E2>;
26}
27
28impl<T, E: std::fmt::Debug> DebugMapErr<T, E> for Result<T, E> {
29 fn debug_map_err<E2>(self, target: E2) -> Result<T, E2> {
30 self.map_err(|e| {
31 debug!(error = ?e);
32 target
33 })
34 }
35}
36
37pub(super) struct SyncedAccountData {
38 pub(super) wrapped_account_cryptographic_state: WrappedAccountCryptographicState,
39 pub(super) folders: Vec<Folder>,
40 pub(super) ciphers: Vec<Cipher>,
41 pub(super) sends: Vec<bitwarden_send::Send>,
42 pub(super) emergency_access_memberships: Vec<V1EmergencyAccessMembership>,
43 pub(super) organization_memberships: Vec<V1OrganizationMembership>,
44 pub(super) trusted_devices: Vec<PartialRotateableKeyset>,
45 pub(super) passkeys: Vec<PartialRotateableKeyset>,
46 pub(super) kdf_and_salt: Option<(Kdf, String)>,
47}
48
49#[derive(Debug, Error)]
50#[bitwarden_error(flat)]
51pub(super) enum SyncError {
52 #[error("Network error during sync")]
53 Network,
54 #[error("Failed to parse sync data")]
55 Data,
56}
57
58async fn fetch_organization_public_key(
60 api_client: &ApiClient,
61 organization_id: Uuid,
62) -> Result<PublicKey, SyncError> {
63 let org_details = api_client
64 .organizations_api()
65 .get_public_key(&organization_id.to_string())
66 .await
67 .debug_map_err(SyncError::Network)?
68 .public_key
69 .ok_or(SyncError::Data)?;
70 PublicKey::from_der(&SpkiPublicKeyBytes::from(
71 B64::from_str(&org_details)
72 .debug_map_err(SyncError::Data)?
73 .into_bytes(),
74 ))
75 .debug_map_err(SyncError::Data)
76}
77
78pub(crate) async fn sync_orgs(
81 api_client: &ApiClient,
82) -> Result<Vec<V1OrganizationMembership>, SyncError> {
83 let organizations = api_client
84 .organizations_api()
85 .get_user()
86 .await
87 .debug_map_err(SyncError::Network)?
88 .data
89 .ok_or(SyncError::Data)?
90 .into_iter();
91 let organizations = organizations
92 .into_iter()
93 .filter(|org| org.reset_password_enrolled.unwrap_or(false))
94 .map(async |org| {
95 let id = org.id.ok_or(SyncError::Data)?;
96 let public_key = fetch_organization_public_key(api_client, id).await?;
97 Ok(V1OrganizationMembership {
98 organization_id: id,
99 name: org.name.ok_or(SyncError::Data)?,
100 public_key,
101 })
102 })
103 .collect::<Vec<_>>();
104
105 let mut organization_memberships = Vec::new();
107 for futures in organizations {
108 organization_memberships.push(futures.await?);
109 }
110
111 info!(
112 "Downloaded {} organization memberships",
113 organization_memberships.len()
114 );
115 Ok(organization_memberships)
116}
117
118async fn fetch_user_public_key(
120 api_client: &ApiClient,
121 user_id: Uuid,
122) -> Result<PublicKey, SyncError> {
123 let user_key_response = api_client
124 .users_api()
125 .get_public_key(user_id)
126 .await
127 .debug_map_err(SyncError::Network)?;
128 let public_key_b64 = user_key_response.public_key.ok_or(SyncError::Data)?;
129 PublicKey::from_der(&SpkiPublicKeyBytes::from(
130 B64::from_str(&public_key_b64)
131 .debug_map_err(SyncError::Data)?
132 .into_bytes(),
133 ))
134 .debug_map_err(SyncError::Data)
135}
136
137pub(crate) async fn sync_emergency_access(
139 api_client: &ApiClient,
140) -> Result<Vec<V1EmergencyAccessMembership>, SyncError> {
141 let emergency_access = api_client
142 .emergency_access_api()
143 .get_contacts()
144 .await
145 .debug_map_err(SyncError::Network)?
146 .data
147 .ok_or(SyncError::Data)?
148 .into_iter()
149 .filter(|ea| {
150 ea.status == Some(EmergencyAccessStatusType::Confirmed)
151 || ea.status == Some(EmergencyAccessStatusType::RecoveryInitiated)
152 || ea.status == Some(EmergencyAccessStatusType::RecoveryApproved)
153 })
154 .map(async |ea| {
155 let user_id = ea.grantee_id.ok_or(SyncError::Data)?;
156 let public_key = fetch_user_public_key(api_client, user_id).await?;
157 Ok(V1EmergencyAccessMembership {
158 id: ea.id.ok_or(SyncError::Data)?,
159 grantee_id: user_id,
160 name: ea
162 .name
163 .unwrap_or_else(|| ea.email.unwrap_or_else(|| "Unknown".to_string())),
164 public_key,
165 })
166 })
167 .collect::<Vec<_>>();
168
169 let mut emergency_access_memberships = Vec::new();
171 for futures in emergency_access {
172 emergency_access_memberships.push(futures.await?);
173 }
174
175 info!(
176 "Downloaded {} emergency access memberships",
177 emergency_access_memberships.len()
178 );
179 Ok(emergency_access_memberships)
180}
181
182async fn sync_passkeys(api_client: &ApiClient) -> Result<Vec<PartialRotateableKeyset>, SyncError> {
184 let passkeys = api_client
185 .web_authn_api()
186 .get()
187 .await
188 .debug_map_err(SyncError::Network)?
189 .data
190 .ok_or(SyncError::Data)?
191 .into_iter()
192 .filter(|cred| cred.prf_status == Some(WebAuthnPrfStatus::Enabled))
193 .map(|cred| {
194 Ok(PartialRotateableKeyset {
195 id: Uuid::from_str(&cred.id.ok_or(SyncError::Data)?)
196 .debug_map_err(SyncError::Data)?,
197 encrypted_public_key: EncString::from_str(
198 &cred.encrypted_public_key.ok_or(SyncError::Data)?,
199 )
200 .debug_map_err(SyncError::Data)?,
201 encrypted_user_key: UnsignedSharedKey::from_str(
202 &cred.encrypted_user_key.ok_or(SyncError::Data)?,
203 )
204 .debug_map_err(SyncError::Data)?,
205 })
206 })
207 .collect::<Result<Vec<_>, _>>()?;
208 info!("Downloaded {} passkeys", passkeys.len());
209 Ok(passkeys)
210}
211
212async fn sync_devices(api_client: &ApiClient) -> Result<Vec<PartialRotateableKeyset>, SyncError> {
214 let trusted_devices = api_client
215 .devices_api()
216 .get_all()
217 .await
218 .debug_map_err(SyncError::Network)?
219 .data
220 .ok_or(SyncError::Data)?
221 .into_iter()
222 .filter(|device| device.is_trusted.unwrap_or(false))
223 .map(|device| {
224 Ok(PartialRotateableKeyset {
225 id: device.id.ok_or(SyncError::Data)?,
226 encrypted_public_key: EncString::from_str(
227 &device.encrypted_public_key.ok_or(SyncError::Data)?,
228 )
229 .debug_map_err(SyncError::Data)?,
230 encrypted_user_key: UnsignedSharedKey::from_str(
231 &device.encrypted_user_key.ok_or(SyncError::Data)?,
232 )
233 .debug_map_err(SyncError::Data)?,
234 })
235 })
236 .collect::<Result<Vec<_>, _>>()?;
237 info!("Downloaded {} trusted devices", trusted_devices.len());
238 Ok(trusted_devices)
239}
240
241fn parse_ciphers(
242 ciphers: Option<Vec<bitwarden_api_api::models::CipherDetailsResponseModel>>,
243) -> Result<Vec<Cipher>, SyncError> {
244 let ciphers = ciphers
245 .ok_or(SyncError::Data)?
246 .into_iter()
247 .filter(|c| c.organization_id.is_none())
248 .map(|c| {
249 let _span = debug_span!("deserializing_cipher", cipher_id = ?c.id).entered();
250 Cipher::try_from(c).debug_map_err(SyncError::Data)
251 })
252 .collect::<Result<Vec<_>, _>>()?;
253 info!("Deserialized {} ciphers", ciphers.len());
254 Ok(ciphers)
255}
256
257fn parse_folders(
258 folders: Option<Vec<bitwarden_api_api::models::FolderResponseModel>>,
259) -> Result<Vec<Folder>, SyncError> {
260 let folders = folders
261 .ok_or(SyncError::Data)?
262 .into_iter()
263 .map(|f| {
264 let _span = debug_span!("deserializing_folder", folder_id = ?f.id).entered();
265 Folder::try_from(f).debug_map_err(SyncError::Data)
266 })
267 .collect::<Result<Vec<_>, _>>()?;
268 info!("Deserialized {} folders", folders.len());
269 Ok(folders)
270}
271
272fn parse_sends(
273 sends: Option<Vec<bitwarden_api_api::models::SendResponseModel>>,
274) -> Result<Vec<bitwarden_send::Send>, SyncError> {
275 let sends = sends
276 .ok_or(SyncError::Data)?
277 .into_iter()
278 .map(|s| {
279 let _span = debug_span!("deserializing_send", send_id = ?s.id).entered();
280 bitwarden_send::Send::try_from(s).debug_map_err(SyncError::Data)
281 })
282 .collect::<Result<Vec<_>, _>>()?;
283 info!("Deserialized {} sends", sends.len());
284 Ok(sends)
285}
286
287fn from_kdf(
288 kdf: &bitwarden_api_api::models::MasterPasswordUnlockKdfResponseModel,
289) -> Result<Kdf, ()> {
290 Ok(match kdf.kdf_type {
291 bitwarden_api_api::models::KdfType::PBKDF2_SHA256 => Kdf::PBKDF2 {
292 iterations: std::num::NonZeroU32::new(kdf.iterations.try_into().debug_map_err(())?)
293 .ok_or(())?,
294 },
295 bitwarden_api_api::models::KdfType::Argon2id => {
296 let memory = kdf.memory.ok_or(())?;
297 let parallelism = kdf.parallelism.ok_or(())?;
298 Kdf::Argon2id {
299 iterations: std::num::NonZeroU32::new(kdf.iterations.try_into().debug_map_err(())?)
300 .ok_or(())?,
301 memory: std::num::NonZeroU32::new(memory.try_into().debug_map_err(())?).ok_or(())?,
302 parallelism: std::num::NonZeroU32::new(parallelism.try_into().debug_map_err(())?)
303 .ok_or(())?,
304 }
305 }
306 bitwarden_api_api::models::KdfType::__Unknown(_) => return Err(()),
307 })
308}
309
310fn parse_kdf_and_salt(
313 user_decryption: &Option<Box<bitwarden_api_api::models::UserDecryptionResponseModel>>,
314) -> Result<Option<(Kdf, String)>, SyncError> {
315 let user_decryption_options = user_decryption.as_ref().ok_or(SyncError::Data)?;
316 if let Some(master_password_unlock) = &user_decryption_options.master_password_unlock {
317 let kdf = from_kdf(&master_password_unlock.clone().kdf).debug_map_err(SyncError::Data)?;
318 let salt = master_password_unlock.clone().salt.ok_or(SyncError::Data)?;
319 debug!("Parsed password KDF and salt from sync response");
320 Ok(Some((kdf, salt)))
321 } else {
322 debug!(
323 "User does not have master password decryption options, skipping KDF and salt parsing"
324 );
325 Ok(None)
326 }
327}
328
329pub(super) async fn sync_current_account_data(
330 api_client: &ApiClient,
331) -> Result<SyncedAccountData, SyncError> {
332 info!("Syncing latest vault state from server for key rotation");
333 let sync = api_client
334 .sync_api()
335 .get(Some(true))
336 .await
337 .debug_map_err(SyncError::Network)?;
338
339 let profile = sync.profile.as_ref().ok_or(SyncError::Data)?;
340 let kdf_and_salt = parse_kdf_and_salt(&sync.user_decryption)?;
342 let account_cryptographic_state = profile.account_keys.to_owned().ok_or(SyncError::Data)?;
343 let ciphers = parse_ciphers(sync.ciphers)?;
344 let folders = parse_folders(sync.folders)?;
345 let sends = parse_sends(sync.sends)?;
346 let wrapped_account_cryptographic_state =
347 WrappedAccountCryptographicState::try_from(account_cryptographic_state.as_ref())
348 .debug_map_err(SyncError::Data)?;
349
350 info!("Syncing additional data (organizations, emergency access, devices, passkeys)");
353 let (organization_memberships, emergency_access_memberships, trusted_devices, passkeys) = try_join!(
354 sync_orgs(api_client),
355 sync_emergency_access(api_client),
356 sync_devices(api_client),
357 sync_passkeys(api_client),
358 )?;
359
360 Ok(SyncedAccountData {
361 wrapped_account_cryptographic_state,
362 folders,
363 ciphers,
364 sends,
365 emergency_access_memberships,
366 organization_memberships,
367 trusted_devices,
368 passkeys,
369 kdf_and_salt,
370 })
371}
372
373#[cfg(test)]
374mod tests {
375 use bitwarden_api_api::{
376 apis::ApiClient,
377 models::{
378 DeviceAuthRequestResponseModel, DeviceAuthRequestResponseModelListResponseModel,
379 EmergencyAccessGranteeDetailsResponseModel,
380 EmergencyAccessGranteeDetailsResponseModelListResponseModel, FolderResponseModel,
381 KdfType, MasterPasswordUnlockKdfResponseModel, MasterPasswordUnlockResponseModel,
382 OrganizationPublicKeyResponseModel, PrivateKeysResponseModel,
383 ProfileOrganizationResponseModel, ProfileOrganizationResponseModelListResponseModel,
384 ProfileResponseModel, PublicKeyEncryptionKeyPairResponseModel, SendResponseModel,
385 SendType, SyncResponseModel, UserDecryptionResponseModel, UserKeyResponseModel,
386 WebAuthnCredentialResponseModel, WebAuthnCredentialResponseModelListResponseModel,
387 WebAuthnPrfStatus,
388 },
389 };
390 use bitwarden_encoding::B64;
391 use bitwarden_send::SendId;
392 use bitwarden_vault::{CipherId, FolderId};
393
394 use super::*;
395
396 const TEST_ENC_STRING: &str = "2.STIyTrfDZN/JXNDN9zNEMw==|NDLum8BHZpPNYhJo9ggSkg==|UCsCLlBO3QzdPwvMAWs2VVwuE6xwOx/vxOooPObqnEw=";
397 const KEY_ENC_STRING: &str = "2.KLv/j0V4Ebs0dwyPdtt4vw==|Nczvv+DTkeP466cP/wMDnGK6W9zEIg5iHLhcuQG6s+M=|SZGsfuIAIaGZ7/kzygaVUau3LeOvJUlolENBOU+LX7g=";
398 const TEST_UNSIGNED_SHARED_KEY: &str = "4.AAAAAAAAAAAAAAAAAAAAAA==";
399
400 const TEST_RSA_PUBLIC_KEY_BYTES: &[u8] = &[
401 48, 130, 1, 34, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0, 3, 130, 1, 15, 0,
402 48, 130, 1, 10, 2, 130, 1, 1, 0, 173, 4, 54, 63, 125, 12, 254, 38, 115, 34, 95, 164, 148,
403 115, 86, 140, 129, 74, 19, 70, 212, 212, 130, 163, 105, 249, 101, 120, 154, 46, 194, 250,
404 229, 242, 156, 67, 109, 179, 187, 134, 59, 235, 60, 107, 144, 163, 35, 22, 109, 230, 134,
405 243, 44, 243, 79, 84, 76, 11, 64, 56, 236, 167, 98, 26, 30, 213, 143, 105, 52, 92, 129, 92,
406 88, 22, 115, 135, 63, 215, 79, 8, 11, 183, 124, 10, 73, 231, 170, 110, 210, 178, 22, 100,
407 76, 75, 118, 202, 252, 204, 67, 204, 152, 6, 244, 208, 161, 146, 103, 225, 233, 239, 88,
408 195, 88, 150, 230, 111, 62, 142, 12, 157, 184, 155, 34, 84, 237, 111, 11, 97, 56, 152, 130,
409 14, 72, 123, 140, 47, 137, 5, 97, 166, 4, 147, 111, 23, 65, 78, 63, 208, 198, 50, 161, 39,
410 80, 143, 100, 194, 37, 252, 194, 53, 207, 166, 168, 250, 165, 121, 9, 207, 90, 36, 213,
411 211, 84, 255, 14, 205, 114, 135, 217, 137, 105, 232, 58, 169, 222, 10, 13, 138, 203, 16,
412 12, 122, 72, 227, 95, 160, 111, 54, 200, 198, 143, 156, 15, 143, 196, 50, 150, 204, 144,
413 255, 162, 248, 50, 28, 47, 66, 9, 83, 158, 67, 9, 50, 147, 174, 147, 200, 199, 238, 190,
414 248, 60, 114, 218, 32, 209, 120, 218, 17, 234, 14, 128, 192, 166, 33, 60, 73, 227, 108,
415 201, 41, 160, 81, 133, 171, 205, 221, 2, 3, 1, 0, 1,
416 ];
417
418 fn test_public_key_b64() -> String {
419 B64::from(TEST_RSA_PUBLIC_KEY_BYTES.to_vec()).to_string()
420 }
421
422 fn create_test_folder(id: uuid::Uuid) -> FolderResponseModel {
423 FolderResponseModel {
424 object: Some("folder".to_string()),
425 id: Some(id),
426 name: Some(TEST_ENC_STRING.to_string()),
427 revision_date: Some("2024-01-01T00:00:00Z".to_string()),
428 }
429 }
430
431 fn create_test_cipher(id: uuid::Uuid) -> bitwarden_api_api::models::CipherDetailsResponseModel {
432 bitwarden_api_api::models::CipherDetailsResponseModel {
433 object: Some("cipher".to_string()),
434 id: Some(id),
435 organization_id: None,
436 r#type: Some(bitwarden_api_api::models::CipherType::Login),
437 data: None,
438 name: Some(TEST_ENC_STRING.to_string()),
439 notes: None,
440 login: None,
441 card: None,
442 identity: None,
443 secure_note: None,
444 ssh_key: None,
445 bank_account: None,
446 drivers_license: None,
447 passport: None,
448 fields: None,
449 password_history: None,
450 attachments: None,
451 organization_use_totp: Some(false),
452 revision_date: Some("2024-01-01T00:00:00Z".to_string()),
453 creation_date: Some("2024-01-01T00:00:00Z".to_string()),
454 deleted_date: None,
455 reprompt: Some(bitwarden_api_api::models::CipherRepromptType::None),
456 key: None,
457 archived_date: None,
458 folder_id: None,
459 favorite: Some(false),
460 edit: Some(true),
461 view_password: Some(true),
462 permissions: None,
463 collection_ids: None,
464 }
465 }
466
467 fn create_test_send(id: uuid::Uuid) -> SendResponseModel {
468 SendResponseModel {
469 object: Some("send".to_string()),
470 id: Some(id),
471 access_id: Some("access_id".to_string()),
472 r#type: Some(SendType::Text),
473 name: Some(TEST_ENC_STRING.to_string()),
474 notes: None,
475 file: None,
476 text: None,
477 key: Some(KEY_ENC_STRING.to_string()),
478 max_access_count: None,
479 access_count: Some(0),
480 password: None,
481 disabled: Some(false),
482 revision_date: Some("2024-01-01T00:00:00Z".to_string()),
483 expiration_date: None,
484 deletion_date: Some("2024-12-31T00:00:00Z".to_string()),
485 hide_email: Some(false),
486 auth_type: None,
487 emails: None,
488 }
489 }
490
491 fn create_test_user_decryption() -> UserDecryptionResponseModel {
492 UserDecryptionResponseModel {
493 master_password_unlock: Some(Box::new(MasterPasswordUnlockResponseModel {
494 kdf: Box::new(MasterPasswordUnlockKdfResponseModel {
495 kdf_type: KdfType::PBKDF2_SHA256,
496 iterations: 600000,
497 memory: None,
498 parallelism: None,
499 }),
500 master_key_encrypted_user_key: None,
501 salt: Some("test_salt".to_string()),
502 })),
503 web_authn_prf_options: None,
504 v2_upgrade_token: None,
505 }
506 }
507
508 fn create_test_profile(user_id: uuid::Uuid) -> ProfileResponseModel {
509 ProfileResponseModel {
510 id: Some(user_id),
511 account_keys: Some(Box::new(PrivateKeysResponseModel {
512 object: None,
513 signature_key_pair: None,
514 public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
515 object: None,
516 wrapped_private_key: Some(TEST_ENC_STRING.to_string()),
517 public_key: None,
518 signed_public_key: None,
519 }),
520 security_state: None,
521 })),
522 ..ProfileResponseModel::default()
523 }
524 }
525
526 fn create_test_sync_response(user_id: uuid::Uuid) -> SyncResponseModel {
527 SyncResponseModel {
528 object: Some("sync".to_string()),
529 profile: Some(Box::new(create_test_profile(user_id))),
530 folders: Some(vec![create_test_folder(uuid::Uuid::new_v4())]),
531 ciphers: Some(vec![create_test_cipher(uuid::Uuid::new_v4())]),
532 sends: Some(vec![create_test_send(uuid::Uuid::new_v4())]),
533 user_decryption: Some(Box::new(create_test_user_decryption())),
534 ..Default::default()
535 }
536 }
537
538 fn create_test_org_list_response(
539 org_id: uuid::Uuid,
540 ) -> ProfileOrganizationResponseModelListResponseModel {
541 ProfileOrganizationResponseModelListResponseModel {
542 object: None,
543 data: Some(vec![ProfileOrganizationResponseModel {
544 id: Some(org_id),
545 name: Some("Test Org".to_string()),
546 reset_password_enrolled: Some(true),
547 ..ProfileOrganizationResponseModel::new()
548 }]),
549 continuation_token: None,
550 }
551 }
552
553 fn create_test_org_public_key_response() -> OrganizationPublicKeyResponseModel {
554 OrganizationPublicKeyResponseModel {
555 object: None,
556 public_key: Some(test_public_key_b64()),
557 }
558 }
559
560 fn create_test_emergency_access_response(
561 ea_id: uuid::Uuid,
562 grantee_id: uuid::Uuid,
563 ) -> EmergencyAccessGranteeDetailsResponseModelListResponseModel {
564 EmergencyAccessGranteeDetailsResponseModelListResponseModel {
565 object: None,
566 data: Some(vec![EmergencyAccessGranteeDetailsResponseModel {
567 id: Some(ea_id),
568 grantee_id: Some(grantee_id),
569 name: Some("Emergency Contact".to_string()),
570 status: Some(EmergencyAccessStatusType::Confirmed),
571 ..EmergencyAccessGranteeDetailsResponseModel::new()
572 }]),
573 continuation_token: None,
574 }
575 }
576
577 fn create_test_user_key_response() -> UserKeyResponseModel {
578 UserKeyResponseModel {
579 object: None,
580 user_id: None,
581 public_key: Some(test_public_key_b64()),
582 }
583 }
584
585 fn create_test_devices_response(
586 device_id: uuid::Uuid,
587 ) -> DeviceAuthRequestResponseModelListResponseModel {
588 DeviceAuthRequestResponseModelListResponseModel {
589 object: None,
590 data: Some(vec![DeviceAuthRequestResponseModel {
591 id: Some(device_id),
592 is_trusted: Some(true),
593 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
594 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
595 ..DeviceAuthRequestResponseModel::new()
596 }]),
597 continuation_token: None,
598 }
599 }
600
601 fn create_test_passkeys_response(
602 passkey_id: uuid::Uuid,
603 ) -> WebAuthnCredentialResponseModelListResponseModel {
604 WebAuthnCredentialResponseModelListResponseModel {
605 object: None,
606 data: Some(vec![WebAuthnCredentialResponseModel {
607 id: Some(passkey_id.to_string()),
608 prf_status: Some(WebAuthnPrfStatus::Enabled),
609 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
610 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
611 ..WebAuthnCredentialResponseModel::new()
612 }]),
613 continuation_token: None,
614 }
615 }
616
617 #[tokio::test]
618 async fn test_sync_current_account_data_success() {
619 let user_id = uuid::Uuid::new_v4();
620 let org_id = uuid::Uuid::new_v4();
621 let ea_id = uuid::Uuid::new_v4();
622 let grantee_id = uuid::Uuid::new_v4();
623 let device_id = uuid::Uuid::new_v4();
624 let passkey_id = uuid::Uuid::new_v4();
625 let folder_id = uuid::Uuid::new_v4();
626 let cipher_id = uuid::Uuid::new_v4();
627 let send_id = uuid::Uuid::new_v4();
628
629 let api_client = ApiClient::new_mocked(|mock| {
630 mock.sync_api
631 .expect_get()
632 .once()
633 .returning(move |_exclude_domains| {
634 let mut response = create_test_sync_response(user_id);
635 response.folders = Some(vec![create_test_folder(folder_id)]);
636 response.ciphers = Some(vec![create_test_cipher(cipher_id)]);
637 response.sends = Some(vec![create_test_send(send_id)]);
638 Ok(response)
639 });
640 mock.organizations_api
641 .expect_get_user()
642 .once()
643 .returning(move || Ok(create_test_org_list_response(org_id)));
644 mock.organizations_api
645 .expect_get_public_key()
646 .once()
647 .returning(move |_id| Ok(create_test_org_public_key_response()));
648 mock.emergency_access_api
649 .expect_get_contacts()
650 .once()
651 .returning(move || Ok(create_test_emergency_access_response(ea_id, grantee_id)));
652 mock.users_api
653 .expect_get_public_key()
654 .once()
655 .returning(move |_user_id| Ok(create_test_user_key_response()));
656 mock.devices_api
657 .expect_get_all()
658 .once()
659 .returning(move || Ok(create_test_devices_response(device_id)));
660 mock.web_authn_api
661 .expect_get()
662 .once()
663 .returning(move || Ok(create_test_passkeys_response(passkey_id)));
664 });
665
666 let result = sync_current_account_data(&api_client).await;
667 let data = result.unwrap();
668
669 assert_eq!(data.folders.len(), 1);
671 assert_eq!(data.folders[0].id, Some(FolderId::new(folder_id)));
672 assert_eq!(data.folders[0].name, TEST_ENC_STRING.parse().unwrap());
673
674 assert_eq!(data.ciphers.len(), 1);
676 assert_eq!(data.ciphers[0].id, Some(CipherId::new(cipher_id)));
677 assert_eq!(data.ciphers[0].name, TEST_ENC_STRING.parse().unwrap());
678
679 assert_eq!(data.sends.len(), 1);
681 assert_eq!(data.sends[0].id, Some(SendId::new(send_id)));
682 assert_eq!(data.sends[0].name, TEST_ENC_STRING.parse().unwrap());
683 assert_eq!(data.sends[0].key, KEY_ENC_STRING.parse().unwrap());
684
685 assert_eq!(data.organization_memberships.len(), 1);
686 assert_eq!(data.organization_memberships[0].organization_id, org_id);
687 assert_eq!(data.emergency_access_memberships.len(), 1);
688 assert_eq!(data.emergency_access_memberships[0].id, ea_id);
689 assert_eq!(data.trusted_devices.len(), 1);
690 assert_eq!(data.trusted_devices[0].id, device_id);
691 assert_eq!(data.passkeys.len(), 1);
692 assert_eq!(data.passkeys[0].id, passkey_id);
693 assert!(data.kdf_and_salt.is_some());
694 let (kdf, salt) = data.kdf_and_salt.unwrap();
695 assert_eq!(salt, "test_salt");
696 assert!(matches!(kdf, Kdf::PBKDF2 { iterations } if iterations.get() == 600000));
697 assert!(matches!(
698 data.wrapped_account_cryptographic_state,
699 WrappedAccountCryptographicState::V1 { .. }
700 ));
701
702 if let ApiClient::Mock(mut mock) = api_client {
703 mock.sync_api.checkpoint();
704 mock.organizations_api.checkpoint();
705 mock.emergency_access_api.checkpoint();
706 mock.users_api.checkpoint();
707 mock.devices_api.checkpoint();
708 mock.web_authn_api.checkpoint();
709 }
710 }
711
712 #[tokio::test]
713 async fn test_sync_current_account_data_network_error() {
714 let api_client = ApiClient::new_mocked(|mock| {
715 mock.sync_api
716 .expect_get()
717 .once()
718 .returning(move |_exclude_domains| {
719 Err(serde_json::Error::io(std::io::Error::other("API error")).into())
720 });
721 mock.organizations_api.expect_get_user().never();
722 mock.organizations_api.expect_get_public_key().never();
723 mock.emergency_access_api.expect_get_contacts().never();
724 mock.users_api.expect_get_public_key().never();
725 mock.devices_api.expect_get_all().never();
726 mock.web_authn_api.expect_get().never();
727 });
728
729 let result = sync_current_account_data(&api_client).await;
730
731 assert!(matches!(result, Err(SyncError::Network)));
732
733 if let ApiClient::Mock(mut mock) = api_client {
734 mock.sync_api.checkpoint();
735 mock.organizations_api.checkpoint();
736 mock.emergency_access_api.checkpoint();
737 mock.users_api.checkpoint();
738 mock.devices_api.checkpoint();
739 mock.web_authn_api.checkpoint();
740 }
741 }
742
743 #[test]
744 fn test_parse_ciphers_filters_organization_ciphers() {
745 let personal_cipher_id = uuid::Uuid::new_v4();
746 let organization_cipher_id = uuid::Uuid::new_v4();
747
748 let personal_cipher = create_test_cipher(personal_cipher_id);
749 let mut organization_cipher = create_test_cipher(organization_cipher_id);
750 organization_cipher.organization_id = Some(uuid::Uuid::new_v4());
751
752 let ciphers = parse_ciphers(Some(vec![personal_cipher, organization_cipher])).unwrap();
753
754 assert_eq!(ciphers.len(), 1);
755 assert_eq!(ciphers[0].id, Some(CipherId::new(personal_cipher_id)));
756 }
757
758 #[tokio::test]
759 async fn test_fetch_organization_public_key_success() {
760 let org_id = uuid::Uuid::new_v4();
761 let expected_public_key_b64 = test_public_key_b64();
762
763 let api_client = ApiClient::new_mocked(|mock| {
764 let expected_public_key_b64 = expected_public_key_b64.clone();
765 mock.organizations_api
766 .expect_get_public_key()
767 .once()
768 .withf(move |id| id == org_id.to_string())
769 .returning(move |_| {
770 Ok(OrganizationPublicKeyResponseModel {
771 object: None,
772 public_key: Some(expected_public_key_b64.clone()),
773 })
774 });
775 });
776
777 let result = fetch_organization_public_key(&api_client, org_id).await;
778
779 assert!(result.is_ok());
780 let public_key = result.unwrap();
781
782 let expected_public_key = PublicKey::from_der(&SpkiPublicKeyBytes::from(
784 TEST_RSA_PUBLIC_KEY_BYTES.to_vec(),
785 ))
786 .unwrap();
787 assert_eq!(
788 public_key.to_der().unwrap(),
789 expected_public_key.to_der().unwrap()
790 );
791
792 if let ApiClient::Mock(mut mock) = api_client {
793 mock.organizations_api.checkpoint();
794 }
795 }
796
797 #[tokio::test]
798 async fn test_fetch_organization_public_key_network_error() {
799 let org_id = uuid::Uuid::new_v4();
800
801 let api_client = ApiClient::new_mocked(|mock| {
802 mock.organizations_api
803 .expect_get_public_key()
804 .once()
805 .returning(move |_| {
806 Err(serde_json::Error::io(std::io::Error::other("Network error")).into())
807 });
808 });
809
810 let result = fetch_organization_public_key(&api_client, org_id).await;
811
812 assert!(matches!(result, Err(SyncError::Network)));
813
814 if let ApiClient::Mock(mut mock) = api_client {
815 mock.organizations_api.checkpoint();
816 }
817 }
818
819 #[tokio::test]
820 async fn test_sync_orgs_success_multiple_orgs() {
821 let org_id1 = uuid::Uuid::new_v4();
822 let org_id2 = uuid::Uuid::new_v4();
823 let org_id3 = uuid::Uuid::new_v4();
824 let org_name1 = "Organization One".to_string();
825 let org_name2 = "Organization Two".to_string();
826 let org_name3 = "Organization Three".to_string();
827 let expected_public_key_b64 = test_public_key_b64();
828
829 let api_client = ApiClient::new_mocked(|mock| {
830 let org_name1 = org_name1.clone();
831 let org_name2 = org_name2.clone();
832 let org_name3 = org_name3.clone();
833 mock.organizations_api
834 .expect_get_user()
835 .once()
836 .returning(move || {
837 Ok(ProfileOrganizationResponseModelListResponseModel {
838 object: None,
839 data: Some(vec![
840 ProfileOrganizationResponseModel {
841 id: Some(org_id1),
842 name: Some(org_name1.clone()),
843 reset_password_enrolled: Some(true),
844 ..ProfileOrganizationResponseModel::new()
845 },
846 ProfileOrganizationResponseModel {
847 id: Some(org_id2),
848 name: Some(org_name2.clone()),
849 reset_password_enrolled: Some(true),
850 ..ProfileOrganizationResponseModel::new()
851 },
852 ProfileOrganizationResponseModel {
853 id: Some(org_id3),
854 name: Some(org_name3.clone()),
855 reset_password_enrolled: Some(true),
856 ..ProfileOrganizationResponseModel::new()
857 },
858 ]),
859 continuation_token: None,
860 })
861 });
862
863 let expected_public_key_b64 = expected_public_key_b64.clone();
864 mock.organizations_api
865 .expect_get_public_key()
866 .times(3)
867 .returning(move |_| {
868 Ok(OrganizationPublicKeyResponseModel {
869 object: None,
870 public_key: Some(expected_public_key_b64.clone()),
871 })
872 });
873 });
874
875 let result = sync_orgs(&api_client).await;
876 let memberships = result.unwrap();
877
878 assert_eq!(memberships.len(), 3);
879 assert_eq!(memberships[0].organization_id, org_id1);
880 assert_eq!(memberships[0].name, org_name1);
881 assert_eq!(memberships[1].organization_id, org_id2);
882 assert_eq!(memberships[1].name, org_name2);
883 assert_eq!(memberships[2].organization_id, org_id3);
884 assert_eq!(memberships[2].name, org_name3);
885
886 let expected_public_key = PublicKey::from_der(&SpkiPublicKeyBytes::from(
888 TEST_RSA_PUBLIC_KEY_BYTES.to_vec(),
889 ))
890 .unwrap();
891 for membership in &memberships {
892 assert_eq!(
893 membership.public_key.to_der().unwrap(),
894 expected_public_key.to_der().unwrap()
895 );
896 }
897
898 if let ApiClient::Mock(mut mock) = api_client {
899 mock.organizations_api.checkpoint();
900 }
901 }
902
903 #[tokio::test]
904 async fn test_sync_orgs_network_error() {
905 let api_client = ApiClient::new_mocked(|mock| {
906 mock.organizations_api
907 .expect_get_user()
908 .once()
909 .returning(move || {
910 Err(serde_json::Error::io(std::io::Error::other("Network error")).into())
911 });
912
913 mock.organizations_api.expect_get_public_key().never();
914 });
915
916 let result = sync_orgs(&api_client).await;
917
918 assert!(matches!(result, Err(SyncError::Network)));
919
920 if let ApiClient::Mock(mut mock) = api_client {
921 mock.organizations_api.checkpoint();
922 }
923 }
924
925 #[tokio::test]
926 async fn test_sync_orgs_public_key_fetch_fails() {
927 let org_id = uuid::Uuid::new_v4();
928
929 let api_client = ApiClient::new_mocked(|mock| {
930 mock.organizations_api
931 .expect_get_user()
932 .once()
933 .returning(move || {
934 Ok(ProfileOrganizationResponseModelListResponseModel {
935 object: None,
936 data: Some(vec![ProfileOrganizationResponseModel {
937 id: Some(org_id),
938 name: Some("Test Org".to_string()),
939 reset_password_enrolled: Some(true),
940 ..ProfileOrganizationResponseModel::new()
941 }]),
942 continuation_token: None,
943 })
944 });
945
946 mock.organizations_api
947 .expect_get_public_key()
948 .once()
949 .returning(move |_| {
950 Err(serde_json::Error::io(std::io::Error::other("Network error")).into())
951 });
952 });
953
954 let result = sync_orgs(&api_client).await;
955 assert!(matches!(result, Err(SyncError::Network)));
956
957 if let ApiClient::Mock(mut mock) = api_client {
958 mock.organizations_api.checkpoint();
959 }
960 }
961
962 #[tokio::test]
963 async fn test_sync_passkeys_success_multiple_passkeys() {
964 let passkey_id1 = uuid::Uuid::new_v4();
965 let passkey_id2 = uuid::Uuid::new_v4();
966 let passkey_id3 = uuid::Uuid::new_v4();
967
968 let api_client = ApiClient::new_mocked(|mock| {
969 mock.web_authn_api.expect_get().once().returning(move || {
970 Ok(WebAuthnCredentialResponseModelListResponseModel {
971 object: None,
972 data: Some(vec![
973 WebAuthnCredentialResponseModel {
974 id: Some(passkey_id1.to_string()),
975 prf_status: Some(WebAuthnPrfStatus::Enabled),
976 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
977 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
978 ..WebAuthnCredentialResponseModel::new()
979 },
980 WebAuthnCredentialResponseModel {
981 id: Some(passkey_id2.to_string()),
982 prf_status: Some(WebAuthnPrfStatus::Enabled),
983 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
984 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
985 ..WebAuthnCredentialResponseModel::new()
986 },
987 WebAuthnCredentialResponseModel {
988 id: Some(passkey_id3.to_string()),
989 prf_status: Some(WebAuthnPrfStatus::Enabled),
990 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
991 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
992 ..WebAuthnCredentialResponseModel::new()
993 },
994 ]),
995 continuation_token: None,
996 })
997 });
998 });
999
1000 let result = sync_passkeys(&api_client).await;
1001 let passkeys = result.unwrap();
1002
1003 assert_eq!(passkeys.len(), 3);
1004 assert_eq!(passkeys[0].id, passkey_id1);
1005 assert_eq!(passkeys[1].id, passkey_id2);
1006 assert_eq!(passkeys[2].id, passkey_id3);
1007
1008 for passkey in &passkeys {
1010 assert_eq!(
1011 passkey.encrypted_public_key.to_string(),
1012 TEST_ENC_STRING.to_string()
1013 );
1014 assert_eq!(
1015 passkey.encrypted_user_key.to_string(),
1016 TEST_UNSIGNED_SHARED_KEY.to_string()
1017 );
1018 }
1019
1020 if let ApiClient::Mock(mut mock) = api_client {
1021 mock.web_authn_api.checkpoint();
1022 }
1023 }
1024
1025 #[tokio::test]
1026 async fn test_sync_passkeys_filters_passkeys_without_prf_encryption_enabled() {
1027 let enabled_passkey_id = uuid::Uuid::new_v4();
1028 let supported_passkey_id = uuid::Uuid::new_v4();
1029 let unsupported_passkey_id = uuid::Uuid::new_v4();
1030 let no_prf_status_passkey_id = uuid::Uuid::new_v4();
1031
1032 let api_client = ApiClient::new_mocked(|mock| {
1033 mock.web_authn_api.expect_get().once().returning(move || {
1034 Ok(WebAuthnCredentialResponseModelListResponseModel {
1035 object: None,
1036 data: Some(vec![
1037 WebAuthnCredentialResponseModel {
1038 id: Some(enabled_passkey_id.to_string()),
1039 prf_status: Some(WebAuthnPrfStatus::Enabled),
1040 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
1041 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
1042 ..WebAuthnCredentialResponseModel::new()
1043 },
1044 WebAuthnCredentialResponseModel {
1045 id: Some(supported_passkey_id.to_string()),
1046 prf_status: Some(WebAuthnPrfStatus::Supported),
1047 encrypted_user_key: None,
1049 encrypted_public_key: None,
1050 ..WebAuthnCredentialResponseModel::new()
1051 },
1052 WebAuthnCredentialResponseModel {
1053 id: Some(unsupported_passkey_id.to_string()),
1054 prf_status: Some(WebAuthnPrfStatus::Unsupported),
1055 encrypted_user_key: None,
1056 encrypted_public_key: None,
1057 ..WebAuthnCredentialResponseModel::new()
1058 },
1059 WebAuthnCredentialResponseModel {
1060 id: Some(no_prf_status_passkey_id.to_string()),
1061 prf_status: None,
1062 encrypted_user_key: None,
1063 encrypted_public_key: None,
1064 ..WebAuthnCredentialResponseModel::new()
1065 },
1066 ]),
1067 continuation_token: None,
1068 })
1069 });
1070 });
1071
1072 let result = sync_passkeys(&api_client).await;
1073 let passkeys = result.unwrap();
1074
1075 assert_eq!(passkeys.len(), 1);
1077 assert_eq!(passkeys[0].id, enabled_passkey_id);
1078 assert_eq!(
1079 passkeys[0].encrypted_public_key.to_string(),
1080 TEST_ENC_STRING.to_string()
1081 );
1082 assert_eq!(
1083 passkeys[0].encrypted_user_key.to_string(),
1084 TEST_UNSIGNED_SHARED_KEY.to_string()
1085 );
1086
1087 if let ApiClient::Mock(mut mock) = api_client {
1088 mock.web_authn_api.checkpoint();
1089 }
1090 }
1091
1092 #[tokio::test]
1093 async fn test_sync_passkeys_network_error() {
1094 let api_client = ApiClient::new_mocked(|mock| {
1095 mock.web_authn_api.expect_get().once().returning(move || {
1096 Err(serde_json::Error::io(std::io::Error::other("Network error")).into())
1097 });
1098 });
1099
1100 let result = sync_passkeys(&api_client).await;
1101
1102 assert!(matches!(result, Err(SyncError::Network)));
1103
1104 if let ApiClient::Mock(mut mock) = api_client {
1105 mock.web_authn_api.checkpoint();
1106 }
1107 }
1108
1109 #[tokio::test]
1110 async fn test_sync_devices_success_multiple_devices() {
1111 let device_id1 = uuid::Uuid::new_v4();
1112 let device_id2 = uuid::Uuid::new_v4();
1113 let device_id3 = uuid::Uuid::new_v4();
1114 let untrusted_device_id = uuid::Uuid::new_v4();
1115
1116 let api_client = ApiClient::new_mocked(|mock| {
1117 mock.devices_api.expect_get_all().once().returning(move || {
1118 Ok(DeviceAuthRequestResponseModelListResponseModel {
1119 object: None,
1120 data: Some(vec![
1121 DeviceAuthRequestResponseModel {
1122 id: Some(device_id1),
1123 is_trusted: Some(true),
1124 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
1125 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
1126 ..DeviceAuthRequestResponseModel::new()
1127 },
1128 DeviceAuthRequestResponseModel {
1129 id: Some(device_id2),
1130 is_trusted: Some(true),
1131 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
1132 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
1133 ..DeviceAuthRequestResponseModel::new()
1134 },
1135 DeviceAuthRequestResponseModel {
1136 id: Some(untrusted_device_id),
1137 is_trusted: Some(false), encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
1139 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
1140 ..DeviceAuthRequestResponseModel::new()
1141 },
1142 DeviceAuthRequestResponseModel {
1143 id: Some(device_id3),
1144 is_trusted: Some(true),
1145 encrypted_user_key: Some(TEST_UNSIGNED_SHARED_KEY.to_string()),
1146 encrypted_public_key: Some(TEST_ENC_STRING.to_string()),
1147 ..DeviceAuthRequestResponseModel::new()
1148 },
1149 ]),
1150 continuation_token: None,
1151 })
1152 });
1153 });
1154
1155 let result = sync_devices(&api_client).await;
1156 let devices = result.unwrap();
1157
1158 assert_eq!(devices.len(), 3);
1160 assert_eq!(devices[0].id, device_id1);
1162 assert_eq!(devices[1].id, device_id2);
1163 assert_eq!(devices[2].id, device_id3);
1164
1165 for device in &devices {
1167 assert_eq!(
1168 device.encrypted_public_key.to_string(),
1169 TEST_ENC_STRING.to_string()
1170 );
1171 assert_eq!(
1172 device.encrypted_user_key.to_string(),
1173 TEST_UNSIGNED_SHARED_KEY.to_string()
1174 );
1175 }
1176
1177 if let ApiClient::Mock(mut mock) = api_client {
1178 mock.devices_api.checkpoint();
1179 }
1180 }
1181
1182 #[tokio::test]
1183 async fn test_sync_devices_network_error() {
1184 let api_client = ApiClient::new_mocked(|mock| {
1185 mock.devices_api.expect_get_all().once().returning(move || {
1186 Err(serde_json::Error::io(std::io::Error::other("Network error")).into())
1187 });
1188 });
1189
1190 let result = sync_devices(&api_client).await;
1191
1192 assert!(matches!(result, Err(SyncError::Network)));
1193
1194 if let ApiClient::Mock(mut mock) = api_client {
1195 mock.devices_api.checkpoint();
1196 }
1197 }
1198
1199 #[tokio::test]
1200 async fn test_fetch_user_public_key_success() {
1201 let user_id = uuid::Uuid::new_v4();
1202 let expected_public_key_b64 = test_public_key_b64();
1203
1204 let api_client = ApiClient::new_mocked(|mock| {
1205 let expected_public_key_b64 = expected_public_key_b64.clone();
1206 mock.users_api
1207 .expect_get_public_key()
1208 .once()
1209 .withf(move |id| id == &user_id)
1210 .returning(move |_| {
1211 Ok(UserKeyResponseModel {
1212 object: None,
1213 user_id: None,
1214 public_key: Some(expected_public_key_b64.clone()),
1215 })
1216 });
1217 });
1218
1219 let result = fetch_user_public_key(&api_client, user_id).await;
1220 let public_key = result.unwrap();
1221
1222 let expected_public_key = PublicKey::from_der(&SpkiPublicKeyBytes::from(
1224 TEST_RSA_PUBLIC_KEY_BYTES.to_vec(),
1225 ))
1226 .unwrap();
1227 assert_eq!(
1228 public_key.to_der().unwrap(),
1229 expected_public_key.to_der().unwrap()
1230 );
1231
1232 if let ApiClient::Mock(mut mock) = api_client {
1233 mock.users_api.checkpoint();
1234 }
1235 }
1236
1237 #[tokio::test]
1238 async fn test_fetch_user_public_key_network_error() {
1239 let user_id = uuid::Uuid::new_v4();
1240
1241 let api_client = ApiClient::new_mocked(|mock| {
1242 mock.users_api
1243 .expect_get_public_key()
1244 .once()
1245 .returning(move |_| {
1246 Err(serde_json::Error::io(std::io::Error::other("Network error")).into())
1247 });
1248 });
1249
1250 let result = fetch_user_public_key(&api_client, user_id).await;
1251
1252 assert!(matches!(result, Err(SyncError::Network)));
1253
1254 if let ApiClient::Mock(mut mock) = api_client {
1255 mock.users_api.checkpoint();
1256 }
1257 }
1258
1259 #[tokio::test]
1260 async fn test_sync_emergency_access_success_multiple_contacts() {
1261 let ea_id1 = uuid::Uuid::new_v4();
1262 let ea_id2 = uuid::Uuid::new_v4();
1263 let ea_id3 = uuid::Uuid::new_v4();
1264 let grantee_id1 = uuid::Uuid::new_v4();
1265 let grantee_id2 = uuid::Uuid::new_v4();
1266 let grantee_id3 = uuid::Uuid::new_v4();
1267 let ea_name1 = "Contact One".to_string();
1268 let ea_name2 = "Contact Two".to_string();
1269 let ea_name3 = "Contact Three".to_string();
1270 let expected_public_key_b64 = test_public_key_b64();
1271
1272 let api_client = ApiClient::new_mocked(|mock| {
1273 let ea_name1 = ea_name1.clone();
1274 let ea_name2 = ea_name2.clone();
1275 let ea_name3 = ea_name3.clone();
1276 mock.emergency_access_api
1277 .expect_get_contacts()
1278 .once()
1279 .returning(move || {
1280 Ok(
1281 EmergencyAccessGranteeDetailsResponseModelListResponseModel {
1282 object: None,
1283 data: Some(vec![
1284 EmergencyAccessGranteeDetailsResponseModel {
1285 id: Some(ea_id1),
1286 grantee_id: Some(grantee_id1),
1287 name: Some(ea_name1.clone()),
1288 status: Some(EmergencyAccessStatusType::Confirmed),
1289 ..EmergencyAccessGranteeDetailsResponseModel::new()
1290 },
1291 EmergencyAccessGranteeDetailsResponseModel {
1292 id: Some(ea_id2),
1293 grantee_id: Some(grantee_id2),
1294 name: Some(ea_name2.clone()),
1295 status: Some(EmergencyAccessStatusType::RecoveryInitiated),
1296 ..EmergencyAccessGranteeDetailsResponseModel::new()
1297 },
1298 EmergencyAccessGranteeDetailsResponseModel {
1299 id: Some(ea_id3),
1300 grantee_id: Some(grantee_id3),
1301 name: Some(ea_name3.clone()),
1302 status: Some(EmergencyAccessStatusType::RecoveryApproved),
1303 ..EmergencyAccessGranteeDetailsResponseModel::new()
1304 },
1305 ]),
1306 continuation_token: None,
1307 },
1308 )
1309 });
1310
1311 let expected_public_key_b64 = expected_public_key_b64.clone();
1312 mock.users_api
1313 .expect_get_public_key()
1314 .times(3)
1315 .returning(move |_| {
1316 Ok(UserKeyResponseModel {
1317 object: None,
1318 user_id: None,
1319 public_key: Some(expected_public_key_b64.clone()),
1320 })
1321 });
1322 });
1323
1324 let result = sync_emergency_access(&api_client).await;
1325 let memberships = result.unwrap();
1326
1327 assert_eq!(memberships.len(), 3);
1328 assert_eq!(memberships[0].id, ea_id1);
1329 assert_eq!(memberships[0].name, ea_name1);
1330 assert_eq!(memberships[1].id, ea_id2);
1331 assert_eq!(memberships[1].name, ea_name2);
1332 assert_eq!(memberships[2].id, ea_id3);
1333 assert_eq!(memberships[2].name, ea_name3);
1334
1335 let expected_public_key = PublicKey::from_der(&SpkiPublicKeyBytes::from(
1337 TEST_RSA_PUBLIC_KEY_BYTES.to_vec(),
1338 ))
1339 .unwrap();
1340 for membership in &memberships {
1341 assert_eq!(
1342 membership.public_key.to_der().unwrap(),
1343 expected_public_key.to_der().unwrap()
1344 );
1345 }
1346
1347 if let ApiClient::Mock(mut mock) = api_client {
1348 mock.emergency_access_api.checkpoint();
1349 mock.users_api.checkpoint();
1350 }
1351 }
1352
1353 #[tokio::test]
1354 async fn test_sync_emergency_access_network_error() {
1355 let api_client = ApiClient::new_mocked(|mock| {
1356 mock.emergency_access_api
1357 .expect_get_contacts()
1358 .once()
1359 .returning(move || {
1360 Err(serde_json::Error::io(std::io::Error::other("Network error")).into())
1361 });
1362
1363 mock.users_api.expect_get_public_key().never();
1364 });
1365
1366 let result = sync_emergency_access(&api_client).await;
1367
1368 assert!(matches!(result, Err(SyncError::Network)));
1369
1370 if let ApiClient::Mock(mut mock) = api_client {
1371 mock.emergency_access_api.checkpoint();
1372 mock.users_api.checkpoint();
1373 }
1374 }
1375
1376 #[tokio::test]
1377 async fn test_sync_emergency_access_user_key_fetch_fails() {
1378 let ea_id = uuid::Uuid::new_v4();
1379 let grantee_id = uuid::Uuid::new_v4();
1380
1381 let api_client = ApiClient::new_mocked(|mock| {
1382 mock.emergency_access_api
1383 .expect_get_contacts()
1384 .once()
1385 .returning(move || {
1386 Ok(
1387 EmergencyAccessGranteeDetailsResponseModelListResponseModel {
1388 object: None,
1389 data: Some(vec![EmergencyAccessGranteeDetailsResponseModel {
1390 id: Some(ea_id),
1391 grantee_id: Some(grantee_id),
1392 name: Some("Test Contact".to_string()),
1393 status: Some(EmergencyAccessStatusType::Confirmed),
1394 ..EmergencyAccessGranteeDetailsResponseModel::new()
1395 }]),
1396 continuation_token: None,
1397 },
1398 )
1399 });
1400
1401 mock.users_api
1402 .expect_get_public_key()
1403 .once()
1404 .returning(move |_| {
1405 Err(serde_json::Error::io(std::io::Error::other("Network error")).into())
1406 });
1407 });
1408
1409 let result = sync_emergency_access(&api_client).await;
1410 assert!(matches!(result, Err(SyncError::Network)));
1411
1412 if let ApiClient::Mock(mut mock) = api_client {
1413 mock.emergency_access_api.checkpoint();
1414 mock.users_api.checkpoint();
1415 }
1416 }
1417
1418 #[tokio::test]
1419 async fn test_sync_emergency_access_filters_contacts_with_non_allowed_statuses() {
1420 let confirmed_id = uuid::Uuid::new_v4();
1421 let recovery_initiated_id = uuid::Uuid::new_v4();
1422 let recovery_approved_id = uuid::Uuid::new_v4();
1423 let expected_public_key_b64 = test_public_key_b64();
1424
1425 let api_client = ApiClient::new_mocked(|mock| {
1426 mock.emergency_access_api
1427 .expect_get_contacts()
1428 .once()
1429 .returning(move || {
1430 Ok(
1431 EmergencyAccessGranteeDetailsResponseModelListResponseModel {
1432 object: None,
1433 data: Some(vec![
1434 EmergencyAccessGranteeDetailsResponseModel {
1435 id: Some(confirmed_id),
1436 grantee_id: Some(uuid::Uuid::new_v4()),
1437 status: Some(EmergencyAccessStatusType::Confirmed),
1438 ..EmergencyAccessGranteeDetailsResponseModel::new()
1439 },
1440 EmergencyAccessGranteeDetailsResponseModel {
1441 id: Some(recovery_initiated_id),
1442 grantee_id: Some(uuid::Uuid::new_v4()),
1443 status: Some(EmergencyAccessStatusType::RecoveryInitiated),
1444 ..EmergencyAccessGranteeDetailsResponseModel::new()
1445 },
1446 EmergencyAccessGranteeDetailsResponseModel {
1447 id: Some(recovery_approved_id),
1448 grantee_id: Some(uuid::Uuid::new_v4()),
1449 status: Some(EmergencyAccessStatusType::RecoveryApproved),
1450 ..EmergencyAccessGranteeDetailsResponseModel::new()
1451 },
1452 EmergencyAccessGranteeDetailsResponseModel {
1453 id: Some(uuid::Uuid::new_v4()),
1454 grantee_id: Some(uuid::Uuid::new_v4()),
1455 status: Some(EmergencyAccessStatusType::Invited),
1456 ..EmergencyAccessGranteeDetailsResponseModel::new()
1457 },
1458 EmergencyAccessGranteeDetailsResponseModel {
1459 id: Some(uuid::Uuid::new_v4()),
1460 grantee_id: Some(uuid::Uuid::new_v4()),
1461 status: Some(EmergencyAccessStatusType::Accepted),
1462 ..EmergencyAccessGranteeDetailsResponseModel::new()
1463 },
1464 EmergencyAccessGranteeDetailsResponseModel {
1465 id: Some(uuid::Uuid::new_v4()),
1466 grantee_id: Some(uuid::Uuid::new_v4()),
1467 status: None,
1468 ..EmergencyAccessGranteeDetailsResponseModel::new()
1469 },
1470 ]),
1471 continuation_token: None,
1472 },
1473 )
1474 });
1475
1476 let expected_public_key_b64 = expected_public_key_b64.clone();
1477 mock.users_api
1478 .expect_get_public_key()
1479 .times(3)
1481 .returning(move |_| {
1482 Ok(UserKeyResponseModel {
1483 object: None,
1484 user_id: None,
1485 public_key: Some(expected_public_key_b64.clone()),
1486 })
1487 });
1488 });
1489
1490 let result = sync_emergency_access(&api_client).await;
1491 let memberships = result.unwrap();
1492
1493 assert_eq!(memberships.len(), 3);
1495 assert_eq!(memberships[0].id, confirmed_id);
1496 assert_eq!(memberships[1].id, recovery_initiated_id);
1497 assert_eq!(memberships[2].id, recovery_approved_id);
1498
1499 if let ApiClient::Mock(mut mock) = api_client {
1500 mock.emergency_access_api.checkpoint();
1501 mock.users_api.checkpoint();
1502 }
1503 }
1504
1505 #[tokio::test]
1506 async fn test_sync_emergency_access_all_non_allowed_statuses_returns_empty() {
1507 let api_client = ApiClient::new_mocked(|mock| {
1508 mock.emergency_access_api
1509 .expect_get_contacts()
1510 .once()
1511 .returning(move || {
1512 Ok(
1513 EmergencyAccessGranteeDetailsResponseModelListResponseModel {
1514 object: None,
1515 data: Some(vec![
1516 EmergencyAccessGranteeDetailsResponseModel {
1517 id: Some(uuid::Uuid::new_v4()),
1518 grantee_id: Some(uuid::Uuid::new_v4()),
1519 status: Some(EmergencyAccessStatusType::Invited),
1520 ..EmergencyAccessGranteeDetailsResponseModel::new()
1521 },
1522 EmergencyAccessGranteeDetailsResponseModel {
1523 id: Some(uuid::Uuid::new_v4()),
1524 grantee_id: Some(uuid::Uuid::new_v4()),
1525 status: Some(EmergencyAccessStatusType::Accepted),
1526 ..EmergencyAccessGranteeDetailsResponseModel::new()
1527 },
1528 EmergencyAccessGranteeDetailsResponseModel {
1529 id: Some(uuid::Uuid::new_v4()),
1530 grantee_id: Some(uuid::Uuid::new_v4()),
1531 status: None,
1532 ..EmergencyAccessGranteeDetailsResponseModel::new()
1533 },
1534 ]),
1535 continuation_token: None,
1536 },
1537 )
1538 });
1539
1540 mock.users_api.expect_get_public_key().never();
1541 });
1542
1543 let result = sync_emergency_access(&api_client).await;
1544 let memberships = result.unwrap();
1545
1546 assert!(memberships.is_empty());
1547
1548 if let ApiClient::Mock(mut mock) = api_client {
1549 mock.emergency_access_api.checkpoint();
1550 mock.users_api.checkpoint();
1551 }
1552 }
1553
1554 #[tokio::test]
1555 async fn test_sync_orgs_filters_non_enrolled_orgs() {
1556 let org_id_enrolled1 = uuid::Uuid::new_v4();
1557 let org_id_not_enrolled = uuid::Uuid::new_v4();
1558 let org_id_none_enrolled = uuid::Uuid::new_v4();
1559 let org_id_enrolled2 = uuid::Uuid::new_v4();
1560 let expected_public_key_b64 = test_public_key_b64();
1561
1562 let api_client = ApiClient::new_mocked(|mock| {
1563 mock.organizations_api
1564 .expect_get_user()
1565 .once()
1566 .returning(move || {
1567 Ok(ProfileOrganizationResponseModelListResponseModel {
1568 object: None,
1569 data: Some(vec![
1570 ProfileOrganizationResponseModel {
1571 id: Some(org_id_enrolled1),
1572 name: Some("Enrolled Org 1".to_string()),
1573 reset_password_enrolled: Some(true),
1574 ..ProfileOrganizationResponseModel::new()
1575 },
1576 ProfileOrganizationResponseModel {
1577 id: Some(org_id_not_enrolled),
1578 name: Some("Not Enrolled Org".to_string()),
1579 reset_password_enrolled: Some(false),
1580 ..ProfileOrganizationResponseModel::new()
1581 },
1582 ProfileOrganizationResponseModel {
1583 id: Some(org_id_none_enrolled),
1584 name: Some("None Enrolled Org".to_string()),
1585 reset_password_enrolled: None,
1586 ..ProfileOrganizationResponseModel::new()
1587 },
1588 ProfileOrganizationResponseModel {
1589 id: Some(org_id_enrolled2),
1590 name: Some("Enrolled Org 2".to_string()),
1591 reset_password_enrolled: Some(true),
1592 ..ProfileOrganizationResponseModel::new()
1593 },
1594 ]),
1595 continuation_token: None,
1596 })
1597 });
1598
1599 let expected_public_key_b64 = expected_public_key_b64.clone();
1600 mock.organizations_api
1601 .expect_get_public_key()
1602 .times(2)
1603 .returning(move |_| {
1604 Ok(OrganizationPublicKeyResponseModel {
1605 object: None,
1606 public_key: Some(expected_public_key_b64.clone()),
1607 })
1608 });
1609 });
1610
1611 let result = sync_orgs(&api_client).await;
1612 let memberships = result.unwrap();
1613
1614 assert_eq!(memberships.len(), 2);
1615 assert_eq!(memberships[0].organization_id, org_id_enrolled1);
1616 assert_eq!(memberships[0].name, "Enrolled Org 1");
1617 assert_eq!(memberships[1].organization_id, org_id_enrolled2);
1618 assert_eq!(memberships[1].name, "Enrolled Org 2");
1619
1620 if let ApiClient::Mock(mut mock) = api_client {
1621 mock.organizations_api.checkpoint();
1622 }
1623 }
1624
1625 #[tokio::test]
1626 async fn test_sync_orgs_all_not_enrolled_returns_empty() {
1627 let api_client = ApiClient::new_mocked(|mock| {
1628 mock.organizations_api
1629 .expect_get_user()
1630 .once()
1631 .returning(move || {
1632 Ok(ProfileOrganizationResponseModelListResponseModel {
1633 object: None,
1634 data: Some(vec![
1635 ProfileOrganizationResponseModel {
1636 id: Some(uuid::Uuid::new_v4()),
1637 name: Some("Org A".to_string()),
1638 reset_password_enrolled: Some(false),
1639 ..ProfileOrganizationResponseModel::new()
1640 },
1641 ProfileOrganizationResponseModel {
1642 id: Some(uuid::Uuid::new_v4()),
1643 name: Some("Org B".to_string()),
1644 reset_password_enrolled: None,
1645 ..ProfileOrganizationResponseModel::new()
1646 },
1647 ]),
1648 continuation_token: None,
1649 })
1650 });
1651
1652 mock.organizations_api.expect_get_public_key().never();
1653 });
1654
1655 let result = sync_orgs(&api_client).await;
1656 let memberships = result.unwrap();
1657
1658 assert_eq!(memberships.len(), 0);
1659
1660 if let ApiClient::Mock(mut mock) = api_client {
1661 mock.organizations_api.checkpoint();
1662 }
1663 }
1664}