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