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