bitwarden_vault/cipher/attachment_client/
delete.rs1use bitwarden_core::{ApiError, MissingFieldError};
2use bitwarden_error::bitwarden_error;
3use bitwarden_state::repository::{RepositoryError, RepositoryOption};
4use thiserror::Error;
5#[cfg(feature = "wasm")]
6use wasm_bindgen::prelude::wasm_bindgen;
7
8use crate::{AttachmentsClient, Cipher, CipherId, VaultParseError, cipher::cipher::PartialCipher};
9
10#[allow(missing_docs)]
11#[bitwarden_error(flat)]
12#[derive(Debug, Error)]
13pub enum CipherDeleteAttachmentError {
14 #[error(transparent)]
15 Api(#[from] ApiError),
16 #[error(transparent)]
17 Repository(#[from] RepositoryError),
18 #[error(transparent)]
19 MissingField(#[from] MissingFieldError),
20 #[error(transparent)]
21 VaultParse(#[from] VaultParseError),
22}
23
24impl<T> From<bitwarden_api_api::apis::Error<T>> for CipherDeleteAttachmentError {
25 fn from(value: bitwarden_api_api::apis::Error<T>) -> Self {
26 Self::Api(value.into())
27 }
28}
29
30#[cfg_attr(feature = "wasm", wasm_bindgen)]
31impl AttachmentsClient {
32 pub async fn delete_attachment(
35 &self,
36 cipher_id: CipherId,
37 attachment_id: String,
38 ) -> Result<Cipher, CipherDeleteAttachmentError> {
39 let repository = self.repository.require()?;
40
41 let response = self
42 .api_configurations
43 .api_client
44 .ciphers_api()
45 .delete_attachment(cipher_id.into(), &attachment_id)
46 .await?;
47
48 let existing_cipher = repository.get(cipher_id).await?;
49 let cipher_response = response
50 .cipher
51 .map(|c| *c)
52 .ok_or(MissingFieldError("cipher"))?;
53 let cipher = cipher_response.merge_with_cipher(existing_cipher)?;
54
55 repository.set(cipher_id, cipher.clone()).await?;
56
57 Ok(cipher)
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use std::sync::Arc;
64
65 use bitwarden_api_api::{
66 apis::ApiClient,
67 models::{CipherMiniResponseModel, DeleteAttachmentResponseModel},
68 };
69 use bitwarden_core::{client::ApiConfigurations, key_management::KeySlotIds};
70 use bitwarden_crypto::KeyStore;
71 use bitwarden_state::repository::Repository;
72 use bitwarden_test::MemoryRepository;
73
74 use super::*;
75 use crate::{Attachment, CipherRepromptType, CipherType};
76
77 const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
78 const TEST_ATTACHMENT_ID: &str = "uf7bkexzag04d3cw04jsbqqkbpbwhxs0";
79 const TEST_CIPHER_NAME: &str = "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=";
80 const TEST_FILE_NAME: &str = "2.mV50WiLq6duhwGbhM1TO0A==|dTufWNH8YTPP0EMlNLIpFA==|QHp+7OM8xHtEmCfc9QPXJ0Ro2BeakzvLgxJZ7NdLuDc=";
81
82 fn client_with_api_and_repo(
83 api_client: ApiClient,
84 repository: MemoryRepository<Cipher>,
85 ) -> AttachmentsClient {
86 AttachmentsClient {
87 key_store: KeyStore::<KeySlotIds>::default(),
88 api_configurations: Arc::new(ApiConfigurations::from_api_client(api_client)),
89 repository: Some(Arc::new(repository)),
90 http_client: reqwest::Client::new(),
91 }
92 }
93
94 fn test_cipher() -> Cipher {
95 Cipher {
96 id: TEST_CIPHER_ID.parse().ok(),
97 name: Some(TEST_CIPHER_NAME.parse().unwrap()),
98 r#type: CipherType::Login,
99 attachments: Some(vec![Attachment {
100 id: Some(TEST_ATTACHMENT_ID.to_string()),
101 url: Some("http://localhost:4000/attachments/test".to_string()),
102 file_name: Some(TEST_FILE_NAME.parse().unwrap()),
103 key: None,
104 size: Some("65".to_string()),
105 size_name: Some("65 Bytes".to_string()),
106 }]),
107 organization_id: None,
108 folder_id: None,
109 collection_ids: vec![],
110 key: None,
111 notes: None,
112 login: None,
113 identity: None,
114 card: None,
115 secure_note: None,
116 ssh_key: None,
117 bank_account: None,
118 drivers_license: None,
119 passport: None,
120 favorite: false,
121 reprompt: CipherRepromptType::None,
122 organization_use_totp: true,
123 edit: true,
124 permissions: None,
125 view_password: true,
126 local_data: None,
127 fields: None,
128 password_history: None,
129 creation_date: "2024-05-31T11:20:58.4566667Z".parse().unwrap(),
130 deleted_date: None,
131 revision_date: "2024-05-31T11:20:58.4566667Z".parse().unwrap(),
132 archived_date: None,
133 data: None,
134 }
135 }
136
137 #[tokio::test]
138 async fn returns_updated_cipher_on_success() {
139 let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
140 let api_client = ApiClient::new_mocked(|mock| {
141 mock.ciphers_api
142 .expect_delete_attachment()
143 .returning(|id, attachment_id| {
144 assert_eq!(&id.to_string(), TEST_CIPHER_ID);
145 assert_eq!(attachment_id, TEST_ATTACHMENT_ID);
146 Ok(DeleteAttachmentResponseModel {
147 object: None,
148 cipher: Some(Box::new(CipherMiniResponseModel {
149 id: Some(TEST_CIPHER_ID.try_into().unwrap()),
150 name: Some(TEST_CIPHER_NAME.to_string()),
151 r#type: Some(bitwarden_api_api::models::CipherType::Login),
152 creation_date: Some("2024-05-31T11:20:58.4566667Z".to_string()),
153 revision_date: Some("2024-05-31T11:20:58.4566667Z".to_string()),
154 attachments: None,
155 ..Default::default()
156 })),
157 })
158 });
159 });
160
161 let repository = MemoryRepository::<Cipher>::default();
162 repository.set(cipher_id, test_cipher()).await.unwrap();
163 let client = client_with_api_and_repo(api_client, repository);
164
165 let result = client
166 .delete_attachment(cipher_id, TEST_ATTACHMENT_ID.to_string())
167 .await
168 .unwrap();
169
170 assert!(result.attachments.is_none());
171
172 let repo_cipher = client
173 .repository
174 .as_ref()
175 .unwrap()
176 .get(cipher_id)
177 .await
178 .unwrap()
179 .unwrap();
180 assert!(repo_cipher.attachments.is_none());
181 }
182
183 #[tokio::test]
184 async fn errors_when_response_has_no_cipher() {
185 let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
186 let api_client = ApiClient::new_mocked(|mock| {
187 mock.ciphers_api
188 .expect_delete_attachment()
189 .returning(|_id, _attachment_id| {
190 Ok(DeleteAttachmentResponseModel {
191 object: None,
192 cipher: None,
193 })
194 });
195 });
196
197 let repository = MemoryRepository::<Cipher>::default();
198 repository.set(cipher_id, test_cipher()).await.unwrap();
199 let client = client_with_api_and_repo(api_client, repository);
200
201 let result = client
202 .delete_attachment(cipher_id, TEST_ATTACHMENT_ID.to_string())
203 .await;
204
205 assert!(result.is_err());
206 }
207}