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
94 #[tokio::test]
95 async fn test_create_folder() {
96 let store: KeyStore<KeyIds> = KeyStore::default();
97 #[allow(deprecated)]
98 let _ = store.context_mut().set_symmetric_key(
99 SymmetricKeyId::User,
100 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
101 );
102
103 let folder_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
104
105 let (_server, api_config) = start_api_mock(vec![Mock::given(matchers::path("/folders"))
106 .respond_with(move |req: &Request| {
107 let body: FolderRequestModel = req.body_json().unwrap();
108 ResponseTemplate::new(201).set_body_json(FolderResponseModel {
109 id: Some(folder_id),
110 name: Some(body.name),
111 revision_date: Some("2025-01-01T00:00:00Z".to_string()),
112 object: Some("folder".to_string()),
113 })
114 })
115 .expect(1)])
116 .await;
117
118 let repository = MemoryRepository::<Folder>::default();
119
120 let result = create_folder(
121 &store,
122 &api_config,
123 &repository,
124 FolderAddEditRequest {
125 name: "test".to_string(),
126 },
127 )
128 .await
129 .unwrap();
130
131 assert_eq!(
132 result,
133 FolderView {
134 id: Some(folder_id),
135 name: "test".to_string(),
136 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
137 }
138 );
139
140 assert_eq!(
142 store
143 .decrypt(
144 &repository
145 .get(folder_id.to_string())
146 .await
147 .unwrap()
148 .unwrap()
149 )
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 #[allow(deprecated)]
159 let _ = store.context_mut().set_symmetric_key(
160 SymmetricKeyId::User,
161 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
162 );
163
164 let (_server, api_config) = start_api_mock(vec![
165 Mock::given(matchers::path("/folders")).respond_with(ResponseTemplate::new(500))
166 ])
167 .await;
168
169 let repository = MemoryRepository::<Folder>::default();
170
171 let result = create_folder(
172 &store,
173 &api_config,
174 &repository,
175 FolderAddEditRequest {
176 name: "test".to_string(),
177 },
178 )
179 .await;
180
181 assert!(result.is_err());
182 assert!(matches!(result.unwrap_err(), CreateFolderError::Api(_)));
183 }
184}