Skip to main content

bitwarden_vault/cipher/cipher_client/
move_many.rs

1use bitwarden_api_api::models::CipherBulkMoveRequestModel;
2use bitwarden_core::ApiError;
3use bitwarden_error::bitwarden_error;
4use bitwarden_state::repository::{RepositoryError, RepositoryOption};
5use thiserror::Error;
6#[cfg(feature = "wasm")]
7use wasm_bindgen::prelude::wasm_bindgen;
8
9use crate::{CipherId, CiphersClient, FolderId};
10
11#[allow(missing_docs)]
12#[bitwarden_error(flat)]
13#[derive(Debug, Error)]
14pub enum MoveCipherError {
15    #[error(transparent)]
16    Api(#[from] ApiError),
17    #[error(transparent)]
18    Repository(#[from] RepositoryError),
19}
20
21impl<T> From<bitwarden_api_api::apis::Error<T>> for MoveCipherError {
22    fn from(value: bitwarden_api_api::apis::Error<T>) -> Self {
23        Self::Api(value.into())
24    }
25}
26
27#[cfg_attr(feature = "wasm", wasm_bindgen)]
28impl CiphersClient {
29    /// Moves multiple [`Cipher`](crate::Cipher) objects to a folder, or clears their folder when
30    /// `folder_id` is `None`.
31    pub async fn move_many(
32        &self,
33        cipher_ids: Vec<CipherId>,
34        folder_id: Option<FolderId>,
35    ) -> Result<(), MoveCipherError> {
36        self.api_configurations
37            .api_client
38            .ciphers_api()
39            .move_many(Some(CipherBulkMoveRequestModel {
40                ids: cipher_ids.iter().map(|id| id.to_string()).collect(),
41                folder_id: folder_id.map(|id| id.to_string()),
42            }))
43            .await?;
44
45        let repository = self.repository.require()?;
46
47        let mut updated_ciphers = Vec::new();
48        for cipher_id in cipher_ids {
49            if let Some(mut cipher) = repository.get(cipher_id).await? {
50                cipher.folder_id = folder_id;
51                updated_ciphers.push((cipher_id, cipher));
52            }
53        }
54        repository.set_bulk(updated_ciphers).await?;
55        Ok(())
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use std::sync::Arc;
62
63    use bitwarden_api_api::apis::ApiClient;
64    use bitwarden_core::{
65        client::ApiConfigurations, key_management::create_test_crypto_with_user_key,
66    };
67    use bitwarden_crypto::SymmetricCryptoKey;
68    use bitwarden_state::repository::Repository;
69    use bitwarden_test::MemoryRepository;
70
71    use crate::{Cipher, CipherId, CiphersClient, FolderId};
72
73    const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
74    const TEST_CIPHER_ID_2: &str = "6faa9684-c793-4a2d-8a12-b33900187098";
75    const TEST_FOLDER_ID: &str = "7faa9684-c793-4a2d-8a12-b33900187099";
76
77    fn generate_test_cipher() -> Cipher {
78        Cipher {
79            id: TEST_CIPHER_ID.parse().ok(),
80            name: "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".parse().unwrap(),
81            r#type: crate::CipherType::Login,
82            notes: Default::default(),
83            organization_id: Default::default(),
84            folder_id: Default::default(),
85            favorite: Default::default(),
86            reprompt: Default::default(),
87            fields: Default::default(),
88            collection_ids: Default::default(),
89            key: Default::default(),
90            login: Default::default(),
91            identity: Default::default(),
92            card: Default::default(),
93            secure_note: Default::default(),
94            ssh_key: Default::default(),
95            bank_account: Default::default(),
96            drivers_license: Default::default(),
97            passport: Default::default(),
98            organization_use_totp: Default::default(),
99            edit: Default::default(),
100            permissions: Default::default(),
101            view_password: Default::default(),
102            local_data: Default::default(),
103            attachments: Default::default(),
104            password_history: Default::default(),
105            creation_date: Default::default(),
106            deleted_date: Default::default(),
107            revision_date: Default::default(),
108            archived_date: Default::default(),
109            data: Default::default(),
110        }
111    }
112
113    fn create_test_client(api_client: ApiClient) -> (CiphersClient, Arc<MemoryRepository<Cipher>>) {
114        let repository = Arc::new(MemoryRepository::<Cipher>::default());
115        #[allow(deprecated)]
116        let client = CiphersClient {
117            key_store: create_test_crypto_with_user_key(
118                SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
119            ),
120            api_configurations: Arc::new(ApiConfigurations::from_api_client(api_client)),
121            repository: Some(repository.clone() as Arc<dyn Repository<Cipher>>),
122            client: bitwarden_core::Client::new_test(None),
123        };
124        (client, repository)
125    }
126
127    #[tokio::test]
128    async fn test_move_many_updates_folder_id() {
129        let api_client = ApiClient::new_mocked(|mock| {
130            mock.ciphers_api.expect_move_many().returning(|_| Ok(()));
131        });
132
133        let (client, repository) = create_test_client(api_client);
134
135        let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
136        let cipher_id_2: CipherId = TEST_CIPHER_ID_2.parse().unwrap();
137        let folder_id: FolderId = TEST_FOLDER_ID.parse().unwrap();
138
139        repository
140            .set(cipher_id, generate_test_cipher())
141            .await
142            .unwrap();
143        let mut cipher_2 = generate_test_cipher();
144        cipher_2.id = Some(cipher_id_2);
145        repository.set(cipher_id_2, cipher_2).await.unwrap();
146
147        client
148            .move_many(vec![cipher_id, cipher_id_2], Some(folder_id))
149            .await
150            .unwrap();
151
152        let c1: Cipher = repository.get(cipher_id).await.unwrap().unwrap();
153        let c2: Cipher = repository.get(cipher_id_2).await.unwrap().unwrap();
154        assert_eq!(c1.folder_id, Some(folder_id));
155        assert_eq!(c2.folder_id, Some(folder_id));
156    }
157
158    #[tokio::test]
159    async fn test_move_many_clears_folder_id() {
160        let api_client = ApiClient::new_mocked(|mock| {
161            mock.ciphers_api.expect_move_many().returning(|_| Ok(()));
162        });
163
164        let (client, repository) = create_test_client(api_client);
165
166        let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
167        let folder_id: FolderId = TEST_FOLDER_ID.parse().unwrap();
168
169        let mut cipher = generate_test_cipher();
170        cipher.folder_id = Some(folder_id);
171        repository.set(cipher_id, cipher).await.unwrap();
172
173        client.move_many(vec![cipher_id], None).await.unwrap();
174
175        let c: Cipher = repository.get(cipher_id).await.unwrap().unwrap();
176        assert!(c.folder_id.is_none());
177    }
178
179    #[tokio::test]
180    async fn test_move_many_skips_missing_ciphers() {
181        let api_client = ApiClient::new_mocked(|mock| {
182            mock.ciphers_api.expect_move_many().returning(|_| Ok(()));
183        });
184
185        let (client, _repository) = create_test_client(api_client);
186        let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
187        let folder_id: FolderId = TEST_FOLDER_ID.parse().unwrap();
188
189        let result = client.move_many(vec![cipher_id], Some(folder_id)).await;
190        assert!(result.is_ok());
191    }
192}