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