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.set(require!(folder.id), folder.clone()).await?;
82
83    Ok(key_store.decrypt(&folder)?)
84}
85
86#[cfg(test)]
87mod tests {
88    use bitwarden_api_api::{apis::ApiClient, models::FolderResponseModel};
89    use bitwarden_crypto::SymmetricKeyAlgorithm;
90    use bitwarden_test::MemoryRepository;
91    use uuid::uuid;
92
93    use super::*;
94    use crate::FolderId;
95
96    #[tokio::test]
97    async fn test_create_folder() {
98        let store: KeyStore<KeyIds> = KeyStore::default();
99        {
100            let mut ctx = store.context_mut();
101            let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
102            ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User)
103                .unwrap();
104        }
105
106        let folder_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
107
108        let api_client = ApiClient::new_mocked(move |mock| {
109            mock.folders_api
110                .expect_post()
111                .returning(move |model| {
112                    Ok(FolderResponseModel {
113                        id: Some(folder_id),
114                        name: Some(model.unwrap().name),
115                        revision_date: Some("2025-01-01T00:00:00Z".to_string()),
116                        object: Some("folder".to_string()),
117                    })
118                })
119                .once();
120        });
121
122        let repository = MemoryRepository::<Folder>::default();
123
124        let result = create_folder(
125            &store,
126            &api_client,
127            &repository,
128            FolderAddEditRequest {
129                name: "test".to_string(),
130            },
131        )
132        .await
133        .unwrap();
134
135        let folder_id = FolderId::new(folder_id);
136
137        assert_eq!(
138            result,
139            FolderView {
140                id: Some(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(&repository.get(folder_id).await.unwrap().unwrap())
150                .unwrap(),
151            result
152        );
153    }
154
155    #[tokio::test]
156    async fn test_create_folder_http_error() {
157        let store: KeyStore<KeyIds> = KeyStore::default();
158        {
159            let mut ctx = store.context_mut();
160            let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
161            ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User)
162                .unwrap();
163        }
164
165        let api_client = ApiClient::new_mocked(move |mock| {
166            mock.folders_api.expect_post().returning(move |_model| {
167                Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other(
168                    "Simulated error",
169                )))
170            });
171        });
172
173        let repository = MemoryRepository::<Folder>::default();
174
175        let result = create_folder(
176            &store,
177            &api_client,
178            &repository,
179            FolderAddEditRequest {
180                name: "test".to_string(),
181            },
182        )
183        .await;
184
185        assert!(result.is_err());
186        assert!(matches!(result.unwrap_err(), CreateFolderError::Api(_)));
187    }
188}