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, 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<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
67pub(super) async fn create_folder<R: Repository<Folder> + ?Sized>(
68    key_store: &KeyStore<KeySlotIds>,
69    api_client: &bitwarden_api_api::apis::ApiClient,
70    repository: &R,
71    request: FolderAddEditRequest,
72) -> Result<FolderView, CreateFolderError> {
73    let folder_request = key_store.encrypt(request)?;
74
75    let resp = api_client
76        .folders_api()
77        .post(Some(folder_request))
78        .await
79        .map_err(ApiError::from)?;
80
81    let folder: Folder = resp.try_into()?;
82
83    repository.set(require!(folder.id), folder.clone()).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<KeySlotIds> = 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, SymmetricKeySlotId::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        let folder_id = FolderId::new(folder_id);
138
139        assert_eq!(
140            result,
141            FolderView {
142                id: Some(folder_id),
143                name: "test".to_string(),
144                revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
145            }
146        );
147
148        // Confirm the folder was stored in the repository
149        assert_eq!(
150            store
151                .decrypt(&repository.get(folder_id).await.unwrap().unwrap())
152                .unwrap(),
153            result
154        );
155    }
156
157    #[tokio::test]
158    async fn test_create_folder_http_error() {
159        let store: KeyStore<KeySlotIds> = KeyStore::default();
160        {
161            let mut ctx = store.context_mut();
162            let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
163            ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
164                .unwrap();
165        }
166
167        let api_client = ApiClient::new_mocked(move |mock| {
168            mock.folders_api.expect_post().returning(move |_model| {
169                Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other(
170                    "Simulated error",
171                )))
172            });
173        });
174
175        let repository = MemoryRepository::<Folder>::default();
176
177        let result = create_folder(
178            &store,
179            &api_client,
180            &repository,
181            FolderAddEditRequest {
182                name: "test".to_string(),
183            },
184        )
185        .await;
186
187        assert!(result.is_err());
188        assert!(matches!(result.unwrap_err(), CreateFolderError::Api(_)));
189    }
190}