bitwarden_send/
send_sync_handler.rs1use 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
11async 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#[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#[allow(missing_docs)]
66#[bitwarden_error(flat)]
67#[derive(Debug, Error)]
68pub enum SendSyncError {
69 #[error(transparent)]
70 Repository(#[from] RepositoryError),
71}
72
73#[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 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
98pub trait SendSyncHandlerClientExt {
100 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 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 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 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}