1use bitwarden_api_base::AuthRequired;
2use bitwarden_core::{ApiError, MissingFieldError, key_management::SymmetricKeySlotId, require};
3use bitwarden_crypto::{CryptoError, EncString, OctetStreamBytes, PrimitiveEncryptable};
4use bitwarden_error::bitwarden_error;
5use bitwarden_state::repository::RepositoryError;
6use serde::{Deserialize, Serialize};
7use serde_repr::{Deserialize_repr, Serialize_repr};
8use thiserror::Error;
9#[cfg(feature = "wasm")]
10use tsify::Tsify;
11#[cfg(feature = "wasm")]
12use wasm_bindgen::prelude::*;
13
14use crate::{
15 EmptyEmailListError, Send, SendAddRequest, SendId, SendParseError, SendView,
16 send_client::SendClient,
17};
18
19#[derive(Clone, Copy, Serialize_repr, Deserialize_repr, Debug, PartialEq)]
21#[repr(u8)]
22#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
23#[cfg_attr(feature = "wasm", wasm_bindgen)]
24pub enum FileUploadType {
25 Direct = 0,
27 Azure = 1,
29}
30
31impl TryFrom<bitwarden_api_api::models::FileUploadType> for FileUploadType {
32 type Error = MissingFieldError;
33
34 fn try_from(t: bitwarden_api_api::models::FileUploadType) -> Result<Self, Self::Error> {
35 Ok(match t {
36 bitwarden_api_api::models::FileUploadType::Direct => FileUploadType::Direct,
37 bitwarden_api_api::models::FileUploadType::Azure => FileUploadType::Azure,
38 bitwarden_api_api::models::FileUploadType::__Unknown(_) => {
39 return Err(MissingFieldError("file_upload_type"));
40 }
41 })
42 }
43}
44
45#[derive(Serialize, Deserialize, Debug)]
49#[serde(rename_all = "camelCase")]
50#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi))]
51pub struct CreateFileSendResponse {
52 pub send: SendView,
54 pub url: String,
56 pub file_upload_type: FileUploadType,
58 pub file_id: String,
60 pub encrypted_file_name: String,
62 #[cfg_attr(feature = "wasm", tsify(type = "Uint8Array"))]
67 #[serde(with = "serde_bytes")]
68 pub encrypted_file_buffer: Vec<u8>,
69}
70
71#[allow(missing_docs)]
72#[bitwarden_error(flat)]
73#[derive(Debug, Error)]
74pub enum CreateFileSendError {
75 #[error(transparent)]
77 Api(#[from] ApiError),
78 #[error(transparent)]
80 Crypto(#[from] CryptoError),
81 #[error(transparent)]
83 EmptyEmailList(#[from] EmptyEmailListError),
84 #[error(transparent)]
86 MissingField(#[from] MissingFieldError),
87 #[error(transparent)]
89 Repository(#[from] RepositoryError),
90 #[error(transparent)]
92 SendParse(#[from] SendParseError),
93}
94
95#[allow(missing_docs)]
96#[bitwarden_error(flat)]
97#[derive(Debug, Error)]
98pub enum UploadSendFileError {
99 #[error(transparent)]
101 Api(#[from] ApiError),
102 #[error(transparent)]
104 Reqwest(reqwest::Error),
105 #[error(transparent)]
107 RenewFileUploadUrl(#[from] RenewFileUploadUrlError),
108}
109
110#[allow(missing_docs)]
111#[bitwarden_error(flat)]
112#[derive(Debug, Error)]
113pub enum RenewFileUploadUrlError {
114 #[error(transparent)]
116 Api(#[from] ApiError),
117 #[error(transparent)]
119 MissingField(#[from] MissingFieldError),
120}
121
122#[cfg_attr(feature = "wasm", wasm_bindgen)]
123impl SendClient {
124 pub async fn create_file_send(
135 &self,
136 request: SendAddRequest,
137 file_buffer: Vec<u8>,
138 ) -> Result<CreateFileSendResponse, CreateFileSendError> {
139 request.auth.validate()?;
140
141 let key_store = self.client.internal.get_key_store();
142 let config = self.client.internal.get_api_configurations();
143 let repository = self.get_repository()?;
144
145 let mut send_request = key_store.encrypt(request)?;
146
147 let encrypted_file_buffer =
152 encrypt_file_buffer_with_send_key(key_store, &send_request.key, &file_buffer)?;
153 send_request.file_length = Some(encrypted_file_buffer.len() as i64);
154
155 let resp = config
156 .api_client
157 .sends_api()
158 .post_file(Some(send_request))
159 .await?;
160
161 let url = require!(resp.url);
162 let file_upload_type: FileUploadType = require!(resp.file_upload_type).try_into()?;
163 let send_response = *require!(resp.send_response);
164
165 let send: Send = send_response.try_into()?;
166
167 let file_id = send
168 .file
169 .as_ref()
170 .and_then(|f| f.id.clone())
171 .ok_or(MissingFieldError("file.id"))?;
172
173 let encrypted_file_name = send
174 .file
175 .as_ref()
176 .map(|f| f.file_name.to_string())
177 .ok_or(MissingFieldError("file.file_name"))?;
178
179 let send_view = key_store.decrypt(&send)?;
180
181 let send_id = require!(send.id);
182 repository.set(send_id, send.clone()).await?;
183
184 Ok(CreateFileSendResponse {
185 send: send_view,
186 url,
187 file_upload_type,
188 file_id,
189 encrypted_file_name,
190 encrypted_file_buffer,
191 })
192 }
193
194 pub async fn upload_send_file(
208 &self,
209 send_id: SendId,
210 file_id: String,
211 encrypted_file_name: String,
212 file_upload_type: FileUploadType,
213 upload_url: String,
214 data: Vec<u8>,
215 ) -> Result<(), UploadSendFileError> {
216 match file_upload_type {
217 FileUploadType::Direct => {
218 self.upload_send_file_direct(send_id, file_id, encrypted_file_name, data)
219 .await
220 }
221 FileUploadType::Azure => {
222 self.upload_send_file_azure(send_id, file_id, upload_url, data)
223 .await
224 }
225 }
226 }
227
228 async fn upload_send_file_direct(
230 &self,
231 send_id: SendId,
232 file_id: String,
233 encrypted_file_name: String,
234 data: Vec<u8>,
235 ) -> Result<(), UploadSendFileError> {
236 let config = self.client.internal.get_api_configurations();
237
238 let url = format!(
239 "{}/sends/{}/file/{}",
240 config.api_config.base_path,
241 bitwarden_api_base::urlencode(send_id.to_string()),
242 bitwarden_api_base::urlencode(&file_id),
243 );
244
245 let part = reqwest::multipart::Part::bytes(data)
246 .file_name(encrypted_file_name)
247 .mime_str("application/octet-stream")
248 .map_err(UploadSendFileError::Reqwest)?;
249
250 let form = reqwest::multipart::Form::new().part("data", part);
251
252 let req_builder = config
253 .api_config
254 .client
255 .post(url)
256 .with_extension(AuthRequired::Bearer)
257 .multipart(form);
258
259 bitwarden_api_base::process_with_empty_response(req_builder).await?;
260
261 Ok(())
262 }
263
264 async fn upload_send_file_azure(
267 &self,
268 send_id: SendId,
269 file_id: String,
270 upload_url: String,
271 data: Vec<u8>,
272 ) -> Result<(), UploadSendFileError> {
273 match self.put_azure_blob(&upload_url, data.clone()).await {
274 Ok(()) => Ok(()),
275 Err(_) => {
278 let renewed = self.renew_file_upload_url(send_id, file_id).await?;
279 self.put_azure_blob(&renewed, data).await
280 }
281 }
282 }
283
284 async fn put_azure_blob(&self, url: &str, data: Vec<u8>) -> Result<(), UploadSendFileError> {
292 let config = self.client.internal.get_api_configurations();
293
294 let mut req = config
295 .api_config
296 .client
297 .put(url)
298 .header("x-ms-blob-type", "BlockBlob")
299 .header("content-type", "application/octet-stream");
300
301 if let Some(version) = reqwest::Url::parse(url).ok().and_then(|u| {
305 u.query_pairs()
306 .find(|(k, _)| k == "sv")
307 .map(|(_, v)| v.into_owned())
308 }) {
309 req = req.header("x-ms-version", version);
310 }
311
312 let req = req.body(data);
313
314 bitwarden_api_base::process_with_empty_response(req).await?;
315
316 Ok(())
317 }
318
319 pub async fn renew_file_upload_url(
323 &self,
324 send_id: SendId,
325 file_id: String,
326 ) -> Result<String, RenewFileUploadUrlError> {
327 let config = self.client.internal.get_api_configurations();
328
329 let resp = config
330 .api_client
331 .sends_api()
332 .renew_file_upload(&send_id.to_string(), &file_id)
333 .await?;
334
335 Ok(require!(resp.url))
336 }
337}
338
339fn encrypt_file_buffer_with_send_key(
345 key_store: &bitwarden_crypto::KeyStore<bitwarden_core::key_management::KeySlotIds>,
346 wrapped_send_key: &str,
347 buffer: &[u8],
348) -> Result<Vec<u8>, CryptoError> {
349 let wrapped_send_key: EncString = wrapped_send_key.parse()?;
350 let mut ctx = key_store.context();
351 let send_key = Send::get_key(&mut ctx, &wrapped_send_key, SymmetricKeySlotId::User)?;
352 let encrypted = OctetStreamBytes::from(buffer).encrypt(&mut ctx, send_key)?;
353 encrypted.to_buffer()
354}
355
356#[cfg(test)]
357mod tests {
358 use std::sync::Arc;
359
360 use bitwarden_api_api::models::{
361 FileUploadType, SendFileModel, SendFileUploadDataResponseModel, SendResponseModel,
362 };
363 use bitwarden_core::{
364 Client, ClientSettings, DeviceType,
365 key_management::{KeySlotIds, SymmetricKeySlotId},
366 };
367 use bitwarden_crypto::{KeyStore, SymmetricKeyAlgorithm};
368 use bitwarden_state::repository::Repository;
369 use bitwarden_test::{MemoryRepository, start_api_mock};
370 use uuid::uuid;
371 use wiremock::{
372 Mock, MockServer, ResponseTemplate,
373 matchers::{method, path, path_regex},
374 };
375
376 use super::*;
377 use crate::{AuthType, Send, SendAuthType, SendClientExt, SendId, SendType, SendViewType};
378
379 const SEND_ID: &str = "25afb11c-9c95-4db5-8bac-c21cb204a3f1";
380 const FILE_ID: &str = "file-id-abc";
381
382 fn make_test_client(server: &MockServer) -> (Client, Arc<MemoryRepository<Send>>) {
386 let settings = ClientSettings {
387 identity_url: server.uri(),
388 api_url: server.uri(),
389 user_agent: "Bitwarden Test".into(),
390 device_type: DeviceType::SDK,
391 device_identifier: None,
392 bitwarden_client_version: None,
393 bitwarden_package_type: None,
394 };
395 let client = Client::new(Some(settings));
396
397 {
399 let key_store: &KeyStore<KeySlotIds> = client.internal.get_key_store();
400 let mut ctx = key_store.context_mut();
401 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
402 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
403 .unwrap();
404 }
405
406 let repository = Arc::new(MemoryRepository::<Send>::default());
407 client
408 .platform()
409 .state()
410 .register_client_managed(repository.clone());
411
412 (client, repository)
413 }
414
415 fn sample_request() -> SendAddRequest {
416 SendAddRequest {
417 name: "test-file-send".to_string(),
418 notes: None,
419 view_type: SendViewType::File(crate::SendFileView {
423 id: None,
424 file_name: "secret.txt".to_string(),
425 size: None,
426 size_name: None,
427 }),
428 max_access_count: None,
429 disabled: false,
430 hide_email: false,
431 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
432 expiration_date: None,
433 auth: SendAuthType::None,
434 }
435 }
436
437 fn echo_file_send_response(
440 request: bitwarden_api_api::models::SendRequestModel,
441 ) -> SendResponseModel {
442 let encrypted_name = request.name.clone();
443 SendResponseModel {
444 id: Some(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1")),
445 name: request.name,
446 revision_date: Some("2025-01-01T00:00:00Z".to_string()),
447 object: Some("send".to_string()),
448 access_id: None,
449 r#type: Some(bitwarden_api_api::models::SendType::File),
450 auth_type: Some(bitwarden_api_api::models::AuthType::None),
451 notes: request.notes,
452 file: Some(Box::new(SendFileModel {
453 id: Some(FILE_ID.to_string()),
454 file_name: encrypted_name,
457 size: Some("123".to_string()),
458 size_name: Some("123 B".to_string()),
459 })),
460 text: None,
461 data: None,
462 key: Some(request.key),
463 max_access_count: request.max_access_count,
464 access_count: Some(0),
465 password: request.password,
466 emails: request.emails,
467 disabled: Some(request.disabled),
468 expiration_date: request.expiration_date,
469 deletion_date: Some(request.deletion_date),
470 hide_email: request.hide_email,
471 }
472 }
473
474 #[tokio::test]
477 async fn test_create_file_send() {
478 let upload_url = "https://upload.example.com/abc";
479 let captured: Arc<std::sync::Mutex<Option<bitwarden_api_api::models::SendRequestModel>>> =
481 Arc::new(std::sync::Mutex::new(None));
482 let captured_mock = captured.clone();
483 let mock = Mock::given(method("POST"))
484 .and(path("/sends/file/v2"))
485 .respond_with(move |req: &wiremock::Request| {
486 let body: bitwarden_api_api::models::SendRequestModel =
487 serde_json::from_slice(&req.body).expect("request body should be valid JSON");
488 *captured_mock.lock().unwrap() = Some(body.clone());
489 let send_response = echo_file_send_response(body);
490 let response = SendFileUploadDataResponseModel {
491 object: Some("send-fileUpload".to_string()),
492 url: Some(upload_url.to_string()),
493 file_upload_type: Some(FileUploadType::Azure),
494 send_response: Some(Box::new(send_response)),
495 };
496 ResponseTemplate::new(200).set_body_json(&response)
497 });
498
499 let (server, _config) = start_api_mock(vec![mock]).await;
500 let (client, repository) = make_test_client(&server);
501
502 let plaintext = b"the quick brown fox".to_vec();
503 let result = client
504 .sends()
505 .create_file_send(sample_request(), plaintext)
506 .await
507 .unwrap();
508
509 assert_eq!(result.url, upload_url);
510 assert_eq!(result.file_upload_type, crate::FileUploadType::Azure);
511 assert_eq!(result.file_id, FILE_ID);
512 assert!(!result.encrypted_file_name.is_empty());
513 assert_eq!(result.send.id, Some(SendId::new(SEND_ID.parse().unwrap())));
514 assert_eq!(result.send.name, "test-file-send");
515 assert_eq!(result.send.r#type, SendType::File);
516 assert_eq!(result.send.auth_type, AuthType::None);
517
518 let request_body = captured.lock().unwrap().clone().expect("request captured");
522 assert_eq!(
523 request_body.file_length,
524 Some(result.encrypted_file_buffer.len() as i64),
525 "file_length must equal the encrypted buffer length"
526 );
527 assert!(
528 request_body.file.and_then(|f| f.size).is_none(),
529 "file.size must not be set on the create request"
530 );
531
532 assert!(result.encrypted_file_buffer.len() > b"the quick brown fox".len());
535
536 let stored: Option<Send> = repository
538 .get(SendId::new(SEND_ID.parse().unwrap()))
539 .await
540 .unwrap();
541 assert!(stored.is_some(), "send should be stored in the repository");
542 }
543
544 #[tokio::test]
545 async fn test_create_file_send_errors_when_file_upload_type_missing() {
546 let mock = Mock::given(method("POST"))
547 .and(path("/sends/file/v2"))
548 .respond_with(move |req: &wiremock::Request| {
549 let body: bitwarden_api_api::models::SendRequestModel =
550 serde_json::from_slice(&req.body).unwrap();
551 let send_response = echo_file_send_response(body);
552 let response = SendFileUploadDataResponseModel {
553 object: Some("send-fileUpload".to_string()),
554 url: Some("https://upload.example.com/abc".to_string()),
555 file_upload_type: None,
558 send_response: Some(Box::new(send_response)),
559 };
560 ResponseTemplate::new(200).set_body_json(&response)
561 });
562
563 let (server, _config) = start_api_mock(vec![mock]).await;
564 let (client, _repository) = make_test_client(&server);
565
566 let err = client
567 .sends()
568 .create_file_send(sample_request(), b"file-bytes".to_vec())
569 .await
570 .unwrap_err();
571
572 assert!(matches!(err, CreateFileSendError::MissingField(_)));
573 }
574
575 #[tokio::test]
576 async fn test_create_file_send_http_error() {
577 let mock = Mock::given(method("POST"))
578 .and(path("/sends/file/v2"))
579 .respond_with(ResponseTemplate::new(500));
580
581 let (server, _config) = start_api_mock(vec![mock]).await;
582 let (client, _repository) = make_test_client(&server);
583
584 let err = client
585 .sends()
586 .create_file_send(sample_request(), b"file-bytes".to_vec())
587 .await
588 .unwrap_err();
589
590 assert!(matches!(err, CreateFileSendError::Api(_)));
591 }
592
593 #[tokio::test]
594 async fn test_create_file_send_empty_email_list_validation() {
595 let server = MockServer::start().await;
597 let (client, _repository) = make_test_client(&server);
598
599 let mut request = sample_request();
600 request.auth = SendAuthType::Emails { emails: vec![] };
601
602 let err = client
603 .sends()
604 .create_file_send(request, b"file-bytes".to_vec())
605 .await
606 .unwrap_err();
607
608 assert!(matches!(err, CreateFileSendError::EmptyEmailList(_)));
609 assert!(
610 server.received_requests().await.unwrap().is_empty(),
611 "validation failure should short-circuit before any HTTP call"
612 );
613 }
614
615 #[tokio::test]
618 async fn test_upload_send_file() {
619 let send_id = SendId::new(SEND_ID.parse().unwrap());
620 let file_id = FILE_ID.to_string();
621 let encrypted_file_name = "2.encrypted-name|abc|def".to_string();
622 let data = b"encrypted-file-bytes".to_vec();
623
624 let mock = Mock::given(method("POST"))
625 .and(path(format!("/sends/{}/file/{}", SEND_ID, FILE_ID)))
626 .respond_with(ResponseTemplate::new(200));
627
628 let (server, _config) = start_api_mock(vec![mock]).await;
629 let (client, _repository) = make_test_client(&server);
630
631 client
632 .sends()
633 .upload_send_file(
634 send_id,
635 file_id,
636 encrypted_file_name,
637 crate::FileUploadType::Direct,
638 "https://ignored.example.com".to_string(),
640 data.clone(),
641 )
642 .await
643 .unwrap();
644
645 let requests = server.received_requests().await.unwrap();
647 assert_eq!(requests.len(), 1);
648 let req = &requests[0];
649 let content_type = req
650 .headers
651 .get("content-type")
652 .map(|v| v.to_str().unwrap())
653 .unwrap_or_default();
654 assert!(
655 content_type.starts_with("multipart/form-data"),
656 "expected multipart/form-data, got {content_type}"
657 );
658 let body = &req.body;
660 assert!(
661 body.windows(data.len()).any(|w| w == data.as_slice()),
662 "request body should contain the encrypted file bytes"
663 );
664 let body_str = String::from_utf8_lossy(body);
666 assert!(
667 body_str.contains("name=\"data\""),
668 "multipart should include the 'data' part name"
669 );
670 assert!(
671 body_str.contains("filename=\"2.encrypted-name|abc|def\""),
672 "multipart should include the encrypted file name as the part's filename"
673 );
674 }
675
676 #[tokio::test]
677 async fn test_upload_send_file_http_error() {
678 let send_id = SendId::new(SEND_ID.parse().unwrap());
679 let file_id = FILE_ID.to_string();
680
681 let mock = Mock::given(method("POST"))
682 .and(path(format!("/sends/{}/file/{}", SEND_ID, FILE_ID)))
683 .respond_with(ResponseTemplate::new(500));
684
685 let (server, _config) = start_api_mock(vec![mock]).await;
686 let (client, _repository) = make_test_client(&server);
687
688 let err = client
689 .sends()
690 .upload_send_file(
691 send_id,
692 file_id,
693 "encrypted-name".to_string(),
694 crate::FileUploadType::Direct,
695 "https://ignored.example.com".to_string(),
696 b"data".to_vec(),
697 )
698 .await
699 .unwrap_err();
700
701 assert!(matches!(err, UploadSendFileError::Api(_)));
702 }
703
704 #[tokio::test]
708 async fn test_upload_send_file_azure() {
709 let send_id = SendId::new(SEND_ID.parse().unwrap());
710 let file_id = FILE_ID.to_string();
711 let data = b"encrypted-file-bytes".to_vec();
712
713 let blob_path = "/container/blob";
716
717 let mock = Mock::given(method("PUT"))
718 .and(path(blob_path))
719 .respond_with(move |req: &wiremock::Request| {
720 assert_eq!(
722 req.headers
723 .get("x-ms-blob-type")
724 .map(|v| v.to_str().unwrap()),
725 Some("BlockBlob")
726 );
727 assert_eq!(
729 req.headers.get("x-ms-version").map(|v| v.to_str().unwrap()),
730 Some("2024-01-01")
731 );
732 assert_eq!(req.body, b"encrypted-file-bytes");
734 ResponseTemplate::new(201)
735 });
736
737 let (server, _config) = start_api_mock(vec![mock]).await;
738 let (client, _repository) = make_test_client(&server);
739
740 let upload_url = format!("{}{}?sv=2024-01-01&sig=abc", server.uri(), blob_path);
741
742 client
743 .sends()
744 .upload_send_file(
745 send_id,
746 file_id,
747 "encrypted-name".to_string(),
748 crate::FileUploadType::Azure,
749 upload_url,
750 data,
751 )
752 .await
753 .unwrap();
754 }
755
756 #[tokio::test]
760 async fn test_upload_send_file_azure_renews_and_retries_on_failure() {
761 let send_id = SendId::new(SEND_ID.parse().unwrap());
762 let file_id = FILE_ID.to_string();
763 let data = b"encrypted-file-bytes".to_vec();
764
765 let (server, _config) = start_api_mock(vec![]).await;
768
769 Mock::given(method("PUT"))
771 .and(path("/container/expired"))
772 .respond_with(ResponseTemplate::new(403))
773 .expect(1)
774 .mount(&server)
775 .await;
776
777 Mock::given(method("PUT"))
778 .and(path("/container/renewed"))
779 .respond_with(ResponseTemplate::new(201))
780 .expect(1)
781 .mount(&server)
782 .await;
783
784 let renewed_url = format!("{}/container/renewed?sv=2024-01-01&sig=fresh", server.uri());
786 Mock::given(method("GET"))
787 .and(path_regex(r"^/sends/[a-f0-9-]+/file/[^/]+$"))
788 .respond_with(move |_req: &wiremock::Request| {
789 let response = SendFileUploadDataResponseModel {
790 object: Some("send-fileUpload".to_string()),
791 url: Some(renewed_url.clone()),
792 file_upload_type: Some(FileUploadType::Azure),
793 send_response: None,
794 };
795 ResponseTemplate::new(200).set_body_json(&response)
796 })
797 .expect(1)
798 .mount(&server)
799 .await;
800
801 let (client, _repository) = make_test_client(&server);
802
803 let expired_url = format!("{}/container/expired?sv=2024-01-01&sig=old", server.uri());
804
805 client
806 .sends()
807 .upload_send_file(
808 send_id,
809 file_id,
810 "encrypted-name".to_string(),
811 crate::FileUploadType::Azure,
812 expired_url,
813 data,
814 )
815 .await
816 .unwrap();
817 }
818
819 #[tokio::test]
822 async fn test_renew_file_upload_url() {
823 let send_id = SendId::new(SEND_ID.parse().unwrap());
824 let file_id = FILE_ID.to_string();
825 let new_url = "https://upload.example.com/renewed";
826
827 let mock = Mock::given(method("GET"))
829 .and(path_regex(r"^/sends/[a-f0-9-]+/file/[^/]+$"))
830 .respond_with(move |req: &wiremock::Request| {
831 assert!(
833 req.url
834 .path()
835 .ends_with(&format!("/sends/{}/file/{}", SEND_ID, FILE_ID))
836 );
837 let response = SendFileUploadDataResponseModel {
838 object: Some("send-fileUpload".to_string()),
839 url: Some(new_url.to_string()),
840 file_upload_type: Some(FileUploadType::Azure),
841 send_response: None,
842 };
843 ResponseTemplate::new(200).set_body_json(&response)
844 });
845
846 let (server, _config) = start_api_mock(vec![mock]).await;
847 let (client, _repository) = make_test_client(&server);
848
849 let url = client
850 .sends()
851 .renew_file_upload_url(send_id, file_id)
852 .await
853 .unwrap();
854
855 assert_eq!(url, new_url);
856 }
857
858 #[tokio::test]
859 async fn test_renew_file_upload_url_missing_url() {
860 let send_id = SendId::new(SEND_ID.parse().unwrap());
861 let file_id = FILE_ID.to_string();
862
863 let mock = Mock::given(method("GET"))
866 .and(path_regex(r"^/sends/[a-f0-9-]+/file/[^/]+$"))
867 .respond_with(ResponseTemplate::new(200).set_body_json(
868 &SendFileUploadDataResponseModel {
869 object: Some("send-fileUpload".to_string()),
870 url: None,
871 file_upload_type: None,
872 send_response: None,
873 },
874 ));
875
876 let (server, _config) = start_api_mock(vec![mock]).await;
877 let (client, _repository) = make_test_client(&server);
878
879 let err = client
880 .sends()
881 .renew_file_upload_url(send_id, file_id)
882 .await
883 .unwrap_err();
884
885 assert!(matches!(err, RenewFileUploadUrlError::MissingField(_)));
886 }
887
888 #[tokio::test]
889 async fn test_renew_file_upload_url_http_error() {
890 let send_id = SendId::new(SEND_ID.parse().unwrap());
891 let file_id = FILE_ID.to_string();
892
893 let mock = Mock::given(method("GET"))
894 .and(path_regex(r"^/sends/[a-f0-9-]+/file/[^/]+$"))
895 .respond_with(ResponseTemplate::new(500));
896
897 let (server, _config) = start_api_mock(vec![mock]).await;
898 let (client, _repository) = make_test_client(&server);
899
900 let err = client
901 .sends()
902 .renew_file_upload_url(send_id, file_id)
903 .await
904 .unwrap_err();
905
906 assert!(matches!(err, RenewFileUploadUrlError::Api(_)));
907 }
908}