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#[cfg(feature = "wasm")]
9use wasm_bindgen::prelude::*;
10
11use crate::{Send, SendClientExt as _, SendId};
12
13async 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#[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#[allow(missing_docs)]
68#[bitwarden_error(flat)]
69#[derive(Debug, Error)]
70pub enum SendSyncError {
71 #[error(transparent)]
72 Repository(#[from] RepositoryError),
73}
74
75#[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 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
100pub trait SendSyncHandlerClientExt {
102 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 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 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 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}