bitwarden_pam/access_requests/
client.rs1use std::sync::Arc;
2
3use bitwarden_core::{FromClient, client::ApiConfigurations};
4#[cfg(feature = "wasm")]
5use wasm_bindgen::prelude::wasm_bindgen;
6
7use super::models::AccessRequestView;
8use crate::{AccessRequestId, error::LeasingError, leases::AccessLeaseView};
9
10#[cfg_attr(feature = "wasm", wasm_bindgen)]
19#[derive(FromClient)]
20pub struct AccessRequestsClient {
21 pub(crate) api_configurations: Arc<ApiConfigurations>,
22}
23
24#[cfg_attr(feature = "wasm", wasm_bindgen)]
25impl AccessRequestsClient {
26 pub async fn list_mine(&self) -> Result<Vec<AccessRequestView>, LeasingError> {
28 let response = self
29 .api_configurations
30 .api_client
31 .access_requests_api()
32 .get_mine()
33 .await?;
34
35 response
36 .data
37 .unwrap_or_default()
38 .into_iter()
39 .map(AccessRequestView::try_from)
40 .collect()
41 }
42
43 pub async fn get(&self, id: AccessRequestId) -> Result<AccessRequestView, LeasingError> {
45 let response = self
46 .api_configurations
47 .api_client
48 .access_requests_api()
49 .get_details(id.into())
50 .await?;
51
52 AccessRequestView::try_from(response)
53 }
54
55 pub async fn activate(&self, id: AccessRequestId) -> Result<AccessLeaseView, LeasingError> {
61 let response = self
62 .api_configurations
63 .api_client
64 .access_requests_api()
65 .activate(id.into())
66 .await?;
67
68 AccessLeaseView::try_from(response)
69 }
70
71 pub async fn cancel(&self, id: AccessRequestId) -> Result<(), LeasingError> {
73 self.api_configurations
74 .api_client
75 .access_requests_api()
76 .revoke(id.into())
77 .await?;
78
79 Ok(())
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use bitwarden_api_api::{
86 apis::ApiClient,
87 models::{
88 AccessLeaseResponseModel, AccessLeaseStatus as ApiAccessLeaseStatus,
89 AccessRequestDetailsResponseModel, AccessRequestDetailsResponseModelListResponseModel,
90 AccessRequestStatus as ApiAccessRequestStatus,
91 },
92 };
93 use uuid::uuid;
94
95 use super::*;
96 use crate::{AccessLeaseStatus, AccessRequestStatus};
97
98 fn request_id() -> AccessRequestId {
99 AccessRequestId::new(uuid!("44444444-4444-4444-4444-444444444444"))
100 }
101
102 fn client(api_client: ApiClient) -> AccessRequestsClient {
103 AccessRequestsClient {
104 api_configurations: Arc::new(ApiConfigurations::from_api_client(api_client)),
105 }
106 }
107
108 fn sample_request() -> AccessRequestDetailsResponseModel {
109 AccessRequestDetailsResponseModel {
110 id: Some(request_id().into()),
111 cipher_id: Some(uuid!("55555555-5555-5555-5555-555555555555")),
112 collection_id: Some(uuid!("66666666-6666-6666-6666-666666666666")),
113 requester_id: Some(uuid!("88888888-8888-8888-8888-888888888888")),
114 status: Some(ApiAccessRequestStatus::Approved),
115 lease_not_before: Some("2025-01-01T00:00:00Z".to_string()),
116 lease_not_after: Some("2025-01-01T01:00:00Z".to_string()),
117 submitted_at: Some("2025-01-01T00:00:00Z".to_string()),
118 ..Default::default()
119 }
120 }
121
122 fn sample_lease() -> AccessLeaseResponseModel {
123 AccessLeaseResponseModel {
124 id: Some(uuid!("33333333-3333-3333-3333-333333333333")),
125 request_id: Some(request_id().into()),
126 cipher_id: Some(uuid!("55555555-5555-5555-5555-555555555555")),
127 collection_id: Some(uuid!("66666666-6666-6666-6666-666666666666")),
128 requester_id: Some(uuid!("88888888-8888-8888-8888-888888888888")),
129 status: Some(ApiAccessLeaseStatus::Active),
130 not_before: Some("2025-01-01T00:00:00Z".to_string()),
131 not_after: Some("2025-01-01T01:00:00Z".to_string()),
132 ..Default::default()
133 }
134 }
135
136 #[tokio::test]
137 async fn list_mine_returns_views() {
138 let api_client = ApiClient::new_mocked(move |mock| {
139 mock.access_requests_api
140 .expect_get_mine()
141 .returning(move || {
142 let mut list = AccessRequestDetailsResponseModelListResponseModel::new();
143 list.data = Some(vec![sample_request()]);
144 Ok(list)
145 })
146 .once();
147 });
148
149 let result = client(api_client).list_mine().await.unwrap();
150
151 assert_eq!(result.len(), 1);
152 assert_eq!(result[0].id, request_id());
153 assert_eq!(result[0].status, AccessRequestStatus::Approved);
154 }
155
156 #[tokio::test]
157 async fn get_returns_view() {
158 let api_client = ApiClient::new_mocked(move |mock| {
159 mock.access_requests_api
160 .expect_get_details()
161 .returning(move |_id| Ok(sample_request()))
162 .once();
163 });
164
165 let result = client(api_client).get(request_id()).await.unwrap();
166
167 assert_eq!(result.id, request_id());
168 }
169
170 #[tokio::test]
171 async fn activate_mints_a_lease() {
172 let api_client = ApiClient::new_mocked(move |mock| {
173 mock.access_requests_api
174 .expect_activate()
175 .returning(move |_id| Ok(sample_lease()))
176 .once();
177 });
178
179 let lease = client(api_client).activate(request_id()).await.unwrap();
180
181 assert_eq!(lease.request_id, request_id());
182 assert_eq!(lease.status, AccessLeaseStatus::Active);
183 }
184
185 #[tokio::test]
186 async fn activate_surfaces_api_error() {
187 let api_client = ApiClient::new_mocked(move |mock| {
188 mock.access_requests_api
189 .expect_activate()
190 .returning(move |_id| {
191 Err(bitwarden_api_api::apis::Error::Response(
192 bitwarden_api_api::apis::ResponseContent {
193 status: reqwest::StatusCode::CONFLICT,
194 message: "Not approved".to_string(),
195 },
196 ))
197 })
198 .once();
199 });
200
201 let result = client(api_client).activate(request_id()).await;
202
203 assert!(matches!(result, Err(LeasingError::Api(_))));
204 }
205
206 #[tokio::test]
207 async fn cancel_succeeds() {
208 let api_client = ApiClient::new_mocked(move |mock| {
209 mock.access_requests_api
210 .expect_revoke()
211 .returning(move |_id| Ok(()))
212 .once();
213 });
214
215 let result = client(api_client).cancel(request_id()).await;
216
217 assert!(result.is_ok());
218 }
219}