Skip to main content

bitwarden_auth/send_access/
client.rs

1use bitwarden_core::Client;
2#[cfg(feature = "wasm")]
3use wasm_bindgen::prelude::*;
4
5use crate::send_access::{
6    SendAccessTokenError, SendAccessTokenRequest, SendAccessTokenResponse,
7    access_token_response::UnexpectedIdentityError,
8    api::{
9        SendAccessTokenApiErrorResponse, SendAccessTokenApiSuccessResponse,
10        SendAccessTokenRequestPayload,
11    },
12};
13
14/// The `SendAccessClient` is used to interact with the Bitwarden API to get send access tokens.
15#[derive(Clone)]
16#[cfg_attr(feature = "wasm", wasm_bindgen)]
17pub struct SendAccessClient {
18    pub(crate) client: Client,
19}
20
21impl SendAccessClient {
22    pub(crate) fn new(client: Client) -> Self {
23        Self { client }
24    }
25}
26
27#[cfg_attr(feature = "wasm", wasm_bindgen)]
28impl SendAccessClient {
29    /// Requests a new send access token.
30    pub async fn request_send_access_token(
31        &self,
32        request: SendAccessTokenRequest,
33    ) -> Result<SendAccessTokenResponse, SendAccessTokenError> {
34        // Convert the request to the appropriate format for sending.
35        let payload: SendAccessTokenRequestPayload = request.into();
36
37        // When building other identity token requests, we used to send credentials: "include" on
38        // non-web clients or if the env had a base URL. See client's
39        // apiService.getCredentials() for example. However, it doesn't seem necessary for
40        // this request, so we are not including it here. If needed, we can revisit this and
41        // add it back in.
42
43        let configurations = self.client.internal.get_api_configurations();
44
45        // save off url in variable for re-use
46        let url = format!("{}/connect/token", configurations.identity_config.base_path);
47
48        let request: reqwest_middleware::RequestBuilder = configurations
49            .identity_config
50            .client
51            .post(&url)
52            .header(reqwest::header::ACCEPT, "application/json")
53            .header(reqwest::header::CACHE_CONTROL, "no-store")
54            .form(&payload);
55
56        // Because of the ? operator, any errors from sending the request are automatically
57        // wrapped in SendAccessTokenError::Unexpected as an UnexpectedIdentityError::Reqwest
58        // variant and returned.
59        // note: we had to manually built a trait to map reqwest::Error to SendAccessTokenError.
60        let response: reqwest::Response = request.send().await?;
61
62        let response_status = response.status();
63
64        // handle success and error responses
65        // If the response is 2xx, we can deserialize it into SendAccessToken
66        if response_status.is_success() {
67            let send_access_token: SendAccessTokenApiSuccessResponse = response.json().await?;
68            return Ok(send_access_token.into());
69        }
70
71        let err_response = match response.json::<SendAccessTokenApiErrorResponse>().await {
72            // If the response is a 400 with a specific error type, we can deserialize it into
73            // SendAccessTokenApiErrorResponse and then convert it into
74            // SendAccessTokenError::Expected later on.
75            Ok(err) => err,
76            Err(_) => {
77                // This handles any 4xx that aren't specifically handled above
78                // as well as any other non-2xx responses (5xx, etc).
79
80                let error_string = format!(
81                    "Received response status {} against {}",
82                    response_status, url
83                );
84
85                return Err(SendAccessTokenError::Unexpected(UnexpectedIdentityError(
86                    error_string,
87                )));
88            }
89        };
90
91        Err(SendAccessTokenError::Expected(err_response))
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use bitwarden_core::{Client as CoreClient, ClientSettings, DeviceType};
98    use bitwarden_test::start_api_mock;
99    use wiremock::{
100        Mock, MockServer, ResponseTemplate,
101        matchers::{self, body_string_contains},
102    };
103
104    use crate::{
105        AuthClientExt,
106        api::enums::{GrantType, Scope},
107        send_access::{
108            SendAccessClient, SendAccessCredentials, SendAccessTokenError, SendAccessTokenRequest,
109            SendAccessTokenResponse, SendEmailCredentials, SendEmailOtpCredentials,
110            SendPasswordCredentials, UnexpectedIdentityError,
111            api::{
112                SendAccessTokenApiErrorResponse, SendAccessTokenInvalidGrantError,
113                SendAccessTokenInvalidRequestError,
114            },
115        },
116    };
117
118    fn make_send_client(mock_server: &MockServer) -> SendAccessClient {
119        let settings = ClientSettings {
120            identity_url: format!("http://{}/identity", mock_server.address()),
121            api_url: format!("http://{}/api", mock_server.address()),
122            user_agent: "Bitwarden Rust-SDK [TEST]".into(),
123            device_type: DeviceType::SDK,
124            device_identifier: None,
125            bitwarden_client_version: None,
126            bitwarden_package_type: None,
127        };
128        let core_client = CoreClient::new(Some(settings));
129        core_client.auth_new().send_access()
130    }
131
132    mod request_send_access_token_success_tests {
133
134        use super::*;
135
136        #[tokio::test]
137        async fn request_send_access_token_anon_send_success() {
138            let scope_value = serde_json::to_value(Scope::ApiSendAccess).unwrap();
139            let scope_str = scope_value.as_str().unwrap();
140
141            let grant_type_value = serde_json::to_value(GrantType::SendAccess).unwrap();
142            let grant_type_str = grant_type_value.as_str().unwrap();
143
144            // Create a mock success response
145            let raw_success = serde_json::json!({
146                "access_token": "token",
147                "token_type": "bearer",
148                "expires_in":   3600,
149                "scope": scope_str
150            });
151
152            // Construct the real Request type
153            let req = SendAccessTokenRequest {
154                send_id: "test_send_id".into(),
155                send_access_credentials: None, // No credentials for this test
156            };
157
158            let mock = Mock::given(matchers::method("POST"))
159                .and(matchers::path("identity/connect/token"))
160                // expect the headers we set in the client
161                .and(matchers::header(
162                    reqwest::header::CONTENT_TYPE.as_str(),
163                    "application/x-www-form-urlencoded",
164                ))
165                .and(matchers::header(
166                    reqwest::header::ACCEPT.as_str(),
167                    "application/json",
168                ))
169                .and(matchers::header(
170                    reqwest::header::CACHE_CONTROL.as_str(),
171                    "no-store",
172                ))
173                // expect the body to contain the fields we set in our payload object
174                .and(body_string_contains("client_id=send"))
175                .and(body_string_contains(format!(
176                    "grant_type={}",
177                    grant_type_str
178                )))
179                .and(body_string_contains(format!("scope={}", scope_str)))
180                .and(body_string_contains(format!("send_id={}", req.send_id)))
181                // respond with the mock success response
182                .respond_with(ResponseTemplate::new(200).set_body_json(raw_success));
183
184            // Spin up a server and register mock with it
185            let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
186
187            // Create a send access client
188            let send_access_client = make_send_client(&mock_server);
189
190            let token: SendAccessTokenResponse = send_access_client
191                .request_send_access_token(req)
192                .await
193                .unwrap();
194
195            assert_eq!(token.token, "token");
196            assert!(token.expires_at > 0);
197        }
198
199        #[tokio::test]
200        async fn request_send_access_token_password_protected_send_success() {
201            let scope_value = serde_json::to_value(Scope::ApiSendAccess).unwrap();
202            let scope_str = scope_value.as_str().unwrap();
203
204            let grant_type_value = serde_json::to_value(GrantType::SendAccess).unwrap();
205            let grant_type_str = grant_type_value.as_str().unwrap();
206
207            // Create a mock success response
208            let raw_success = serde_json::json!({
209                "access_token": "token",
210                "token_type": "bearer",
211                "expires_in":   3600,
212                "scope": scope_str
213            });
214
215            let password_hash_b64 = "valid-hash";
216
217            let password_credentials = SendPasswordCredentials {
218                password_hash_b64: password_hash_b64.into(),
219            };
220
221            let req = SendAccessTokenRequest {
222                send_id: "valid-send-id".into(),
223                send_access_credentials: Some(SendAccessCredentials::Password(
224                    password_credentials,
225                )),
226            };
227
228            let mock = Mock::given(matchers::method("POST"))
229                .and(matchers::path("identity/connect/token"))
230                // expect the headers we set in the client
231                .and(matchers::header(
232                    reqwest::header::CONTENT_TYPE.as_str(),
233                    "application/x-www-form-urlencoded",
234                ))
235                .and(matchers::header(
236                    reqwest::header::ACCEPT.as_str(),
237                    "application/json",
238                ))
239                .and(matchers::header(
240                    reqwest::header::CACHE_CONTROL.as_str(),
241                    "no-store",
242                ))
243                // expect the body to contain the fields we set in our payload object
244                .and(body_string_contains("client_id=send"))
245                .and(body_string_contains(format!(
246                    "grant_type={}",
247                    grant_type_str
248                )))
249                .and(body_string_contains(format!("scope={}", scope_str)))
250                .and(body_string_contains(format!("send_id={}", req.send_id)))
251                .and(body_string_contains(format!(
252                    "password_hash_b64={}",
253                    password_hash_b64
254                )))
255                // respond with the mock success response
256                .respond_with(ResponseTemplate::new(200).set_body_json(raw_success));
257
258            // Spin up a server and register mock with it
259            let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
260
261            // Create a send access client
262            let send_access_client = make_send_client(&mock_server);
263
264            let token: SendAccessTokenResponse = send_access_client
265                .request_send_access_token(req)
266                .await
267                .unwrap();
268
269            assert_eq!(token.token, "token");
270            assert!(token.expires_at > 0);
271        }
272
273        #[tokio::test]
274        async fn request_send_access_token_email_otp_protected_send_success() {
275            let scope_value = serde_json::to_value(Scope::ApiSendAccess).unwrap();
276            let scope_str = scope_value.as_str().unwrap();
277
278            let grant_type_value = serde_json::to_value(GrantType::SendAccess).unwrap();
279            let grant_type_str = grant_type_value.as_str().unwrap();
280
281            // Create a mock success response
282            let raw_success = serde_json::json!({
283                "access_token": "token",
284                "token_type": "bearer",
285                "expires_in":   3600,
286                "scope": scope_str
287            });
288
289            let email = "[email protected]";
290            let otp: &str = "valid_otp";
291
292            let email_otp_credentials = SendEmailOtpCredentials {
293                email: email.into(),
294                otp: otp.into(),
295            };
296
297            let req = SendAccessTokenRequest {
298                send_id: "valid-send-id".into(),
299                send_access_credentials: Some(SendAccessCredentials::EmailOtp(
300                    email_otp_credentials,
301                )),
302            };
303
304            let mock = Mock::given(matchers::method("POST"))
305                .and(matchers::path("identity/connect/token"))
306                // expect the headers we set in the client
307                .and(matchers::header(
308                    reqwest::header::CONTENT_TYPE.as_str(),
309                    "application/x-www-form-urlencoded",
310                ))
311                .and(matchers::header(
312                    reqwest::header::ACCEPT.as_str(),
313                    "application/json",
314                ))
315                .and(matchers::header(
316                    reqwest::header::CACHE_CONTROL.as_str(),
317                    "no-store",
318                ))
319                // expect the body to contain the fields we set in our payload object
320                .and(body_string_contains("client_id=send"))
321                .and(body_string_contains(format!(
322                    "grant_type={}",
323                    grant_type_str
324                )))
325                .and(body_string_contains(format!("scope={}", scope_str)))
326                .and(body_string_contains(format!("send_id={}", req.send_id)))
327                .and(body_string_contains("email=valid%40email.com"))
328                .and(body_string_contains(format!("otp={}", otp)))
329                // respond with the mock success response
330                .respond_with(ResponseTemplate::new(200).set_body_json(raw_success));
331
332            // Spin up a server and register mock with it
333            let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
334
335            // Create a send access client
336            let send_access_client = make_send_client(&mock_server);
337
338            let token: SendAccessTokenResponse = send_access_client
339                .request_send_access_token(req)
340                .await
341                .unwrap();
342
343            assert_eq!(token.token, "token");
344            assert!(token.expires_at > 0);
345        }
346    }
347
348    mod request_send_access_token_invalid_request_tests {
349        use super::*;
350
351        #[tokio::test]
352        async fn request_send_access_token_invalid_request_send_id_required_error() {
353            // Create a mock error response
354            let error_description = "send_id is required.".into();
355            let raw_error = serde_json::json!({
356                "error": "invalid_request",
357                "error_description": error_description,
358                "send_access_error_type": "send_id_required"
359            });
360
361            // Register the mock for the request
362            let mock = Mock::given(matchers::method("POST"))
363                .and(matchers::path("identity/connect/token"))
364                .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
365
366            // Spin up a server and register mock with it
367            let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
368
369            // Create a send access client
370            let send_access_client = make_send_client(&mock_server);
371
372            // Construct the request without a send_id to trigger an error
373            let req = SendAccessTokenRequest {
374                send_id: "".into(),
375                send_access_credentials: None, // No credentials for this test
376            };
377
378            let result = send_access_client.request_send_access_token(req).await;
379
380            assert!(result.is_err());
381
382            let err = result.unwrap_err();
383            match err {
384                SendAccessTokenError::Expected(api_err) => {
385                    assert_eq!(
386                        api_err,
387                        SendAccessTokenApiErrorResponse::InvalidRequest {
388                            send_access_error_type: Some(
389                                SendAccessTokenInvalidRequestError::SendIdRequired
390                            ),
391                            error_description: Some(error_description),
392                        }
393                    );
394                }
395                other => panic!("expected Response variant, got {:?}", other),
396            }
397        }
398
399        #[tokio::test]
400        async fn request_send_access_token_invalid_request_password_hash_required_error() {
401            // Create a mock error response
402            let error_description = "password_hash_b64 is required.".into();
403            let raw_error = serde_json::json!({
404                "error": "invalid_request",
405                "error_description": error_description,
406                "send_access_error_type": "password_hash_b64_required"
407            });
408
409            // Register the mock for the request
410            let mock = Mock::given(matchers::method("POST"))
411                .and(matchers::path("identity/connect/token"))
412                .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
413
414            // Spin up a server and register mock with it
415            let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
416
417            // Create a send access client
418            let send_access_client = make_send_client(&mock_server);
419
420            // Construct the request with a send_id but no credentials to trigger the error
421            let req = SendAccessTokenRequest {
422                send_id: "test_send_id".into(),
423                send_access_credentials: None, // No credentials for this test
424            };
425
426            let result = send_access_client.request_send_access_token(req).await;
427
428            assert!(result.is_err());
429
430            let err = result.unwrap_err();
431            match err {
432                SendAccessTokenError::Expected(api_err) => {
433                    assert_eq!(
434                        api_err,
435                        SendAccessTokenApiErrorResponse::InvalidRequest {
436                            send_access_error_type: Some(
437                                SendAccessTokenInvalidRequestError::PasswordHashB64Required
438                            ),
439                            error_description: Some(error_description),
440                        }
441                    );
442                }
443                other => panic!("expected Response variant, got {:?}", other),
444            }
445        }
446
447        #[tokio::test]
448        async fn request_send_access_token_invalid_request_email_required_error() {
449            // Create a mock error response
450            let error_description = "email is required.".into();
451            let raw_error = serde_json::json!({
452                "error": "invalid_request",
453                "error_description": error_description,
454                "send_access_error_type": "email_required"
455            });
456
457            // Register the mock for the request
458            let mock = Mock::given(matchers::method("POST"))
459                .and(matchers::path("identity/connect/token"))
460                .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
461
462            // Spin up a server and register mock with it
463            let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
464
465            // Create a send access client
466            let send_access_client = make_send_client(&mock_server);
467
468            // Construct the request with a send_id but no credentials to trigger the error
469            let req = SendAccessTokenRequest {
470                send_id: "test_send_id".into(),
471                send_access_credentials: None, // No credentials for this test
472            };
473
474            let result = send_access_client.request_send_access_token(req).await;
475
476            assert!(result.is_err());
477
478            let err = result.unwrap_err();
479            match err {
480                SendAccessTokenError::Expected(api_err) => {
481                    assert_eq!(
482                        api_err,
483                        SendAccessTokenApiErrorResponse::InvalidRequest {
484                            send_access_error_type: Some(
485                                SendAccessTokenInvalidRequestError::EmailRequired
486                            ),
487                            error_description: Some(error_description),
488                        }
489                    );
490                }
491                other => panic!("expected Response variant, got {:?}", other),
492            }
493        }
494
495        #[tokio::test]
496        async fn request_send_access_token_invalid_request_email_otp_required_error() {
497            // Create a mock error response
498            let error_description =
499                "email and otp are required. An OTP has been sent to the email address provided."
500                    .into();
501            let raw_error = serde_json::json!({
502                "error": "invalid_request",
503                "error_description": error_description,
504                "send_access_error_type": "email_and_otp_required"
505            });
506
507            // Create the mock for the request
508            let mock = Mock::given(matchers::method("POST"))
509                .and(matchers::path("identity/connect/token"))
510                .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
511
512            // Spin up a server and register mock with it
513            let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
514
515            // Create a send access client
516            let send_access_client = make_send_client(&mock_server);
517
518            // Construct the request with a send_id and email credential
519            let email_credentials = SendEmailCredentials {
520                email: "[email protected]".into(),
521            };
522
523            let req = SendAccessTokenRequest {
524                send_id: "test_send_id".into(),
525                send_access_credentials: Some(SendAccessCredentials::Email(email_credentials)),
526            };
527
528            let result = send_access_client.request_send_access_token(req).await;
529
530            assert!(result.is_err());
531
532            let err = result.unwrap_err();
533            match err {
534                SendAccessTokenError::Expected(api_err) => {
535                    assert_eq!(
536                        api_err,
537                        SendAccessTokenApiErrorResponse::InvalidRequest {
538                            send_access_error_type: Some(
539                                SendAccessTokenInvalidRequestError::EmailAndOtpRequired
540                            ),
541                            error_description: Some(error_description),
542                        }
543                    );
544                }
545                other => panic!("expected Response variant, got {:?}", other),
546            }
547        }
548
549        #[tokio::test]
550        async fn request_send_access_token_invalid_request_email_credential_unrecognized_email_masked_as_otp_required()
551         {
552            // Create a mock error response
553            let error_description = "email and otp are required.".into();
554            let raw_error = serde_json::json!({
555                "error": "invalid_request",
556                "error_description": error_description,
557                "send_access_error_type": "email_and_otp_required"
558            });
559
560            // Register the mock for the request
561            let mock = Mock::given(matchers::method("POST"))
562                .and(matchers::path("identity/connect/token"))
563                .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
564
565            // Spin up a server and register mock with it
566            let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
567
568            // Create a send access client
569            let send_access_client = make_send_client(&mock_server);
570
571            // Construct the request
572            let email_credentials = SendEmailCredentials {
573                email: "invalid-email".into(),
574            };
575            let req = SendAccessTokenRequest {
576                send_id: "valid-send-id".into(),
577                send_access_credentials: Some(SendAccessCredentials::Email(email_credentials)),
578            };
579
580            let result = send_access_client.request_send_access_token(req).await;
581
582            assert!(result.is_err());
583
584            let err = result.unwrap_err();
585            match err {
586                SendAccessTokenError::Expected(api_err) => {
587                    // Now assert the inner enum:
588                    assert_eq!(
589                        api_err,
590                        SendAccessTokenApiErrorResponse::InvalidRequest {
591                            send_access_error_type: Some(
592                                SendAccessTokenInvalidRequestError::EmailAndOtpRequired
593                            ),
594                            error_description: Some(error_description),
595                        }
596                    );
597                }
598                other => panic!("expected Response variant, got {:?}", other),
599            }
600        }
601
602        #[tokio::test]
603        async fn request_send_access_token_invalid_request_email_otp_credential_invalid_otp_masked_as_otp_required()
604         {
605            // When an email+OTP is sent with an invalid OTP, the server returns
606            // email_and_otp_required (not otp_invalid) to prevent email enumeration.
607            let error_description = "email and otp are required.".into();
608            let raw_error = serde_json::json!({
609                "error": "invalid_request",
610                "error_description": error_description,
611                "send_access_error_type": "email_and_otp_required"
612            });
613
614            // Create the mock for the request
615            let mock = Mock::given(matchers::method("POST"))
616                .and(matchers::path("identity/connect/token"))
617                .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
618
619            // Spin up a server and register mock with it
620            let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
621
622            // Create a send access client
623            let send_access_client = make_send_client(&mock_server);
624
625            // Construct the request
626            let email_otp_credentials = SendEmailOtpCredentials {
627                email: "[email protected]".into(),
628                otp: "invalid_otp".into(),
629            };
630            let req = SendAccessTokenRequest {
631                send_id: "valid-send-id".into(),
632                send_access_credentials: Some(SendAccessCredentials::EmailOtp(
633                    email_otp_credentials,
634                )),
635            };
636
637            let result = send_access_client.request_send_access_token(req).await;
638
639            assert!(result.is_err());
640
641            let err = result.unwrap_err();
642            match err {
643                SendAccessTokenError::Expected(api_err) => {
644                    assert_eq!(
645                        api_err,
646                        SendAccessTokenApiErrorResponse::InvalidRequest {
647                            send_access_error_type: Some(
648                                SendAccessTokenInvalidRequestError::EmailAndOtpRequired
649                            ),
650                            error_description: Some(error_description),
651                        }
652                    );
653                }
654                other => panic!("expected Response variant, got {:?}", other),
655            }
656        }
657
658        #[tokio::test]
659        async fn request_send_access_token_invalid_request_email_otp_credential_unrecognized_email_masked_as_otp_required()
660         {
661            // When an email+OTP is sent where the email is not in the Send's allowed list,
662            // the server returns email_and_otp_required (not email_invalid) to prevent email
663            // enumeration. The server checks email validity before OTP, so even a valid OTP
664            // paired with an unrecognized email returns the same generic response.
665            let error_description = "email and otp are required.".into();
666            let raw_error = serde_json::json!({
667                "error": "invalid_request",
668                "error_description": error_description,
669                "send_access_error_type": "email_and_otp_required"
670            });
671
672            // Create the mock for the request
673            let mock = Mock::given(matchers::method("POST"))
674                .and(matchers::path("identity/connect/token"))
675                .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
676
677            // Spin up a server and register mock with it
678            let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
679
680            // Create a send access client
681            let send_access_client = make_send_client(&mock_server);
682
683            // Construct the request with an email not in the Send's allowed list
684            let email_otp_credentials = SendEmailOtpCredentials {
685                email: "[email protected]".into(),
686                otp: "any_otp".into(),
687            };
688            let req = SendAccessTokenRequest {
689                send_id: "valid-send-id".into(),
690                send_access_credentials: Some(SendAccessCredentials::EmailOtp(
691                    email_otp_credentials,
692                )),
693            };
694
695            let result = send_access_client.request_send_access_token(req).await;
696
697            assert!(result.is_err());
698
699            let err = result.unwrap_err();
700            match err {
701                SendAccessTokenError::Expected(api_err) => {
702                    assert_eq!(
703                        api_err,
704                        SendAccessTokenApiErrorResponse::InvalidRequest {
705                            send_access_error_type: Some(
706                                SendAccessTokenInvalidRequestError::EmailAndOtpRequired
707                            ),
708                            error_description: Some(error_description),
709                        }
710                    );
711                }
712                other => panic!("expected Response variant, got {:?}", other),
713            }
714        }
715    }
716
717    mod request_send_access_token_invalid_grant_tests {
718
719        use super::*;
720
721        #[tokio::test]
722        async fn request_send_access_token_invalid_grant_invalid_send_id_error() {
723            // Create a mock error response
724            let error_description = "send_id is invalid.".into();
725            let raw_error = serde_json::json!({
726                "error": "invalid_grant",
727                "error_description": error_description,
728                "send_access_error_type": "send_id_invalid"
729            });
730
731            // Create the mock for the request
732            let mock = Mock::given(matchers::method("POST"))
733                .and(matchers::path("identity/connect/token"))
734                .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
735
736            // Spin up a server and register mock with it
737            let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
738
739            // Create a send access client
740            let send_access_client = make_send_client(&mock_server);
741
742            // Construct the request with an invalid send_id to trigger an error
743            let req = SendAccessTokenRequest {
744                send_id: "invalid-send-id".into(),
745                send_access_credentials: None, // No credentials for this test
746            };
747
748            let result = send_access_client.request_send_access_token(req).await;
749
750            assert!(result.is_err());
751
752            let err = result.unwrap_err();
753            match err {
754                SendAccessTokenError::Expected(api_err) => {
755                    // Now assert the inner enum:
756                    assert_eq!(
757                        api_err,
758                        SendAccessTokenApiErrorResponse::InvalidGrant {
759                            send_access_error_type: Some(
760                                SendAccessTokenInvalidGrantError::SendIdInvalid
761                            ),
762                            error_description: Some(error_description),
763                        }
764                    );
765                }
766                other => panic!("expected Response variant, got {:?}", other),
767            }
768        }
769
770        #[tokio::test]
771        async fn request_send_access_token_invalid_grant_invalid_password_hash_error() {
772            // Create a mock error response
773            let error_description = "password_hash_b64 is invalid.".into();
774            let raw_error = serde_json::json!({
775                "error": "invalid_grant",
776                "error_description": error_description,
777                "send_access_error_type": "password_hash_b64_invalid"
778            });
779
780            // Create the mock for the request
781            let mock = Mock::given(matchers::method("POST"))
782                .and(matchers::path("identity/connect/token"))
783                .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
784
785            // Spin up a server and register mock with it
786            let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
787
788            // Create a send access client
789            let send_access_client = make_send_client(&mock_server);
790
791            // Construct the request
792            let password_credentials = SendPasswordCredentials {
793                password_hash_b64: "invalid-hash".into(),
794            };
795
796            let req = SendAccessTokenRequest {
797                send_id: "valid-send-id".into(),
798                send_access_credentials: Some(SendAccessCredentials::Password(
799                    password_credentials,
800                )),
801            };
802
803            let result = send_access_client.request_send_access_token(req).await;
804
805            assert!(result.is_err());
806
807            let err = result.unwrap_err();
808            match err {
809                SendAccessTokenError::Expected(api_err) => {
810                    // Now assert the inner enum:
811                    assert_eq!(
812                        api_err,
813                        SendAccessTokenApiErrorResponse::InvalidGrant {
814                            send_access_error_type: Some(
815                                SendAccessTokenInvalidGrantError::PasswordHashB64Invalid
816                            ),
817                            error_description: Some(error_description),
818                        }
819                    );
820                }
821                other => panic!("expected Response variant, got {:?}", other),
822            }
823        }
824    }
825
826    mod request_send_access_token_unexpected_error_tests {
827
828        use super::*;
829
830        async fn run_case(status_code: u16, reason: &str) {
831            let mock = Mock::given(matchers::method("POST"))
832                .and(matchers::path("identity/connect/token"))
833                .respond_with(ResponseTemplate::new(status_code));
834
835            let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
836            let send_access_client = make_send_client(&mock_server);
837
838            let req = SendAccessTokenRequest {
839                send_id: "test_send_id".into(),
840                send_access_credentials: None,
841            };
842
843            let result = send_access_client.request_send_access_token(req).await;
844
845            assert!(result.is_err());
846
847            let err = result.expect_err(&format!(
848                "expected Err for status {} {} against http://{}/identity/connect/token",
849                status_code,
850                reason,
851                mock_server.address()
852            ));
853
854            match err {
855                SendAccessTokenError::Unexpected(api_err) => {
856                    let expected = UnexpectedIdentityError(format!(
857                        "Received response status {} {} against http://{}/identity/connect/token",
858                        status_code,
859                        reason,
860                        mock_server.address()
861                    ));
862                    assert_eq!(api_err, expected, "mismatch for status {}", status_code);
863                }
864                other => panic!("expected Unexpected variant, got {:?}", other),
865            }
866        }
867
868        #[tokio::test]
869        async fn request_send_access_token_unexpected_statuses() {
870            let cases = [
871                // 4xx (client errors) — excluding 400 Bad Request as we handle those as expected
872                // errors.
873                (401, "Unauthorized"),
874                (402, "Payment Required"),
875                (403, "Forbidden"),
876                (404, "Not Found"),
877                (405, "Method Not Allowed"),
878                (406, "Not Acceptable"),
879                (407, "Proxy Authentication Required"),
880                (408, "Request Timeout"),
881                (409, "Conflict"),
882                (410, "Gone"),
883                (411, "Length Required"),
884                (412, "Precondition Failed"),
885                (413, "Payload Too Large"),
886                (414, "URI Too Long"),
887                (415, "Unsupported Media Type"),
888                (416, "Range Not Satisfiable"),
889                (417, "Expectation Failed"),
890                (421, "Misdirected Request"),
891                (422, "Unprocessable Entity"),
892                (423, "Locked"),
893                (424, "Failed Dependency"),
894                (425, "Too Early"),
895                (426, "Upgrade Required"),
896                (428, "Precondition Required"),
897                (429, "Too Many Requests"),
898                (431, "Request Header Fields Too Large"),
899                (451, "Unavailable For Legal Reasons"),
900                // 5xx (server errors)
901                (500, "Internal Server Error"),
902                (501, "Not Implemented"),
903                (502, "Bad Gateway"),
904                (503, "Service Unavailable"),
905                (504, "Gateway Timeout"),
906                (505, "HTTP Version Not Supported"),
907                (506, "Variant Also Negotiates"),
908                (507, "Insufficient Storage"),
909                (508, "Loop Detected"),
910                (510, "Not Extended"),
911                (511, "Network Authentication Required"),
912            ];
913
914            for (code, reason) in cases {
915                run_case(code, reason).await;
916            }
917        }
918    }
919}