Skip to main content

bitwarden_sync/
sync_client.rs

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
31#[allow(missing_docs)]
32#[derive(Serialize, Deserialize, Debug, Clone)]
33#[serde(rename_all = "camelCase", deny_unknown_fields)]
34pub struct SyncRequest {
35    /// Skip the revision-date check and always sync.
36    #[serde(default)]
37    pub force: bool,
38    /// Exclude the subdomains from the response, defaults to false
39    pub exclude_subdomains: Option<bool>,
40}
41
42/// Client for performing sync operations with event support
43///
44/// This client wraps the core sync functionality and provides hooks
45/// for registering event handlers that can respond to sync operations.
46pub struct SyncClient {
47    api_configurations: Arc<ApiConfigurations>,
48    sync_handlers: HandlerRegistry<dyn SyncHandler>,
49    error_handlers: HandlerRegistry<dyn SyncErrorHandler>,
50    sync_lock: Mutex<()>,
51    last_sync: Option<Setting<DateTime<Utc>>>,
52}
53
54impl SyncClient {
55    /// Create a new SyncClient from a Bitwarden client
56    pub fn new(client: Client) -> Self {
57        Self {
58            api_configurations: client.get_part(),
59            sync_handlers: HandlerRegistry::new(),
60            error_handlers: HandlerRegistry::new(),
61            sync_lock: Mutex::new(()),
62            last_sync: client.platform().state().setting(LAST_SYNC).ok(),
63        }
64    }
65
66    /// Get the timestamp of the last successful sync, if any.
67    pub async fn last_sync(&self) -> Option<DateTime<Utc>> {
68        match self.last_sync.as_ref()?.get().await {
69            Ok(value) => value,
70            Err(e) => {
71                tracing::warn!("Failed to read last sync timestamp: {e}");
72                None
73            }
74        }
75    }
76
77    /// Register a sync handler for sync operations
78    ///
79    /// Handlers are called in registration order. If any handler returns an error,
80    /// the sync operation is aborted immediately and subsequent handlers are not called.
81    pub fn register_sync_handler(&self, handler: Arc<dyn SyncHandler>) {
82        self.sync_handlers.register(handler);
83    }
84
85    /// Register an error handler for sync operations
86    ///
87    /// Error handlers are called when any error occurs during sync, including
88    /// API errors and handler errors. All error handlers are always called
89    /// regardless of individual failures.
90    pub fn register_error_handler(&self, handler: Arc<dyn SyncErrorHandler>) {
91        self.error_handlers.register(handler);
92    }
93
94    /// Perform a full sync operation
95    ///
96    /// This method:
97    /// 1. Performs the sync with the Bitwarden API
98    /// 2. On success, dispatches `on_sync` with the sync response to all registered handlers
99    /// 3. Dispatches `on_sync_complete` to all handlers for post-processing
100    /// 4. On error (from API or handlers), notifies all registered error handlers
101    ///
102    /// Handlers receive the raw API models and are responsible for converting to domain types
103    /// as needed. This allows each handler to decide how to process the data.
104    ///
105    /// If any error occurs, all registered error handlers are notified before
106    /// the error is returned to the caller.
107    pub async fn sync(&self, request: SyncRequest) -> Result<SyncResponseModel, SyncError> {
108        // Wait for any in-progress sync to complete before starting a new one
109        let _guard = self.sync_lock.lock().await;
110
111        let result = async {
112            let response = self.perform_sync(&request).await?;
113            self.run_handlers(&response).await?;
114            Ok(response)
115        }
116        .await;
117
118        if let Err(ref error) = result {
119            self.run_error_handlers(error).await;
120        }
121
122        result
123    }
124
125    /// Run sync handlers for a completed sync operation
126    ///
127    /// Executes two phases sequentially:
128    /// 1. Calls [`SyncHandler::on_sync`] on all handlers with the response
129    /// 2. Calls [`SyncHandler::on_sync_complete`] on all handlers
130    ///
131    /// Stops on first error and returns it immediately.
132    async fn run_handlers(&self, response: &SyncResponseModel) -> Result<(), SyncError> {
133        let handlers = self.sync_handlers.handlers();
134
135        for handler in &handlers {
136            handler
137                .on_sync(response)
138                .await
139                .map_err(SyncError::HandlerFailed)?;
140        }
141
142        for handler in &handlers {
143            handler.on_sync_complete().await;
144        }
145
146        Ok(())
147    }
148
149    /// Run all error handlers for a sync error
150    ///
151    /// All error handlers are called sequentially in registration order.
152    async fn run_error_handlers(&self, error: &SyncError) {
153        for handler in &self.error_handlers.handlers() {
154            handler.on_error(error).await;
155        }
156    }
157
158    /// Performs the actual sync operation with the Bitwarden API
159    async fn perform_sync(&self, input: &SyncRequest) -> Result<SyncResponseModel, SyncError> {
160        let sync = self
161            .api_configurations
162            .api_client
163            .sync_api()
164            .get(input.exclude_subdomains)
165            .await
166            .map_err(|e| SyncError::Api(e.into()))?;
167
168        Ok(sync)
169    }
170}
171
172/// Extension trait to add sync() method to Client
173///
174/// This trait provides a convenient way to create a SyncClient from
175/// a Bitwarden Client instance.
176pub trait SyncClientExt {
177    /// Create a new SyncClient for this client
178    fn sync(&self) -> SyncClient;
179}
180
181impl SyncClientExt for Client {
182    fn sync(&self) -> SyncClient {
183        SyncClient::new(self.clone())
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use std::sync::{Arc, Mutex};
190
191    use super::*;
192
193    struct TestHandler {
194        name: String,
195        execution_log: Arc<Mutex<Vec<String>>>,
196        should_fail: bool,
197    }
198
199    #[async_trait::async_trait]
200    impl SyncHandler for TestHandler {
201        async fn on_sync(&self, _response: &SyncResponseModel) -> Result<(), SyncHandlerError> {
202            self.execution_log.lock().unwrap().push(self.name.clone());
203            if self.should_fail {
204                Err("Handler failed".into())
205            } else {
206                Ok(())
207            }
208        }
209    }
210
211    struct TestErrorHandler {
212        name: String,
213        error_log: Arc<Mutex<Vec<String>>>,
214    }
215
216    #[async_trait::async_trait]
217    impl SyncErrorHandler for TestErrorHandler {
218        async fn on_error(&self, _error: &SyncError) {
219            self.error_log.lock().unwrap().push(self.name.clone());
220        }
221    }
222
223    /// Helper to create a SyncClient with a mocked API client.
224    fn test_client(api_client: bitwarden_api_api::apis::ApiClient) -> SyncClient {
225        let dummy_config = bitwarden_api_api::Configuration::new(String::new());
226        SyncClient {
227            api_configurations: Arc::new(ApiConfigurations {
228                api_client,
229                identity_client: bitwarden_api_identity::apis::ApiClient::new_mocked(|_| {}),
230                api_config: dummy_config.clone(),
231                identity_config: dummy_config,
232                device_type: bitwarden_core::client::DeviceType::SDK,
233            }),
234            sync_handlers: HandlerRegistry::new(),
235            error_handlers: HandlerRegistry::new(),
236            sync_lock: tokio::sync::Mutex::new(()),
237            last_sync: None,
238        }
239    }
240
241    #[tokio::test]
242    async fn test_handlers_execute_in_registration_order() {
243        let client = test_client(bitwarden_api_api::apis::ApiClient::new_mocked(|_| {}));
244        let log = Arc::new(Mutex::new(Vec::new()));
245
246        client.register_sync_handler(Arc::new(TestHandler {
247            name: "first".to_string(),
248            execution_log: log.clone(),
249            should_fail: false,
250        }));
251        client.register_sync_handler(Arc::new(TestHandler {
252            name: "second".to_string(),
253            execution_log: log.clone(),
254            should_fail: false,
255        }));
256        client.register_sync_handler(Arc::new(TestHandler {
257            name: "third".to_string(),
258            execution_log: log.clone(),
259            should_fail: false,
260        }));
261
262        let response = SyncResponseModel::default();
263        client.run_handlers(&response).await.unwrap();
264
265        assert_eq!(
266            *log.lock().unwrap(),
267            vec!["first", "second", "third"],
268            "Handlers should execute in registration order"
269        );
270    }
271
272    #[tokio::test]
273    async fn test_handler_error_stops_subsequent_handlers() {
274        let client = test_client(bitwarden_api_api::apis::ApiClient::new_mocked(|_| {}));
275        let log = Arc::new(Mutex::new(Vec::new()));
276
277        client.register_sync_handler(Arc::new(TestHandler {
278            name: "first".to_string(),
279            execution_log: log.clone(),
280            should_fail: false,
281        }));
282        client.register_sync_handler(Arc::new(TestHandler {
283            name: "second".to_string(),
284            execution_log: log.clone(),
285            should_fail: true,
286        }));
287        client.register_sync_handler(Arc::new(TestHandler {
288            name: "third".to_string(),
289            execution_log: log.clone(),
290            should_fail: false,
291        }));
292
293        let response = SyncResponseModel::default();
294        let result = client.run_handlers(&response).await;
295
296        assert!(result.is_err(), "Should return error when handler fails");
297        assert_eq!(
298            *log.lock().unwrap(),
299            vec!["first", "second"],
300            "Third handler should not execute after second handler fails"
301        );
302    }
303
304    #[tokio::test]
305    async fn test_sync_success_calls_handlers_and_returns_response() {
306        let client = test_client(bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
307            mock.sync_api
308                .expect_get()
309                .returning(|_| Ok(SyncResponseModel::default()));
310        }));
311        let sync_log = Arc::new(Mutex::new(Vec::new()));
312        let error_log = Arc::new(Mutex::new(Vec::new()));
313
314        client.register_sync_handler(Arc::new(TestHandler {
315            name: "handler".to_string(),
316            execution_log: sync_log.clone(),
317            should_fail: false,
318        }));
319        client.register_error_handler(Arc::new(TestErrorHandler {
320            name: "error_handler".to_string(),
321            error_log: error_log.clone(),
322        }));
323
324        let result = client
325            .sync(SyncRequest {
326                force: false,
327                exclude_subdomains: None,
328            })
329            .await;
330
331        assert!(result.is_ok(), "Sync should succeed");
332        assert_eq!(
333            *sync_log.lock().unwrap(),
334            vec!["handler"],
335            "Sync handler should be called on success"
336        );
337        assert!(
338            error_log.lock().unwrap().is_empty(),
339            "Error handlers should not be called on success"
340        );
341    }
342
343    #[tokio::test]
344    async fn test_sync_error_notifies_error_handlers() {
345        let client = test_client(bitwarden_api_api::apis::ApiClient::new_mocked(|mock| {
346            mock.sync_api
347                .expect_get()
348                .returning(|_| Err(std::io::Error::other("test error").into()));
349        }));
350        let error_log = Arc::new(Mutex::new(Vec::new()));
351
352        client.register_error_handler(Arc::new(TestErrorHandler {
353            name: "first".to_string(),
354            error_log: error_log.clone(),
355        }));
356        client.register_error_handler(Arc::new(TestErrorHandler {
357            name: "second".to_string(),
358            error_log: error_log.clone(),
359        }));
360
361        // sync() will fail due to the mocked error, which should trigger all error handlers
362        let result = client
363            .sync(SyncRequest {
364                force: false,
365                exclude_subdomains: None,
366            })
367            .await;
368
369        assert!(result.is_err());
370        assert_eq!(
371            *error_log.lock().unwrap(),
372            vec!["first", "second"],
373            "All error handlers should be called on sync failure"
374        );
375    }
376}