Skip to main content

bitwarden_vault/cipher/attachment_client/admin/
download_url.rs

1use bitwarden_core::{ApiError, MissingFieldError};
2use bitwarden_error::bitwarden_error;
3use reqwest::StatusCode;
4use thiserror::Error;
5#[cfg(feature = "wasm")]
6use wasm_bindgen::prelude::wasm_bindgen;
7
8use crate::{AttachmentAdminClient, CipherId};
9
10#[allow(missing_docs)]
11#[bitwarden_error(flat)]
12#[derive(Debug, Error)]
13pub enum CipherAdminGetAttachmentDownloadUrlError {
14    #[error(transparent)]
15    Api(#[from] ApiError),
16    #[error(transparent)]
17    MissingField(#[from] MissingFieldError),
18    #[error("Attachment not found")]
19    NotFound,
20}
21
22impl<T> From<bitwarden_api_api::apis::Error<T>> for CipherAdminGetAttachmentDownloadUrlError {
23    fn from(value: bitwarden_api_api::apis::Error<T>) -> Self {
24        Self::Api(value.into())
25    }
26}
27
28#[cfg_attr(feature = "wasm", wasm_bindgen)]
29impl AttachmentAdminClient {
30    /// Fetches the download URL for an attachment from the admin API. The admin client has
31    /// no local repository to fall back to on 404, so a server-side 404 is surfaced as
32    /// [`CipherAdminGetAttachmentDownloadUrlError::NotFound`] for the caller to handle.
33    pub async fn get_attachment_download_url(
34        &self,
35        cipher_id: CipherId,
36        attachment_id: String,
37    ) -> Result<String, CipherAdminGetAttachmentDownloadUrlError> {
38        let response = self
39            .api_configurations
40            .api_client
41            .ciphers_api()
42            .get_attachment_data_admin(cipher_id.into(), &attachment_id)
43            .await
44            .map_err(|e| match e {
45                bitwarden_api_api::apis::Error::Response(content)
46                    if content.status == StatusCode::NOT_FOUND =>
47                {
48                    CipherAdminGetAttachmentDownloadUrlError::NotFound
49                }
50                other => other.into(),
51            })?;
52
53        response.url.ok_or_else(|| MissingFieldError("url").into())
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use std::sync::Arc;
60
61    use bitwarden_api_api::{apis::ApiClient, models::AttachmentResponseModel};
62    use bitwarden_core::client::ApiConfigurations;
63    use reqwest::StatusCode;
64
65    use super::*;
66
67    const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
68    const TEST_ATTACHMENT_ID: &str = "uf7bkexzag04d3cw04jsbqqkbpbwhxs0";
69    const TEST_API_URL: &str = "http://localhost:4000/attachments/test/api";
70
71    fn client_with_api(api_client: ApiClient) -> AttachmentAdminClient {
72        AttachmentAdminClient {
73            api_configurations: Arc::new(ApiConfigurations::from_api_client(api_client)),
74        }
75    }
76
77    #[tokio::test]
78    async fn returns_url_from_api_response() {
79        let api_client = ApiClient::new_mocked(|mock| {
80            mock.ciphers_api
81                .expect_get_attachment_data_admin()
82                .returning(|id, attachment_id| {
83                    assert_eq!(&id.to_string(), TEST_CIPHER_ID);
84                    assert_eq!(attachment_id, TEST_ATTACHMENT_ID);
85                    Ok(AttachmentResponseModel {
86                        id: Some(TEST_ATTACHMENT_ID.to_string()),
87                        url: Some(TEST_API_URL.to_string()),
88                        ..Default::default()
89                    })
90                });
91        });
92
93        let client = client_with_api(api_client);
94        let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
95        let url = client
96            .get_attachment_download_url(cipher_id, TEST_ATTACHMENT_ID.to_string())
97            .await
98            .unwrap();
99
100        assert_eq!(url, TEST_API_URL);
101    }
102
103    #[tokio::test]
104    async fn returns_missing_field_when_response_has_no_url() {
105        let api_client = ApiClient::new_mocked(|mock| {
106            mock.ciphers_api
107                .expect_get_attachment_data_admin()
108                .returning(|_id, _attachment_id| {
109                    Ok(AttachmentResponseModel {
110                        id: Some(TEST_ATTACHMENT_ID.to_string()),
111                        url: None,
112                        ..Default::default()
113                    })
114                });
115        });
116
117        let client = client_with_api(api_client);
118        let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
119        let err = client
120            .get_attachment_download_url(cipher_id, TEST_ATTACHMENT_ID.to_string())
121            .await
122            .unwrap_err();
123
124        assert!(matches!(
125            err,
126            CipherAdminGetAttachmentDownloadUrlError::MissingField(_)
127        ));
128    }
129
130    #[tokio::test]
131    async fn returns_not_found_on_404() {
132        let api_client = ApiClient::new_mocked(|mock| {
133            mock.ciphers_api
134                .expect_get_attachment_data_admin()
135                .returning(|_id, _attachment_id| {
136                    Err(bitwarden_api_api::apis::Error::Response(
137                        bitwarden_api_api::apis::ResponseContent {
138                            status: StatusCode::NOT_FOUND,
139                            message: String::new(),
140                        },
141                    ))
142                });
143        });
144
145        let client = client_with_api(api_client);
146        let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
147        let err = client
148            .get_attachment_download_url(cipher_id, TEST_ATTACHMENT_ID.to_string())
149            .await
150            .unwrap_err();
151
152        assert!(matches!(
153            err,
154            CipherAdminGetAttachmentDownloadUrlError::NotFound
155        ));
156    }
157
158    #[tokio::test]
159    async fn propagates_non_404_api_errors() {
160        let api_client = ApiClient::new_mocked(|mock| {
161            mock.ciphers_api
162                .expect_get_attachment_data_admin()
163                .returning(|_id, _attachment_id| {
164                    Err(bitwarden_api_api::apis::Error::Response(
165                        bitwarden_api_api::apis::ResponseContent {
166                            status: StatusCode::INTERNAL_SERVER_ERROR,
167                            message: "bitwarden".to_string(),
168                        },
169                    ))
170                });
171        });
172
173        let client = client_with_api(api_client);
174        let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
175        let err = client
176            .get_attachment_download_url(cipher_id, TEST_ATTACHMENT_ID.to_string())
177            .await
178            .unwrap_err();
179
180        assert!(matches!(
181            err,
182            CipherAdminGetAttachmentDownloadUrlError::Api(_)
183        ));
184    }
185}