bitwarden_vault/folder/
create.rs1use 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, KeyStoreContext, PrimitiveEncryptable,
9};
10use bitwarden_error::bitwarden_error;
11use bitwarden_state::repository::{RepositoryError, RepositoryOption};
12use serde::{Deserialize, Serialize};
13use thiserror::Error;
14
15use crate::{Folder, FolderView, FoldersClient, VaultParseError};
16
17#[derive(Serialize, Deserialize, Debug)]
19#[serde(rename_all = "camelCase")]
20#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
21#[bitwarden_ffi::wasm_record]
22pub struct FolderAddEditRequest {
23 pub name: String,
25}
26
27impl CompositeEncryptable<KeySlotIds, SymmetricKeySlotId, FolderRequestModel>
28 for FolderAddEditRequest
29{
30 fn encrypt_composite(
31 &self,
32 ctx: &mut KeyStoreContext<KeySlotIds>,
33 key: SymmetricKeySlotId,
34 ) -> Result<FolderRequestModel, CryptoError> {
35 Ok(FolderRequestModel {
36 name: self.name.encrypt(ctx, key)?.to_string(),
37 })
38 }
39}
40
41impl IdentifyKey<SymmetricKeySlotId> for FolderAddEditRequest {
42 fn key_identifier(&self) -> SymmetricKeySlotId {
43 SymmetricKeySlotId::User
44 }
45}
46
47#[allow(missing_docs)]
48#[bitwarden_error(flat)]
49#[derive(Debug, Error)]
50pub enum CreateFolderError {
51 #[error(transparent)]
52 Crypto(#[from] CryptoError),
53 #[error(transparent)]
54 Api(#[from] ApiError),
55 #[error(transparent)]
56 VaultParse(#[from] VaultParseError),
57 #[error(transparent)]
58 MissingField(#[from] MissingFieldError),
59 #[error(transparent)]
60 Repository(#[from] RepositoryError),
61}
62
63#[bitwarden_ffi::wasm_export]
64impl FoldersClient {
65 pub async fn create(
67 &self,
68 request: FolderAddEditRequest,
69 ) -> Result<FolderView, CreateFolderError> {
70 let folder_request = self.key_store.encrypt(request)?;
71
72 let resp = self
73 .api_configurations
74 .api_client
75 .folders_api()
76 .post(Some(folder_request))
77 .await?;
78
79 let folder: Folder = resp.try_into()?;
80
81 self.repository
82 .require()?
83 .set(require!(folder.id), folder.clone())
84 .await?;
85
86 Ok(self.key_store.decrypt(&folder)?)
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use std::sync::Arc;
93
94 use bitwarden_api_api::{apis::ApiClient, models::FolderResponseModel};
95 use bitwarden_core::{
96 client::ApiConfigurations, key_management::create_test_crypto_with_user_key,
97 };
98 use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
99 use bitwarden_test::MemoryRepository;
100 use uuid::uuid;
101
102 use super::*;
103 use crate::FolderId;
104
105 fn create_client(api_client: ApiClient) -> FoldersClient {
106 FoldersClient {
107 key_store: create_test_crypto_with_user_key(SymmetricCryptoKey::make(
108 SymmetricKeyAlgorithm::Aes256CbcHmac,
109 )),
110 api_configurations: Arc::new(ApiConfigurations::from_api_client(api_client)),
111 repository: Some(Arc::new(MemoryRepository::<Folder>::default())),
112 }
113 }
114
115 #[tokio::test]
116 async fn test_create_folder() {
117 let folder_id = FolderId::new(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"));
118
119 let client = create_client(ApiClient::new_mocked(move |mock| {
120 mock.folders_api
121 .expect_post()
122 .returning(move |model| {
123 Ok(FolderResponseModel {
124 id: Some(folder_id.into()),
125 name: Some(model.unwrap().name),
126 revision_date: Some("2025-01-01T00:00:00Z".to_string()),
127 object: Some("folder".to_string()),
128 })
129 })
130 .once();
131 }));
132
133 let result = client
134 .create(FolderAddEditRequest {
135 name: "test".to_string(),
136 })
137 .await
138 .unwrap();
139
140 assert_eq!(
141 result,
142 FolderView {
143 id: Some(folder_id),
144 name: "test".to_string(),
145 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
146 }
147 );
148
149 assert_eq!(
151 client
152 .key_store
153 .decrypt(
154 &client
155 .repository
156 .as_ref()
157 .unwrap()
158 .get(folder_id)
159 .await
160 .unwrap()
161 .unwrap()
162 )
163 .unwrap(),
164 result
165 );
166 }
167
168 #[tokio::test]
169 async fn test_create_folder_http_error() {
170 let client = create_client(ApiClient::new_mocked(move |mock| {
171 mock.folders_api
172 .expect_post()
173 .returning(move |_model| Err(std::io::Error::other("Simulated error").into()));
174 }));
175
176 let result = client
177 .create(FolderAddEditRequest {
178 name: "test".to_string(),
179 })
180 .await;
181
182 assert!(result.is_err());
183 assert!(matches!(result.unwrap_err(), CreateFolderError::Api(_)));
184 }
185}