1use bitwarden_core::{ApiError, MissingFieldError, key_management::KeySlotIds, require};
2use bitwarden_crypto::{CryptoError, KeyStore};
3use bitwarden_error::bitwarden_error;
4use bitwarden_state::repository::{Repository, RepositoryError};
5use thiserror::Error;
6#[cfg(feature = "wasm")]
7use wasm_bindgen::prelude::*;
8
9use crate::{Send, SendId, SendView, error::SendParseError, send_client::SendClient};
10
11#[allow(missing_docs)]
12#[bitwarden_error(flat)]
13#[derive(Debug, Error)]
14pub enum RemoveSendPasswordError {
15 #[error(transparent)]
16 Api(#[from] ApiError),
17 #[error(transparent)]
18 Crypto(#[from] CryptoError),
19 #[error(transparent)]
20 MissingField(#[from] MissingFieldError),
21 #[error(transparent)]
22 Repository(#[from] RepositoryError),
23 #[error(transparent)]
24 SendParse(#[from] SendParseError),
25}
26
27async fn remove_send_password<R: Repository<Send> + ?Sized>(
28 key_store: &KeyStore<KeySlotIds>,
29 api_client: &bitwarden_api_api::apis::ApiClient,
30 repository: &R,
31 send_id: SendId,
32) -> Result<SendView, RemoveSendPasswordError> {
33 let resp = api_client
34 .sends_api()
35 .put_remove_password(&send_id.to_string())
36 .await
37 .map_err(ApiError::from)?;
38
39 let send: Send = resp.try_into()?;
40
41 repository.set(require!(send.id), send.clone()).await?;
42
43 Ok(key_store.decrypt(&send)?)
44}
45
46#[cfg_attr(feature = "wasm", wasm_bindgen)]
47impl SendClient {
48 pub async fn remove_password(
50 &self,
51 send_id: SendId,
52 ) -> Result<SendView, RemoveSendPasswordError> {
53 let key_store = self.client.internal.get_key_store();
54 let config = self.client.internal.get_api_configurations();
55 let repository = self.get_repository()?;
56
57 remove_send_password(key_store, &config.api_client, repository.as_ref(), send_id).await
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use bitwarden_api_api::{apis::ApiClient, models::SendResponseModel};
64 use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
65 use bitwarden_crypto::{KeyStore, SymmetricKeyAlgorithm};
66 use bitwarden_test::MemoryRepository;
67 use uuid::uuid;
68
69 use super::*;
70 use crate::{AuthType, Send, SendId, SendTextView, SendType, SendView};
71
72 async fn make_store_with_send(
73 send_id: uuid::Uuid,
74 ) -> (KeyStore<KeySlotIds>, MemoryRepository<Send>) {
75 let store: KeyStore<KeySlotIds> = KeyStore::default();
76 {
77 let mut ctx = store.context_mut();
78 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
79 ctx.persist_symmetric_key(local_key_id, SymmetricKeySlotId::User)
80 .unwrap();
81 }
82
83 let repository = MemoryRepository::<Send>::default();
84 let send_view = SendView {
85 id: None,
86 access_id: None,
87 name: "Test Send".to_string(),
88 notes: None,
89 key: None,
90 new_password: None,
91 has_password: false,
92 r#type: SendType::Text,
93 file: None,
94 text: Some(SendTextView {
95 text: Some("Secret text".to_string()),
96 hidden: false,
97 }),
98 max_access_count: None,
99 access_count: 0,
100 disabled: false,
101 hide_email: false,
102 revision_date: "2025-01-01T00:00:00Z".parse().unwrap(),
103 deletion_date: "2025-01-10T00:00:00Z".parse().unwrap(),
104 expiration_date: None,
105 emails: Vec::new(),
106 auth_type: AuthType::None,
107 };
108 let mut send = store.encrypt(send_view).unwrap();
109 send.id = Some(SendId::new(send_id));
110 repository.set(SendId::new(send_id), send).await.unwrap();
111
112 (store, repository)
113 }
114
115 #[tokio::test]
116 async fn test_remove_send_password() {
117 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
118 let (store, repository) = make_store_with_send(send_id).await;
119
120 let stored = repository.get(SendId::new(send_id)).await.unwrap().unwrap();
122 let stored_name = stored.name.to_string();
123 let stored_key = stored.key.to_string();
124 let stored_deletion_date = stored.deletion_date.to_rfc3339();
125
126 let api_client = ApiClient::new_mocked(move |mock| {
127 let name = stored_name.clone();
128 let key = stored_key.clone();
129 let deletion_date = stored_deletion_date.clone();
130 mock.sends_api
131 .expect_put_remove_password()
132 .returning(move |_id| {
133 Ok(SendResponseModel {
134 id: Some(send_id),
135 name: Some(name.clone()),
136 revision_date: Some("2025-01-02T00:00:00Z".to_string()),
137 object: Some("send".to_string()),
138 access_id: None,
139 r#type: Some(bitwarden_api_api::models::SendType::Text),
140 auth_type: Some(bitwarden_api_api::models::AuthType::None),
141 notes: None,
142 file: None,
143 text: None,
144 key: Some(key.clone()),
145 max_access_count: None,
146 access_count: Some(0),
147 password: None,
148 emails: None,
149 disabled: Some(false),
150 expiration_date: None,
151 deletion_date: Some(deletion_date.clone()),
152 hide_email: Some(false),
153 })
154 })
155 .once();
156 });
157
158 let result =
159 remove_send_password(&store, &api_client, &repository, SendId::new(send_id)).await;
160
161 assert!(result.is_ok());
162 let view = result.unwrap();
163 assert_eq!(view.id, Some(SendId::new(send_id)));
164 assert!(!view.has_password);
165 assert_eq!(view.auth_type, AuthType::None);
166 }
167
168 #[tokio::test]
169 async fn test_remove_send_password_http_error() {
170 let send_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1");
171 let (store, repository) = make_store_with_send(send_id).await;
172
173 let api_client = ApiClient::new_mocked(move |mock| {
174 mock.sends_api
175 .expect_put_remove_password()
176 .returning(move |_id| {
177 Err(bitwarden_api_api::apis::Error::Io(std::io::Error::other(
178 "Simulated error",
179 )))
180 })
181 .once();
182 });
183
184 let result =
185 remove_send_password(&store, &api_client, &repository, SendId::new(send_id)).await;
186
187 assert!(result.is_err());
188 assert!(matches!(
189 result.unwrap_err(),
190 RemoveSendPasswordError::Api(_)
191 ));
192 }
193}