Skip to main content

bitwarden_vault/cipher/attachment_client/
renew.rs

1use bitwarden_core::{ApiError, MissingFieldError};
2use bitwarden_error::bitwarden_error;
3use thiserror::Error;
4#[cfg(feature = "wasm")]
5use wasm_bindgen::prelude::wasm_bindgen;
6
7use crate::{AttachmentsClient, CipherId};
8
9#[allow(missing_docs)]
10#[bitwarden_error(flat)]
11#[derive(Debug, Error)]
12pub enum CipherRenewFileUploadUrlError {
13    #[error(transparent)]
14    Api(#[from] ApiError),
15    #[error(transparent)]
16    MissingField(#[from] MissingFieldError),
17}
18
19impl<T> From<bitwarden_api_api::apis::Error<T>> for CipherRenewFileUploadUrlError {
20    fn from(value: bitwarden_api_api::apis::Error<T>) -> Self {
21        Self::Api(value.into())
22    }
23}
24
25#[cfg_attr(feature = "wasm", wasm_bindgen)]
26impl AttachmentsClient {
27    /// Returns a renewed upload URL for an attachment.
28    /// Does not modify the attachment slot.
29    pub async fn renew_file_upload_url(
30        &self,
31        cipher_id: CipherId,
32        attachment_id: String,
33    ) -> Result<String, CipherRenewFileUploadUrlError> {
34        let response = self
35            .api_configurations
36            .api_client
37            .ciphers_api()
38            .renew_file_upload_url(cipher_id.into(), &attachment_id)
39            .await?;
40
41        response.url.ok_or_else(|| MissingFieldError("url").into())
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use std::sync::Arc;
48
49    use bitwarden_api_api::{apis::ApiClient, models::AttachmentUploadDataResponseModel};
50    use bitwarden_core::{client::ApiConfigurations, key_management::KeySlotIds};
51    use bitwarden_crypto::KeyStore;
52    use reqwest::StatusCode;
53
54    use super::*;
55
56    const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
57    const TEST_ATTACHMENT_ID: &str = "uf7bkexzag04d3cw04jsbqqkbpbwhxs0";
58    const TEST_RENEW_URL: &str = "http://localhost:4000/attachments/test/renewed";
59
60    fn client_with_api(api_client: ApiClient) -> AttachmentsClient {
61        AttachmentsClient {
62            key_store: KeyStore::<KeySlotIds>::default(),
63            api_configurations: Arc::new(ApiConfigurations::from_api_client(api_client)),
64            repository: None,
65            http_client: reqwest::Client::new(),
66        }
67    }
68
69    #[tokio::test]
70    async fn returns_url_from_api_response() {
71        let api_client = ApiClient::new_mocked(|mock| {
72            mock.ciphers_api
73                .expect_renew_file_upload_url()
74                .returning(|id, attachment_id| {
75                    assert_eq!(&id.to_string(), TEST_CIPHER_ID);
76                    assert_eq!(attachment_id, TEST_ATTACHMENT_ID);
77                    Ok(AttachmentUploadDataResponseModel {
78                        url: Some(TEST_RENEW_URL.to_string()),
79                        ..Default::default()
80                    })
81                });
82        });
83
84        let client = client_with_api(api_client);
85        let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
86        let url = client
87            .renew_file_upload_url(cipher_id, TEST_ATTACHMENT_ID.to_string())
88            .await
89            .unwrap();
90
91        assert_eq!(url, TEST_RENEW_URL);
92    }
93
94    #[tokio::test]
95    async fn returns_missing_field_when_response_has_no_url() {
96        let api_client = ApiClient::new_mocked(|mock| {
97            mock.ciphers_api
98                .expect_renew_file_upload_url()
99                .returning(|_id, _attachment_id| {
100                    Ok(AttachmentUploadDataResponseModel {
101                        url: None,
102                        ..Default::default()
103                    })
104                });
105        });
106
107        let client = client_with_api(api_client);
108        let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
109        let err = client
110            .renew_file_upload_url(cipher_id, TEST_ATTACHMENT_ID.to_string())
111            .await
112            .unwrap_err();
113
114        assert!(matches!(
115            err,
116            CipherRenewFileUploadUrlError::MissingField(_)
117        ));
118    }
119
120    #[tokio::test]
121    async fn propagates_api_errors() {
122        let api_client = ApiClient::new_mocked(|mock| {
123            mock.ciphers_api
124                .expect_renew_file_upload_url()
125                .returning(|_id, _attachment_id| {
126                    Err(bitwarden_api_api::apis::Error::Response(
127                        bitwarden_api_api::apis::ResponseContent {
128                            status: StatusCode::INTERNAL_SERVER_ERROR,
129                            message: "boom".to_string(),
130                        },
131                    ))
132                });
133        });
134
135        let client = client_with_api(api_client);
136        let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
137        let err = client
138            .renew_file_upload_url(cipher_id, TEST_ATTACHMENT_ID.to_string())
139            .await
140            .unwrap_err();
141
142        assert!(matches!(err, CipherRenewFileUploadUrlError::Api(_)));
143    }
144}