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
94    #[tokio::test]
95    async fn test_create_folder() {
96        let store: KeyStore<KeyIds> = KeyStore::default();
97        #[allow(deprecated)]
98        let _ = store.context_mut().set_symmetric_key(
99            SymmetricKeyId::User,
100            SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
101        );
102
103        let folder_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
104
105        let (_server, api_config) = start_api_mock(vec![Mock::given(matchers::path("/folders"))
106            .respond_with(move |req: &Request| {
107                let body: FolderRequestModel = req.body_json().unwrap();
108                ResponseTemplate::new(201).set_body_json(FolderResponseModel {
109                    id: Some(folder_id),
110                    name: Some(body.name),
111                    revision_date: Some("2025-01-01T00:00:00Z".to_string()),
112                    object: Some("folder".to_string()),
113                })
114            })
115            .expect(1)])
116        .await;
117
118        let repository = MemoryRepository::<Folder>::default();
119
120        let result = create_folder(
121            &store,
122            &api_config,
123            &repository,
124            FolderAddEditRequest {
125                name: "test".to_string(),
126            },
127        )
128        .await
129        .unwrap();
130
131        assert_eq!(
132            result,
133            FolderView {
134                id: Some(folder_id),
135                name: "test".to_string(),
136                revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
137            }
138        );
139
140        // Confirm the folder was stored in the repository
141        assert_eq!(
142            store
143                .decrypt(
144                    &repository
145                        .get(folder_id.to_string())
146                        .await
147                        .unwrap()
148                        .unwrap()
149                )
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        #[allow(deprecated)]
159        let _ = store.context_mut().set_symmetric_key(
160            SymmetricKeyId::User,
161            SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
162        );
163
164        let (_server, api_config) = start_api_mock(vec![
165            Mock::given(matchers::path("/folders")).respond_with(ResponseTemplate::new(500))
166        ])
167        .await;
168
169        let repository = MemoryRepository::<Folder>::default();
170
171        let result = create_folder(
172            &store,
173            &api_config,
174            &repository,
175            FolderAddEditRequest {
176                name: "test".to_string(),
177            },
178        )
179        .await;
180
181        assert!(result.is_err());
182        assert!(matches!(result.unwrap_err(), CreateFolderError::Api(_)));
183    }
184}