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