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