Skip to main content

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