1use anyhow::{Context, Result};
12use reqwest::{Client, Response};
13use url::Url;
14
15use crate::drive::auth::{DriveCredentials, DriveSession};
16use crate::drive::error::DriveError;
17use crate::request_log;
18use crate::utils::env::{EnvSource, SystemEnv};
19use crate::utils::http::{connect_timeout, read_timeout, retry_if};
20
21pub struct DriveClient {
23 client: Client,
24 base_url: String,
25 session: DriveSession,
26}
27
28impl std::fmt::Debug for DriveClient {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 f.debug_struct("DriveClient")
34 .field("base_url", &self.base_url)
35 .finish_non_exhaustive()
36 }
37}
38
39impl DriveClient {
40 const DEFAULT_BASE_URL: &'static str = "https://www.googleapis.com";
50
51 pub fn new(base_url: &str, credentials: &DriveCredentials) -> Result<Self> {
56 let client = Client::builder()
57 .connect_timeout(connect_timeout())
58 .read_timeout(read_timeout())
59 .build()
60 .context("Failed to build HTTP client")?;
61 let session = DriveSession::new(client.clone(), credentials);
62 Ok(Self {
63 client,
64 base_url: base_url.trim_end_matches('/').to_string(),
65 session,
66 })
67 }
68
69 pub fn from_credentials(credentials: &DriveCredentials) -> Result<Self> {
78 Self::from_credentials_with(&SystemEnv, credentials)
79 }
80
81 pub(crate) fn from_credentials_with(
85 env: &impl EnvSource,
86 credentials: &DriveCredentials,
87 ) -> Result<Self> {
88 let base_url = env
89 .var(crate::drive::auth::DRIVE_API_URL)
90 .filter(|s| !s.is_empty())
91 .unwrap_or_else(|| Self::DEFAULT_BASE_URL.to_string());
92 Self::new(&base_url, credentials)
93 }
94
95 #[must_use]
97 pub fn base_url(&self) -> &str {
98 &self.base_url
99 }
100
101 pub(crate) fn api_url(base_url: &str, path: &str) -> Result<Url> {
107 Url::parse(&format!("{base_url}{path}")).context("Invalid Drive base URL")
108 }
109
110 pub(crate) async fn parse_response<T: serde::de::DeserializeOwned>(
112 &self,
113 response: Response,
114 context: &'static str,
115 ) -> Result<T> {
116 if !response.status().is_success() {
117 return Err(Self::response_to_error(response).await.into());
118 }
119 response.json().await.context(context)
120 }
121
122 pub(crate) async fn get_parsed<T: serde::de::DeserializeOwned>(
124 &self,
125 url: &str,
126 context: &'static str,
127 ) -> Result<T> {
128 let response = self.get_json(url).await?;
129 self.parse_response(response, context).await
130 }
131
132 pub async fn get_json(&self, url: &str) -> Result<Response> {
138 self.send_authorized(url, "GET", |client, token| {
139 client
140 .get(url)
141 .bearer_auth(token)
142 .header("Accept", "application/json")
143 })
144 .await
145 }
146
147 pub async fn get_bytes(&self, url: &str) -> Result<Response> {
153 self.send_authorized(url, "GET", |client, token| {
154 client.get(url).bearer_auth(token)
155 })
156 .await
157 }
158
159 pub async fn post_json<T: serde::Serialize + Sync + ?Sized>(
162 &self,
163 url: &str,
164 body: &T,
165 ) -> Result<Response> {
166 self.send_authorized(url, "POST", |client, token| {
167 client
168 .post(url)
169 .bearer_auth(token)
170 .header("Content-Type", "application/json")
171 .json(body)
172 })
173 .await
174 }
175
176 pub async fn patch_json<T: serde::Serialize + Sync + ?Sized>(
180 &self,
181 url: &str,
182 body: &T,
183 ) -> Result<Response> {
184 self.send_authorized(url, "PATCH", |client, token| {
185 client
186 .patch(url)
187 .bearer_auth(token)
188 .header("Content-Type", "application/json")
189 .json(body)
190 })
191 .await
192 }
193
194 async fn send_authorized<F>(
206 &self,
207 url: &str,
208 method: &'static str,
209 build: F,
210 ) -> Result<Response>
211 where
212 F: Fn(&Client, &str) -> reqwest::RequestBuilder + Send + Sync,
213 {
214 let token = self
215 .session
216 .access_token()
217 .await
218 .context("Failed to obtain a Drive access token")?;
219 let response = self
220 .send_once(url, method, &build, token.expose_secret())
221 .await?;
222 if response.status().as_u16() != 401 {
223 return Ok(response);
224 }
225 let refreshed = self
226 .session
227 .force_refresh(&token)
228 .await
229 .context("Failed to refresh the Drive access token after a 401")?;
230 self.send_once(url, method, &build, refreshed.expose_secret())
231 .await
232 }
233
234 async fn send_once<F>(
235 &self,
236 url: &str,
237 method: &'static str,
238 build: &F,
239 token: &str,
240 ) -> Result<Response>
241 where
242 F: Fn(&Client, &str) -> reqwest::RequestBuilder + Send + Sync,
243 {
244 retry_if(
245 || build(&self.client, token),
246 |started, result| {
247 request_log::record_http_result("drive", method, url, started, result);
248 },
249 |status, body| status == 429 || is_drive_quota_exceeded(status, body),
250 )
251 .await
252 .with_context(|| format!("Failed to send {method} request to Drive API"))
253 }
254
255 pub async fn response_to_error(response: Response) -> DriveError {
266 let status = response.status().as_u16();
267 let raw = response.text().await.unwrap_or_default();
268 let value = serde_json::from_str::<serde_json::Value>(&raw).ok();
269 let reason = value.as_ref().and_then(drive_error_reason);
270 let body = value
271 .as_ref()
272 .and_then(drive_error_message)
273 .map(|message| match &reason {
274 Some(r) => format!("{message} (reason: {r})"),
275 None => message,
276 })
277 .unwrap_or(raw);
278 DriveError::ApiRequestFailed {
279 status,
280 body,
281 reason,
282 }
283 }
284}
285
286fn drive_error_reason(value: &serde_json::Value) -> Option<String> {
289 value
290 .get("error")
291 .and_then(|e| e.get("errors"))
292 .and_then(|e| e.as_array())
293 .and_then(|a| a.first())
294 .and_then(|e| e.get("reason"))
295 .and_then(|r| r.as_str())
296 .map(str::to_string)
297}
298
299fn drive_error_message(value: &serde_json::Value) -> Option<String> {
302 value
303 .get("error")?
304 .get("message")?
305 .as_str()
306 .map(str::to_string)
307}
308
309fn is_drive_quota_exceeded(status: u16, body: &[u8]) -> bool {
324 if status != 403 {
325 return false;
326 }
327 let Ok(text) = std::str::from_utf8(body) else {
328 return false;
329 };
330 let Ok(value) = serde_json::from_str::<serde_json::Value>(text) else {
331 return false;
332 };
333 matches!(
334 drive_error_reason(&value).as_deref(),
335 Some("userRateLimitExceeded")
336 )
337}
338
339#[cfg(test)]
343pub(crate) mod test_support {
344 use super::DriveClient;
345 use crate::drive::auth::{DriveCredentials, DriveSession};
346
347 pub(crate) fn replace_session(
350 client: &mut DriveClient,
351 credentials: &DriveCredentials,
352 token_endpoint: &str,
353 ) {
354 client.session = DriveSession::new_with_token_endpoint(
355 client.client.clone(),
356 credentials,
357 token_endpoint,
358 );
359 }
360}
361
362#[cfg(test)]
363#[allow(clippy::unwrap_used, clippy::expect_used)]
364mod tests {
365 use super::*;
366 use crate::drive::auth::DriveScope;
367 use crate::utils::secret::Secret;
368
369 fn test_credentials() -> DriveCredentials {
370 DriveCredentials {
371 client_id: "client-1".to_string(),
372 client_secret: Secret::new("secret-1"),
373 refresh_token: Secret::new("refresh-1"),
374 scope: DriveScope::ReadOnly,
375 }
376 }
377
378 #[test]
379 fn is_drive_quota_exceeded_false_on_non_utf8_body() {
380 assert!(!is_drive_quota_exceeded(403, &[0xff, 0xfe]));
381 }
382
383 #[test]
384 fn is_drive_quota_exceeded_false_on_non_403_status() {
385 assert!(!is_drive_quota_exceeded(
386 429,
387 br#"{"error":{"errors":[{"reason":"userRateLimitExceeded"}]}}"#
388 ));
389 }
390
391 #[test]
392 fn is_drive_quota_exceeded_true_on_matching_403_reason() {
393 assert!(is_drive_quota_exceeded(
394 403,
395 br#"{"error":{"errors":[{"reason":"userRateLimitExceeded"}]}}"#
396 ));
397 }
398
399 #[test]
400 fn new_client_strips_trailing_slash() {
401 let client = DriveClient::new("https://www.googleapis.com/", &test_credentials()).unwrap();
402 assert_eq!(client.base_url(), "https://www.googleapis.com");
403 }
404
405 #[test]
406 fn new_client_preserves_clean_url() {
407 let client = DriveClient::new("https://www.googleapis.com", &test_credentials()).unwrap();
408 assert_eq!(client.base_url(), "https://www.googleapis.com");
409 }
410
411 #[test]
412 fn from_credentials_uses_drive_api_host() {
413 let env = crate::test_support::env::MapEnv::new();
417 let client = DriveClient::from_credentials_with(&env, &test_credentials()).unwrap();
418 assert_eq!(client.base_url(), "https://www.googleapis.com");
419 }
420
421 #[test]
422 fn from_credentials_honours_api_url_override() {
423 let env = crate::test_support::env::MapEnv::new().with(
424 crate::drive::auth::DRIVE_API_URL,
425 "http://proxy.example:8080",
426 );
427 let client = DriveClient::from_credentials_with(&env, &test_credentials()).unwrap();
428 assert_eq!(client.base_url(), "http://proxy.example:8080");
429 }
430
431 #[test]
432 fn from_credentials_ignores_empty_api_url_override() {
433 let env =
434 crate::test_support::env::MapEnv::new().with(crate::drive::auth::DRIVE_API_URL, "");
435 let client = DriveClient::from_credentials_with(&env, &test_credentials()).unwrap();
436 assert_eq!(client.base_url(), "https://www.googleapis.com");
437 }
438
439 #[test]
440 fn client_debug_never_mentions_session_field() {
441 let client = DriveClient::new("https://www.googleapis.com", &test_credentials()).unwrap();
442 let debug = format!("{client:?}");
443 assert!(!debug.contains("secret-1"));
444 assert!(!debug.contains("refresh-1"));
445 assert!(!debug.contains("session"));
446 assert!(debug.contains("DriveClient"));
447 }
448
449 async fn client_with_bootstrapped_token(server: &wiremock::MockServer) -> DriveClient {
454 wiremock::Mock::given(wiremock::matchers::method("POST"))
459 .and(wiremock::matchers::path("/token"))
460 .respond_with(
461 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
462 "access_token": "bootstrap-token",
463 "expires_in": 3600,
464 })),
465 )
466 .up_to_n_times(1)
467 .with_priority(1)
468 .mount(server)
469 .await;
470
471 let mut client = DriveClient::new(&server.uri(), &test_credentials()).unwrap();
472 client.session = DriveSession::new_with_token_endpoint(
473 client.client.clone(),
474 &test_credentials(),
475 &format!("{}/token", server.uri()),
476 );
477 client
478 }
479
480 #[tokio::test]
481 async fn get_json_sends_bearer_auth_header() {
482 let server = wiremock::MockServer::start().await;
483 let client = client_with_bootstrapped_token(&server).await;
484 wiremock::Mock::given(wiremock::matchers::method("GET"))
485 .and(wiremock::matchers::path("/test"))
486 .and(wiremock::matchers::header(
487 "Authorization",
488 "Bearer bootstrap-token",
489 ))
490 .respond_with(
491 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({"ok": true})),
492 )
493 .expect(1)
494 .mount(&server)
495 .await;
496
497 let resp = client
498 .get_json(&format!("{}/test", server.uri()))
499 .await
500 .unwrap();
501 assert!(resp.status().is_success());
502 }
503
504 #[tokio::test]
505 async fn get_bytes_sends_bearer_auth_header_without_json_accept_header() {
506 let server = wiremock::MockServer::start().await;
507 let client = client_with_bootstrapped_token(&server).await;
508 wiremock::Mock::given(wiremock::matchers::method("GET"))
509 .and(wiremock::matchers::path("/test"))
510 .and(wiremock::matchers::header(
511 "Authorization",
512 "Bearer bootstrap-token",
513 ))
514 .respond_with(
515 wiremock::ResponseTemplate::new(200).set_body_bytes(b"raw bytes".to_vec()),
516 )
517 .expect(1)
518 .mount(&server)
519 .await;
520
521 let resp = client
522 .get_bytes(&format!("{}/test", server.uri()))
523 .await
524 .unwrap();
525 assert!(resp.status().is_success());
526 let bytes = resp.bytes().await.unwrap();
527 assert_eq!(bytes.as_ref(), b"raw bytes");
528 }
529
530 #[tokio::test]
531 async fn get_bytes_retries_on_429() {
532 let server = wiremock::MockServer::start().await;
533 let client = client_with_bootstrapped_token(&server).await;
534 wiremock::Mock::given(wiremock::matchers::method("GET"))
535 .and(wiremock::matchers::path("/test"))
536 .respond_with(wiremock::ResponseTemplate::new(429).append_header("Retry-After", "0"))
537 .up_to_n_times(1)
538 .with_priority(1)
539 .mount(&server)
540 .await;
541 wiremock::Mock::given(wiremock::matchers::method("GET"))
542 .and(wiremock::matchers::path("/test"))
543 .respond_with(wiremock::ResponseTemplate::new(200).set_body_bytes(b"ok".to_vec()))
544 .with_priority(2)
545 .mount(&server)
546 .await;
547
548 let resp = client
549 .get_bytes(&format!("{}/test", server.uri()))
550 .await
551 .unwrap();
552 assert_eq!(resp.status().as_u16(), 200);
553 }
554
555 #[tokio::test]
556 async fn get_bytes_refreshes_and_retries_once_on_401() {
557 let server = wiremock::MockServer::start().await;
558 let client = client_with_bootstrapped_token(&server).await;
559 wiremock::Mock::given(wiremock::matchers::method("POST"))
560 .and(wiremock::matchers::path("/token"))
561 .respond_with(
562 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
563 "access_token": "refreshed-token",
564 "expires_in": 3600,
565 })),
566 )
567 .up_to_n_times(1)
568 .with_priority(2)
569 .mount(&server)
570 .await;
571 wiremock::Mock::given(wiremock::matchers::method("GET"))
572 .and(wiremock::matchers::path("/test"))
573 .and(wiremock::matchers::header(
574 "Authorization",
575 "Bearer bootstrap-token",
576 ))
577 .respond_with(wiremock::ResponseTemplate::new(401))
578 .expect(1)
579 .mount(&server)
580 .await;
581 wiremock::Mock::given(wiremock::matchers::method("GET"))
582 .and(wiremock::matchers::path("/test"))
583 .and(wiremock::matchers::header(
584 "Authorization",
585 "Bearer refreshed-token",
586 ))
587 .respond_with(wiremock::ResponseTemplate::new(200).set_body_bytes(b"ok".to_vec()))
588 .expect(1)
589 .mount(&server)
590 .await;
591
592 let resp = client
593 .get_bytes(&format!("{}/test", server.uri()))
594 .await
595 .unwrap();
596 assert_eq!(resp.status().as_u16(), 200);
597 }
598
599 #[tokio::test]
600 async fn get_bytes_propagates_network_errors() {
601 let client = DriveClient::new("http://127.0.0.1:1", &test_credentials()).unwrap();
602 let result = client.get_bytes("http://127.0.0.1:1/test").await;
603 assert!(result.is_err());
604 }
605
606 #[tokio::test]
607 async fn post_json_sends_body_and_bearer_auth() {
608 let server = wiremock::MockServer::start().await;
609 let client = client_with_bootstrapped_token(&server).await;
610 wiremock::Mock::given(wiremock::matchers::method("POST"))
611 .and(wiremock::matchers::path("/test"))
612 .and(wiremock::matchers::header(
613 "Authorization",
614 "Bearer bootstrap-token",
615 ))
616 .and(wiremock::matchers::body_json(serde_json::json!({"k": "v"})))
617 .respond_with(wiremock::ResponseTemplate::new(200))
618 .expect(1)
619 .mount(&server)
620 .await;
621
622 let resp = client
623 .post_json(
624 &format!("{}/test", server.uri()),
625 &serde_json::json!({"k": "v"}),
626 )
627 .await
628 .unwrap();
629 assert!(resp.status().is_success());
630 }
631
632 #[tokio::test]
633 async fn patch_json_sends_body_and_bearer_auth() {
634 let server = wiremock::MockServer::start().await;
635 let client = client_with_bootstrapped_token(&server).await;
636 wiremock::Mock::given(wiremock::matchers::method("PATCH"))
637 .and(wiremock::matchers::path("/test"))
638 .and(wiremock::matchers::header(
639 "Authorization",
640 "Bearer bootstrap-token",
641 ))
642 .and(wiremock::matchers::body_json(
643 serde_json::json!({"name": "new-name"}),
644 ))
645 .respond_with(wiremock::ResponseTemplate::new(200))
646 .expect(1)
647 .mount(&server)
648 .await;
649
650 let resp = client
651 .patch_json(
652 &format!("{}/test", server.uri()),
653 &serde_json::json!({"name": "new-name"}),
654 )
655 .await
656 .unwrap();
657 assert!(resp.status().is_success());
658 }
659
660 #[tokio::test]
661 async fn get_json_retries_on_429() {
662 let server = wiremock::MockServer::start().await;
663 let client = client_with_bootstrapped_token(&server).await;
664 wiremock::Mock::given(wiremock::matchers::method("GET"))
665 .and(wiremock::matchers::path("/test"))
666 .respond_with(wiremock::ResponseTemplate::new(429).append_header("Retry-After", "0"))
667 .up_to_n_times(1)
668 .with_priority(1)
669 .mount(&server)
670 .await;
671 wiremock::Mock::given(wiremock::matchers::method("GET"))
672 .and(wiremock::matchers::path("/test"))
673 .respond_with(wiremock::ResponseTemplate::new(200))
674 .with_priority(2)
675 .mount(&server)
676 .await;
677
678 let resp = client
679 .get_json(&format!("{}/test", server.uri()))
680 .await
681 .unwrap();
682 assert_eq!(resp.status().as_u16(), 200);
683 }
684
685 #[tokio::test]
686 async fn get_json_retries_403_user_rate_limit_exceeded_then_succeeds() {
687 let server = wiremock::MockServer::start().await;
688 let client = client_with_bootstrapped_token(&server).await;
689 wiremock::Mock::given(wiremock::matchers::method("GET"))
690 .and(wiremock::matchers::path("/test"))
691 .respond_with(
692 wiremock::ResponseTemplate::new(403)
693 .append_header("Retry-After", "0")
694 .set_body_json(serde_json::json!({
695 "error": {"message": "User Rate Limit Exceeded", "errors": [{"reason": "userRateLimitExceeded"}]}
696 })),
697 )
698 .up_to_n_times(1)
699 .with_priority(1)
700 .mount(&server)
701 .await;
702 wiremock::Mock::given(wiremock::matchers::method("GET"))
703 .and(wiremock::matchers::path("/test"))
704 .respond_with(wiremock::ResponseTemplate::new(200))
705 .with_priority(2)
706 .mount(&server)
707 .await;
708
709 let resp = client
710 .get_json(&format!("{}/test", server.uri()))
711 .await
712 .unwrap();
713 assert_eq!(resp.status().as_u16(), 200);
714 }
715
716 #[tokio::test]
717 async fn get_json_does_not_retry_insufficient_permissions_403() {
718 let server = wiremock::MockServer::start().await;
719 let client = client_with_bootstrapped_token(&server).await;
720 wiremock::Mock::given(wiremock::matchers::method("GET"))
721 .and(wiremock::matchers::path("/test"))
722 .respond_with(
723 wiremock::ResponseTemplate::new(403).set_body_json(serde_json::json!({
724 "error": {"message": "Insufficient Permission", "errors": [{"reason": "insufficientPermissions"}]}
725 })),
726 )
727 .expect(1)
728 .mount(&server)
729 .await;
730
731 let resp = client
732 .get_json(&format!("{}/test", server.uri()))
733 .await
734 .unwrap();
735 assert_eq!(resp.status().as_u16(), 403);
736 }
737
738 #[tokio::test]
739 async fn get_json_refreshes_and_retries_once_on_401() {
740 let server = wiremock::MockServer::start().await;
741 let client = client_with_bootstrapped_token(&server).await;
742 wiremock::Mock::given(wiremock::matchers::method("POST"))
744 .and(wiremock::matchers::path("/token"))
745 .respond_with(
746 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
747 "access_token": "refreshed-token",
748 "expires_in": 3600,
749 })),
750 )
751 .up_to_n_times(1)
752 .with_priority(2)
753 .mount(&server)
754 .await;
755 wiremock::Mock::given(wiremock::matchers::method("GET"))
756 .and(wiremock::matchers::path("/test"))
757 .and(wiremock::matchers::header(
758 "Authorization",
759 "Bearer bootstrap-token",
760 ))
761 .respond_with(wiremock::ResponseTemplate::new(401))
762 .expect(1)
763 .mount(&server)
764 .await;
765 wiremock::Mock::given(wiremock::matchers::method("GET"))
766 .and(wiremock::matchers::path("/test"))
767 .and(wiremock::matchers::header(
768 "Authorization",
769 "Bearer refreshed-token",
770 ))
771 .respond_with(wiremock::ResponseTemplate::new(200))
772 .expect(1)
773 .mount(&server)
774 .await;
775
776 let resp = client
777 .get_json(&format!("{}/test", server.uri()))
778 .await
779 .unwrap();
780 assert_eq!(resp.status().as_u16(), 200);
781 }
782
783 #[tokio::test]
784 async fn get_json_does_not_retry_a_second_time_on_persistent_401() {
785 let server = wiremock::MockServer::start().await;
786 let client = client_with_bootstrapped_token(&server).await;
787 wiremock::Mock::given(wiremock::matchers::method("POST"))
788 .and(wiremock::matchers::path("/token"))
789 .respond_with(
790 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
791 "access_token": "still-rejected-token",
792 "expires_in": 3600,
793 })),
794 )
795 .up_to_n_times(1)
796 .with_priority(2)
797 .mount(&server)
798 .await;
799 wiremock::Mock::given(wiremock::matchers::method("GET"))
800 .and(wiremock::matchers::path("/test"))
801 .respond_with(
802 wiremock::ResponseTemplate::new(401).set_body_string("still unauthorized"),
803 )
804 .expect(2)
805 .mount(&server)
806 .await;
807
808 let resp = client
809 .get_json(&format!("{}/test", server.uri()))
810 .await
811 .unwrap();
812 assert_eq!(resp.status().as_u16(), 401);
813 }
814
815 #[tokio::test]
816 async fn response_to_error_extracts_drive_message_and_reason() {
817 let server = wiremock::MockServer::start().await;
818 let client = client_with_bootstrapped_token(&server).await;
819 wiremock::Mock::given(wiremock::matchers::method("GET"))
823 .and(wiremock::matchers::path("/test"))
824 .respond_with(
825 wiremock::ResponseTemplate::new(403)
826 .append_header("Retry-After", "0")
827 .set_body_json(serde_json::json!({
828 "error": {
829 "message": "User Rate Limit Exceeded",
830 "errors": [{"reason": "userRateLimitExceeded"}],
831 }
832 })),
833 )
834 .mount(&server)
835 .await;
836
837 let resp = client
838 .get_json(&format!("{}/test", server.uri()))
839 .await
840 .unwrap();
841 let err = DriveClient::response_to_error(resp).await;
842 let msg = err.to_string();
843 assert!(msg.contains("User Rate Limit Exceeded"));
844 assert!(msg.contains("userRateLimitExceeded"));
845 assert_eq!(err.reason(), Some("userRateLimitExceeded"));
846 }
847
848 #[tokio::test]
849 async fn response_to_error_omits_reason_suffix_when_absent() {
850 let server = wiremock::MockServer::start().await;
851 let client = client_with_bootstrapped_token(&server).await;
852 wiremock::Mock::given(wiremock::matchers::method("GET"))
853 .and(wiremock::matchers::path("/test"))
854 .respond_with(
855 wiremock::ResponseTemplate::new(400).set_body_json(serde_json::json!({
856 "error": {
857 "message": "Invalid request",
858 }
859 })),
860 )
861 .mount(&server)
862 .await;
863
864 let resp = client
865 .get_json(&format!("{}/test", server.uri()))
866 .await
867 .unwrap();
868 let err = DriveClient::response_to_error(resp).await;
869 let msg = err.to_string();
870 assert!(msg.contains("Invalid request"));
871 assert!(!msg.contains("reason:"));
872 assert_eq!(err.reason(), None);
873 }
874
875 #[tokio::test]
876 async fn response_to_error_falls_back_to_raw_body_when_not_drive_shaped() {
877 let server = wiremock::MockServer::start().await;
878 let client = client_with_bootstrapped_token(&server).await;
879 wiremock::Mock::given(wiremock::matchers::method("GET"))
880 .and(wiremock::matchers::path("/test"))
881 .respond_with(wiremock::ResponseTemplate::new(500).set_body_string("internal error"))
882 .mount(&server)
883 .await;
884
885 let resp = client
886 .get_json(&format!("{}/test", server.uri()))
887 .await
888 .unwrap();
889 let err = DriveClient::response_to_error(resp).await;
890 assert!(err.to_string().contains("internal error"));
891 }
892
893 #[tokio::test]
894 async fn get_json_propagates_network_errors() {
895 let client = DriveClient::new("http://127.0.0.1:1", &test_credentials()).unwrap();
896 let result = client.get_json("http://127.0.0.1:1/test").await;
897 assert!(result.is_err());
898 }
899
900 #[tokio::test]
901 async fn get_parsed_errors_on_malformed_json_response() {
902 let server = wiremock::MockServer::start().await;
903 let client = client_with_bootstrapped_token(&server).await;
904 wiremock::Mock::given(wiremock::matchers::method("GET"))
905 .and(wiremock::matchers::path("/test"))
906 .respond_with(wiremock::ResponseTemplate::new(200).set_body_string("not json"))
907 .mount(&server)
908 .await;
909
910 let result: Result<serde_json::Value> = client
911 .get_parsed(&format!("{}/test", server.uri()), "test context")
912 .await;
913 assert!(result.is_err());
914 }
915
916 #[tokio::test]
917 async fn get_parsed_errors_on_non_success_status_without_parsing_the_body() {
918 let server = wiremock::MockServer::start().await;
919 let client = client_with_bootstrapped_token(&server).await;
920 wiremock::Mock::given(wiremock::matchers::method("GET"))
921 .and(wiremock::matchers::path("/test"))
922 .respond_with(
923 wiremock::ResponseTemplate::new(404).set_body_json(serde_json::json!({
924 "error": {"message": "File not found"}
925 })),
926 )
927 .mount(&server)
928 .await;
929
930 let result: Result<serde_json::Value> = client
931 .get_parsed(&format!("{}/test", server.uri()), "test context")
932 .await;
933 let err = result.unwrap_err();
934 assert!(err.to_string().contains("File not found"));
935 }
936}