bitwarden_vault/folder/
create.rs1use bitwarden_api_api::{apis::folders_api, models::FolderRequestModel};
2use bitwarden_core::{
3 key_management::{KeyIds, SymmetricKeyId},
4 require, ApiError, MissingFieldError,
5};
6use bitwarden_crypto::{
7 CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, PrimitiveEncryptable,
8};
9use bitwarden_error::bitwarden_error;
10use bitwarden_state::repository::{Repository, RepositoryError};
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13#[cfg(feature = "wasm")]
14use tsify::Tsify;
15#[cfg(feature = "wasm")]
16use wasm_bindgen::prelude::*;
17
18use crate::{Folder, FolderView, VaultParseError};
19
20#[derive(Serialize, Deserialize, Debug)]
22#[serde(rename_all = "camelCase")]
23#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
24#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
25pub struct FolderAddEditRequest {
26 pub name: String,
28}
29
30impl CompositeEncryptable<KeyIds, SymmetricKeyId, FolderRequestModel> for FolderAddEditRequest {
31 fn encrypt_composite(
32 &self,
33 ctx: &mut KeyStoreContext<KeyIds>,
34 key: SymmetricKeyId,
35 ) -> Result<FolderRequestModel, CryptoError> {
36 Ok(FolderRequestModel {
37 name: self.name.encrypt(ctx, key)?.to_string(),
38 })
39 }
40}
41
42impl IdentifyKey<SymmetricKeyId> for FolderAddEditRequest {
43 fn key_identifier(&self) -> SymmetricKeyId {
44 SymmetricKeyId::User
45 }
46}
47
48#[allow(missing_docs)]
49#[bitwarden_error(flat)]
50#[derive(Debug, Error)]
51pub enum CreateFolderError {
52 #[error(transparent)]
53 Crypto(#[from] CryptoError),
54 #[error(transparent)]
55 Api(#[from] ApiError),
56 #[error(transparent)]
57 VaultParse(#[from] VaultParseError),
58 #[error(transparent)]
59 MissingField(#[from] MissingFieldError),
60 #[error(transparent)]
61 RepositoryError(#[from] RepositoryError),
62}
63
64pub(super) async fn create_folder<R: Repository<Folder> + ?Sized>(
65 key_store: &KeyStore<KeyIds>,
66 api_config: &bitwarden_api_api::apis::configuration::Configuration,
67 repository: &R,
68 request: FolderAddEditRequest,
69) -> Result<FolderView, CreateFolderError> {
70 let folder_request = key_store.encrypt(request)?;
71 let resp = folders_api::folders_post(api_config, Some(folder_request))
72 .await
73 .map_err(ApiError::from)?;
74
75 let folder: Folder = resp.try_into()?;
76
77 repository
78 .set(require!(folder.id).to_string(), folder.clone())
79 .await?;
80
81 Ok(key_store.decrypt(&folder)?)
82}
83
84#[cfg(test)]
85mod tests {
86 use bitwarden_api_api::models::FolderResponseModel;
87 use bitwarden_crypto::SymmetricCryptoKey;
88 use bitwarden_test::{start_api_mock, MemoryRepository};
89 use uuid::uuid;
90 use wiremock::{matchers, Mock, Request, ResponseTemplate};
91
92 use super::*;
93 use crate::FolderId;
94
95 #[tokio::test]
96 async fn test_create_folder() {
97 let store: KeyStore<KeyIds> = KeyStore::default();
98 #[allow(deprecated)]
99 let _ = store.context_mut().set_symmetric_key(
100 SymmetricKeyId::User,
101 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
102 );
103
104 let folder_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
105
106 let (_server, api_config) = start_api_mock(vec![Mock::given(matchers::path("/folders"))
107 .respond_with(move |req: &Request| {
108 let body: FolderRequestModel = req.body_json().unwrap();
109 ResponseTemplate::new(201).set_body_json(FolderResponseModel {
110 id: Some(folder_id),
111 name: Some(body.name),
112 revision_date: Some("2025-01-01T00:00:00Z".to_string()),
113 object: Some("folder".to_string()),
114 })
115 })
116 .expect(1)])
117 .await;
118
119 let repository = MemoryRepository::<Folder>::default();
120
121 let result = create_folder(
122 &store,
123 &api_config,
124 &repository,
125 FolderAddEditRequest {
126 name: "test".to_string(),
127 },
128 )
129 .await
130 .unwrap();
131
132 assert_eq!(
133 result,
134 FolderView {
135 id: Some(FolderId::new(folder_id)),
136 name: "test".to_string(),
137 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
138 }
139 );
140
141 assert_eq!(
143 store
144 .decrypt(
145 &repository
146 .get(folder_id.to_string())
147 .await
148 .unwrap()
149 .unwrap()
150 )
151 .unwrap(),
152 result
153 );
154 }
155
156 #[tokio::test]
157 async fn test_create_folder_http_error() {
158 let store: KeyStore<KeyIds> = KeyStore::default();
159 #[allow(deprecated)]
160 let _ = store.context_mut().set_symmetric_key(
161 SymmetricKeyId::User,
162 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
163 );
164
165 let (_server, api_config) = start_api_mock(vec![
166 Mock::given(matchers::path("/folders")).respond_with(ResponseTemplate::new(500))
167 ])
168 .await;
169
170 let repository = MemoryRepository::<Folder>::default();
171
172 let result = create_folder(
173 &store,
174 &api_config,
175 &repository,
176 FolderAddEditRequest {
177 name: "test".to_string(),
178 },
179 )
180 .await;
181
182 assert!(result.is_err());
183 assert!(matches!(result.unwrap_err(), CreateFolderError::Api(_)));
184 }
185}