Skip to main content

bitwarden_emergency_access/
list_trusted.rs

1use bitwarden_core::{ApiError, MissingFieldError};
2use bitwarden_error::bitwarden_error;
3use thiserror::Error;
4
5use crate::{EmergencyAccessClient, GranteeEmergencyAccess};
6
7/// Errors returned when listing the current user's trusted emergency contacts.
8#[bitwarden_error(flat)]
9#[derive(Debug, Error)]
10pub enum EmergencyAccessListTrustedError {
11    /// The request to the server failed.
12    #[error(transparent)]
13    Api(#[from] ApiError),
14    /// A required field was missing from the server response.
15    #[error(transparent)]
16    MissingField(#[from] MissingFieldError),
17}
18
19#[bitwarden_ffi::wasm_export]
20impl EmergencyAccessClient {
21    /// Lists the emergency accesses the current user granted, one per trusted contact.
22    ///
23    /// Called by the grantor.
24    pub async fn list_trusted(
25        &self,
26    ) -> Result<Vec<GranteeEmergencyAccess>, EmergencyAccessListTrustedError> {
27        let response = self
28            .api_configurations
29            .api_client
30            .emergency_access_api()
31            .get_contacts()
32            .await?;
33
34        // A missing list is treated as empty, matching how the clients parse list responses.
35        Ok(response
36            .data
37            .unwrap_or_default()
38            .into_iter()
39            .map(GranteeEmergencyAccess::try_from)
40            .collect::<Result<_, _>>()?)
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use bitwarden_api_api::models::{
47        EmergencyAccessGranteeDetailsResponseModel,
48        EmergencyAccessGranteeDetailsResponseModelListResponseModel, EmergencyAccessStatusType,
49        EmergencyAccessType as ApiEmergencyAccessType,
50    };
51    use bitwarden_core::client::test_accounts::test_bitwarden_com_account;
52
53    use super::*;
54    use crate::{
55        EmergencyAccessClientExt, EmergencyAccessStatus, EmergencyAccessType,
56        test_support::{TEST_EMERGENCY_ACCESS_ID, api_error, test_client, test_id},
57    };
58
59    const TEST_GRANTEE_ID: &str = "060000fb-0922-4dd3-b170-6e15cb5df8c8";
60
61    fn invited() -> EmergencyAccessGranteeDetailsResponseModel {
62        EmergencyAccessGranteeDetailsResponseModel {
63            id: Some(TEST_EMERGENCY_ACCESS_ID.parse().unwrap()),
64            status: Some(EmergencyAccessStatusType::Invited),
65            r#type: Some(ApiEmergencyAccessType::View),
66            wait_time_days: Some(7),
67            email: Some("[email protected]".to_owned()),
68            ..Default::default()
69        }
70    }
71
72    fn confirmed() -> EmergencyAccessGranteeDetailsResponseModel {
73        EmergencyAccessGranteeDetailsResponseModel {
74            id: Some(TEST_EMERGENCY_ACCESS_ID.parse().unwrap()),
75            status: Some(EmergencyAccessStatusType::Confirmed),
76            r#type: Some(ApiEmergencyAccessType::Takeover),
77            wait_time_days: Some(1),
78            grantee_id: Some(TEST_GRANTEE_ID.parse().unwrap()),
79            name: Some("Grantee".to_owned()),
80            email: Some("[email protected]".to_owned()),
81            avatar_color: Some("#175ddc".to_owned()),
82            ..Default::default()
83        }
84    }
85
86    async fn list(
87        data: Option<Vec<EmergencyAccessGranteeDetailsResponseModel>>,
88    ) -> Result<Vec<GranteeEmergencyAccess>, EmergencyAccessListTrustedError> {
89        let response = EmergencyAccessGranteeDetailsResponseModelListResponseModel {
90            data,
91            ..Default::default()
92        };
93
94        let client = test_client(test_bitwarden_com_account(), move |mock| {
95            mock.emergency_access_api
96                .expect_get_contacts()
97                .returning(move || Ok(response.clone()))
98                .once();
99        })
100        .await;
101
102        client.emergency_access().list_trusted().await
103    }
104
105    #[tokio::test]
106    async fn maps_trusted_contacts() {
107        let result = list(Some(vec![invited(), confirmed()])).await.unwrap();
108
109        assert_eq!(
110            result,
111            [
112                GranteeEmergencyAccess {
113                    id: test_id(),
114                    grantee_id: None,
115                    name: None,
116                    email: Some("[email protected]".to_owned()),
117                    r#type: EmergencyAccessType::View,
118                    status: EmergencyAccessStatus::Invited,
119                    wait_time_days: 7,
120                    avatar_color: None,
121                },
122                GranteeEmergencyAccess {
123                    id: test_id(),
124                    grantee_id: Some(TEST_GRANTEE_ID.parse().unwrap()),
125                    name: Some("Grantee".to_owned()),
126                    email: Some("[email protected]".to_owned()),
127                    r#type: EmergencyAccessType::Takeover,
128                    status: EmergencyAccessStatus::Confirmed,
129                    wait_time_days: 1,
130                    avatar_color: Some("#175ddc".to_owned()),
131                },
132            ]
133        );
134    }
135
136    #[tokio::test]
137    async fn returns_empty_list_when_data_is_missing() {
138        assert!(list(None).await.unwrap().is_empty());
139    }
140
141    #[tokio::test]
142    async fn fails_when_status_is_unknown() {
143        let unknown = EmergencyAccessGranteeDetailsResponseModel {
144            status: Some(EmergencyAccessStatusType::__Unknown(42)),
145            ..confirmed()
146        };
147
148        let result = list(Some(vec![unknown])).await;
149
150        assert!(matches!(
151            result,
152            Err(EmergencyAccessListTrustedError::MissingField(_))
153        ));
154    }
155
156    #[tokio::test]
157    async fn fails_when_request_fails() {
158        let client = test_client(test_bitwarden_com_account(), |mock| {
159            mock.emergency_access_api
160                .expect_get_contacts()
161                .returning(api_error)
162                .once();
163        })
164        .await;
165
166        let result = client.emergency_access().list_trusted().await;
167
168        assert!(matches!(
169            result,
170            Err(EmergencyAccessListTrustedError::Api(_))
171        ));
172    }
173}