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