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::SymmetricKeyAlgorithm;
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        {
102            let mut ctx = store.context_mut();
103            let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
104            ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User)
105                .unwrap();
106        }
107
108        let folder_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
109
110        let api_client = ApiClient::new_mocked(move |mock| {
111            mock.folders_api
112                .expect_post()
113                .returning(move |model| {
114                    Ok(FolderResponseModel {
115                        id: Some(folder_id),
116                        name: Some(model.unwrap().name),
117                        revision_date: Some("2025-01-01T00:00:00Z".to_string()),
118                        object: Some("folder".to_string()),
119                    })
120                })
121                .once();
122        });
123
124        let repository = MemoryRepository::<Folder>::default();
125
126        let result = create_folder(
127            &store,
128            &api_client,
129            &repository,
130            FolderAddEditRequest {
131                name: "test".to_string(),
132            },
133        )
134        .await
135        .unwrap();
136
137        assert_eq!(
138            result,
139            FolderView {
140                id: Some(FolderId::new(folder_id)),
141                name: "test".to_string(),
142                revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
143            }
144        );
145
146        // Confirm the folder was stored in the repository
147        assert_eq!(
148            store
149                .decrypt(
150                    &repository
151                        .get(folder_id.to_string())
152                        .await
153                        .unwrap()
154                        .unwrap()
155                )
156                .unwrap(),
157            result
158        );
159    }
160
161    #[tokio::test]
162    async fn test_create_folder_http_error() {
163        let store: KeyStore<KeyIds> = KeyStore::default();
164        {
165            let mut ctx = store.context_mut();
166            let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
167            ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User)
168                .unwrap();
169        }
170
171        let api_client = ApiClient::new_mocked(move |mock| {
172            mock.folders_api.expect_post().returning(move |_model| {
173                Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other(
174                    "Simulated error",
175                )))
176            });
177        });
178
179        let repository = MemoryRepository::<Folder>::default();
180
181        let result = create_folder(
182            &store,
183            &api_client,
184            &repository,
185            FolderAddEditRequest {
186                name: "test".to_string(),
187            },
188        )
189        .await;
190
191        assert!(result.is_err());
192        assert!(matches!(result.unwrap_err(), CreateFolderError::Api(_)));
193    }
194}