Skip to main content

bitwarden_send/
fetch.rs

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    /// Fetch a single [Send] by its ID from the server and persist it to local state.
39    ///
40    /// Unlike [`SendClient::get`], which only reads from local state, this makes a network request
41    /// and refreshes the locally stored copy. Returns the still-encrypted [Send] — matching what a
42    /// sync-notification diff needs to compare `revision_date` and update encrypted state without
43    /// a decrypt round-trip — rather than a [`SendView`](crate::SendView); callers that want the
44    /// decrypted form can pass the result to [`SendClient::decrypt`].
45    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    /// Builds a key store with a user key and returns an encrypted send (not yet stored) so tests
65    /// can hand realistic encrypted fields back through the mocked API.
66    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        // The result is still encrypted, not a decrypted view: decrypting it (with the store that
167        // has the matching key) reproduces the original plaintext send.
168        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        // The fetched send should have been persisted to the repository.
179        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}