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().await;
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::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_otp_sent"
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::EmailAndOtpRequiredOtpSent
543                            ),
544                            error_description: Some(error_description),
545                        }
546                    );
547                }
548                other => panic!("expected Response variant, got {:?}", other),
549            }
550        }
551    }
552
553    mod request_send_access_token_invalid_grant_tests {
554
555        use super::*;
556
557        #[tokio::test]
558        async fn request_send_access_token_invalid_grant_invalid_send_id_error() {
559            // Create a mock error response
560            let error_description = "send_id is invalid.".into();
561            let raw_error = serde_json::json!({
562                "error": "invalid_grant",
563                "error_description": error_description,
564                "send_access_error_type": "send_id_invalid"
565            });
566
567            // Create the mock for the request
568            let mock = Mock::given(matchers::method("POST"))
569                .and(matchers::path("identity/connect/token"))
570                .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
571
572            // Spin up a server and register mock with it
573            let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
574
575            // Create a send access client
576            let send_access_client = make_send_client(&mock_server);
577
578            // Construct the request without a send_id to trigger an error
579            let req = SendAccessTokenRequest {
580                send_id: "invalid-send-id".into(),
581                send_access_credentials: None, // No credentials for this test
582            };
583
584            let result = send_access_client.request_send_access_token(req).await;
585
586            assert!(result.is_err());
587
588            let err = result.unwrap_err();
589            match err {
590                SendAccessTokenError::Expected(api_err) => {
591                    // Now assert the inner enum:
592                    assert_eq!(
593                        api_err,
594                        SendAccessTokenApiErrorResponse::InvalidGrant {
595                            send_access_error_type: Some(
596                                SendAccessTokenInvalidGrantError::SendIdInvalid
597                            ),
598                            error_description: Some(error_description),
599                        }
600                    );
601                }
602                other => panic!("expected Response variant, got {:?}", other),
603            }
604        }
605
606        #[tokio::test]
607        async fn request_send_access_token_invalid_grant_invalid_password_hash_error() {
608            // Create a mock error response
609            let error_description = "password_hash_b64 is invalid.".into();
610            let raw_error = serde_json::json!({
611                "error": "invalid_grant",
612                "error_description": error_description,
613                "send_access_error_type": "password_hash_b64_invalid"
614            });
615
616            // Create the mock for the request
617            let mock = Mock::given(matchers::method("POST"))
618                .and(matchers::path("identity/connect/token"))
619                .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
620
621            // Spin up a server and register mock with it
622            let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
623
624            // Create a send access client
625            let send_access_client = make_send_client(&mock_server);
626
627            // Construct the request
628            let password_credentials = SendPasswordCredentials {
629                password_hash_b64: "invalid-hash".into(),
630            };
631
632            let req = SendAccessTokenRequest {
633                send_id: "valid-send-id".into(),
634                send_access_credentials: Some(SendAccessCredentials::Password(
635                    password_credentials,
636                )),
637            };
638
639            let result = send_access_client.request_send_access_token(req).await;
640
641            assert!(result.is_err());
642
643            let err = result.unwrap_err();
644            match err {
645                SendAccessTokenError::Expected(api_err) => {
646                    // Now assert the inner enum:
647                    assert_eq!(
648                        api_err,
649                        SendAccessTokenApiErrorResponse::InvalidGrant {
650                            send_access_error_type: Some(
651                                SendAccessTokenInvalidGrantError::PasswordHashB64Invalid
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_grant_invalid_email_error() {
663            // Create a mock error response
664            let error_description = "email is invalid.".into();
665            let raw_error = serde_json::json!({
666                "error": "invalid_grant",
667                "error_description": error_description,
668                "send_access_error_type": "email_invalid"
669            });
670
671            // Register the mock for the request
672            let mock = Mock::given(matchers::method("POST"))
673                .and(matchers::path("identity/connect/token"))
674                .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
675
676            // Spin up a server and register mock with it
677            let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
678
679            // Create a send access client
680            let send_access_client = make_send_client(&mock_server);
681
682            // Construct the request
683            let email_credentials = SendEmailCredentials {
684                email: "invalid-email".into(),
685            };
686            let req = SendAccessTokenRequest {
687                send_id: "valid-send-id".into(),
688                send_access_credentials: Some(SendAccessCredentials::Email(email_credentials)),
689            };
690
691            let result = send_access_client.request_send_access_token(req).await;
692
693            assert!(result.is_err());
694
695            let err = result.unwrap_err();
696            match err {
697                SendAccessTokenError::Expected(api_err) => {
698                    // Now assert the inner enum:
699                    assert_eq!(
700                        api_err,
701                        SendAccessTokenApiErrorResponse::InvalidGrant {
702                            send_access_error_type: Some(
703                                SendAccessTokenInvalidGrantError::EmailInvalid
704                            ),
705                            error_description: Some(error_description),
706                        }
707                    );
708                }
709                other => panic!("expected Response variant, got {:?}", other),
710            }
711        }
712
713        #[tokio::test]
714        async fn request_send_access_token_invalid_grant_invalid_otp_error() {
715            // Create a mock error response
716            let error_description = "otp is invalid.".into();
717            let raw_error = serde_json::json!({
718                "error": "invalid_grant",
719                "error_description": error_description,
720                "send_access_error_type": "otp_invalid"
721            });
722
723            // Create the mock for the request
724            let mock = Mock::given(matchers::method("POST"))
725                .and(matchers::path("identity/connect/token"))
726                .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
727
728            // Spin up a server and register mock with it
729            let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
730
731            // Create a send access client
732            let send_access_client = make_send_client(&mock_server);
733
734            // Construct the request
735            let email_otp_credentials = SendEmailOtpCredentials {
736                email: "[email protected]".into(),
737                otp: "valid_otp".into(),
738            };
739            let req = SendAccessTokenRequest {
740                send_id: "valid-send-id".into(),
741                send_access_credentials: Some(SendAccessCredentials::EmailOtp(
742                    email_otp_credentials,
743                )),
744            };
745
746            let result = send_access_client.request_send_access_token(req).await;
747
748            assert!(result.is_err());
749
750            let err = result.unwrap_err();
751            match err {
752                SendAccessTokenError::Expected(api_err) => {
753                    // Now assert the inner enum:
754                    assert_eq!(
755                        api_err,
756                        SendAccessTokenApiErrorResponse::InvalidGrant {
757                            send_access_error_type: Some(
758                                SendAccessTokenInvalidGrantError::OtpInvalid
759                            ),
760                            error_description: Some(error_description),
761                        }
762                    );
763                }
764                other => panic!("expected Response variant, got {:?}", other),
765            }
766        }
767    }
768
769    mod request_send_access_token_unexpected_error_tests {
770
771        use super::*;
772
773        async fn run_case(status_code: u16, reason: &str) {
774            let mock = Mock::given(matchers::method("POST"))
775                .and(matchers::path("identity/connect/token"))
776                .respond_with(ResponseTemplate::new(status_code));
777
778            let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
779            let send_access_client = make_send_client(&mock_server);
780
781            let req = SendAccessTokenRequest {
782                send_id: "test_send_id".into(),
783                send_access_credentials: None,
784            };
785
786            let result = send_access_client.request_send_access_token(req).await;
787
788            assert!(result.is_err());
789
790            let err = result.expect_err(&format!(
791                "expected Err for status {} {} against http://{}/identity/connect/token",
792                status_code,
793                reason,
794                mock_server.address()
795            ));
796
797            match err {
798                SendAccessTokenError::Unexpected(api_err) => {
799                    let expected = UnexpectedIdentityError(format!(
800                        "Received response status {} {} against http://{}/identity/connect/token",
801                        status_code,
802                        reason,
803                        mock_server.address()
804                    ));
805                    assert_eq!(api_err, expected, "mismatch for status {}", status_code);
806                }
807                other => panic!("expected Unexpected variant, got {:?}", other),
808            }
809        }
810
811        #[tokio::test]
812        async fn request_send_access_token_unexpected_statuses() {
813            let cases = [
814                // 4xx (client errors) — excluding 400 Bad Request as we handle those as expected
815                // errors.
816                (401, "Unauthorized"),
817                (402, "Payment Required"),
818                (403, "Forbidden"),
819                (404, "Not Found"),
820                (405, "Method Not Allowed"),
821                (406, "Not Acceptable"),
822                (407, "Proxy Authentication Required"),
823                (408, "Request Timeout"),
824                (409, "Conflict"),
825                (410, "Gone"),
826                (411, "Length Required"),
827                (412, "Precondition Failed"),
828                (413, "Payload Too Large"),
829                (414, "URI Too Long"),
830                (415, "Unsupported Media Type"),
831                (416, "Range Not Satisfiable"),
832                (417, "Expectation Failed"),
833                (421, "Misdirected Request"),
834                (422, "Unprocessable Entity"),
835                (423, "Locked"),
836                (424, "Failed Dependency"),
837                (425, "Too Early"),
838                (426, "Upgrade Required"),
839                (428, "Precondition Required"),
840                (429, "Too Many Requests"),
841                (431, "Request Header Fields Too Large"),
842                (451, "Unavailable For Legal Reasons"),
843                // 5xx (server errors)
844                (500, "Internal Server Error"),
845                (501, "Not Implemented"),
846                (502, "Bad Gateway"),
847                (503, "Service Unavailable"),
848                (504, "Gateway Timeout"),
849                (505, "HTTP Version Not Supported"),
850                (506, "Variant Also Negotiates"),
851                (507, "Insufficient Storage"),
852                (508, "Loop Detected"),
853                (510, "Not Extended"),
854                (511, "Network Authentication Required"),
855            ];
856
857            for (code, reason) in cases {
858                run_case(code, reason).await;
859            }
860        }
861    }
862}