bitwarden_vault/folder/
edit.rs

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