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