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