bitwarden_vault/folder/
create.rs1use bitwarden_api_api::models::FolderRequestModel;
2use bitwarden_core::{
3 ApiError, MissingFieldError,
4 key_management::{KeyIds, SymmetricKeyId},
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#[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<KeyIds, SymmetricKeyId, FolderRequestModel> for FolderAddEditRequest {
32 fn encrypt_composite(
33 &self,
34 ctx: &mut KeyStoreContext<KeyIds>,
35 key: SymmetricKeyId,
36 ) -> Result<FolderRequestModel, CryptoError> {
37 Ok(FolderRequestModel {
38 name: self.name.encrypt(ctx, key)?.to_string(),
39 })
40 }
41}
42
43impl IdentifyKey<SymmetricKeyId> for FolderAddEditRequest {
44 fn key_identifier(&self) -> SymmetricKeyId {
45 SymmetricKeyId::User
46 }
47}
48
49#[allow(missing_docs)]
50#[bitwarden_error(flat)]
51#[derive(Debug, Error)]
52pub enum CreateFolderError {
53 #[error(transparent)]
54 Crypto(#[from] CryptoError),
55 #[error(transparent)]
56 Api(#[from] ApiError),
57 #[error(transparent)]
58 VaultParse(#[from] VaultParseError),
59 #[error(transparent)]
60 MissingField(#[from] MissingFieldError),
61 #[error(transparent)]
62 Repository(#[from] RepositoryError),
63}
64
65pub(super) async fn create_folder<R: Repository<Folder> + ?Sized>(
66 key_store: &KeyStore<KeyIds>,
67 api_client: &bitwarden_api_api::apis::ApiClient,
68 repository: &R,
69 request: FolderAddEditRequest,
70) -> Result<FolderView, CreateFolderError> {
71 let folder_request = key_store.encrypt(request)?;
72
73 let resp = api_client
74 .folders_api()
75 .post(Some(folder_request))
76 .await
77 .map_err(ApiError::from)?;
78
79 let folder: Folder = resp.try_into()?;
80
81 repository
82 .set(require!(folder.id).to_string(), folder.clone())
83 .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::SymmetricCryptoKey;
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<KeyIds> = KeyStore::default();
101 #[allow(deprecated)]
102 let _ = store.context_mut().set_symmetric_key(
103 SymmetricKeyId::User,
104 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
105 );
106
107 let folder_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
108
109 let api_client = ApiClient::new_mocked(move |mock| {
110 mock.folders_api
111 .expect_post()
112 .returning(move |model| {
113 Ok(FolderResponseModel {
114 id: Some(folder_id),
115 name: Some(model.unwrap().name),
116 revision_date: Some("2025-01-01T00:00:00Z".to_string()),
117 object: Some("folder".to_string()),
118 })
119 })
120 .once();
121 });
122
123 let repository = MemoryRepository::<Folder>::default();
124
125 let result = create_folder(
126 &store,
127 &api_client,
128 &repository,
129 FolderAddEditRequest {
130 name: "test".to_string(),
131 },
132 )
133 .await
134 .unwrap();
135
136 assert_eq!(
137 result,
138 FolderView {
139 id: Some(FolderId::new(folder_id)),
140 name: "test".to_string(),
141 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
142 }
143 );
144
145 assert_eq!(
147 store
148 .decrypt(
149 &repository
150 .get(folder_id.to_string())
151 .await
152 .unwrap()
153 .unwrap()
154 )
155 .unwrap(),
156 result
157 );
158 }
159
160 #[tokio::test]
161 async fn test_create_folder_http_error() {
162 let store: KeyStore<KeyIds> = KeyStore::default();
163 #[allow(deprecated)]
164 let _ = store.context_mut().set_symmetric_key(
165 SymmetricKeyId::User,
166 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
167 );
168
169 let api_client = ApiClient::new_mocked(move |mock| {
170 mock.folders_api.expect_post().returning(move |_model| {
171 Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other(
172 "Simulated error",
173 )))
174 });
175 });
176
177 let repository = MemoryRepository::<Folder>::default();
178
179 let result = create_folder(
180 &store,
181 &api_client,
182 &repository,
183 FolderAddEditRequest {
184 name: "test".to_string(),
185 },
186 )
187 .await;
188
189 assert!(result.is_err());
190 assert!(matches!(result.unwrap_err(), CreateFolderError::Api(_)));
191 }
192}