Skip to main content

bitwarden_send/
send_sync_handler.rs

1use std::sync::Arc;
2
3use bitwarden_core::{Client, FromClient, require};
4use bitwarden_error::bitwarden_error;
5use bitwarden_state::repository::{Repository, RepositoryError, RepositoryOption};
6use bitwarden_sync::{SyncHandler, SyncHandlerError};
7use thiserror::Error;
8#[cfg(feature = "wasm")]
9use wasm_bindgen::prelude::*;
10
11use crate::{Send, SendClientExt as _, SendId};
12
13/// Persists `sends`, dropping any with a missing id (logged, not fatal).
14async fn replace_sends(
15    repository: Arc<dyn Repository<Send>>,
16    sends: Vec<Send>,
17) -> Result<(), RepositoryError> {
18    let sends: Vec<(SendId, Send)> = sends
19        .into_iter()
20        .filter_map(|send| {
21            let id = send.id.or_else(|| {
22                tracing::error!("Skipping send with missing id");
23                None
24            })?;
25            Some((id, send))
26        })
27        .collect();
28
29    repository.replace_all(sends).await
30}
31
32/// Sync handler for sends
33///
34/// This handler persists sends to SDK-managed storage.
35#[derive(FromClient)]
36pub struct SendSyncHandler {
37    repository: Option<Arc<dyn Repository<Send>>>,
38}
39
40#[async_trait::async_trait]
41impl SyncHandler for SendSyncHandler {
42    async fn on_sync(
43        &self,
44        response: &bitwarden_api_api::models::SyncResponseModel,
45    ) -> Result<(), SyncHandlerError> {
46        let repository = self.repository.require()?.clone();
47        let api_sends = require!(response.sends.as_ref());
48
49        let sends: Vec<Send> = api_sends
50            .iter()
51            .filter_map(|s| {
52                Send::try_from(s.clone())
53                    .inspect_err(
54                        |e| tracing::error!(id = ?s.id, error = ?e, "Failed to deserialize send"),
55                    )
56                    .ok()
57            })
58            .collect();
59
60        replace_sends(repository, sends).await?;
61
62        Ok(())
63    }
64}
65
66/// Errors returned by [`SendSyncHandlerClient::on_sync`].
67#[allow(missing_docs)]
68#[bitwarden_error(flat)]
69#[derive(Debug, Error)]
70pub enum SendSyncError {
71    #[error(transparent)]
72    Repository(#[from] RepositoryError),
73}
74
75/// Client for the Send sync handler, for callers that fetch sync data themselves (e.g. the Web
76/// client) and only want the Send-persistence part of a sync run.
77#[derive(Clone)]
78#[cfg_attr(feature = "uniffi", derive(uniffi::Object))]
79#[cfg_attr(feature = "wasm", wasm_bindgen)]
80pub struct SendSyncHandlerClient {
81    client: Client,
82}
83
84impl SendSyncHandlerClient {
85    fn new(client: Client) -> Self {
86        Self { client }
87    }
88}
89
90#[cfg_attr(feature = "wasm", wasm_bindgen)]
91#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
92impl SendSyncHandlerClient {
93    /// Persists the sends from a sync response. Call this after each sync.
94    pub async fn on_sync(&self, sends: Vec<Send>) -> Result<(), SendSyncError> {
95        let repository = self.client.sends().get_repository()?;
96        Ok(replace_sends(repository, sends).await?)
97    }
98}
99
100/// Extension trait to add the Send sync handler client to the main Bitwarden SDK client.
101pub trait SendSyncHandlerClientExt {
102    /// Get the Send sync handler client.
103    fn send_sync_handler(&self) -> SendSyncHandlerClient;
104}
105
106impl SendSyncHandlerClientExt for Client {
107    fn send_sync_handler(&self) -> SendSyncHandlerClient {
108        SendSyncHandlerClient::new(self.clone())
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use std::sync::Arc;
115
116    use bitwarden_api_api::models::{SendResponseModel, SyncResponseModel};
117    use bitwarden_test::MemoryRepository;
118
119    use super::*;
120
121    /// Valid EncString in type 2 format (Aes256CbcHmac): `2.<iv>|<data>|<mac>`
122    const ENCRYPTED_STRING: &str = "2.AAAAAAAAAAAAAAAAAAAAAA==|AAAAAAAAAAAAAAAAAAAAAA==|AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
123
124    fn make_send_response(id: uuid::Uuid) -> SendResponseModel {
125        SendResponseModel {
126            object: Some("send".to_string()),
127            id: Some(id),
128            access_id: None,
129            r#type: Some(bitwarden_api_api::models::SendType::Text),
130            auth_type: Some(bitwarden_api_api::models::AuthType::None),
131            name: Some(ENCRYPTED_STRING.to_string()),
132            notes: None,
133            file: None,
134            text: None,
135            data: None,
136            key: Some(ENCRYPTED_STRING.to_string()),
137            max_access_count: None,
138            access_count: Some(0),
139            password: None,
140            emails: None,
141            disabled: Some(false),
142            expiration_date: None,
143            deletion_date: Some("2025-01-10T00:00:00Z".to_string()),
144            revision_date: Some("2025-01-01T00:00:00Z".to_string()),
145            hide_email: Some(false),
146        }
147    }
148
149    #[tokio::test]
150    async fn test_on_sync_replaces_existing_sends() {
151        let repository = Arc::new(MemoryRepository::<Send>::default());
152        let handler = SendSyncHandler {
153            repository: Some(repository.clone()),
154        };
155
156        // First sync with two sends
157        let id1 = uuid::Uuid::new_v4();
158        let id2 = uuid::Uuid::new_v4();
159        let response = SyncResponseModel {
160            sends: Some(vec![make_send_response(id1), make_send_response(id2)]),
161            ..Default::default()
162        };
163        handler.on_sync(&response).await.unwrap();
164        assert_eq!(repository.list().await.unwrap().len(), 2);
165
166        // Second sync with only one send — old ones should be gone
167        let id3 = uuid::Uuid::new_v4();
168        let response = SyncResponseModel {
169            sends: Some(vec![make_send_response(id3)]),
170            ..Default::default()
171        };
172        handler.on_sync(&response).await.unwrap();
173
174        let stored = repository.list().await.unwrap();
175        assert_eq!(stored.len(), 1);
176        assert!(repository.get(SendId::new(id1)).await.unwrap().is_none());
177        assert!(repository.get(SendId::new(id2)).await.unwrap().is_none());
178        assert!(repository.get(SendId::new(id3)).await.unwrap().is_some());
179    }
180
181    #[tokio::test]
182    async fn test_on_sync_no_sends_returns_error() {
183        let repository = Arc::new(MemoryRepository::<Send>::default());
184        let handler = SendSyncHandler {
185            repository: Some(repository.clone()),
186        };
187
188        let response = SyncResponseModel::default();
189        let result = handler.on_sync(&response).await;
190        assert!(result.is_err());
191    }
192
193    fn test_send(id: uuid::Uuid) -> Send {
194        Send {
195            id: Some(SendId::new(id)),
196            access_id: None,
197            r#type: crate::SendType::Text,
198            name: ENCRYPTED_STRING.parse().unwrap(),
199            notes: None,
200            file: None,
201            text: None,
202            data: None,
203            key: ENCRYPTED_STRING.parse().unwrap(),
204            max_access_count: None,
205            access_count: 0,
206            password: None,
207            disabled: false,
208            revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
209            expiration_date: None,
210            deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
211            hide_email: false,
212            emails: None,
213            auth_type: crate::AuthType::None,
214        }
215    }
216
217    fn make_test_client() -> (bitwarden_core::Client, Arc<MemoryRepository<Send>>) {
218        let client = bitwarden_core::Client::new(None);
219        let repository = Arc::new(MemoryRepository::<Send>::default());
220        client
221            .platform()
222            .state()
223            .register_client_managed(repository.clone());
224        (client, repository)
225    }
226
227    #[tokio::test]
228    async fn test_send_sync_handler_client_on_sync_persists_sends() {
229        let (client, repository) = make_test_client();
230        let handler = client.send_sync_handler();
231
232        let id = uuid::Uuid::new_v4();
233        handler.on_sync(vec![test_send(id)]).await.unwrap();
234
235        let stored = repository.list().await.unwrap();
236        assert_eq!(stored.len(), 1);
237        assert!(repository.get(SendId::new(id)).await.unwrap().is_some());
238    }
239
240    #[tokio::test]
241    async fn test_send_sync_handler_client_on_sync_replaces_existing_sends() {
242        let (client, repository) = make_test_client();
243        let handler = client.send_sync_handler();
244
245        let id1 = uuid::Uuid::new_v4();
246        let id2 = uuid::Uuid::new_v4();
247        handler
248            .on_sync(vec![test_send(id1), test_send(id2)])
249            .await
250            .unwrap();
251        assert_eq!(repository.list().await.unwrap().len(), 2);
252
253        let id3 = uuid::Uuid::new_v4();
254        handler.on_sync(vec![test_send(id3)]).await.unwrap();
255
256        let stored = repository.list().await.unwrap();
257        assert_eq!(stored.len(), 1);
258        assert!(repository.get(SendId::new(id1)).await.unwrap().is_none());
259        assert!(repository.get(SendId::new(id2)).await.unwrap().is_none());
260        assert!(repository.get(SendId::new(id3)).await.unwrap().is_some());
261    }
262}