1use std::sync::Arc;
2
3use bitwarden_api_api::models::SyncResponseModel;
4use bitwarden_core::{
5 Client,
6 client::{ApiConfigurations, FromClientPart},
7};
8use bitwarden_state::Setting;
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use thiserror::Error;
12use tokio::sync::Mutex;
13
14use crate::{
15 SyncErrorHandler, SyncHandler, SyncHandlerError, registry::HandlerRegistry, state::LAST_SYNC,
16};
17
18#[allow(missing_docs)]
19#[derive(Debug, Error)]
20pub enum SyncError {
21 #[error(transparent)]
22 Api(#[from] bitwarden_core::ApiError),
23
24 #[error("Sync event handler failed: {0}")]
25 HandlerFailed(#[source] SyncHandlerError),
26
27 #[error("Account has been deleted on the server.")]
28 AccountDeleted,
29
30 #[error(transparent)]
31 Setting(#[from] bitwarden_state::SettingsError),
32
33 #[error("Server returned an unrepresentable revision date.")]
34 InvalidRevisionDate,
35}
36
37#[allow(missing_docs)]
38#[derive(Serialize, Deserialize, Debug, Clone)]
39#[serde(rename_all = "camelCase", deny_unknown_fields)]
40pub struct SyncRequest {
41 #[serde(default)]
43 pub force: bool,
44 pub exclude_subdomains: Option<bool>,
46}
47
48pub struct SyncClient {
53 api_configurations: Arc<ApiConfigurations>,
54 sync_handlers: HandlerRegistry<dyn SyncHandler>,
55 error_handlers: HandlerRegistry<dyn SyncErrorHandler>,
56 sync_lock: Mutex<()>,
57 last_sync: Option<Setting<DateTime<Utc>>>,
58}
59
60impl SyncClient {
61 pub fn new(client: Client) -> Self {
63 Self {
64 api_configurations: client.get_part(),
65 sync_handlers: HandlerRegistry::new(),
66 error_handlers: HandlerRegistry::new(),
67 sync_lock: Mutex::new(()),
68 last_sync: client.platform().state().setting(LAST_SYNC).ok(),
69 }
70 }
71
72 pub async fn last_sync(&self) -> Option<DateTime<Utc>> {
74 match self.last_sync.as_ref()?.get().await {
75 Ok(value) => value,
76 Err(e) => {
77 tracing::warn!("Failed to read last sync timestamp: {e}");
78 None
79 }
80 }
81 }
82
83 pub fn register_sync_handler(&self, handler: Arc<dyn SyncHandler>) {
88 self.sync_handlers.register(handler);
89 }
90
91 pub fn register_error_handler(&self, handler: Arc<dyn SyncErrorHandler>) {
97 self.error_handlers.register(handler);
98 }
99
100 pub async fn sync(&self, request: SyncRequest) -> Result<bool, SyncError> {
125 let _guard = self.sync_lock.lock().await;
127
128 let sync_start = Utc::now();
134
135 let needs_sync = if request.force {
136 true
137 } else {
138 match self.needs_sync().await {
139 Ok(needed) => needed,
140 Err(e) => {
141 self.run_error_handlers(&e).await;
142 return Err(e);
143 }
144 }
145 };
146
147 if !needs_sync {
148 self.update_last_sync(sync_start).await;
153 return Ok(false);
154 }
155
156 let result = async {
157 let response = self.perform_sync(&request).await?;
158 self.run_handlers(&response).await?;
159 Ok(response)
160 }
161 .await;
162
163 match result {
164 Ok(_) => {
165 self.update_last_sync(sync_start).await;
166 Ok(true)
167 }
168 Err(error) => {
169 self.run_error_handlers(&error).await;
170 Err(error)
171 }
172 }
173 }
174
175 async fn needs_sync(&self) -> Result<bool, SyncError> {
176 let Some(last_sync_setting) = self.last_sync.as_ref() else {
177 return Ok(true); };
179 let Some(last_sync) = last_sync_setting.get().await? else {
180 return Ok(true); };
182
183 let revision_ms = self
184 .api_configurations
185 .api_client
186 .accounts_api()
187 .get_account_revision_date()
188 .await?;
189
190 if revision_ms < 0 {
191 return Err(SyncError::AccountDeleted);
192 }
193
194 Ok(DateTime::<Utc>::from_timestamp_millis(revision_ms)
195 .ok_or(SyncError::InvalidRevisionDate)?
196 > last_sync)
197 }
198
199 async fn update_last_sync(&self, now: DateTime<Utc>) {
200 if let Some(setting) = self.last_sync.as_ref()
201 && let Err(e) = setting.update(now).await
202 {
203 tracing::warn!("Failed to update last sync timestamp: {e}");
204 }
205 }
206
207 async fn run_handlers(&self, response: &SyncResponseModel) -> Result<(), SyncError> {
215 let handlers = self.sync_handlers.handlers();
216
217 for handler in &handlers {
218 handler
219 .on_sync(response)
220 .await
221 .map_err(SyncError::HandlerFailed)?;
222 }
223
224 for handler in &handlers {
225 handler.on_sync_complete().await;
226 }
227
228 Ok(())
229 }
230
231 async fn run_error_handlers(&self, error: &SyncError) {
235 for handler in &self.error_handlers.handlers() {
236 handler.on_error(error).await;
237 }
238 }
239
240 async fn perform_sync(&self, input: &SyncRequest) -> Result<SyncResponseModel, SyncError> {
242 let sync = self
243 .api_configurations
244 .api_client
245 .sync_api()
246 .get(input.exclude_subdomains)
247 .await?;
248
249 Ok(sync)
250 }
251}
252
253pub trait SyncClientExt {
258 fn sync(&self) -> SyncClient;
260}
261
262impl SyncClientExt for Client {
263 fn sync(&self) -> SyncClient {
264 SyncClient::new(self.clone())
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use std::sync::{Arc, Mutex};
271
272 use chrono::{Duration, Utc};
273
274 use super::*;
275
276 struct TestHandler {
277 name: String,
278 execution_log: Arc<Mutex<Vec<String>>>,
279 should_fail: bool,
280 }
281
282 #[async_trait::async_trait]
283 impl SyncHandler for TestHandler {
284 async fn on_sync(&self, _response: &SyncResponseModel) -> Result<(), SyncHandlerError> {
285 self.execution_log.lock().unwrap().push(self.name.clone());
286 if self.should_fail {
287 Err("Handler failed".into())
288 } else {
289 Ok(())
290 }
291 }
292 }
293
294 struct TestErrorHandler {
295 name: String,
296 error_log: Arc<Mutex<Vec<String>>>,
297 }
298
299 #[async_trait::async_trait]
300 impl SyncErrorHandler for TestErrorHandler {
301 async fn on_error(&self, _error: &SyncError) {
302 self.error_log.lock().unwrap().push(self.name.clone());
303 }
304 }
305
306 fn test_client(api_client: bitwarden_api_api::apis::ApiClient) -> SyncClient {
308 let dummy_config = bitwarden_api_api::Configuration::new(String::new());
309 SyncClient {
310 api_configurations: Arc::new(ApiConfigurations {
311 api_client,
312 identity_client: bitwarden_api_identity::apis::ApiClient::new_mocked(|_| {}),
313 api_config: dummy_config.clone(),
314 identity_config: dummy_config,
315 device_type: bitwarden_core::client::DeviceType::SDK,
316 }),
317 sync_handlers: HandlerRegistry::new(),
318 error_handlers: HandlerRegistry::new(),
319 sync_lock: tokio::sync::Mutex::new(()),
320 last_sync: None,
321 }
322 }
323
324 async fn test_client_with_last_sync(
329 api_client: bitwarden_api_api::apis::ApiClient,
330 stored_last_sync: Option<DateTime<Utc>>,
331 ) -> SyncClient {
332 let repo: Arc<dyn bitwarden_state::repository::Repository<bitwarden_state::SettingItem>> =
333 Arc::new(bitwarden_test::MemoryRepository::<
334 bitwarden_state::SettingItem,
335 >::default());
336 let setting = bitwarden_state::Setting::new(repo, crate::state::LAST_SYNC);
337 if let Some(dt) = stored_last_sync {
338 setting.update(dt).await.expect("pre-populate last_sync");
339 }
340 let mut client = test_client(api_client);
341 client.last_sync = Some(setting);
342 client
343 }
344
345 #[tokio::test]
346 async fn test_handlers_execute_in_registration_order() {
347 let client = test_client(bitwarden_api_api::apis::ApiClient::new_mocked(|_| {}));
348 let log = Arc::new(Mutex::new(Vec::new()));
349
350 client.register_sync_handler(Arc::new(TestHandler {
351 name: "first".to_string(),
352 execution_log: log.clone(),
353 should_fail: false,
354 }));
355 client.register_sync_handler(Arc::new(TestHandler {
356 name: "second".to_string(),
357 execution_log: log.clone(),
358 should_fail: false,
359 }));
360 client.register_sync_handler(Arc::new(TestHandler {
361 name: "third".to_string(),
362 execution_log: log.clone(),
363 should_fail: false,
364 }));
365
366 let response = SyncResponseModel::default();
367 client.run_handlers(&response).await.unwrap();
368
369 assert_eq!(
370 *log.lock().unwrap(),
371 vec!["first", "second", "third"],
372 "Handlers should execute in registration order"
373 );
374 }
375
376 #[tokio::test]
377 async fn test_handler_error_stops_subsequent_handlers() {
378 let client = test_client(bitwarden_api_api::apis::ApiClient::new_mocked(|_| {}));
379 let log = Arc::new(Mutex::new(Vec::new()));
380
381 client.register_sync_handler(Arc::new(TestHandler {
382 name: "first".to_string(),
383 execution_log: log.clone(),
384 should_fail: false,
385 }));
386 client.register_sync_handler(Arc::new(TestHandler {
387 name: "second".to_string(),
388 execution_log: log.clone(),
389 should_fail: true,
390 }));
391 client.register_sync_handler(Arc::new(TestHandler {
392 name: "third".to_string(),
393 execution_log: log.clone(),
394 should_fail: false,
395 }));
396
397 let response = SyncResponseModel::default();
398 let result = client.run_handlers(&response).await;
399
400 assert!(result.is_err(), "Should return error when handler fails");
401 assert_eq!(
402 *log.lock().unwrap(),
403 vec!["first", "second"],
404 "Third handler should not execute after second handler fails"
405 );
406 }
407
408 #[tokio::test]
409 async fn test_sync_success_calls_handlers_and_returns_response() {
410 let client = test_client(bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
411 mock.sync_api
412 .expect_get()
413 .returning(|_| Ok(SyncResponseModel::default()));
414 }));
415 let sync_log = Arc::new(Mutex::new(Vec::new()));
416 let error_log = Arc::new(Mutex::new(Vec::new()));
417
418 client.register_sync_handler(Arc::new(TestHandler {
419 name: "handler".to_string(),
420 execution_log: sync_log.clone(),
421 should_fail: false,
422 }));
423 client.register_error_handler(Arc::new(TestErrorHandler {
424 name: "error_handler".to_string(),
425 error_log: error_log.clone(),
426 }));
427
428 let result = client
429 .sync(SyncRequest {
430 force: false,
431 exclude_subdomains: None,
432 })
433 .await;
434
435 assert!(result.is_ok(), "Sync should succeed");
436 assert_eq!(
437 *sync_log.lock().unwrap(),
438 vec!["handler"],
439 "Sync handler should be called on success"
440 );
441 assert!(
442 error_log.lock().unwrap().is_empty(),
443 "Error handlers should not be called on success"
444 );
445 }
446
447 #[tokio::test]
448 async fn test_sync_error_notifies_error_handlers() {
449 let client = test_client(bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
450 mock.sync_api
451 .expect_get()
452 .returning(|_| Err(std::io::Error::other("test error").into()));
453 }));
454 let error_log = Arc::new(Mutex::new(Vec::new()));
455
456 client.register_error_handler(Arc::new(TestErrorHandler {
457 name: "first".to_string(),
458 error_log: error_log.clone(),
459 }));
460 client.register_error_handler(Arc::new(TestErrorHandler {
461 name: "second".to_string(),
462 error_log: error_log.clone(),
463 }));
464
465 let result = client
467 .sync(SyncRequest {
468 force: false,
469 exclude_subdomains: None,
470 })
471 .await;
472
473 assert!(result.is_err());
474 assert_eq!(
475 *error_log.lock().unwrap(),
476 vec!["first", "second"],
477 "All error handlers should be called on sync failure"
478 );
479 }
480
481 #[tokio::test]
482 async fn test_first_sync_skips_revision_check() {
483 let client = test_client_with_last_sync(
485 bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
486 mock.sync_api
487 .expect_get()
488 .returning(|_| Ok(SyncResponseModel::default()));
489 }),
491 None,
492 )
493 .await;
494
495 let result = client
496 .sync(SyncRequest {
497 force: false,
498 exclude_subdomains: None,
499 })
500 .await;
501
502 assert!(result.is_ok_and(|v| v));
503 }
504
505 #[tokio::test]
506 async fn test_revision_check_skips_sync_when_up_to_date() {
507 let stored_last_sync = Utc::now();
508 let server_revision_ms = (stored_last_sync - Duration::seconds(60)).timestamp_millis();
510
511 let sync_log = Arc::new(Mutex::new(Vec::<String>::new()));
512 let sync_log_clone = sync_log.clone();
513
514 let client = test_client_with_last_sync(
515 bitwarden_api_api::apis::ApiClient::new_mocked(move |mock| {
516 mock.accounts_api
517 .expect_get_account_revision_date()
518 .returning(move || Ok(server_revision_ms));
519 }),
521 Some(stored_last_sync),
522 )
523 .await;
524
525 client.register_sync_handler(Arc::new(TestHandler {
526 name: "should_not_run".to_string(),
527 execution_log: sync_log_clone,
528 should_fail: false,
529 }));
530
531 let result = client
532 .sync(SyncRequest {
533 force: false,
534 exclude_subdomains: None,
535 })
536 .await;
537
538 assert!(result.is_ok_and(|v| !v), "Expected Ok(false) skip result");
539 assert!(
540 sync_log.lock().unwrap().is_empty(),
541 "Sync handler must not be called on skip"
542 );
543 }
544
545 #[tokio::test]
546 async fn test_force_bypasses_revision_check() {
547 let stored_last_sync = Utc::now();
548
549 let client = test_client_with_last_sync(
550 bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
551 mock.sync_api
552 .expect_get()
553 .returning(|_| Ok(SyncResponseModel::default()));
554 }),
556 Some(stored_last_sync),
557 )
558 .await;
559
560 let result = client
561 .sync(SyncRequest {
562 force: true,
563 exclude_subdomains: None,
564 })
565 .await;
566
567 assert!(result.is_ok_and(|v| v));
568 }
569
570 #[tokio::test]
571 async fn test_account_deleted_error() {
572 let stored_last_sync = Utc::now();
573 let error_log = Arc::new(Mutex::new(Vec::<String>::new()));
574 let error_log_clone = error_log.clone();
575
576 let client = test_client_with_last_sync(
577 bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
578 mock.accounts_api
579 .expect_get_account_revision_date()
580 .returning(|| Ok(-1i64));
581 }),
582 Some(stored_last_sync),
583 )
584 .await;
585
586 client.register_error_handler(Arc::new(TestErrorHandler {
587 name: "error_handler".to_string(),
588 error_log: error_log_clone,
589 }));
590
591 let result = client
592 .sync(SyncRequest {
593 force: false,
594 exclude_subdomains: None,
595 })
596 .await;
597
598 assert!(
599 matches!(result, Err(SyncError::AccountDeleted)),
600 "Expected AccountDeleted error"
601 );
602 assert_eq!(
603 *error_log.lock().unwrap(),
604 vec!["error_handler"],
605 "Error handler must be called for AccountDeleted"
606 );
607 }
608
609 #[tokio::test]
610 async fn test_revision_fetch_failure_does_not_bump_last_sync() {
611 let stored_last_sync =
612 DateTime::<Utc>::from_timestamp_millis(1_000_000).expect("valid timestamp");
613 let error_log = Arc::new(Mutex::new(Vec::<String>::new()));
614 let error_log_clone = error_log.clone();
615
616 let repo: Arc<dyn bitwarden_state::repository::Repository<bitwarden_state::SettingItem>> =
618 Arc::new(bitwarden_test::MemoryRepository::<
619 bitwarden_state::SettingItem,
620 >::default());
621 let setting = bitwarden_state::Setting::new(repo, crate::state::LAST_SYNC);
622 setting
623 .update(stored_last_sync)
624 .await
625 .expect("pre-populate last_sync");
626
627 let mut client = test_client(bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
628 mock.accounts_api
629 .expect_get_account_revision_date()
630 .returning(|| Err(std::io::Error::other("network error").into()));
631 }));
632 client.last_sync = Some(setting.clone());
633
634 client.register_error_handler(Arc::new(TestErrorHandler {
635 name: "error_handler".to_string(),
636 error_log: error_log_clone,
637 }));
638
639 let result = client
640 .sync(SyncRequest {
641 force: false,
642 exclude_subdomains: None,
643 })
644 .await;
645
646 assert!(
647 result.is_err(),
648 "Expected error from revision fetch failure"
649 );
650 assert_eq!(
651 setting.get().await.unwrap(),
652 Some(stored_last_sync),
653 "last_sync must not be bumped on error"
654 );
655 assert!(
656 !error_log.lock().unwrap().is_empty(),
657 "Error handler must be called"
658 );
659 }
660}