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