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