bitwarden_vault/folder/
edit.rs1use bitwarden_core::{ApiError, MissingFieldError, key_management::KeyIds};
2use bitwarden_crypto::{CryptoError, KeyStore};
3use bitwarden_error::bitwarden_error;
4use bitwarden_state::repository::{Repository, RepositoryError};
5use thiserror::Error;
6#[cfg(feature = "wasm")]
7use wasm_bindgen::prelude::*;
8
9use crate::{
10 Folder, FolderAddEditRequest, FolderId, FolderView, ItemNotFoundError, VaultParseError,
11};
12
13#[allow(missing_docs)]
14#[bitwarden_error(flat)]
15#[derive(Debug, Error)]
16pub enum EditFolderError {
17 #[error(transparent)]
18 ItemNotFound(#[from] ItemNotFoundError),
19 #[error(transparent)]
20 Crypto(#[from] CryptoError),
21 #[error(transparent)]
22 Api(#[from] ApiError),
23 #[error(transparent)]
24 VaultParse(#[from] VaultParseError),
25 #[error(transparent)]
26 MissingField(#[from] MissingFieldError),
27 #[error(transparent)]
28 Repository(#[from] RepositoryError),
29 #[error(transparent)]
30 Uuid(#[from] uuid::Error),
31}
32
33pub(super) async fn edit_folder<R: Repository<Folder> + ?Sized>(
34 key_store: &KeyStore<KeyIds>,
35 api_client: &bitwarden_api_api::apis::ApiClient,
36 repository: &R,
37 folder_id: FolderId,
38 request: FolderAddEditRequest,
39) -> Result<FolderView, EditFolderError> {
40 let id = folder_id.to_string();
41
42 repository.get(id.clone()).await?.ok_or(ItemNotFoundError)?;
44
45 let folder_request = key_store.encrypt(request)?;
46
47 let resp = api_client
48 .folders_api()
49 .put(&id, Some(folder_request))
50 .await
51 .map_err(ApiError::from)?;
52
53 let folder: Folder = resp.try_into()?;
54
55 debug_assert!(folder.id.unwrap_or_default() == folder_id);
56
57 repository.set(id, folder.clone()).await?;
58
59 Ok(key_store.decrypt(&folder)?)
60}
61
62#[cfg(test)]
63mod tests {
64 use bitwarden_api_api::{apis::ApiClient, models::FolderResponseModel};
65 use bitwarden_core::key_management::SymmetricKeyId;
66 use bitwarden_crypto::{PrimitiveEncryptable, SymmetricKeyAlgorithm};
67 use bitwarden_test::MemoryRepository;
68 use uuid::uuid;
69
70 use super::*;
71 use crate::FolderId;
72
73 async fn repository_add_folder(
74 repository: &MemoryRepository<Folder>,
75 store: &KeyStore<KeyIds>,
76 folder_id: FolderId,
77 name: &str,
78 ) {
79 let folder = Folder {
80 id: Some(folder_id),
81 name: name
82 .encrypt(&mut store.context(), SymmetricKeyId::User)
83 .unwrap(),
84 revision_date: "2024-01-01T00:00:00Z".parse().unwrap(),
85 };
86 repository.set(folder_id.to_string(), folder).await.unwrap();
87 }
88
89 #[tokio::test]
90 async fn test_edit_folder() {
91 let store: KeyStore<KeyIds> = KeyStore::default();
92 {
93 let mut ctx = store.context_mut();
94 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
95 ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User)
96 .unwrap();
97 }
98
99 let folder_id: FolderId = "25afb11c-9c95-4db5-8bac-c21cb204a3f1".parse().unwrap();
100
101 let api_client = ApiClient::new_mocked(move |mock| {
102 mock.folders_api
103 .expect_put()
104 .returning(move |id, model| {
105 assert_eq!(id, folder_id.to_string());
106 Ok(FolderResponseModel {
107 object: Some("folder".to_string()),
108 id: Some(folder_id.into()),
109 name: Some(model.unwrap().name),
110 revision_date: Some("2025-01-01T00:00:00Z".to_string()),
111 })
112 })
113 .once();
114 });
115
116 let repository = MemoryRepository::<Folder>::default();
117 repository_add_folder(&repository, &store, folder_id, "old_name").await;
118
119 let result = edit_folder(
120 &store,
121 &api_client,
122 &repository,
123 folder_id,
124 FolderAddEditRequest {
125 name: "test".to_string(),
126 },
127 )
128 .await
129 .unwrap();
130
131 assert_eq!(
132 result,
133 FolderView {
134 id: Some(folder_id),
135 name: "test".to_string(),
136 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
137 }
138 );
139 }
140
141 #[tokio::test]
142 async fn test_edit_folder_does_not_exist() {
143 let store: KeyStore<KeyIds> = KeyStore::default();
144
145 let repository = MemoryRepository::<Folder>::default();
146 let folder_id = FolderId::new(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"));
147
148 let api_client = ApiClient::new_mocked(|_| {});
149
150 let result = edit_folder(
151 &store,
152 &api_client,
153 &repository,
154 folder_id,
155 FolderAddEditRequest {
156 name: "test".to_string(),
157 },
158 )
159 .await;
160
161 assert!(result.is_err());
162 assert!(matches!(
163 result.unwrap_err(),
164 EditFolderError::ItemNotFound(_)
165 ));
166 }
167
168 #[tokio::test]
169 async fn test_edit_folder_http_error() {
170 let store: KeyStore<KeyIds> = KeyStore::default();
171 {
172 let mut ctx = store.context_mut();
173 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
174 ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User)
175 .unwrap();
176 }
177
178 let folder_id: FolderId = "25afb11c-9c95-4db5-8bac-c21cb204a3f1".parse().unwrap();
179
180 let api_client = ApiClient::new_mocked(move |mock| {
181 mock.folders_api.expect_put().returning(move |_id, _model| {
182 Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other(
183 "Simulated error",
184 )))
185 });
186 });
187
188 let repository = MemoryRepository::<Folder>::default();
189 repository_add_folder(&repository, &store, folder_id, "old_name").await;
190
191 let result = edit_folder(
192 &store,
193 &api_client,
194 &repository,
195 folder_id,
196 FolderAddEditRequest {
197 name: "test".to_string(),
198 },
199 )
200 .await;
201
202 assert!(result.is_err());
203 assert!(matches!(result.unwrap_err(), EditFolderError::Api(_)));
204 }
205}