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