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