bitwarden_vault/cipher/cipher_client/
delete_attachment.rs1use bitwarden_core::{ApiError, MissingFieldError};
2use bitwarden_error::bitwarden_error;
3use bitwarden_state::repository::{Repository, RepositoryError};
4use thiserror::Error;
5#[cfg(feature = "wasm")]
6use wasm_bindgen::prelude::wasm_bindgen;
7
8use crate::{Cipher, CipherId, CiphersClient, 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
30pub async fn delete_attachment<R: Repository<Cipher> + ?Sized>(
33 cipher_id: CipherId,
34 attachment_id: &str,
35 api_client: &bitwarden_api_api::apis::ApiClient,
36 repository: &R,
37) -> Result<Cipher, CipherDeleteAttachmentError> {
38 let api = api_client.ciphers_api();
39
40 let response = api
41 .delete_attachment(cipher_id.into(), attachment_id)
42 .await?;
43
44 let existing_cipher = repository.get(cipher_id).await?;
45 let cipher_response = response
46 .cipher
47 .map(|c| *c)
48 .ok_or(MissingFieldError("cipher"))?;
49 let cipher = cipher_response.merge_with_cipher(existing_cipher)?;
50
51 repository.set(cipher_id, cipher.clone()).await?;
52
53 Ok(cipher)
54}
55
56#[cfg_attr(feature = "wasm", wasm_bindgen)]
57impl CiphersClient {
58 pub async fn delete_attachment(
61 &self,
62 cipher_id: CipherId,
63 attachment_id: String,
64 ) -> Result<Cipher, CipherDeleteAttachmentError> {
65 let configs = self.client.internal.get_api_configurations();
66 delete_attachment(
67 cipher_id,
68 &attachment_id,
69 &configs.api_client,
70 &*self.get_repository()?,
71 )
72 .await
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use bitwarden_api_api::{
79 apis::ApiClient,
80 models::{CipherMiniResponseModel, DeleteAttachmentResponseModel},
81 };
82 use bitwarden_state::repository::Repository;
83 use bitwarden_test::MemoryRepository;
84
85 use super::*;
86 use crate::Attachment;
87
88 const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
89 const TEST_ATTACHMENT_ID: &str = "uf7bkexzag04d3cw04jsbqqkbpbwhxs0";
90
91 fn generate_test_cipher() -> Cipher {
92 Cipher {
93 id: TEST_CIPHER_ID.parse().ok(),
94 name: "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".parse().unwrap(),
95 r#type: crate::CipherType::Login,
96 attachments: Some(vec![Attachment {
97 id: Some(TEST_ATTACHMENT_ID.to_string()),
98 url: Some("http://localhost:4000/attachments/test".to_string()),
99 file_name: Some("2.mV50WiLq6duhwGbhM1TO0A==|dTufWNH8YTPP0EMlNLIpFA==|QHp+7OM8xHtEmCfc9QPXJ0Ro2BeakzvLgxJZ7NdLuDc=".parse().unwrap()),
100 key: None,
101 size: Some("65".to_string()),
102 size_name: Some("65 Bytes".to_string()),
103 }]),
104 notes: Default::default(),
105 organization_id: Default::default(),
106 folder_id: Default::default(),
107 favorite: Default::default(),
108 reprompt: Default::default(),
109 fields: Default::default(),
110 collection_ids: Default::default(),
111 key: Default::default(),
112 login: Default::default(),
113 identity: Default::default(),
114 card: Default::default(),
115 secure_note: Default::default(),
116 ssh_key: Default::default(),
117 bank_account: Default::default(),
118 organization_use_totp: Default::default(),
119 edit: Default::default(),
120 permissions: Default::default(),
121 view_password: Default::default(),
122 local_data: Default::default(),
123 password_history: Default::default(),
124 creation_date: Default::default(),
125 deleted_date: Default::default(),
126 revision_date: Default::default(),
127 archived_date: Default::default(),
128 data: Default::default(),
129 }
130 }
131
132 #[tokio::test]
133 async fn test_delete_attachment() {
134 let cipher = generate_test_cipher();
135 let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
136
137 let api_client = ApiClient::new_mocked(move |mock| {
138 mock.ciphers_api
139 .expect_delete_attachment()
140 .returning(move |id, attachment_id| {
141 assert_eq!(&id.to_string(), TEST_CIPHER_ID);
142 assert_eq!(attachment_id, TEST_ATTACHMENT_ID);
143 Ok(DeleteAttachmentResponseModel {
144 object: None,
145 cipher: Some(Box::new(CipherMiniResponseModel {
146 id: Some(TEST_CIPHER_ID.try_into().unwrap()),
147 name: Some("2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".to_string()),
148 r#type: Some(bitwarden_api_api::models::CipherType::Login),
149 creation_date: Some("2024-05-31T11:20:58.4566667Z".to_string()),
150 revision_date: Some("2024-05-31T11:20:58.4566667Z".to_string()),
151 attachments: None,
152 ..Default::default()
153 })),
154 })
155 });
156 });
157
158 let repository = MemoryRepository::<Cipher>::default();
159 repository.set(cipher_id, cipher).await.unwrap();
160
161 let result = delete_attachment(cipher_id, TEST_ATTACHMENT_ID, &api_client, &repository)
162 .await
163 .unwrap();
164
165 assert!(result.attachments.is_none());
167
168 let repo_cipher = repository.get(cipher_id).await.unwrap().unwrap();
170 assert!(repo_cipher.attachments.is_none());
171 }
172
173 #[tokio::test]
174 async fn test_delete_attachment_missing_cipher_in_response() {
175 let cipher = generate_test_cipher();
176 let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
177
178 let api_client = ApiClient::new_mocked(move |mock| {
179 mock.ciphers_api
180 .expect_delete_attachment()
181 .returning(move |_id, _attachment_id| {
182 Ok(DeleteAttachmentResponseModel {
183 object: None,
184 cipher: None,
185 })
186 });
187 });
188
189 let repository = MemoryRepository::<Cipher>::default();
190 repository.set(cipher_id, cipher).await.unwrap();
191
192 let result =
193 delete_attachment(cipher_id, TEST_ATTACHMENT_ID, &api_client, &repository).await;
194
195 assert!(
196 result.is_err(),
197 "Should fail when API response has no cipher"
198 );
199 }
200}