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.set(require!(folder.id), folder.clone()).await?;
82
83 Ok(key_store.decrypt(&folder)?)
84}
85
86#[cfg(test)]
87mod tests {
88 use bitwarden_api_api::{apis::ApiClient, models::FolderResponseModel};
89 use bitwarden_crypto::SymmetricKeyAlgorithm;
90 use bitwarden_test::MemoryRepository;
91 use uuid::uuid;
92
93 use super::*;
94 use crate::FolderId;
95
96 #[tokio::test]
97 async fn test_create_folder() {
98 let store: KeyStore<KeyIds> = KeyStore::default();
99 {
100 let mut ctx = store.context_mut();
101 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
102 ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User)
103 .unwrap();
104 }
105
106 let folder_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
107
108 let api_client = ApiClient::new_mocked(move |mock| {
109 mock.folders_api
110 .expect_post()
111 .returning(move |model| {
112 Ok(FolderResponseModel {
113 id: Some(folder_id),
114 name: Some(model.unwrap().name),
115 revision_date: Some("2025-01-01T00:00:00Z".to_string()),
116 object: Some("folder".to_string()),
117 })
118 })
119 .once();
120 });
121
122 let repository = MemoryRepository::<Folder>::default();
123
124 let result = create_folder(
125 &store,
126 &api_client,
127 &repository,
128 FolderAddEditRequest {
129 name: "test".to_string(),
130 },
131 )
132 .await
133 .unwrap();
134
135 let folder_id = FolderId::new(folder_id);
136
137 assert_eq!(
138 result,
139 FolderView {
140 id: Some(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(&repository.get(folder_id).await.unwrap().unwrap())
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 {
159 let mut ctx = store.context_mut();
160 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
161 ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::User)
162 .unwrap();
163 }
164
165 let api_client = ApiClient::new_mocked(move |mock| {
166 mock.folders_api.expect_post().returning(move |_model| {
167 Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other(
168 "Simulated error",
169 )))
170 });
171 });
172
173 let repository = MemoryRepository::<Folder>::default();
174
175 let result = create_folder(
176 &store,
177 &api_client,
178 &repository,
179 FolderAddEditRequest {
180 name: "test".to_string(),
181 },
182 )
183 .await;
184
185 assert!(result.is_err());
186 assert!(matches!(result.unwrap_err(), CreateFolderError::Api(_)));
187 }
188}