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#[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 pub async fn request_send_access_token(
31 &self,
32 request: SendAccessTokenRequest,
33 ) -> Result<SendAccessTokenResponse, SendAccessTokenError> {
34 let payload: SendAccessTokenRequestPayload = request.into();
36
37 let configurations = self.client.internal.get_api_configurations().await;
44
45 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 let response: reqwest::Response = request.send().await?;
64
65 let response_status = response.status();
66
67 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 Ok(err) => err,
79 Err(_) => {
80 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 bitwarden_client_version: None,
128 };
129 let core_client = CoreClient::new(Some(settings));
130 core_client.auth_new().send_access()
131 }
132
133 mod request_send_access_token_success_tests {
134
135 use super::*;
136
137 #[tokio::test]
138 async fn request_send_access_token_anon_send_success() {
139 let scope_value = serde_json::to_value(Scope::ApiSendAccess).unwrap();
140 let scope_str = scope_value.as_str().unwrap();
141
142 let grant_type_value = serde_json::to_value(GrantType::SendAccess).unwrap();
143 let grant_type_str = grant_type_value.as_str().unwrap();
144
145 let raw_success = serde_json::json!({
147 "access_token": "token",
148 "token_type": "bearer",
149 "expires_in": 3600,
150 "scope": scope_str
151 });
152
153 let req = SendAccessTokenRequest {
155 send_id: "test_send_id".into(),
156 send_access_credentials: None, };
158
159 let mock = Mock::given(matchers::method("POST"))
160 .and(matchers::path("identity/connect/token"))
161 .and(matchers::header(
163 reqwest::header::CONTENT_TYPE.as_str(),
164 "application/x-www-form-urlencoded",
165 ))
166 .and(matchers::header(
167 reqwest::header::ACCEPT.as_str(),
168 "application/json",
169 ))
170 .and(matchers::header(
171 reqwest::header::CACHE_CONTROL.as_str(),
172 "no-store",
173 ))
174 .and(body_string_contains("client_id=send"))
176 .and(body_string_contains(format!(
177 "grant_type={}",
178 grant_type_str
179 )))
180 .and(body_string_contains(format!("scope={}", scope_str)))
181 .and(body_string_contains(format!("send_id={}", req.send_id)))
182 .respond_with(ResponseTemplate::new(200).set_body_json(raw_success));
184
185 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
187
188 let send_access_client = make_send_client(&mock_server);
190
191 let token: SendAccessTokenResponse = send_access_client
192 .request_send_access_token(req)
193 .await
194 .unwrap();
195
196 assert_eq!(token.token, "token");
197 assert!(token.expires_at > 0);
198 }
199
200 #[tokio::test]
201 async fn request_send_access_token_password_protected_send_success() {
202 let scope_value = serde_json::to_value(Scope::ApiSendAccess).unwrap();
203 let scope_str = scope_value.as_str().unwrap();
204
205 let grant_type_value = serde_json::to_value(GrantType::SendAccess).unwrap();
206 let grant_type_str = grant_type_value.as_str().unwrap();
207
208 let raw_success = serde_json::json!({
210 "access_token": "token",
211 "token_type": "bearer",
212 "expires_in": 3600,
213 "scope": scope_str
214 });
215
216 let password_hash_b64 = "valid-hash";
217
218 let password_credentials = SendPasswordCredentials {
219 password_hash_b64: password_hash_b64.into(),
220 };
221
222 let req = SendAccessTokenRequest {
223 send_id: "valid-send-id".into(),
224 send_access_credentials: Some(SendAccessCredentials::Password(
225 password_credentials,
226 )),
227 };
228
229 let mock = Mock::given(matchers::method("POST"))
230 .and(matchers::path("identity/connect/token"))
231 .and(matchers::header(
233 reqwest::header::CONTENT_TYPE.as_str(),
234 "application/x-www-form-urlencoded",
235 ))
236 .and(matchers::header(
237 reqwest::header::ACCEPT.as_str(),
238 "application/json",
239 ))
240 .and(matchers::header(
241 reqwest::header::CACHE_CONTROL.as_str(),
242 "no-store",
243 ))
244 .and(body_string_contains("client_id=send"))
246 .and(body_string_contains(format!(
247 "grant_type={}",
248 grant_type_str
249 )))
250 .and(body_string_contains(format!("scope={}", scope_str)))
251 .and(body_string_contains(format!("send_id={}", req.send_id)))
252 .and(body_string_contains(format!(
253 "password_hash_b64={}",
254 password_hash_b64
255 )))
256 .respond_with(ResponseTemplate::new(200).set_body_json(raw_success));
258
259 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
261
262 let send_access_client = make_send_client(&mock_server);
264
265 let token: SendAccessTokenResponse = send_access_client
266 .request_send_access_token(req)
267 .await
268 .unwrap();
269
270 assert_eq!(token.token, "token");
271 assert!(token.expires_at > 0);
272 }
273
274 #[tokio::test]
275 async fn request_send_access_token_email_otp_protected_send_success() {
276 let scope_value = serde_json::to_value(Scope::ApiSendAccess).unwrap();
277 let scope_str = scope_value.as_str().unwrap();
278
279 let grant_type_value = serde_json::to_value(GrantType::SendAccess).unwrap();
280 let grant_type_str = grant_type_value.as_str().unwrap();
281
282 let raw_success = serde_json::json!({
284 "access_token": "token",
285 "token_type": "bearer",
286 "expires_in": 3600,
287 "scope": scope_str
288 });
289
290 let email = "[email protected]";
291 let otp: &str = "valid_otp";
292
293 let email_otp_credentials = SendEmailOtpCredentials {
294 email: email.into(),
295 otp: otp.into(),
296 };
297
298 let req = SendAccessTokenRequest {
299 send_id: "valid-send-id".into(),
300 send_access_credentials: Some(SendAccessCredentials::EmailOtp(
301 email_otp_credentials,
302 )),
303 };
304
305 let mock = Mock::given(matchers::method("POST"))
306 .and(matchers::path("identity/connect/token"))
307 .and(matchers::header(
309 reqwest::header::CONTENT_TYPE.as_str(),
310 "application/x-www-form-urlencoded",
311 ))
312 .and(matchers::header(
313 reqwest::header::ACCEPT.as_str(),
314 "application/json",
315 ))
316 .and(matchers::header(
317 reqwest::header::CACHE_CONTROL.as_str(),
318 "no-store",
319 ))
320 .and(body_string_contains("client_id=send"))
322 .and(body_string_contains(format!(
323 "grant_type={}",
324 grant_type_str
325 )))
326 .and(body_string_contains(format!("scope={}", scope_str)))
327 .and(body_string_contains(format!("send_id={}", req.send_id)))
328 .and(body_string_contains("email=valid%40email.com"))
329 .and(body_string_contains(format!("otp={}", otp)))
330 .respond_with(ResponseTemplate::new(200).set_body_json(raw_success));
332
333 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
335
336 let send_access_client = make_send_client(&mock_server);
338
339 let token: SendAccessTokenResponse = send_access_client
340 .request_send_access_token(req)
341 .await
342 .unwrap();
343
344 assert_eq!(token.token, "token");
345 assert!(token.expires_at > 0);
346 }
347 }
348
349 mod request_send_access_token_invalid_request_tests {
350 use super::*;
351
352 #[tokio::test]
353 async fn request_send_access_token_invalid_request_send_id_required_error() {
354 let error_description = "send_id is required.".into();
356 let raw_error = serde_json::json!({
357 "error": "invalid_request",
358 "error_description": error_description,
359 "send_access_error_type": "send_id_required"
360 });
361
362 let mock = Mock::given(matchers::method("POST"))
364 .and(matchers::path("identity/connect/token"))
365 .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
366
367 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
369
370 let send_access_client = make_send_client(&mock_server);
372
373 let req = SendAccessTokenRequest {
375 send_id: "".into(),
376 send_access_credentials: None, };
378
379 let result = send_access_client.request_send_access_token(req).await;
380
381 assert!(result.is_err());
382
383 let err = result.unwrap_err();
384 match err {
385 SendAccessTokenError::Expected(api_err) => {
386 assert_eq!(
387 api_err,
388 SendAccessTokenApiErrorResponse::InvalidRequest {
389 send_access_error_type: Some(
390 SendAccessTokenInvalidRequestError::SendIdRequired
391 ),
392 error_description: Some(error_description),
393 }
394 );
395 }
396 other => panic!("expected Response variant, got {:?}", other),
397 }
398 }
399
400 #[tokio::test]
401 async fn request_send_access_token_invalid_request_password_hash_required_error() {
402 let error_description = "password_hash_b64 is required.".into();
404 let raw_error = serde_json::json!({
405 "error": "invalid_request",
406 "error_description": error_description,
407 "send_access_error_type": "password_hash_b64_required"
408 });
409
410 let mock = Mock::given(matchers::method("POST"))
412 .and(matchers::path("identity/connect/token"))
413 .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
414
415 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
417
418 let send_access_client = make_send_client(&mock_server);
420
421 let req = SendAccessTokenRequest {
423 send_id: "test_send_id".into(),
424 send_access_credentials: None, };
426
427 let result = send_access_client.request_send_access_token(req).await;
428
429 assert!(result.is_err());
430
431 let err = result.unwrap_err();
432 match err {
433 SendAccessTokenError::Expected(api_err) => {
434 assert_eq!(
435 api_err,
436 SendAccessTokenApiErrorResponse::InvalidRequest {
437 send_access_error_type: Some(
438 SendAccessTokenInvalidRequestError::PasswordHashB64Required
439 ),
440 error_description: Some(error_description),
441 }
442 );
443 }
444 other => panic!("expected Response variant, got {:?}", other),
445 }
446 }
447
448 #[tokio::test]
449 async fn request_send_access_token_invalid_request_email_required_error() {
450 let error_description = "email is required.".into();
452 let raw_error = serde_json::json!({
453 "error": "invalid_request",
454 "error_description": error_description,
455 "send_access_error_type": "email_required"
456 });
457
458 let mock = Mock::given(matchers::method("POST"))
460 .and(matchers::path("identity/connect/token"))
461 .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
462
463 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
465
466 let send_access_client = make_send_client(&mock_server);
468
469 let req = SendAccessTokenRequest {
471 send_id: "test_send_id".into(),
472 send_access_credentials: None, };
474
475 let result = send_access_client.request_send_access_token(req).await;
476
477 assert!(result.is_err());
478
479 let err = result.unwrap_err();
480 match err {
481 SendAccessTokenError::Expected(api_err) => {
482 assert_eq!(
483 api_err,
484 SendAccessTokenApiErrorResponse::InvalidRequest {
485 send_access_error_type: Some(
486 SendAccessTokenInvalidRequestError::EmailRequired
487 ),
488 error_description: Some(error_description),
489 }
490 );
491 }
492 other => panic!("expected Response variant, got {:?}", other),
493 }
494 }
495
496 #[tokio::test]
497 async fn request_send_access_token_invalid_request_email_otp_required_error() {
498 let error_description =
500 "email and otp are required. An OTP has been sent to the email address provided."
501 .into();
502 let raw_error = serde_json::json!({
503 "error": "invalid_request",
504 "error_description": error_description,
505 "send_access_error_type": "email_and_otp_required_otp_sent"
506 });
507
508 let mock = Mock::given(matchers::method("POST"))
510 .and(matchers::path("identity/connect/token"))
511 .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
512
513 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
515
516 let send_access_client = make_send_client(&mock_server);
518
519 let email_credentials = SendEmailCredentials {
521 email: "[email protected]".into(),
522 };
523
524 let req = SendAccessTokenRequest {
525 send_id: "test_send_id".into(),
526 send_access_credentials: Some(SendAccessCredentials::Email(email_credentials)),
527 };
528
529 let result = send_access_client.request_send_access_token(req).await;
530
531 assert!(result.is_err());
532
533 let err = result.unwrap_err();
534 match err {
535 SendAccessTokenError::Expected(api_err) => {
536 assert_eq!(
537 api_err,
538 SendAccessTokenApiErrorResponse::InvalidRequest {
539 send_access_error_type: Some(
540 SendAccessTokenInvalidRequestError::EmailAndOtpRequiredOtpSent
541 ),
542 error_description: Some(error_description),
543 }
544 );
545 }
546 other => panic!("expected Response variant, got {:?}", other),
547 }
548 }
549 }
550
551 mod request_send_access_token_invalid_grant_tests {
552
553 use super::*;
554
555 #[tokio::test]
556 async fn request_send_access_token_invalid_grant_invalid_send_id_error() {
557 let error_description = "send_id is invalid.".into();
559 let raw_error = serde_json::json!({
560 "error": "invalid_grant",
561 "error_description": error_description,
562 "send_access_error_type": "send_id_invalid"
563 });
564
565 let mock = Mock::given(matchers::method("POST"))
567 .and(matchers::path("identity/connect/token"))
568 .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
569
570 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
572
573 let send_access_client = make_send_client(&mock_server);
575
576 let req = SendAccessTokenRequest {
578 send_id: "invalid-send-id".into(),
579 send_access_credentials: None, };
581
582 let result = send_access_client.request_send_access_token(req).await;
583
584 assert!(result.is_err());
585
586 let err = result.unwrap_err();
587 match err {
588 SendAccessTokenError::Expected(api_err) => {
589 assert_eq!(
591 api_err,
592 SendAccessTokenApiErrorResponse::InvalidGrant {
593 send_access_error_type: Some(
594 SendAccessTokenInvalidGrantError::SendIdInvalid
595 ),
596 error_description: Some(error_description),
597 }
598 );
599 }
600 other => panic!("expected Response variant, got {:?}", other),
601 }
602 }
603
604 #[tokio::test]
605 async fn request_send_access_token_invalid_grant_invalid_password_hash_error() {
606 let error_description = "password_hash_b64 is invalid.".into();
608 let raw_error = serde_json::json!({
609 "error": "invalid_grant",
610 "error_description": error_description,
611 "send_access_error_type": "password_hash_b64_invalid"
612 });
613
614 let mock = Mock::given(matchers::method("POST"))
616 .and(matchers::path("identity/connect/token"))
617 .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
618
619 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
621
622 let send_access_client = make_send_client(&mock_server);
624
625 let password_credentials = SendPasswordCredentials {
627 password_hash_b64: "invalid-hash".into(),
628 };
629
630 let req = SendAccessTokenRequest {
631 send_id: "valid-send-id".into(),
632 send_access_credentials: Some(SendAccessCredentials::Password(
633 password_credentials,
634 )),
635 };
636
637 let result = send_access_client.request_send_access_token(req).await;
638
639 assert!(result.is_err());
640
641 let err = result.unwrap_err();
642 match err {
643 SendAccessTokenError::Expected(api_err) => {
644 assert_eq!(
646 api_err,
647 SendAccessTokenApiErrorResponse::InvalidGrant {
648 send_access_error_type: Some(
649 SendAccessTokenInvalidGrantError::PasswordHashB64Invalid
650 ),
651 error_description: Some(error_description),
652 }
653 );
654 }
655 other => panic!("expected Response variant, got {:?}", other),
656 }
657 }
658
659 #[tokio::test]
660 async fn request_send_access_token_invalid_grant_invalid_email_error() {
661 let error_description = "email is invalid.".into();
663 let raw_error = serde_json::json!({
664 "error": "invalid_grant",
665 "error_description": error_description,
666 "send_access_error_type": "email_invalid"
667 });
668
669 let mock = Mock::given(matchers::method("POST"))
671 .and(matchers::path("identity/connect/token"))
672 .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
673
674 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
676
677 let send_access_client = make_send_client(&mock_server);
679
680 let email_credentials = SendEmailCredentials {
682 email: "invalid-email".into(),
683 };
684 let req = SendAccessTokenRequest {
685 send_id: "valid-send-id".into(),
686 send_access_credentials: Some(SendAccessCredentials::Email(email_credentials)),
687 };
688
689 let result = send_access_client.request_send_access_token(req).await;
690
691 assert!(result.is_err());
692
693 let err = result.unwrap_err();
694 match err {
695 SendAccessTokenError::Expected(api_err) => {
696 assert_eq!(
698 api_err,
699 SendAccessTokenApiErrorResponse::InvalidGrant {
700 send_access_error_type: Some(
701 SendAccessTokenInvalidGrantError::EmailInvalid
702 ),
703 error_description: Some(error_description),
704 }
705 );
706 }
707 other => panic!("expected Response variant, got {:?}", other),
708 }
709 }
710
711 #[tokio::test]
712 async fn request_send_access_token_invalid_grant_invalid_otp_error() {
713 let error_description = "otp is invalid.".into();
715 let raw_error = serde_json::json!({
716 "error": "invalid_grant",
717 "error_description": error_description,
718 "send_access_error_type": "otp_invalid"
719 });
720
721 let mock = Mock::given(matchers::method("POST"))
723 .and(matchers::path("identity/connect/token"))
724 .respond_with(ResponseTemplate::new(400).set_body_json(raw_error));
725
726 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
728
729 let send_access_client = make_send_client(&mock_server);
731
732 let email_otp_credentials = SendEmailOtpCredentials {
734 email: "[email protected]".into(),
735 otp: "valid_otp".into(),
736 };
737 let req = SendAccessTokenRequest {
738 send_id: "valid-send-id".into(),
739 send_access_credentials: Some(SendAccessCredentials::EmailOtp(
740 email_otp_credentials,
741 )),
742 };
743
744 let result = send_access_client.request_send_access_token(req).await;
745
746 assert!(result.is_err());
747
748 let err = result.unwrap_err();
749 match err {
750 SendAccessTokenError::Expected(api_err) => {
751 assert_eq!(
753 api_err,
754 SendAccessTokenApiErrorResponse::InvalidGrant {
755 send_access_error_type: Some(
756 SendAccessTokenInvalidGrantError::OtpInvalid
757 ),
758 error_description: Some(error_description),
759 }
760 );
761 }
762 other => panic!("expected Response variant, got {:?}", other),
763 }
764 }
765 }
766
767 mod request_send_access_token_unexpected_error_tests {
768
769 use super::*;
770
771 async fn run_case(status_code: u16, reason: &str) {
772 let mock = Mock::given(matchers::method("POST"))
773 .and(matchers::path("identity/connect/token"))
774 .respond_with(ResponseTemplate::new(status_code));
775
776 let (mock_server, _api_config) = start_api_mock(vec![mock]).await;
777 let send_access_client = make_send_client(&mock_server);
778
779 let req = SendAccessTokenRequest {
780 send_id: "test_send_id".into(),
781 send_access_credentials: None,
782 };
783
784 let result = send_access_client.request_send_access_token(req).await;
785
786 assert!(result.is_err());
787
788 let err = result.expect_err(&format!(
789 "expected Err for status {} {} against http://{}/identity/connect/token",
790 status_code,
791 reason,
792 mock_server.address()
793 ));
794
795 match err {
796 SendAccessTokenError::Unexpected(api_err) => {
797 let expected = UnexpectedIdentityError(format!(
798 "Received response status {} {} against http://{}/identity/connect/token",
799 status_code,
800 reason,
801 mock_server.address()
802 ));
803 assert_eq!(api_err, expected, "mismatch for status {}", status_code);
804 }
805 other => panic!("expected Unexpected variant, got {:?}", other),
806 }
807 }
808
809 #[tokio::test]
810 async fn request_send_access_token_unexpected_statuses() {
811 let cases = [
812 (401, "Unauthorized"),
815 (402, "Payment Required"),
816 (403, "Forbidden"),
817 (404, "Not Found"),
818 (405, "Method Not Allowed"),
819 (406, "Not Acceptable"),
820 (407, "Proxy Authentication Required"),
821 (408, "Request Timeout"),
822 (409, "Conflict"),
823 (410, "Gone"),
824 (411, "Length Required"),
825 (412, "Precondition Failed"),
826 (413, "Payload Too Large"),
827 (414, "URI Too Long"),
828 (415, "Unsupported Media Type"),
829 (416, "Range Not Satisfiable"),
830 (417, "Expectation Failed"),
831 (421, "Misdirected Request"),
832 (422, "Unprocessable Entity"),
833 (423, "Locked"),
834 (424, "Failed Dependency"),
835 (425, "Too Early"),
836 (426, "Upgrade Required"),
837 (428, "Precondition Required"),
838 (429, "Too Many Requests"),
839 (431, "Request Header Fields Too Large"),
840 (451, "Unavailable For Legal Reasons"),
841 (500, "Internal Server Error"),
843 (501, "Not Implemented"),
844 (502, "Bad Gateway"),
845 (503, "Service Unavailable"),
846 (504, "Gateway Timeout"),
847 (505, "HTTP Version Not Supported"),
848 (506, "Variant Also Negotiates"),
849 (507, "Insufficient Storage"),
850 (508, "Loop Detected"),
851 (510, "Not Extended"),
852 (511, "Network Authentication Required"),
853 ];
854
855 for (code, reason) in cases {
856 run_case(code, reason).await;
857 }
858 }
859 }
860}