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