1use bitwarden_api_api::{apis::ApiClient, models};
2use bitwarden_core::ApiError;
3use bitwarden_error::bitwarden_error;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7#[cfg(feature = "wasm")]
8use tsify::Tsify;
9#[cfg(feature = "wasm")]
10use wasm_bindgen::prelude::*;
11
12use crate::{SendParseError, SendType, send_client::SendClient};
13
14#[derive(Debug, Serialize, Deserialize)]
20#[serde(rename_all = "camelCase")]
21#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
22pub struct SendAccessResponse {
23 pub id: Option<String>,
25 #[serde(rename = "type")]
27 pub type_: Option<SendType>,
28 pub name: Option<String>,
30 pub text: Option<SendAccessTextResponse>,
32 pub file: Option<SendAccessFileResponse>,
34 pub expiration_date: Option<DateTime<Utc>>,
36 pub creator_identifier: Option<String>,
38}
39
40#[derive(Debug, Serialize, Deserialize)]
42#[serde(rename_all = "camelCase")]
43#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
44pub struct SendAccessTextResponse {
45 pub text: Option<String>,
47 pub hidden: bool,
49}
50
51#[derive(Debug, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
55pub struct SendAccessFileResponse {
56 pub id: Option<String>,
58 pub file_name: Option<String>,
60 pub size: Option<String>,
62 pub size_name: Option<String>,
64}
65
66#[derive(Debug, Serialize, Deserialize)]
68#[serde(rename_all = "camelCase")]
69#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
70pub struct SendFileDownloadData {
71 pub id: Option<String>,
73 pub url: Option<String>,
75}
76
77#[bitwarden_error(flat)]
81#[derive(Debug, Error)]
82pub enum AccessSendError {
83 #[error(transparent)]
85 Api(#[from] ApiError),
86 #[error(transparent)]
90 Parse(#[from] SendParseError),
91}
92
93#[bitwarden_error(flat)]
95#[derive(Debug, Error)]
96pub enum GetFileDownloadDataError {
97 #[error(transparent)]
99 Api(#[from] ApiError),
100}
101
102async fn access_send_v1(
105 api_client: &ApiClient,
106 send_id: &str,
107 password: Option<String>,
108) -> Result<SendAccessResponse, AccessSendError> {
109 let resp = api_client
110 .sends_api()
111 .access(send_id, Some(models::SendAccessRequestModel { password }))
112 .await
113 .map_err(ApiError::from)?;
114 Ok(resp.try_into()?)
115}
116
117async fn access_send(
118 api_client: &ApiClient,
119 access_token: &str,
120) -> Result<SendAccessResponse, AccessSendError> {
121 let resp = api_client
122 .sends_api()
123 .access_using_auth(access_token)
124 .await
125 .map_err(ApiError::from)?;
126 Ok(resp.try_into()?)
127}
128
129async fn get_file_download_data_v1(
130 api_client: &ApiClient,
131 send_id: &str,
132 file_id: &str,
133 password: Option<String>,
134) -> Result<SendFileDownloadData, GetFileDownloadDataError> {
135 let resp = api_client
136 .sends_api()
137 .get_send_file_download_data(
138 send_id,
139 file_id,
140 Some(models::SendAccessRequestModel { password }),
141 )
142 .await
143 .map_err(ApiError::from)?;
144 Ok(resp.into())
145}
146
147async fn get_file_download_data(
148 api_client: &ApiClient,
149 file_id: &str,
150 access_token: &str,
151) -> Result<SendFileDownloadData, GetFileDownloadDataError> {
152 let resp = api_client
153 .sends_api()
154 .get_send_file_download_data_using_auth(file_id, access_token)
155 .await
156 .map_err(ApiError::from)?;
157 Ok(resp.into())
158}
159
160impl TryFrom<models::SendAccessResponseModel> for SendAccessResponse {
163 type Error = SendParseError;
164
165 fn try_from(r: models::SendAccessResponseModel) -> Result<Self, Self::Error> {
166 Ok(SendAccessResponse {
167 id: r.id,
168 type_: r.r#type.map(SendType::try_from).transpose()?,
169 name: r.name,
170 text: r.text.map(|t| SendAccessTextResponse {
171 text: t.text,
172 hidden: t.hidden.unwrap_or(false),
173 }),
174 file: r.file.map(|f| SendAccessFileResponse {
175 id: f.id,
176 file_name: f.file_name,
177 size: f.size,
178 size_name: f.size_name,
179 }),
180 expiration_date: r.expiration_date.map(|s| s.parse()).transpose()?,
181 creator_identifier: r.creator_identifier,
182 })
183 }
184}
185
186impl From<models::SendFileDownloadDataResponseModel> for SendFileDownloadData {
187 fn from(r: models::SendFileDownloadDataResponseModel) -> Self {
188 SendFileDownloadData {
189 id: r.id,
190 url: r.url,
191 }
192 }
193}
194
195#[cfg_attr(feature = "wasm", wasm_bindgen)]
198impl SendClient {
199 pub async fn access_send_v1(
204 &self,
205 send_id: String,
206 password: Option<String>,
207 ) -> Result<SendAccessResponse, AccessSendError> {
208 let config = self.client.internal.get_api_configurations();
209 access_send_v1(&config.api_client, &send_id, password).await
210 }
211
212 pub async fn access_send(
216 &self,
217 access_token: String,
218 ) -> Result<SendAccessResponse, AccessSendError> {
219 let config = self.client.internal.get_api_configurations();
220 access_send(&config.api_client, &access_token).await
221 }
222
223 pub async fn get_file_download_data_v1(
227 &self,
228 send_id: String,
229 file_id: String,
230 password: Option<String>,
231 ) -> Result<SendFileDownloadData, GetFileDownloadDataError> {
232 let config = self.client.internal.get_api_configurations();
233 get_file_download_data_v1(&config.api_client, &send_id, &file_id, password).await
234 }
235
236 pub async fn get_file_download_data(
239 &self,
240 access_token: String,
241 file_id: String,
242 ) -> Result<SendFileDownloadData, GetFileDownloadDataError> {
243 let config = self.client.internal.get_api_configurations();
244 get_file_download_data(&config.api_client, &file_id, &access_token).await
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use bitwarden_api_api::{
251 apis::ApiClient,
252 models::{
253 SendAccessResponseModel, SendFileDownloadDataResponseModel, SendFileModel,
254 SendTextModel, SendType,
255 },
256 };
257
258 use super::*;
259
260 const SEND_ID: &str = "25afb11c-9c95-4db5-8bac-c21cb204a3f1";
261 const FILE_ID: &str = "file-id-abc";
262 const ACCESS_TOKEN: &str = "send-access-token";
263
264 #[tokio::test]
267 async fn test_access_send_v1_text() {
268 let api_client = ApiClient::new_mocked(|mock| {
269 mock.sends_api
270 .expect_access()
271 .returning(|id, request| {
272 assert_eq!(id, SEND_ID);
273 let request = request.expect("request body should be present");
274 assert_eq!(request.password, Some("hashed-password".to_string()));
275 Ok(SendAccessResponseModel {
276 object: Some("send-access".to_string()),
277 id: Some(SEND_ID.to_string()),
278 r#type: Some(SendType::Text),
279 auth_type: None,
280 name: Some("encrypted-name".to_string()),
281 file: None,
282 text: Some(Box::new(SendTextModel {
283 text: Some("encrypted-text".to_string()),
284 hidden: Some(true),
285 })),
286 expiration_date: Some("2025-01-10T00:00:00Z".to_string()),
287 creator_identifier: Some("[email protected]".to_string()),
288 })
289 })
290 .once();
291 });
292
293 let result = access_send_v1(&api_client, SEND_ID, Some("hashed-password".to_string()))
294 .await
295 .unwrap();
296
297 assert_eq!(result.id, Some(SEND_ID.to_string()));
298 assert_eq!(result.type_, Some(crate::SendType::Text));
299 assert_eq!(result.name, Some("encrypted-name".to_string()));
300 let text = result.text.expect("text variant should be populated");
301 assert_eq!(text.text, Some("encrypted-text".to_string()));
302 assert!(text.hidden);
303 assert!(result.file.is_none());
304 assert_eq!(
305 result.expiration_date,
306 Some("2025-01-10T00:00:00Z".parse::<DateTime<Utc>>().unwrap())
307 );
308 assert_eq!(
309 result.creator_identifier,
310 Some("[email protected]".to_string())
311 );
312 }
313
314 #[tokio::test]
315 async fn test_access_send_v1_http_error() {
316 let api_client = ApiClient::new_mocked(|mock| {
317 mock.sends_api
318 .expect_access()
319 .returning(|_id, _request| {
320 Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other(
321 "Simulated error",
322 )))
323 })
324 .once();
325 });
326
327 let result = access_send_v1(&api_client, SEND_ID, None).await;
328
329 assert!(matches!(result.unwrap_err(), AccessSendError::Api(_)));
330 }
331
332 #[tokio::test]
335 async fn test_access_send_file() {
336 let api_client = ApiClient::new_mocked(|mock| {
337 mock.sends_api
338 .expect_access_using_auth()
339 .returning(|token| {
340 assert_eq!(token, ACCESS_TOKEN);
341 Ok(SendAccessResponseModel {
342 object: Some("send-access".to_string()),
343 id: Some(SEND_ID.to_string()),
344 r#type: Some(SendType::File),
345 auth_type: None,
346 name: Some("encrypted-name".to_string()),
347 file: Some(Box::new(SendFileModel {
348 id: Some(FILE_ID.to_string()),
349 file_name: Some("encrypted-file-name".to_string()),
350 size: Some("4200".to_string()),
351 size_name: Some("4.2 KB".to_string()),
352 })),
353 text: None,
354 expiration_date: None,
355 creator_identifier: None,
356 })
357 })
358 .once();
359 });
360
361 let result = access_send(&api_client, ACCESS_TOKEN).await.unwrap();
362
363 assert_eq!(result.id, Some(SEND_ID.to_string()));
364 assert_eq!(result.type_, Some(crate::SendType::File));
365 assert_eq!(result.name, Some("encrypted-name".to_string()));
366 assert!(result.text.is_none());
367 let file = result.file.expect("file variant should be populated");
368 assert_eq!(file.id, Some(FILE_ID.to_string()));
369 assert_eq!(file.file_name, Some("encrypted-file-name".to_string()));
370 assert_eq!(file.size, Some("4200".to_string()));
371 assert_eq!(file.size_name, Some("4.2 KB".to_string()));
372 assert_eq!(result.expiration_date, None);
373 assert_eq!(result.creator_identifier, None);
374 }
375
376 #[tokio::test]
377 async fn test_access_send_http_error() {
378 let api_client = ApiClient::new_mocked(|mock| {
379 mock.sends_api
380 .expect_access_using_auth()
381 .returning(|_token| {
382 Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other(
383 "Simulated error",
384 )))
385 })
386 .once();
387 });
388
389 let result = access_send(&api_client, ACCESS_TOKEN).await;
390
391 assert!(matches!(result.unwrap_err(), AccessSendError::Api(_)));
392 }
393
394 #[tokio::test]
397 async fn test_get_file_download_data_v1() {
398 let api_client = ApiClient::new_mocked(|mock| {
399 mock.sends_api
400 .expect_get_send_file_download_data()
401 .returning(|send_id, file_id, request| {
402 assert_eq!(send_id, SEND_ID);
403 assert_eq!(file_id, FILE_ID);
404 let request = request.expect("request body should be present");
405 assert_eq!(request.password, Some("hashed-password".to_string()));
406 Ok(SendFileDownloadDataResponseModel {
407 object: Some("send-fileDownload".to_string()),
408 id: Some(FILE_ID.to_string()),
409 url: Some("https://example.com/download".to_string()),
410 })
411 })
412 .once();
413 });
414
415 let result = get_file_download_data_v1(
416 &api_client,
417 SEND_ID,
418 FILE_ID,
419 Some("hashed-password".to_string()),
420 )
421 .await
422 .unwrap();
423
424 assert_eq!(result.id, Some(FILE_ID.to_string()));
425 assert_eq!(result.url, Some("https://example.com/download".to_string()));
426 }
427
428 #[tokio::test]
429 async fn test_get_file_download_data_v1_http_error() {
430 let api_client = ApiClient::new_mocked(|mock| {
431 mock.sends_api
432 .expect_get_send_file_download_data()
433 .returning(|_send_id, _file_id, _request| {
434 Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other(
435 "Simulated error",
436 )))
437 })
438 .once();
439 });
440
441 let result = get_file_download_data_v1(&api_client, SEND_ID, FILE_ID, None).await;
442
443 assert!(matches!(
444 result.unwrap_err(),
445 GetFileDownloadDataError::Api(_)
446 ));
447 }
448
449 #[tokio::test]
452 async fn test_get_file_download_data() {
453 let api_client = ApiClient::new_mocked(|mock| {
454 mock.sends_api
455 .expect_get_send_file_download_data_using_auth()
456 .returning(|file_id, token| {
457 assert_eq!(file_id, FILE_ID);
458 assert_eq!(token, ACCESS_TOKEN);
459 Ok(SendFileDownloadDataResponseModel {
460 object: Some("send-fileDownload".to_string()),
461 id: Some(FILE_ID.to_string()),
462 url: Some("https://example.com/download".to_string()),
463 })
464 })
465 .once();
466 });
467
468 let result = get_file_download_data(&api_client, FILE_ID, ACCESS_TOKEN)
469 .await
470 .unwrap();
471
472 assert_eq!(result.id, Some(FILE_ID.to_string()));
473 assert_eq!(result.url, Some("https://example.com/download".to_string()));
474 }
475
476 #[tokio::test]
477 async fn test_get_file_download_data_http_error() {
478 let api_client = ApiClient::new_mocked(|mock| {
479 mock.sends_api
480 .expect_get_send_file_download_data_using_auth()
481 .returning(|_file_id, _token| {
482 Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other(
483 "Simulated error",
484 )))
485 })
486 .once();
487 });
488
489 let result = get_file_download_data(&api_client, FILE_ID, ACCESS_TOKEN).await;
490
491 assert!(matches!(
492 result.unwrap_err(),
493 GetFileDownloadDataError::Api(_)
494 ));
495 }
496}