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 .map_err(|e| SyncError::Api(e.into()))?;
190
191 if revision_ms < 0 {
192 return Err(SyncError::AccountDeleted);
193 }
194
195 Ok(DateTime::<Utc>::from_timestamp_millis(revision_ms)
196 .ok_or(SyncError::InvalidRevisionDate)?
197 > last_sync)
198 }
199
200 async fn update_last_sync(&self, now: DateTime<Utc>) {
201 if let Some(setting) = self.last_sync.as_ref()
202 && let Err(e) = setting.update(now).await
203 {
204 tracing::warn!("Failed to update last sync timestamp: {e}");
205 }
206 }
207
208 async fn run_handlers(&self, response: &SyncResponseModel) -> Result<(), SyncError> {
216 let handlers = self.sync_handlers.handlers();
217
218 for handler in &handlers {
219 handler
220 .on_sync(response)
221 .await
222 .map_err(SyncError::HandlerFailed)?;
223 }
224
225 for handler in &handlers {
226 handler.on_sync_complete().await;
227 }
228
229 Ok(())
230 }
231
232 async fn run_error_handlers(&self, error: &SyncError) {
236 for handler in &self.error_handlers.handlers() {
237 handler.on_error(error).await;
238 }
239 }
240
241 async fn perform_sync(&self, input: &SyncRequest) -> Result<SyncResponseModel, SyncError> {
243 let sync = self
244 .api_configurations
245 .api_client
246 .sync_api()
247 .get(input.exclude_subdomains)
248 .await
249 .map_err(|e| SyncError::Api(e.into()))?;
250
251 Ok(sync)
252 }
253}
254
255pub trait SyncClientExt {
260 fn sync(&self) -> SyncClient;
262}
263
264impl SyncClientExt for Client {
265 fn sync(&self) -> SyncClient {
266 SyncClient::new(self.clone())
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use std::sync::{Arc, Mutex};
273
274 use chrono::{Duration, Utc};
275
276 use super::*;
277
278 struct TestHandler {
279 name: String,
280 execution_log: Arc<Mutex<Vec<String>>>,
281 should_fail: bool,
282 }
283
284 #[async_trait::async_trait]
285 impl SyncHandler for TestHandler {
286 async fn on_sync(&self, _response: &SyncResponseModel) -> Result<(), SyncHandlerError> {
287 self.execution_log.lock().unwrap().push(self.name.clone());
288 if self.should_fail {
289 Err("Handler failed".into())
290 } else {
291 Ok(())
292 }
293 }
294 }
295
296 struct TestErrorHandler {
297 name: String,
298 error_log: Arc<Mutex<Vec<String>>>,
299 }
300
301 #[async_trait::async_trait]
302 impl SyncErrorHandler for TestErrorHandler {
303 async fn on_error(&self, _error: &SyncError) {
304 self.error_log.lock().unwrap().push(self.name.clone());
305 }
306 }
307
308 fn test_client(api_client: bitwarden_api_api::apis::ApiClient) -> SyncClient {
310 let dummy_config = bitwarden_api_api::Configuration::new(String::new());
311 SyncClient {
312 api_configurations: Arc::new(ApiConfigurations {
313 api_client,
314 identity_client: bitwarden_api_identity::apis::ApiClient::new_mocked(|_| {}),
315 api_config: dummy_config.clone(),
316 identity_config: dummy_config,
317 device_type: bitwarden_core::client::DeviceType::SDK,
318 }),
319 sync_handlers: HandlerRegistry::new(),
320 error_handlers: HandlerRegistry::new(),
321 sync_lock: tokio::sync::Mutex::new(()),
322 last_sync: None,
323 }
324 }
325
326 async fn test_client_with_last_sync(
331 api_client: bitwarden_api_api::apis::ApiClient,
332 stored_last_sync: Option<DateTime<Utc>>,
333 ) -> SyncClient {
334 let repo: Arc<dyn bitwarden_state::repository::Repository<bitwarden_state::SettingItem>> =
335 Arc::new(bitwarden_test::MemoryRepository::<
336 bitwarden_state::SettingItem,
337 >::default());
338 let setting = bitwarden_state::Setting::new(repo, crate::state::LAST_SYNC);
339 if let Some(dt) = stored_last_sync {
340 setting.update(dt).await.expect("pre-populate last_sync");
341 }
342 let mut client = test_client(api_client);
343 client.last_sync = Some(setting);
344 client
345 }
346
347 #[tokio::test]
348 async fn test_handlers_execute_in_registration_order() {
349 let client = test_client(bitwarden_api_api::apis::ApiClient::new_mocked(|_| {}));
350 let log = Arc::new(Mutex::new(Vec::new()));
351
352 client.register_sync_handler(Arc::new(TestHandler {
353 name: "first".to_string(),
354 execution_log: log.clone(),
355 should_fail: false,
356 }));
357 client.register_sync_handler(Arc::new(TestHandler {
358 name: "second".to_string(),
359 execution_log: log.clone(),
360 should_fail: false,
361 }));
362 client.register_sync_handler(Arc::new(TestHandler {
363 name: "third".to_string(),
364 execution_log: log.clone(),
365 should_fail: false,
366 }));
367
368 let response = SyncResponseModel::default();
369 client.run_handlers(&response).await.unwrap();
370
371 assert_eq!(
372 *log.lock().unwrap(),
373 vec!["first", "second", "third"],
374 "Handlers should execute in registration order"
375 );
376 }
377
378 #[tokio::test]
379 async fn test_handler_error_stops_subsequent_handlers() {
380 let client = test_client(bitwarden_api_api::apis::ApiClient::new_mocked(|_| {}));
381 let log = Arc::new(Mutex::new(Vec::new()));
382
383 client.register_sync_handler(Arc::new(TestHandler {
384 name: "first".to_string(),
385 execution_log: log.clone(),
386 should_fail: false,
387 }));
388 client.register_sync_handler(Arc::new(TestHandler {
389 name: "second".to_string(),
390 execution_log: log.clone(),
391 should_fail: true,
392 }));
393 client.register_sync_handler(Arc::new(TestHandler {
394 name: "third".to_string(),
395 execution_log: log.clone(),
396 should_fail: false,
397 }));
398
399 let response = SyncResponseModel::default();
400 let result = client.run_handlers(&response).await;
401
402 assert!(result.is_err(), "Should return error when handler fails");
403 assert_eq!(
404 *log.lock().unwrap(),
405 vec!["first", "second"],
406 "Third handler should not execute after second handler fails"
407 );
408 }
409
410 #[tokio::test]
411 async fn test_sync_success_calls_handlers_and_returns_response() {
412 let client = test_client(bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
413 mock.sync_api
414 .expect_get()
415 .returning(|_| Ok(SyncResponseModel::default()));
416 }));
417 let sync_log = Arc::new(Mutex::new(Vec::new()));
418 let error_log = Arc::new(Mutex::new(Vec::new()));
419
420 client.register_sync_handler(Arc::new(TestHandler {
421 name: "handler".to_string(),
422 execution_log: sync_log.clone(),
423 should_fail: false,
424 }));
425 client.register_error_handler(Arc::new(TestErrorHandler {
426 name: "error_handler".to_string(),
427 error_log: error_log.clone(),
428 }));
429
430 let result = client
431 .sync(SyncRequest {
432 force: false,
433 exclude_subdomains: None,
434 })
435 .await;
436
437 assert!(result.is_ok(), "Sync should succeed");
438 assert_eq!(
439 *sync_log.lock().unwrap(),
440 vec!["handler"],
441 "Sync handler should be called on success"
442 );
443 assert!(
444 error_log.lock().unwrap().is_empty(),
445 "Error handlers should not be called on success"
446 );
447 }
448
449 #[tokio::test]
450 async fn test_sync_error_notifies_error_handlers() {
451 let client = test_client(bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
452 mock.sync_api
453 .expect_get()
454 .returning(|_| Err(std::io::Error::other("test error").into()));
455 }));
456 let error_log = Arc::new(Mutex::new(Vec::new()));
457
458 client.register_error_handler(Arc::new(TestErrorHandler {
459 name: "first".to_string(),
460 error_log: error_log.clone(),
461 }));
462 client.register_error_handler(Arc::new(TestErrorHandler {
463 name: "second".to_string(),
464 error_log: error_log.clone(),
465 }));
466
467 let result = client
469 .sync(SyncRequest {
470 force: false,
471 exclude_subdomains: None,
472 })
473 .await;
474
475 assert!(result.is_err());
476 assert_eq!(
477 *error_log.lock().unwrap(),
478 vec!["first", "second"],
479 "All error handlers should be called on sync failure"
480 );
481 }
482
483 #[tokio::test]
484 async fn test_first_sync_skips_revision_check() {
485 let client = test_client_with_last_sync(
487 bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
488 mock.sync_api
489 .expect_get()
490 .returning(|_| Ok(SyncResponseModel::default()));
491 }),
493 None,
494 )
495 .await;
496
497 let result = client
498 .sync(SyncRequest {
499 force: false,
500 exclude_subdomains: None,
501 })
502 .await;
503
504 assert!(result.is_ok_and(|v| v));
505 }
506
507 #[tokio::test]
508 async fn test_revision_check_skips_sync_when_up_to_date() {
509 let stored_last_sync = Utc::now();
510 let server_revision_ms = (stored_last_sync - Duration::seconds(60)).timestamp_millis();
512
513 let sync_log = Arc::new(Mutex::new(Vec::<String>::new()));
514 let sync_log_clone = sync_log.clone();
515
516 let client = test_client_with_last_sync(
517 bitwarden_api_api::apis::ApiClient::new_mocked(move |mock| {
518 mock.accounts_api
519 .expect_get_account_revision_date()
520 .returning(move || Ok(server_revision_ms));
521 }),
523 Some(stored_last_sync),
524 )
525 .await;
526
527 client.register_sync_handler(Arc::new(TestHandler {
528 name: "should_not_run".to_string(),
529 execution_log: sync_log_clone,
530 should_fail: false,
531 }));
532
533 let result = client
534 .sync(SyncRequest {
535 force: false,
536 exclude_subdomains: None,
537 })
538 .await;
539
540 assert!(result.is_ok_and(|v| !v), "Expected Ok(false) skip result");
541 assert!(
542 sync_log.lock().unwrap().is_empty(),
543 "Sync handler must not be called on skip"
544 );
545 }
546
547 #[tokio::test]
548 async fn test_force_bypasses_revision_check() {
549 let stored_last_sync = Utc::now();
550
551 let client = test_client_with_last_sync(
552 bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
553 mock.sync_api
554 .expect_get()
555 .returning(|_| Ok(SyncResponseModel::default()));
556 }),
558 Some(stored_last_sync),
559 )
560 .await;
561
562 let result = client
563 .sync(SyncRequest {
564 force: true,
565 exclude_subdomains: None,
566 })
567 .await;
568
569 assert!(result.is_ok_and(|v| v));
570 }
571
572 #[tokio::test]
573 async fn test_account_deleted_error() {
574 let stored_last_sync = Utc::now();
575 let error_log = Arc::new(Mutex::new(Vec::<String>::new()));
576 let error_log_clone = error_log.clone();
577
578 let client = test_client_with_last_sync(
579 bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
580 mock.accounts_api
581 .expect_get_account_revision_date()
582 .returning(|| Ok(-1i64));
583 }),
584 Some(stored_last_sync),
585 )
586 .await;
587
588 client.register_error_handler(Arc::new(TestErrorHandler {
589 name: "error_handler".to_string(),
590 error_log: error_log_clone,
591 }));
592
593 let result = client
594 .sync(SyncRequest {
595 force: false,
596 exclude_subdomains: None,
597 })
598 .await;
599
600 assert!(
601 matches!(result, Err(SyncError::AccountDeleted)),
602 "Expected AccountDeleted error"
603 );
604 assert_eq!(
605 *error_log.lock().unwrap(),
606 vec!["error_handler"],
607 "Error handler must be called for AccountDeleted"
608 );
609 }
610
611 #[tokio::test]
612 async fn test_revision_fetch_failure_does_not_bump_last_sync() {
613 let stored_last_sync =
614 DateTime::<Utc>::from_timestamp_millis(1_000_000).expect("valid timestamp");
615 let error_log = Arc::new(Mutex::new(Vec::<String>::new()));
616 let error_log_clone = error_log.clone();
617
618 let repo: Arc<dyn bitwarden_state::repository::Repository<bitwarden_state::SettingItem>> =
620 Arc::new(bitwarden_test::MemoryRepository::<
621 bitwarden_state::SettingItem,
622 >::default());
623 let setting = bitwarden_state::Setting::new(repo, crate::state::LAST_SYNC);
624 setting
625 .update(stored_last_sync)
626 .await
627 .expect("pre-populate last_sync");
628
629 let mut client = test_client(bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
630 mock.accounts_api
631 .expect_get_account_revision_date()
632 .returning(|| Err(std::io::Error::other("network error").into()));
633 }));
634 client.last_sync = Some(setting.clone());
635
636 client.register_error_handler(Arc::new(TestErrorHandler {
637 name: "error_handler".to_string(),
638 error_log: error_log_clone,
639 }));
640
641 let result = client
642 .sync(SyncRequest {
643 force: false,
644 exclude_subdomains: None,
645 })
646 .await;
647
648 assert!(
649 result.is_err(),
650 "Expected error from revision fetch failure"
651 );
652 assert_eq!(
653 setting.get().await.unwrap(),
654 Some(stored_last_sync),
655 "last_sync must not be bumped on error"
656 );
657 assert!(
658 !error_log.lock().unwrap().is_empty(),
659 "Error handler must be called"
660 );
661 }
662}