bitwarden_vault/folder/
create.rs

1use bitwarden_api_api::{apis::folders_api, models::FolderRequestModel};
2use bitwarden_core::{
3    key_management::{KeyIds, SymmetricKeyId},
4    require, ApiError, MissingFieldError,
5};
6use bitwarden_crypto::{
7    CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, PrimitiveEncryptable,
8};
9use bitwarden_error::bitwarden_error;
10use bitwarden_state::repository::{Repository, RepositoryError};
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13#[cfg(feature = "wasm")]
14use tsify::Tsify;
15#[cfg(feature = "wasm")]
16use wasm_bindgen::prelude::*;
17
18use crate::{Folder, FolderView, VaultParseError};
19
20/// Request to add or edit a folder.
21#[derive(Serialize, Deserialize, Debug)]
22#[serde(rename_all = "camelCase")]
23#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
24#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
25pub struct FolderAddEditRequest {
26    /// The new name of the folder.
27    pub name: String,
28}
29
30impl CompositeEncryptable<KeyIds, SymmetricKeyId, FolderRequestModel> for FolderAddEditRequest {
31    fn encrypt_composite(
32        &self,
33        ctx: &mut KeyStoreContext<KeyIds>,
34        key: SymmetricKeyId,
35    ) -> Result<FolderRequestModel, CryptoError> {
36        Ok(FolderRequestModel {
37            name: self.name.encrypt(ctx, key)?.to_string(),
38        })
39    }
40}
41
42impl IdentifyKey<SymmetricKeyId> for FolderAddEditRequest {
43    fn key_identifier(&self) -> SymmetricKeyId {
44        SymmetricKeyId::User
45    }
46}
47
48#[allow(missing_docs)]
49#[bitwarden_error(flat)]
50#[derive(Debug, Error)]
51pub enum CreateFolderError {
52    #[error(transparent)]
53    Crypto(#[from] CryptoError),
54    #[error(transparent)]
55    Api(#[from] ApiError),
56    #[error(transparent)]
57    VaultParse(#[from] VaultParseError),
58    #[error(transparent)]
59    MissingField(#[from] MissingFieldError),
60    #[error(transparent)]
61    RepositoryError(#[from] RepositoryError),
62}
63
64pub(super) async fn create_folder<R: Repository<Folder> + ?Sized>(
65    key_store: &KeyStore<KeyIds>,
66    api_config: &bitwarden_api_api::apis::configuration::Configuration,
67    repository: &R,
68    request: FolderAddEditRequest,
69) -> Result<FolderView, CreateFolderError> {
70    let folder_request = key_store.encrypt(request)?;
71    let resp = folders_api::folders_post(api_config, Some(folder_request))
72        .await
73        .map_err(ApiError::from)?;
74
75    let folder: Folder = resp.try_into()?;
76
77    repository
78        .set(require!(folder.id).to_string(), folder.clone())
79        .await?;
80
81    Ok(key_store.decrypt(&folder)?)
82}
83
84#[cfg(test)]
85mod tests {
86    use bitwarden_api_api::models::FolderResponseModel;
87    use bitwarden_crypto::SymmetricCryptoKey;
88    use bitwarden_test::{start_api_mock, MemoryRepository};
89    use uuid::uuid;
90    use wiremock::{matchers, Mock, Request, ResponseTemplate};
91
92    use super::*;
93    use crate::FolderId;
94
95    #[tokio::test]
96    async fn test_create_folder() {
97        let store: KeyStore<KeyIds> = KeyStore::default();
98        #[allow(deprecated)]
99        let _ = store.context_mut().set_symmetric_key(
100            SymmetricKeyId::User,
101            SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
102        );
103
104        let folder_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
105
106        let (_server, api_config) = start_api_mock(vec![Mock::given(matchers::path("/folders"))
107            .respond_with(move |req: &Request| {
108                let body: FolderRequestModel = req.body_json().unwrap();
109                ResponseTemplate::new(201).set_body_json(FolderResponseModel {
110                    id: Some(folder_id),
111                    name: Some(body.name),
112                    revision_date: Some("2025-01-01T00:00:00Z".to_string()),
113                    object: Some("folder".to_string()),
114                })
115            })
116            .expect(1)])
117        .await;
118
119        let repository = MemoryRepository::<Folder>::default();
120
121        let result = create_folder(
122            &store,
123            &api_config,
124            &repository,
125            FolderAddEditRequest {
126                name: "test".to_string(),
127            },
128        )
129        .await
130        .unwrap();
131
132        assert_eq!(
133            result,
134            FolderView {
135                id: Some(FolderId::new(folder_id)),
136                name: "test".to_string(),
137                revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
138            }
139        );
140
141        // Confirm the folder was stored in the repository
142        assert_eq!(
143            store
144                .decrypt(
145                    &repository
146                        .get(folder_id.to_string())
147                        .await
148                        .unwrap()
149                        .unwrap()
150                )
151                .unwrap(),
152            result
153        );
154    }
155
156    #[tokio::test]
157    async fn test_create_folder_http_error() {
158        let store: KeyStore<KeyIds> = KeyStore::default();
159        #[allow(deprecated)]
160        let _ = store.context_mut().set_symmetric_key(
161            SymmetricKeyId::User,
162            SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
163        );
164
165        let (_server, api_config) = start_api_mock(vec![
166            Mock::given(matchers::path("/folders")).respond_with(ResponseTemplate::new(500))
167        ])
168        .await;
169
170        let repository = MemoryRepository::<Folder>::default();
171
172        let result = create_folder(
173            &store,
174            &api_config,
175            &repository,
176            FolderAddEditRequest {
177                name: "test".to_string(),
178            },
179        )
180        .await;
181
182        assert!(result.is_err());
183        assert!(matches!(result.unwrap_err(), CreateFolderError::Api(_)));
184    }
185}