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