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