Skip to main content

bitwarden_auth/send_access/
client.rs

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