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