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::SymmetricKeyAlgorithm;
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 {
102 let mut ctx = store.context_mut();
103 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
104 ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User)
105 .unwrap();
106 }
107
108 let folder_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
109
110 let api_client = ApiClient::new_mocked(move |mock| {
111 mock.folders_api
112 .expect_post()
113 .returning(move |model| {
114 Ok(FolderResponseModel {
115 id: Some(folder_id),
116 name: Some(model.unwrap().name),
117 revision_date: Some("2025-01-01T00:00:00Z".to_string()),
118 object: Some("folder".to_string()),
119 })
120 })
121 .once();
122 });
123
124 let repository = MemoryRepository::<Folder>::default();
125
126 let result = create_folder(
127 &store,
128 &api_client,
129 &repository,
130 FolderAddEditRequest {
131 name: "test".to_string(),
132 },
133 )
134 .await
135 .unwrap();
136
137 assert_eq!(
138 result,
139 FolderView {
140 id: Some(FolderId::new(folder_id)),
141 name: "test".to_string(),
142 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
143 }
144 );
145
146 assert_eq!(
148 store
149 .decrypt(
150 &repository
151 .get(folder_id.to_string())
152 .await
153 .unwrap()
154 .unwrap()
155 )
156 .unwrap(),
157 result
158 );
159 }
160
161 #[tokio::test]
162 async fn test_create_folder_http_error() {
163 let store: KeyStore<KeyIds> = KeyStore::default();
164 {
165 let mut ctx = store.context_mut();
166 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
167 ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User)
168 .unwrap();
169 }
170
171 let api_client = ApiClient::new_mocked(move |mock| {
172 mock.folders_api.expect_post().returning(move |_model| {
173 Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other(
174 "Simulated error",
175 )))
176 });
177 });
178
179 let repository = MemoryRepository::<Folder>::default();
180
181 let result = create_folder(
182 &store,
183 &api_client,
184 &repository,
185 FolderAddEditRequest {
186 name: "test".to_string(),
187 },
188 )
189 .await;
190
191 assert!(result.is_err());
192 assert!(matches!(result.unwrap_err(), CreateFolderError::Api(_)));
193 }
194}