Skip to main content

bitwarden_vault/cipher/attachment_client/
delete.rs

1use 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    /// Deletes an attachment from a cipher, and updates the local repository with the new
27    /// cipher data returned from the API.
28    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}