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