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#[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 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}