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 setting = bitwarden_test::MemorySetting::create();
333 if let Some(dt) = stored_last_sync {
334 setting.update(dt).await.expect("pre-populate last_sync");
335 }
336 let mut client = test_client(api_client);
337 client.last_sync = Some(setting);
338 client
339 }
340
341 #[tokio::test]
342 async fn test_handlers_execute_in_registration_order() {
343 let client = test_client(bitwarden_api_api::apis::ApiClient::new_mocked(|_| {}));
344 let log = Arc::new(Mutex::new(Vec::new()));
345
346 client.register_sync_handler(Arc::new(TestHandler {
347 name: "first".to_string(),
348 execution_log: log.clone(),
349 should_fail: false,
350 }));
351 client.register_sync_handler(Arc::new(TestHandler {
352 name: "second".to_string(),
353 execution_log: log.clone(),
354 should_fail: false,
355 }));
356 client.register_sync_handler(Arc::new(TestHandler {
357 name: "third".to_string(),
358 execution_log: log.clone(),
359 should_fail: false,
360 }));
361
362 let response = SyncResponseModel::default();
363 client.run_handlers(&response).await.unwrap();
364
365 assert_eq!(
366 *log.lock().unwrap(),
367 vec!["first", "second", "third"],
368 "Handlers should execute in registration order"
369 );
370 }
371
372 #[tokio::test]
373 async fn test_handler_error_stops_subsequent_handlers() {
374 let client = test_client(bitwarden_api_api::apis::ApiClient::new_mocked(|_| {}));
375 let log = Arc::new(Mutex::new(Vec::new()));
376
377 client.register_sync_handler(Arc::new(TestHandler {
378 name: "first".to_string(),
379 execution_log: log.clone(),
380 should_fail: false,
381 }));
382 client.register_sync_handler(Arc::new(TestHandler {
383 name: "second".to_string(),
384 execution_log: log.clone(),
385 should_fail: true,
386 }));
387 client.register_sync_handler(Arc::new(TestHandler {
388 name: "third".to_string(),
389 execution_log: log.clone(),
390 should_fail: false,
391 }));
392
393 let response = SyncResponseModel::default();
394 let result = client.run_handlers(&response).await;
395
396 assert!(result.is_err(), "Should return error when handler fails");
397 assert_eq!(
398 *log.lock().unwrap(),
399 vec!["first", "second"],
400 "Third handler should not execute after second handler fails"
401 );
402 }
403
404 #[tokio::test]
405 async fn test_sync_success_calls_handlers_and_returns_response() {
406 let client = test_client(bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
407 mock.sync_api
408 .expect_get()
409 .returning(|_| Ok(SyncResponseModel::default()));
410 }));
411 let sync_log = Arc::new(Mutex::new(Vec::new()));
412 let error_log = Arc::new(Mutex::new(Vec::new()));
413
414 client.register_sync_handler(Arc::new(TestHandler {
415 name: "handler".to_string(),
416 execution_log: sync_log.clone(),
417 should_fail: false,
418 }));
419 client.register_error_handler(Arc::new(TestErrorHandler {
420 name: "error_handler".to_string(),
421 error_log: error_log.clone(),
422 }));
423
424 let result = client
425 .sync(SyncRequest {
426 force: false,
427 exclude_subdomains: None,
428 })
429 .await;
430
431 assert!(result.is_ok(), "Sync should succeed");
432 assert_eq!(
433 *sync_log.lock().unwrap(),
434 vec!["handler"],
435 "Sync handler should be called on success"
436 );
437 assert!(
438 error_log.lock().unwrap().is_empty(),
439 "Error handlers should not be called on success"
440 );
441 }
442
443 #[tokio::test]
444 async fn test_sync_error_notifies_error_handlers() {
445 let client = test_client(bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
446 mock.sync_api
447 .expect_get()
448 .returning(|_| Err(std::io::Error::other("test error").into()));
449 }));
450 let error_log = Arc::new(Mutex::new(Vec::new()));
451
452 client.register_error_handler(Arc::new(TestErrorHandler {
453 name: "first".to_string(),
454 error_log: error_log.clone(),
455 }));
456 client.register_error_handler(Arc::new(TestErrorHandler {
457 name: "second".to_string(),
458 error_log: error_log.clone(),
459 }));
460
461 let result = client
463 .sync(SyncRequest {
464 force: false,
465 exclude_subdomains: None,
466 })
467 .await;
468
469 assert!(result.is_err());
470 assert_eq!(
471 *error_log.lock().unwrap(),
472 vec!["first", "second"],
473 "All error handlers should be called on sync failure"
474 );
475 }
476
477 #[tokio::test]
478 async fn test_first_sync_skips_revision_check() {
479 let client = test_client_with_last_sync(
481 bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
482 mock.sync_api
483 .expect_get()
484 .returning(|_| Ok(SyncResponseModel::default()));
485 }),
487 None,
488 )
489 .await;
490
491 let result = client
492 .sync(SyncRequest {
493 force: false,
494 exclude_subdomains: None,
495 })
496 .await;
497
498 assert!(result.is_ok_and(|v| v));
499 }
500
501 #[tokio::test]
502 async fn test_revision_check_skips_sync_when_up_to_date() {
503 let stored_last_sync = Utc::now();
504 let server_revision_ms = (stored_last_sync - Duration::seconds(60)).timestamp_millis();
506
507 let sync_log = Arc::new(Mutex::new(Vec::<String>::new()));
508 let sync_log_clone = sync_log.clone();
509
510 let client = test_client_with_last_sync(
511 bitwarden_api_api::apis::ApiClient::new_mocked(move |mock| {
512 mock.accounts_api
513 .expect_get_account_revision_date()
514 .returning(move || Ok(server_revision_ms));
515 }),
517 Some(stored_last_sync),
518 )
519 .await;
520
521 client.register_sync_handler(Arc::new(TestHandler {
522 name: "should_not_run".to_string(),
523 execution_log: sync_log_clone,
524 should_fail: false,
525 }));
526
527 let result = client
528 .sync(SyncRequest {
529 force: false,
530 exclude_subdomains: None,
531 })
532 .await;
533
534 assert!(result.is_ok_and(|v| !v), "Expected Ok(false) skip result");
535 assert!(
536 sync_log.lock().unwrap().is_empty(),
537 "Sync handler must not be called on skip"
538 );
539 }
540
541 #[tokio::test]
542 async fn test_force_bypasses_revision_check() {
543 let stored_last_sync = Utc::now();
544
545 let client = test_client_with_last_sync(
546 bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
547 mock.sync_api
548 .expect_get()
549 .returning(|_| Ok(SyncResponseModel::default()));
550 }),
552 Some(stored_last_sync),
553 )
554 .await;
555
556 let result = client
557 .sync(SyncRequest {
558 force: true,
559 exclude_subdomains: None,
560 })
561 .await;
562
563 assert!(result.is_ok_and(|v| v));
564 }
565
566 #[tokio::test]
567 async fn test_account_deleted_error() {
568 let stored_last_sync = Utc::now();
569 let error_log = Arc::new(Mutex::new(Vec::<String>::new()));
570 let error_log_clone = error_log.clone();
571
572 let client = test_client_with_last_sync(
573 bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
574 mock.accounts_api
575 .expect_get_account_revision_date()
576 .returning(|| Ok(-1i64));
577 }),
578 Some(stored_last_sync),
579 )
580 .await;
581
582 client.register_error_handler(Arc::new(TestErrorHandler {
583 name: "error_handler".to_string(),
584 error_log: error_log_clone,
585 }));
586
587 let result = client
588 .sync(SyncRequest {
589 force: false,
590 exclude_subdomains: None,
591 })
592 .await;
593
594 assert!(
595 matches!(result, Err(SyncError::AccountDeleted)),
596 "Expected AccountDeleted error"
597 );
598 assert_eq!(
599 *error_log.lock().unwrap(),
600 vec!["error_handler"],
601 "Error handler must be called for AccountDeleted"
602 );
603 }
604
605 #[tokio::test]
606 async fn test_revision_fetch_failure_does_not_bump_last_sync() {
607 let stored_last_sync =
608 DateTime::<Utc>::from_timestamp_millis(1_000_000).expect("valid timestamp");
609 let error_log = Arc::new(Mutex::new(Vec::<String>::new()));
610 let error_log_clone = error_log.clone();
611
612 let setting = bitwarden_test::MemorySetting::create();
614 setting
615 .update(stored_last_sync)
616 .await
617 .expect("pre-populate last_sync");
618
619 let mut client = test_client(bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
620 mock.accounts_api
621 .expect_get_account_revision_date()
622 .returning(|| Err(std::io::Error::other("network error").into()));
623 }));
624 client.last_sync = Some(setting.clone());
625
626 client.register_error_handler(Arc::new(TestErrorHandler {
627 name: "error_handler".to_string(),
628 error_log: error_log_clone,
629 }));
630
631 let result = client
632 .sync(SyncRequest {
633 force: false,
634 exclude_subdomains: None,
635 })
636 .await;
637
638 assert!(
639 result.is_err(),
640 "Expected error from revision fetch failure"
641 );
642 assert_eq!(
643 setting.get().await.unwrap(),
644 Some(stored_last_sync),
645 "last_sync must not be bumped on error"
646 );
647 assert!(
648 !error_log.lock().unwrap().is_empty(),
649 "Error handler must be called"
650 );
651 }
652}