Skip to main content

bitwarden_vault/folder/
create.rs

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