bitwarden_vault/cipher/cipher_client/
move_many.rs1use 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 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}