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#[cfg(feature = "wasm")]
15use tsify::Tsify;
16#[cfg(feature = "wasm")]
17use wasm_bindgen::prelude::*;
18
19use crate::{Folder, FolderView, FoldersClient, VaultParseError};
20
21#[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 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
67#[cfg_attr(feature = "wasm", wasm_bindgen)]
68impl FoldersClient {
69 pub async fn create(
71 &self,
72 request: FolderAddEditRequest,
73 ) -> Result<FolderView, CreateFolderError> {
74 let folder_request = self.key_store.encrypt(request)?;
75
76 let resp = self
77 .api_configurations
78 .api_client
79 .folders_api()
80 .post(Some(folder_request))
81 .await?;
82
83 let folder: Folder = resp.try_into()?;
84
85 self.repository
86 .require()?
87 .set(require!(folder.id), folder.clone())
88 .await?;
89
90 Ok(self.key_store.decrypt(&folder)?)
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use std::sync::Arc;
97
98 use bitwarden_api_api::{apis::ApiClient, models::FolderResponseModel};
99 use bitwarden_core::{
100 client::ApiConfigurations, key_management::create_test_crypto_with_user_key,
101 };
102 use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
103 use bitwarden_test::MemoryRepository;
104 use uuid::uuid;
105
106 use super::*;
107 use crate::FolderId;
108
109 fn create_client(api_client: ApiClient) -> FoldersClient {
110 FoldersClient {
111 key_store: create_test_crypto_with_user_key(SymmetricCryptoKey::make(
112 SymmetricKeyAlgorithm::Aes256CbcHmac,
113 )),
114 api_configurations: Arc::new(ApiConfigurations::from_api_client(api_client)),
115 repository: Some(Arc::new(MemoryRepository::<Folder>::default())),
116 }
117 }
118
119 #[tokio::test]
120 async fn test_create_folder() {
121 let folder_id = FolderId::new(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"));
122
123 let client = create_client(ApiClient::new_mocked(move |mock| {
124 mock.folders_api
125 .expect_post()
126 .returning(move |model| {
127 Ok(FolderResponseModel {
128 id: Some(folder_id.into()),
129 name: Some(model.unwrap().name),
130 revision_date: Some("2025-01-01T00:00:00Z".to_string()),
131 object: Some("folder".to_string()),
132 })
133 })
134 .once();
135 }));
136
137 let result = client
138 .create(FolderAddEditRequest {
139 name: "test".to_string(),
140 })
141 .await
142 .unwrap();
143
144 assert_eq!(
145 result,
146 FolderView {
147 id: Some(folder_id),
148 name: "test".to_string(),
149 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
150 }
151 );
152
153 assert_eq!(
155 client
156 .key_store
157 .decrypt(
158 &client
159 .repository
160 .as_ref()
161 .unwrap()
162 .get(folder_id)
163 .await
164 .unwrap()
165 .unwrap()
166 )
167 .unwrap(),
168 result
169 );
170 }
171
172 #[tokio::test]
173 async fn test_create_folder_http_error() {
174 let client = create_client(ApiClient::new_mocked(move |mock| {
175 mock.folders_api
176 .expect_post()
177 .returning(move |_model| Err(std::io::Error::other("Simulated error").into()));
178 }));
179
180 let result = client
181 .create(FolderAddEditRequest {
182 name: "test".to_string(),
183 })
184 .await;
185
186 assert!(result.is_err());
187 assert!(matches!(result.unwrap_err(), CreateFolderError::Api(_)));
188 }
189}