1use anyhow::Result;
18use reqwest::Response;
19use url::Url;
20
21use crate::drive::api_client::GoogleApiClient;
22use crate::drive::auth::DriveCredentials;
23use crate::drive::error::DriveError;
24use crate::utils::env::{EnvSource, SystemEnv};
25
26pub(crate) const SERVICE_TAG: &str = "drive";
35
36pub(crate) const API_NAME: &str = "Drive";
40
41pub struct DriveClient {
43 inner: GoogleApiClient,
44}
45
46impl std::fmt::Debug for DriveClient {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 f.debug_struct("DriveClient")
53 .field("base_url", &self.inner.base_url())
54 .finish_non_exhaustive()
55 }
56}
57
58impl DriveClient {
59 const DEFAULT_BASE_URL: &'static str = "https://www.googleapis.com";
69
70 pub fn new(base_url: &str, credentials: &DriveCredentials) -> Result<Self> {
75 Ok(Self {
76 inner: GoogleApiClient::new(base_url, credentials, SERVICE_TAG, API_NAME)?,
77 })
78 }
79
80 pub fn from_credentials(credentials: &DriveCredentials) -> Result<Self> {
89 Self::from_credentials_with(&SystemEnv, credentials)
90 }
91
92 pub(crate) fn from_credentials_with(
96 env: &impl EnvSource,
97 credentials: &DriveCredentials,
98 ) -> Result<Self> {
99 let base_url = env
100 .var(crate::drive::auth::DRIVE_API_URL)
101 .filter(|s| !s.is_empty())
102 .unwrap_or_else(|| Self::DEFAULT_BASE_URL.to_string());
103 Self::new(&base_url, credentials)
104 }
105
106 #[must_use]
108 pub fn base_url(&self) -> &str {
109 self.inner.base_url()
110 }
111
112 #[allow(dead_code)]
118 pub(in crate::drive) fn transport(&self) -> &GoogleApiClient {
119 &self.inner
120 }
121
122 pub(crate) fn api_url(base_url: &str, path: &str) -> Result<Url> {
128 GoogleApiClient::api_url(base_url, path)
129 }
130
131 pub(crate) async fn parse_response<T: serde::de::DeserializeOwned>(
133 &self,
134 response: Response,
135 context: &'static str,
136 ) -> Result<T> {
137 self.inner.parse_response(response, context).await
138 }
139
140 pub(crate) async fn get_parsed<T: serde::de::DeserializeOwned>(
142 &self,
143 url: &str,
144 context: &'static str,
145 ) -> Result<T> {
146 self.inner.get_parsed(url, context).await
147 }
148
149 pub async fn get_json(&self, url: &str) -> Result<Response> {
153 self.inner.get_json(url).await
154 }
155
156 pub async fn get_bytes(&self, url: &str) -> Result<Response> {
162 self.inner.get_bytes(url).await
163 }
164
165 pub async fn post_json<T: serde::Serialize + Sync + ?Sized>(
168 &self,
169 url: &str,
170 body: &T,
171 ) -> Result<Response> {
172 self.inner.post_json(url, body).await
173 }
174
175 pub async fn patch_json<T: serde::Serialize + Sync + ?Sized>(
179 &self,
180 url: &str,
181 body: &T,
182 ) -> Result<Response> {
183 self.inner.patch_json(url, body).await
184 }
185
186 pub async fn post_bytes(&self, url: &str, body: &[u8], content_type: &str) -> Result<Response> {
192 self.inner.post_bytes(url, body, content_type).await
193 }
194
195 pub async fn patch_bytes(
201 &self,
202 url: &str,
203 body: &[u8],
204 content_type: &str,
205 ) -> Result<Response> {
206 self.inner.patch_bytes(url, body, content_type).await
207 }
208
209 pub async fn response_to_error(response: Response) -> DriveError {
219 GoogleApiClient::response_to_error(API_NAME, response).await
220 }
221}
222
223#[cfg(test)]
227#[allow(clippy::expect_used)]
228pub(crate) mod test_support {
229 use super::{DriveClient, GoogleApiClient, API_NAME, SERVICE_TAG};
230 use crate::drive::auth::DriveCredentials;
231
232 pub(crate) fn replace_session(
241 client: &mut DriveClient,
242 credentials: &DriveCredentials,
243 token_endpoint: &str,
244 ) {
245 let base_url = client.base_url().to_string();
246 client.inner = GoogleApiClient::new_with_token_endpoint(
247 &base_url,
248 credentials,
249 SERVICE_TAG,
250 API_NAME,
251 token_endpoint,
252 )
253 .expect("failed to build a test transport");
254 }
255}
256
257#[cfg(test)]
258#[allow(clippy::unwrap_used, clippy::expect_used)]
259mod tests {
260 use super::*;
261 use crate::drive::auth::DriveGrantedScopes;
262 use crate::utils::secret::Secret;
263
264 fn test_credentials() -> DriveCredentials {
265 DriveCredentials {
266 client_id: "client-1".to_string(),
267 client_secret: Secret::new("secret-1"),
268 refresh_token: Secret::new("refresh-1"),
269 scope: DriveGrantedScopes::READONLY,
270 }
271 }
272
273 #[test]
274 fn new_client_strips_trailing_slash() {
275 let client = DriveClient::new("https://www.googleapis.com/", &test_credentials()).unwrap();
276 assert_eq!(client.base_url(), "https://www.googleapis.com");
277 }
278
279 #[test]
280 fn new_client_preserves_clean_url() {
281 let client = DriveClient::new("https://www.googleapis.com", &test_credentials()).unwrap();
282 assert_eq!(client.base_url(), "https://www.googleapis.com");
283 }
284
285 #[test]
286 fn from_credentials_uses_drive_api_host() {
287 let env = crate::test_support::env::MapEnv::new();
291 let client = DriveClient::from_credentials_with(&env, &test_credentials()).unwrap();
292 assert_eq!(client.base_url(), "https://www.googleapis.com");
293 }
294
295 #[test]
296 fn from_credentials_honours_api_url_override() {
297 let env = crate::test_support::env::MapEnv::new().with(
298 crate::drive::auth::DRIVE_API_URL,
299 "http://proxy.example:8080",
300 );
301 let client = DriveClient::from_credentials_with(&env, &test_credentials()).unwrap();
302 assert_eq!(client.base_url(), "http://proxy.example:8080");
303 }
304
305 #[test]
306 fn from_credentials_ignores_empty_api_url_override() {
307 let env =
308 crate::test_support::env::MapEnv::new().with(crate::drive::auth::DRIVE_API_URL, "");
309 let client = DriveClient::from_credentials_with(&env, &test_credentials()).unwrap();
310 assert_eq!(client.base_url(), "https://www.googleapis.com");
311 }
312
313 #[test]
314 fn client_debug_never_mentions_session_field() {
315 let client = DriveClient::new("https://www.googleapis.com", &test_credentials()).unwrap();
316 let debug = format!("{client:?}");
317 assert!(!debug.contains("secret-1"));
318 assert!(!debug.contains("refresh-1"));
319 assert!(!debug.contains("session"));
320 assert!(debug.contains("DriveClient"));
321 }
322
323 async fn client_with_bootstrapped_token(server: &wiremock::MockServer) -> DriveClient {
328 wiremock::Mock::given(wiremock::matchers::method("POST"))
333 .and(wiremock::matchers::path("/token"))
334 .respond_with(
335 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
336 "access_token": "bootstrap-token",
337 "expires_in": 3600,
338 })),
339 )
340 .up_to_n_times(1)
341 .with_priority(1)
342 .mount(server)
343 .await;
344
345 let mut client = DriveClient::new(&server.uri(), &test_credentials()).unwrap();
346 test_support::replace_session(
347 &mut client,
348 &test_credentials(),
349 &format!("{}/token", server.uri()),
350 );
351 client
352 }
353 #[tokio::test]
354 async fn get_json_sends_bearer_auth_header() {
355 let server = wiremock::MockServer::start().await;
356 let client = client_with_bootstrapped_token(&server).await;
357 wiremock::Mock::given(wiremock::matchers::method("GET"))
358 .and(wiremock::matchers::path("/test"))
359 .and(wiremock::matchers::header(
360 "Authorization",
361 "Bearer bootstrap-token",
362 ))
363 .respond_with(
364 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({"ok": true})),
365 )
366 .expect(1)
367 .mount(&server)
368 .await;
369
370 let resp = client
371 .get_json(&format!("{}/test", server.uri()))
372 .await
373 .unwrap();
374 assert!(resp.status().is_success());
375 }
376
377 #[tokio::test]
378 async fn get_bytes_sends_bearer_auth_header_without_json_accept_header() {
379 let server = wiremock::MockServer::start().await;
380 let client = client_with_bootstrapped_token(&server).await;
381 wiremock::Mock::given(wiremock::matchers::method("GET"))
382 .and(wiremock::matchers::path("/test"))
383 .and(wiremock::matchers::header(
384 "Authorization",
385 "Bearer bootstrap-token",
386 ))
387 .respond_with(
388 wiremock::ResponseTemplate::new(200).set_body_bytes(b"raw bytes".to_vec()),
389 )
390 .expect(1)
391 .mount(&server)
392 .await;
393
394 let resp = client
395 .get_bytes(&format!("{}/test", server.uri()))
396 .await
397 .unwrap();
398 assert!(resp.status().is_success());
399 let bytes = resp.bytes().await.unwrap();
400 assert_eq!(bytes.as_ref(), b"raw bytes");
401 }
402
403 #[tokio::test]
404 async fn get_bytes_retries_on_429() {
405 let server = wiremock::MockServer::start().await;
406 let client = client_with_bootstrapped_token(&server).await;
407 wiremock::Mock::given(wiremock::matchers::method("GET"))
408 .and(wiremock::matchers::path("/test"))
409 .respond_with(wiremock::ResponseTemplate::new(429).append_header("Retry-After", "0"))
410 .up_to_n_times(1)
411 .with_priority(1)
412 .mount(&server)
413 .await;
414 wiremock::Mock::given(wiremock::matchers::method("GET"))
415 .and(wiremock::matchers::path("/test"))
416 .respond_with(wiremock::ResponseTemplate::new(200).set_body_bytes(b"ok".to_vec()))
417 .with_priority(2)
418 .mount(&server)
419 .await;
420
421 let resp = client
422 .get_bytes(&format!("{}/test", server.uri()))
423 .await
424 .unwrap();
425 assert_eq!(resp.status().as_u16(), 200);
426 }
427
428 #[tokio::test]
429 async fn get_bytes_refreshes_and_retries_once_on_401() {
430 let server = wiremock::MockServer::start().await;
431 let client = client_with_bootstrapped_token(&server).await;
432 wiremock::Mock::given(wiremock::matchers::method("POST"))
433 .and(wiremock::matchers::path("/token"))
434 .respond_with(
435 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
436 "access_token": "refreshed-token",
437 "expires_in": 3600,
438 })),
439 )
440 .up_to_n_times(1)
441 .with_priority(2)
442 .mount(&server)
443 .await;
444 wiremock::Mock::given(wiremock::matchers::method("GET"))
445 .and(wiremock::matchers::path("/test"))
446 .and(wiremock::matchers::header(
447 "Authorization",
448 "Bearer bootstrap-token",
449 ))
450 .respond_with(wiremock::ResponseTemplate::new(401))
451 .expect(1)
452 .mount(&server)
453 .await;
454 wiremock::Mock::given(wiremock::matchers::method("GET"))
455 .and(wiremock::matchers::path("/test"))
456 .and(wiremock::matchers::header(
457 "Authorization",
458 "Bearer refreshed-token",
459 ))
460 .respond_with(wiremock::ResponseTemplate::new(200).set_body_bytes(b"ok".to_vec()))
461 .expect(1)
462 .mount(&server)
463 .await;
464
465 let resp = client
466 .get_bytes(&format!("{}/test", server.uri()))
467 .await
468 .unwrap();
469 assert_eq!(resp.status().as_u16(), 200);
470 }
471
472 #[tokio::test]
473 async fn get_bytes_propagates_network_errors() {
474 let client = DriveClient::new("http://127.0.0.1:1", &test_credentials()).unwrap();
475 let result = client.get_bytes("http://127.0.0.1:1/test").await;
476 assert!(result.is_err());
477 }
478
479 #[tokio::test]
480 async fn post_json_sends_body_and_bearer_auth() {
481 let server = wiremock::MockServer::start().await;
482 let client = client_with_bootstrapped_token(&server).await;
483 wiremock::Mock::given(wiremock::matchers::method("POST"))
484 .and(wiremock::matchers::path("/test"))
485 .and(wiremock::matchers::header(
486 "Authorization",
487 "Bearer bootstrap-token",
488 ))
489 .and(wiremock::matchers::body_json(serde_json::json!({"k": "v"})))
490 .respond_with(wiremock::ResponseTemplate::new(200))
491 .expect(1)
492 .mount(&server)
493 .await;
494
495 let resp = client
496 .post_json(
497 &format!("{}/test", server.uri()),
498 &serde_json::json!({"k": "v"}),
499 )
500 .await
501 .unwrap();
502 assert!(resp.status().is_success());
503 }
504
505 #[tokio::test]
506 async fn patch_json_sends_body_and_bearer_auth() {
507 let server = wiremock::MockServer::start().await;
508 let client = client_with_bootstrapped_token(&server).await;
509 wiremock::Mock::given(wiremock::matchers::method("PATCH"))
510 .and(wiremock::matchers::path("/test"))
511 .and(wiremock::matchers::header(
512 "Authorization",
513 "Bearer bootstrap-token",
514 ))
515 .and(wiremock::matchers::body_json(
516 serde_json::json!({"name": "new-name"}),
517 ))
518 .respond_with(wiremock::ResponseTemplate::new(200))
519 .expect(1)
520 .mount(&server)
521 .await;
522
523 let resp = client
524 .patch_json(
525 &format!("{}/test", server.uri()),
526 &serde_json::json!({"name": "new-name"}),
527 )
528 .await
529 .unwrap();
530 assert!(resp.status().is_success());
531 }
532
533 #[tokio::test]
534 async fn get_json_retries_on_429() {
535 let server = wiremock::MockServer::start().await;
536 let client = client_with_bootstrapped_token(&server).await;
537 wiremock::Mock::given(wiremock::matchers::method("GET"))
538 .and(wiremock::matchers::path("/test"))
539 .respond_with(wiremock::ResponseTemplate::new(429).append_header("Retry-After", "0"))
540 .up_to_n_times(1)
541 .with_priority(1)
542 .mount(&server)
543 .await;
544 wiremock::Mock::given(wiremock::matchers::method("GET"))
545 .and(wiremock::matchers::path("/test"))
546 .respond_with(wiremock::ResponseTemplate::new(200))
547 .with_priority(2)
548 .mount(&server)
549 .await;
550
551 let resp = client
552 .get_json(&format!("{}/test", server.uri()))
553 .await
554 .unwrap();
555 assert_eq!(resp.status().as_u16(), 200);
556 }
557
558 #[tokio::test]
559 async fn get_json_retries_403_user_rate_limit_exceeded_then_succeeds() {
560 let server = wiremock::MockServer::start().await;
561 let client = client_with_bootstrapped_token(&server).await;
562 wiremock::Mock::given(wiremock::matchers::method("GET"))
563 .and(wiremock::matchers::path("/test"))
564 .respond_with(
565 wiremock::ResponseTemplate::new(403)
566 .append_header("Retry-After", "0")
567 .set_body_json(serde_json::json!({
568 "error": {"message": "User Rate Limit Exceeded", "errors": [{"reason": "userRateLimitExceeded"}]}
569 })),
570 )
571 .up_to_n_times(1)
572 .with_priority(1)
573 .mount(&server)
574 .await;
575 wiremock::Mock::given(wiremock::matchers::method("GET"))
576 .and(wiremock::matchers::path("/test"))
577 .respond_with(wiremock::ResponseTemplate::new(200))
578 .with_priority(2)
579 .mount(&server)
580 .await;
581
582 let resp = client
583 .get_json(&format!("{}/test", server.uri()))
584 .await
585 .unwrap();
586 assert_eq!(resp.status().as_u16(), 200);
587 }
588
589 #[tokio::test]
590 async fn get_json_does_not_retry_insufficient_permissions_403() {
591 let server = wiremock::MockServer::start().await;
592 let client = client_with_bootstrapped_token(&server).await;
593 wiremock::Mock::given(wiremock::matchers::method("GET"))
594 .and(wiremock::matchers::path("/test"))
595 .respond_with(
596 wiremock::ResponseTemplate::new(403).set_body_json(serde_json::json!({
597 "error": {"message": "Insufficient Permission", "errors": [{"reason": "insufficientPermissions"}]}
598 })),
599 )
600 .expect(1)
601 .mount(&server)
602 .await;
603
604 let resp = client
605 .get_json(&format!("{}/test", server.uri()))
606 .await
607 .unwrap();
608 assert_eq!(resp.status().as_u16(), 403);
609 }
610
611 #[tokio::test]
612 async fn get_json_refreshes_and_retries_once_on_401() {
613 let server = wiremock::MockServer::start().await;
614 let client = client_with_bootstrapped_token(&server).await;
615 wiremock::Mock::given(wiremock::matchers::method("POST"))
617 .and(wiremock::matchers::path("/token"))
618 .respond_with(
619 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
620 "access_token": "refreshed-token",
621 "expires_in": 3600,
622 })),
623 )
624 .up_to_n_times(1)
625 .with_priority(2)
626 .mount(&server)
627 .await;
628 wiremock::Mock::given(wiremock::matchers::method("GET"))
629 .and(wiremock::matchers::path("/test"))
630 .and(wiremock::matchers::header(
631 "Authorization",
632 "Bearer bootstrap-token",
633 ))
634 .respond_with(wiremock::ResponseTemplate::new(401))
635 .expect(1)
636 .mount(&server)
637 .await;
638 wiremock::Mock::given(wiremock::matchers::method("GET"))
639 .and(wiremock::matchers::path("/test"))
640 .and(wiremock::matchers::header(
641 "Authorization",
642 "Bearer refreshed-token",
643 ))
644 .respond_with(wiremock::ResponseTemplate::new(200))
645 .expect(1)
646 .mount(&server)
647 .await;
648
649 let resp = client
650 .get_json(&format!("{}/test", server.uri()))
651 .await
652 .unwrap();
653 assert_eq!(resp.status().as_u16(), 200);
654 }
655
656 #[tokio::test]
657 async fn get_json_does_not_retry_a_second_time_on_persistent_401() {
658 let server = wiremock::MockServer::start().await;
659 let client = client_with_bootstrapped_token(&server).await;
660 wiremock::Mock::given(wiremock::matchers::method("POST"))
661 .and(wiremock::matchers::path("/token"))
662 .respond_with(
663 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
664 "access_token": "still-rejected-token",
665 "expires_in": 3600,
666 })),
667 )
668 .up_to_n_times(1)
669 .with_priority(2)
670 .mount(&server)
671 .await;
672 wiremock::Mock::given(wiremock::matchers::method("GET"))
673 .and(wiremock::matchers::path("/test"))
674 .respond_with(
675 wiremock::ResponseTemplate::new(401).set_body_string("still unauthorized"),
676 )
677 .expect(2)
678 .mount(&server)
679 .await;
680
681 let resp = client
682 .get_json(&format!("{}/test", server.uri()))
683 .await
684 .unwrap();
685 assert_eq!(resp.status().as_u16(), 401);
686 }
687
688 #[tokio::test]
689 async fn response_to_error_extracts_drive_message_and_reason() {
690 let server = wiremock::MockServer::start().await;
691 let client = client_with_bootstrapped_token(&server).await;
692 wiremock::Mock::given(wiremock::matchers::method("GET"))
696 .and(wiremock::matchers::path("/test"))
697 .respond_with(
698 wiremock::ResponseTemplate::new(403)
699 .append_header("Retry-After", "0")
700 .set_body_json(serde_json::json!({
701 "error": {
702 "message": "User Rate Limit Exceeded",
703 "errors": [{"reason": "userRateLimitExceeded"}],
704 }
705 })),
706 )
707 .mount(&server)
708 .await;
709
710 let resp = client
711 .get_json(&format!("{}/test", server.uri()))
712 .await
713 .unwrap();
714 let err = DriveClient::response_to_error(resp).await;
715 let msg = err.to_string();
716 assert!(msg.contains("User Rate Limit Exceeded"));
717 assert!(msg.contains("userRateLimitExceeded"));
718 assert_eq!(err.reason(), Some("userRateLimitExceeded"));
719 }
720
721 #[tokio::test]
722 async fn response_to_error_omits_reason_suffix_when_absent() {
723 let server = wiremock::MockServer::start().await;
724 let client = client_with_bootstrapped_token(&server).await;
725 wiremock::Mock::given(wiremock::matchers::method("GET"))
726 .and(wiremock::matchers::path("/test"))
727 .respond_with(
728 wiremock::ResponseTemplate::new(400).set_body_json(serde_json::json!({
729 "error": {
730 "message": "Invalid request",
731 }
732 })),
733 )
734 .mount(&server)
735 .await;
736
737 let resp = client
738 .get_json(&format!("{}/test", server.uri()))
739 .await
740 .unwrap();
741 let err = DriveClient::response_to_error(resp).await;
742 let msg = err.to_string();
743 assert!(msg.contains("Invalid request"));
744 assert!(!msg.contains("reason:"));
745 assert_eq!(err.reason(), None);
746 }
747
748 #[tokio::test]
749 async fn response_to_error_falls_back_to_raw_body_when_not_drive_shaped() {
750 let server = wiremock::MockServer::start().await;
751 let client = client_with_bootstrapped_token(&server).await;
752 wiremock::Mock::given(wiremock::matchers::method("GET"))
753 .and(wiremock::matchers::path("/test"))
754 .respond_with(wiremock::ResponseTemplate::new(500).set_body_string("internal error"))
755 .mount(&server)
756 .await;
757
758 let resp = client
759 .get_json(&format!("{}/test", server.uri()))
760 .await
761 .unwrap();
762 let err = DriveClient::response_to_error(resp).await;
763 assert!(err.to_string().contains("internal error"));
764 }
765
766 #[tokio::test]
767 async fn get_json_propagates_network_errors() {
768 let client = DriveClient::new("http://127.0.0.1:1", &test_credentials()).unwrap();
769 let result = client.get_json("http://127.0.0.1:1/test").await;
770 assert!(result.is_err());
771 }
772
773 #[tokio::test]
774 async fn get_parsed_errors_on_malformed_json_response() {
775 let server = wiremock::MockServer::start().await;
776 let client = client_with_bootstrapped_token(&server).await;
777 wiremock::Mock::given(wiremock::matchers::method("GET"))
778 .and(wiremock::matchers::path("/test"))
779 .respond_with(wiremock::ResponseTemplate::new(200).set_body_string("not json"))
780 .mount(&server)
781 .await;
782
783 let result: Result<serde_json::Value> = client
784 .get_parsed(&format!("{}/test", server.uri()), "test context")
785 .await;
786 assert!(result.is_err());
787 }
788
789 #[tokio::test]
790 async fn get_parsed_errors_on_non_success_status_without_parsing_the_body() {
791 let server = wiremock::MockServer::start().await;
792 let client = client_with_bootstrapped_token(&server).await;
793 wiremock::Mock::given(wiremock::matchers::method("GET"))
794 .and(wiremock::matchers::path("/test"))
795 .respond_with(
796 wiremock::ResponseTemplate::new(404).set_body_json(serde_json::json!({
797 "error": {"message": "File not found"}
798 })),
799 )
800 .mount(&server)
801 .await;
802
803 let result: Result<serde_json::Value> = client
804 .get_parsed(&format!("{}/test", server.uri()), "test context")
805 .await;
806 let err = result.unwrap_err();
807 assert!(err.to_string().contains("File not found"));
808 }
809
810 #[tokio::test]
811 async fn put_json_sends_body_and_bearer_auth() {
812 let server = wiremock::MockServer::start().await;
813 let client = client_with_bootstrapped_token(&server).await;
814 wiremock::Mock::given(wiremock::matchers::method("PUT"))
815 .and(wiremock::matchers::path("/test"))
816 .and(wiremock::matchers::header(
817 "Authorization",
818 "Bearer bootstrap-token",
819 ))
820 .and(wiremock::matchers::body_json(serde_json::json!({
821 "values": [["a", "b"]],
822 })))
823 .respond_with(wiremock::ResponseTemplate::new(200).set_body_string("{}"))
824 .expect(1)
825 .mount(&server)
826 .await;
827
828 let response = client
829 .transport()
830 .put_json(
831 &format!("{}/test", server.uri()),
832 &serde_json::json!({"values": [["a", "b"]]}),
833 )
834 .await
835 .unwrap();
836 assert_eq!(response.status().as_u16(), 200);
837 }
838
839 #[tokio::test]
840 async fn a_drive_failure_still_names_drive() {
841 let server = wiremock::MockServer::start().await;
844 let client = client_with_bootstrapped_token(&server).await;
845 wiremock::Mock::given(wiremock::matchers::method("GET"))
846 .and(wiremock::matchers::path("/test"))
847 .respond_with(wiremock::ResponseTemplate::new(500).set_body_string("boom"))
848 .mount(&server)
849 .await;
850
851 let response = client
852 .get_json(&format!("{}/test", server.uri()))
853 .await
854 .unwrap();
855 let text = DriveClient::response_to_error(response).await.to_string();
856 assert!(text.starts_with("Drive API request failed"), "{text}");
857 }
858
859 #[tokio::test]
860 async fn response_to_error_reads_the_google_rpc_envelope_sheets_returns() {
861 let server = wiremock::MockServer::start().await;
862 let client = client_with_bootstrapped_token(&server).await;
863 wiremock::Mock::given(wiremock::matchers::method("GET"))
864 .and(wiremock::matchers::path("/test"))
865 .respond_with(
866 wiremock::ResponseTemplate::new(403).set_body_json(serde_json::json!({
867 "error": {
868 "code": 403,
869 "message": "The caller does not have permission",
870 "status": "PERMISSION_DENIED",
871 },
872 })),
873 )
874 .mount(&server)
875 .await;
876
877 let response = client
878 .get_json(&format!("{}/test", server.uri()))
879 .await
880 .unwrap();
881 let error = DriveClient::response_to_error(response).await;
882 let DriveError::ApiRequestFailed { reason, .. } = error else {
883 panic!("expected ApiRequestFailed");
884 };
885 assert_eq!(reason.as_deref(), Some("PERMISSION_DENIED"));
886 }
887}