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