Skip to main content

bitwarden_emergency_access/
list_granted.rs

1use bitwarden_core::{ApiError, MissingFieldError};
2use bitwarden_error::bitwarden_error;
3use thiserror::Error;
4
5use crate::{EmergencyAccessClient, GrantorEmergencyAccess};
6
7/// Errors returned when listing the emergency accesses granted to the current user.
8#[bitwarden_error(flat)]
9#[derive(Debug, Error)]
10pub enum EmergencyAccessListGrantedError {
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 granted to the current user, one per grantor.
22    ///
23    /// Called by the grantee.
24    pub async fn list_granted(
25        &self,
26    ) -> Result<Vec<GrantorEmergencyAccess>, EmergencyAccessListGrantedError> {
27        let response = self
28            .api_configurations
29            .api_client
30            .emergency_access_api()
31            .get_grantees()
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(GrantorEmergencyAccess::try_from)
40            .collect::<Result<_, _>>()?)
41    }
42}
43
44#[cfg(test)]
45mod tests {
46    use bitwarden_api_api::models::{
47        EmergencyAccessGrantorDetailsResponseModel,
48        EmergencyAccessGrantorDetailsResponseModelListResponseModel, EmergencyAccessStatusType,
49        EmergencyAccessType as ApiEmergencyAccessType,
50    };
51    use bitwarden_core::client::test_accounts::test_bitwarden_com_account_v2;
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_GRANTOR_ID: &str = "3ed3b8a2-6a1e-4e8b-9b6f-1c2d3e4f5a6b";
60
61    fn approved() -> EmergencyAccessGrantorDetailsResponseModel {
62        EmergencyAccessGrantorDetailsResponseModel {
63            id: Some(TEST_EMERGENCY_ACCESS_ID.parse().unwrap()),
64            status: Some(EmergencyAccessStatusType::RecoveryApproved),
65            r#type: Some(ApiEmergencyAccessType::Takeover),
66            wait_time_days: Some(2),
67            grantor_id: Some(TEST_GRANTOR_ID.parse().unwrap()),
68            name: Some("Grantor".to_owned()),
69            email: Some("[email protected]".to_owned()),
70            avatar_color: None,
71            ..Default::default()
72        }
73    }
74
75    async fn list(
76        data: Option<Vec<EmergencyAccessGrantorDetailsResponseModel>>,
77    ) -> Result<Vec<GrantorEmergencyAccess>, EmergencyAccessListGrantedError> {
78        let response = EmergencyAccessGrantorDetailsResponseModelListResponseModel {
79            data,
80            ..Default::default()
81        };
82
83        let client = test_client(test_bitwarden_com_account_v2(), move |mock| {
84            mock.emergency_access_api
85                .expect_get_grantees()
86                .returning(move || Ok(response.clone()))
87                .once();
88        })
89        .await;
90
91        client.emergency_access().list_granted().await
92    }
93
94    #[tokio::test]
95    async fn maps_grantors() {
96        let result = list(Some(vec![approved()])).await.unwrap();
97
98        assert_eq!(
99            result,
100            [GrantorEmergencyAccess {
101                id: test_id(),
102                grantor_id: TEST_GRANTOR_ID.parse().unwrap(),
103                name: Some("Grantor".to_owned()),
104                email: Some("[email protected]".to_owned()),
105                r#type: EmergencyAccessType::Takeover,
106                status: EmergencyAccessStatus::RecoveryApproved,
107                wait_time_days: 2,
108                avatar_color: None,
109            }]
110        );
111    }
112
113    #[tokio::test]
114    async fn returns_empty_list_when_data_is_missing() {
115        assert!(list(None).await.unwrap().is_empty());
116    }
117
118    #[tokio::test]
119    async fn fails_when_grantor_id_is_missing() {
120        let missing = EmergencyAccessGrantorDetailsResponseModel {
121            grantor_id: None,
122            ..approved()
123        };
124
125        let result = list(Some(vec![missing])).await;
126
127        assert!(matches!(
128            result,
129            Err(EmergencyAccessListGrantedError::MissingField(_))
130        ));
131    }
132
133    #[tokio::test]
134    async fn fails_when_request_fails() {
135        let client = test_client(test_bitwarden_com_account_v2(), |mock| {
136            mock.emergency_access_api
137                .expect_get_grantees()
138                .returning(api_error)
139                .once();
140        })
141        .await;
142
143        let result = client.emergency_access().list_granted().await;
144
145        assert!(matches!(
146            result,
147            Err(EmergencyAccessListGrantedError::Api(_))
148        ));
149    }
150}