bitwarden_vault/folder/
edit.rs

1use 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    // Verify the folder we're updating exists
43    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}