bitwarden_vault/folder/
create.rs

1use bitwarden_api_api::models::FolderRequestModel;
2use bitwarden_core::{
3    ApiError, MissingFieldError,
4    key_management::{KeyIds, SymmetricKeyId},
5    require,
6};
7use bitwarden_crypto::{
8    CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, PrimitiveEncryptable,
9};
10use bitwarden_error::bitwarden_error;
11use bitwarden_state::repository::{Repository, RepositoryError};
12use serde::{Deserialize, Serialize};
13use thiserror::Error;
14#[cfg(feature = "wasm")]
15use tsify::Tsify;
16#[cfg(feature = "wasm")]
17use wasm_bindgen::prelude::*;
18
19use crate::{Folder, FolderView, VaultParseError};
20
21/// Request to add or edit a folder.
22#[derive(Serialize, Deserialize, Debug)]
23#[serde(rename_all = "camelCase")]
24#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
25#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
26pub struct FolderAddEditRequest {
27    /// The new name of the folder.
28    pub name: String,
29}
30
31impl CompositeEncryptable<KeyIds, SymmetricKeyId, FolderRequestModel> for FolderAddEditRequest {
32    fn encrypt_composite(
33        &self,
34        ctx: &mut KeyStoreContext<KeyIds>,
35        key: SymmetricKeyId,
36    ) -> Result<FolderRequestModel, CryptoError> {
37        Ok(FolderRequestModel {
38            name: self.name.encrypt(ctx, key)?.to_string(),
39        })
40    }
41}
42
43impl IdentifyKey<SymmetricKeyId> for FolderAddEditRequest {
44    fn key_identifier(&self) -> SymmetricKeyId {
45        SymmetricKeyId::User
46    }
47}
48
49#[allow(missing_docs)]
50#[bitwarden_error(flat)]
51#[derive(Debug, Error)]
52pub enum CreateFolderError {
53    #[error(transparent)]
54    Crypto(#[from] CryptoError),
55    #[error(transparent)]
56    Api(#[from] ApiError),
57    #[error(transparent)]
58    VaultParse(#[from] VaultParseError),
59    #[error(transparent)]
60    MissingField(#[from] MissingFieldError),
61    #[error(transparent)]
62    Repository(#[from] RepositoryError),
63}
64
65pub(super) async fn create_folder<R: Repository<Folder> + ?Sized>(
66    key_store: &KeyStore<KeyIds>,
67    api_client: &bitwarden_api_api::apis::ApiClient,
68    repository: &R,
69    request: FolderAddEditRequest,
70) -> Result<FolderView, CreateFolderError> {
71    let folder_request = key_store.encrypt(request)?;
72
73    let resp = api_client
74        .folders_api()
75        .post(Some(folder_request))
76        .await
77        .map_err(ApiError::from)?;
78
79    let folder: Folder = resp.try_into()?;
80
81    repository
82        .set(require!(folder.id).to_string(), folder.clone())
83        .await?;
84
85    Ok(key_store.decrypt(&folder)?)
86}
87
88#[cfg(test)]
89mod tests {
90    use bitwarden_api_api::{apis::ApiClient, models::FolderResponseModel};
91    use bitwarden_crypto::SymmetricCryptoKey;
92    use bitwarden_test::MemoryRepository;
93    use uuid::uuid;
94
95    use super::*;
96    use crate::FolderId;
97
98    #[tokio::test]
99    async fn test_create_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 = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
108
109        let api_client = ApiClient::new_mocked(move |mock| {
110            mock.folders_api
111                .expect_post()
112                .returning(move |model| {
113                    Ok(FolderResponseModel {
114                        id: Some(folder_id),
115                        name: Some(model.unwrap().name),
116                        revision_date: Some("2025-01-01T00:00:00Z".to_string()),
117                        object: Some("folder".to_string()),
118                    })
119                })
120                .once();
121        });
122
123        let repository = MemoryRepository::<Folder>::default();
124
125        let result = create_folder(
126            &store,
127            &api_client,
128            &repository,
129            FolderAddEditRequest {
130                name: "test".to_string(),
131            },
132        )
133        .await
134        .unwrap();
135
136        assert_eq!(
137            result,
138            FolderView {
139                id: Some(FolderId::new(folder_id)),
140                name: "test".to_string(),
141                revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
142            }
143        );
144
145        // Confirm the folder was stored in the repository
146        assert_eq!(
147            store
148                .decrypt(
149                    &repository
150                        .get(folder_id.to_string())
151                        .await
152                        .unwrap()
153                        .unwrap()
154                )
155                .unwrap(),
156            result
157        );
158    }
159
160    #[tokio::test]
161    async fn test_create_folder_http_error() {
162        let store: KeyStore<KeyIds> = KeyStore::default();
163        #[allow(deprecated)]
164        let _ = store.context_mut().set_symmetric_key(
165            SymmetricKeyId::User,
166            SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
167        );
168
169        let api_client = ApiClient::new_mocked(move |mock| {
170            mock.folders_api.expect_post().returning(move |_model| {
171                Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other(
172                    "Simulated error",
173                )))
174            });
175        });
176
177        let repository = MemoryRepository::<Folder>::default();
178
179        let result = create_folder(
180            &store,
181            &api_client,
182            &repository,
183            FolderAddEditRequest {
184                name: "test".to_string(),
185            },
186        )
187        .await;
188
189        assert!(result.is_err());
190        assert!(matches!(result.unwrap_err(), CreateFolderError::Api(_)));
191    }
192}