1use crate::client::{HttpConnector, TokenCredentialProvider, http_connector};
19use crate::config::ConfigValue;
20use crate::gcp::client::{GoogleCloudStorageClient, GoogleCloudStorageConfig};
21use crate::gcp::credential::{
22 ApplicationDefaultCredentials, DEFAULT_GCS_BASE_URL, InstanceCredentialProvider,
23 ServiceAccountCredentials,
24};
25use crate::gcp::{
26 GcpCredential, GcpCredentialProvider, GcpSigningCredential, GcpSigningCredentialProvider,
27 GoogleCloudStorage, STORE, credential,
28};
29use crate::{ClientConfigKey, ClientOptions, Result, RetryConfig, StaticCredentialProvider};
30use serde::{Deserialize, Serialize};
31use std::str::FromStr;
32use std::sync::Arc;
33use std::time::Duration;
34use url::Url;
35
36use super::credential::{AuthorizedUserSigningCredentials, InstanceSigningCredentialProvider};
37
38const TOKEN_MIN_TTL: Duration = Duration::from_secs(4 * 60);
39
40#[derive(Debug, thiserror::Error)]
41enum Error {
42 #[error("Missing bucket name")]
43 MissingBucketName {},
44
45 #[error("One of service account path or service account key may be provided.")]
46 ServiceAccountPathAndKeyProvided,
47
48 #[error("Unable parse source url. Url: {}, Error: {}", url, source)]
49 UnableToParseUrl {
50 source: url::ParseError,
51 url: String,
52 },
53
54 #[error(
55 "Unknown url scheme cannot be parsed into storage location: {}",
56 scheme
57 )]
58 UnknownUrlScheme { scheme: String },
59
60 #[error("URL did not match any known pattern for scheme: {}", url)]
61 UrlNotRecognised { url: String },
62
63 #[error("Configuration key: '{}' is not known.", key)]
64 UnknownConfigurationKey { key: String },
65
66 #[error("GCP credential error: {}", source)]
67 Credential { source: credential::Error },
68}
69
70impl From<Error> for crate::Error {
71 fn from(err: Error) -> Self {
72 match err {
73 Error::UnknownConfigurationKey { key } => {
74 Self::UnknownConfigurationKey { store: STORE, key }
75 }
76 _ => Self::Generic {
77 store: STORE,
78 source: Box::new(err),
79 },
80 }
81 }
82}
83
84#[derive(Debug, Clone)]
96pub struct GoogleCloudStorageBuilder {
97 bucket_name: Option<String>,
99 url: Option<String>,
101 base_url: Option<String>,
103 service_account_path: Option<String>,
105 service_account_key: Option<String>,
107 application_credentials_path: Option<String>,
109 retry_config: RetryConfig,
111 client_options: ClientOptions,
113 credentials: Option<GcpCredentialProvider>,
115 skip_signature: ConfigValue<bool>,
117 signing_credentials: Option<GcpSigningCredentialProvider>,
119 http_connector: Option<Arc<dyn HttpConnector>>,
121}
122
123#[derive(PartialEq, Eq, Hash, Clone, Debug, Copy, Serialize, Deserialize)]
135#[non_exhaustive]
136pub enum GoogleConfigKey {
137 ServiceAccount,
145
146 ServiceAccountKey,
152
153 Bucket,
163
164 BaseUrl,
172
173 ApplicationCredentials,
181
182 SkipSignature,
188
189 Client(ClientConfigKey),
191}
192
193impl AsRef<str> for GoogleConfigKey {
194 fn as_ref(&self) -> &str {
195 match self {
196 Self::ServiceAccount => "google_service_account",
197 Self::ServiceAccountKey => "google_service_account_key",
198 Self::Bucket => "google_bucket",
199 Self::BaseUrl => "google_base_url",
200 Self::ApplicationCredentials => "google_application_credentials",
201 Self::SkipSignature => "google_skip_signature",
202 Self::Client(key) => key.as_ref(),
203 }
204 }
205}
206
207impl FromStr for GoogleConfigKey {
208 type Err = crate::Error;
209
210 fn from_str(s: &str) -> Result<Self, Self::Err> {
211 match s {
212 "google_service_account"
213 | "service_account"
214 | "google_service_account_path"
215 | "service_account_path" => Ok(Self::ServiceAccount),
216 "google_service_account_key" | "service_account_key" => Ok(Self::ServiceAccountKey),
217 "google_bucket" | "google_bucket_name" | "bucket" | "bucket_name" => Ok(Self::Bucket),
218 "google_base_url" | "base_url" => Ok(Self::BaseUrl),
219 "google_application_credentials" | "application_credentials" => {
220 Ok(Self::ApplicationCredentials)
221 }
222 "google_skip_signature" | "skip_signature" => Ok(Self::SkipSignature),
223 _ => match s.strip_prefix("google_").unwrap_or(s).parse() {
224 Ok(key) => Ok(Self::Client(key)),
225 Err(_) => Err(Error::UnknownConfigurationKey { key: s.into() }.into()),
226 },
227 }
228 }
229}
230
231impl Default for GoogleCloudStorageBuilder {
232 fn default() -> Self {
233 Self {
234 bucket_name: None,
235 service_account_path: None,
236 service_account_key: None,
237 application_credentials_path: None,
238 retry_config: Default::default(),
239 client_options: ClientOptions::new().with_allow_http(true),
240 url: None,
241 base_url: None,
242 credentials: None,
243 skip_signature: Default::default(),
244 signing_credentials: None,
245 http_connector: None,
246 }
247 }
248}
249
250impl GoogleCloudStorageBuilder {
251 pub fn new() -> Self {
253 Default::default()
254 }
255
256 pub fn from_env() -> Self {
275 let mut builder = Self::default();
276
277 if let Ok(service_account_path) = std::env::var("SERVICE_ACCOUNT") {
278 builder.service_account_path = Some(service_account_path);
279 }
280
281 for (os_key, os_value) in std::env::vars_os() {
282 if let (Some(key), Some(value)) = (os_key.to_str(), os_value.to_str()) {
283 if key.starts_with("GOOGLE_") {
284 if let Ok(config_key) = key.to_ascii_lowercase().parse() {
285 builder = builder.with_config(config_key, value);
286 }
287 }
288 }
289 }
290
291 builder
292 }
293
294 pub fn with_url(mut self, url: impl Into<String>) -> Self {
311 self.url = Some(url.into());
312 self
313 }
314
315 pub fn with_config(mut self, key: GoogleConfigKey, value: impl Into<String>) -> Self {
317 match key {
318 GoogleConfigKey::ServiceAccount => self.service_account_path = Some(value.into()),
319 GoogleConfigKey::ServiceAccountKey => self.service_account_key = Some(value.into()),
320 GoogleConfigKey::Bucket => self.bucket_name = Some(value.into()),
321 GoogleConfigKey::BaseUrl => self.base_url = Some(value.into()),
322 GoogleConfigKey::ApplicationCredentials => {
323 self.application_credentials_path = Some(value.into())
324 }
325 GoogleConfigKey::SkipSignature => self.skip_signature.parse(value),
326 GoogleConfigKey::Client(key) => {
327 self.client_options = self.client_options.with_config(key, value)
328 }
329 };
330 self
331 }
332
333 pub fn get_config_value(&self, key: &GoogleConfigKey) -> Option<String> {
345 match key {
346 GoogleConfigKey::ServiceAccount => self.service_account_path.clone(),
347 GoogleConfigKey::ServiceAccountKey => self.service_account_key.clone(),
348 GoogleConfigKey::Bucket => self.bucket_name.clone(),
349 GoogleConfigKey::BaseUrl => self.base_url.clone(),
350 GoogleConfigKey::ApplicationCredentials => self.application_credentials_path.clone(),
351 GoogleConfigKey::SkipSignature => Some(self.skip_signature.to_string()),
352 GoogleConfigKey::Client(key) => self.client_options.get_config_value(key),
353 }
354 }
355
356 fn parse_url(&mut self, url: &str) -> Result<()> {
361 let parsed = Url::parse(url).map_err(|source| Error::UnableToParseUrl {
362 source,
363 url: url.to_string(),
364 })?;
365
366 let host = parsed.host_str().ok_or_else(|| Error::UrlNotRecognised {
367 url: url.to_string(),
368 })?;
369
370 match parsed.scheme() {
371 "gs" => self.bucket_name = Some(host.to_string()),
372 scheme => {
373 let scheme = scheme.to_string();
374 return Err(Error::UnknownUrlScheme { scheme }.into());
375 }
376 }
377 Ok(())
378 }
379
380 pub fn with_bucket_name(mut self, bucket_name: impl Into<String>) -> Self {
382 self.bucket_name = Some(bucket_name.into());
383 self
384 }
385
386 pub fn with_base_url(mut self, base_url: &str) -> Self {
401 self.base_url = Some(base_url.into());
402 self
403 }
404
405 pub fn with_service_account_path(mut self, service_account_path: impl Into<String>) -> Self {
423 self.service_account_path = Some(service_account_path.into());
424 self
425 }
426
427 pub fn with_service_account_key(mut self, service_account: impl Into<String>) -> Self {
433 self.service_account_key = Some(service_account.into());
434 self
435 }
436
437 pub fn with_application_credentials(
441 mut self,
442 application_credentials_path: impl Into<String>,
443 ) -> Self {
444 self.application_credentials_path = Some(application_credentials_path.into());
445 self
446 }
447
448 pub fn with_skip_signature(mut self, skip_signature: bool) -> Self {
452 self.skip_signature = skip_signature.into();
453 self
454 }
455
456 pub fn with_credentials(mut self, credentials: GcpCredentialProvider) -> Self {
458 self.credentials = Some(credentials);
459 self
460 }
461
462 pub fn with_retry(mut self, retry_config: RetryConfig) -> Self {
464 self.retry_config = retry_config;
465 self
466 }
467
468 pub fn with_proxy_url(mut self, proxy_url: impl Into<String>) -> Self {
470 self.client_options = self.client_options.with_proxy_url(proxy_url);
471 self
472 }
473
474 pub fn with_proxy_ca_certificate(mut self, proxy_ca_certificate: impl Into<String>) -> Self {
476 self.client_options = self
477 .client_options
478 .with_proxy_ca_certificate(proxy_ca_certificate);
479 self
480 }
481
482 pub fn with_proxy_excludes(mut self, proxy_excludes: impl Into<String>) -> Self {
484 self.client_options = self.client_options.with_proxy_excludes(proxy_excludes);
485 self
486 }
487
488 pub fn with_client_options(mut self, options: ClientOptions) -> Self {
490 self.client_options = options;
491 self
492 }
493
494 pub fn with_http_connector<C: HttpConnector>(mut self, connector: C) -> Self {
498 self.http_connector = Some(Arc::new(connector));
499 self
500 }
501
502 pub fn build(mut self) -> Result<GoogleCloudStorage> {
505 if let Some(url) = self.url.take() {
506 self.parse_url(&url)?;
507 }
508
509 let bucket_name = self.bucket_name.ok_or(Error::MissingBucketName {})?;
510
511 let http = http_connector(self.http_connector)?;
512
513 let service_account_credentials =
515 match (self.service_account_path, self.service_account_key) {
516 (Some(path), None) => Some(
517 ServiceAccountCredentials::from_file(path)
518 .map_err(|source| Error::Credential { source })?,
519 ),
520 (None, Some(key)) => Some(
521 ServiceAccountCredentials::from_key(&key)
522 .map_err(|source| Error::Credential { source })?,
523 ),
524 (None, None) => None,
525 (Some(_), Some(_)) => return Err(Error::ServiceAccountPathAndKeyProvided.into()),
526 };
527
528 let application_default_credentials =
531 if service_account_credentials.is_none() && self.credentials.is_none() {
532 ApplicationDefaultCredentials::read(self.application_credentials_path.as_deref())?
534 } else {
535 None
537 };
538
539 let disable_oauth = service_account_credentials
540 .as_ref()
541 .map(|c| c.disable_oauth)
542 .unwrap_or(false);
543
544 let gcs_base_url: String = self
545 .base_url
546 .or_else(|| {
547 service_account_credentials
548 .as_ref()
549 .and_then(|c| c.gcs_base_url.clone())
550 })
551 .unwrap_or_else(|| DEFAULT_GCS_BASE_URL.to_string());
552
553 let credentials = if let Some(credentials) = self.credentials {
554 credentials
555 } else if disable_oauth {
556 Arc::new(StaticCredentialProvider::new(GcpCredential {
557 bearer: "".to_string(),
558 })) as _
559 } else if let Some(credentials) = service_account_credentials.clone() {
560 Arc::new(TokenCredentialProvider::new(
561 credentials.token_provider()?,
562 http.connect(&self.client_options)?,
563 self.retry_config.clone(),
564 )) as _
565 } else if let Some(credentials) = application_default_credentials.clone() {
566 match credentials {
567 ApplicationDefaultCredentials::AuthorizedUser(token) => Arc::new(
568 TokenCredentialProvider::new(
569 token,
570 http.connect(&self.client_options)?,
571 self.retry_config.clone(),
572 )
573 .with_min_ttl(TOKEN_MIN_TTL),
574 ) as _,
575 ApplicationDefaultCredentials::ServiceAccount(token) => {
576 Arc::new(TokenCredentialProvider::new(
577 token.token_provider()?,
578 http.connect(&self.client_options)?,
579 self.retry_config.clone(),
580 )) as _
581 }
582 }
583 } else {
584 Arc::new(
585 TokenCredentialProvider::new(
586 InstanceCredentialProvider::default(),
587 http.connect(&self.client_options.metadata_options())?,
588 self.retry_config.clone(),
589 )
590 .with_min_ttl(TOKEN_MIN_TTL),
591 ) as _
592 };
593
594 let signing_credentials = if let Some(signing_credentials) = self.signing_credentials {
595 signing_credentials
596 } else if disable_oauth {
597 Arc::new(StaticCredentialProvider::new(GcpSigningCredential {
598 email: "".to_string(),
599 private_key: None,
600 })) as _
601 } else if let Some(credentials) = service_account_credentials.clone() {
602 credentials.signing_credentials()?
603 } else if let Some(credentials) = application_default_credentials.clone() {
604 match credentials {
605 ApplicationDefaultCredentials::AuthorizedUser(token) => {
606 Arc::new(TokenCredentialProvider::new(
607 AuthorizedUserSigningCredentials::from(token)?,
608 http.connect(&self.client_options)?,
609 self.retry_config.clone(),
610 )) as _
611 }
612 ApplicationDefaultCredentials::ServiceAccount(token) => {
613 token.signing_credentials()?
614 }
615 }
616 } else {
617 Arc::new(TokenCredentialProvider::new(
618 InstanceSigningCredentialProvider::default(),
619 http.connect(&self.client_options.metadata_options())?,
620 self.retry_config.clone(),
621 )) as _
622 };
623
624 let config = GoogleCloudStorageConfig {
625 base_url: gcs_base_url,
626 credentials,
627 signing_credentials,
628 bucket_name,
629 retry_config: self.retry_config,
630 client_options: self.client_options,
631 skip_signature: self.skip_signature.get()?,
632 };
633
634 let http_client = http.connect(&config.client_options)?;
635 Ok(GoogleCloudStorage {
636 client: Arc::new(GoogleCloudStorageClient::new(config, http_client)?),
637 })
638 }
639}
640
641#[cfg(test)]
642mod tests {
643 use super::*;
644 use std::collections::HashMap;
645 use std::io::Write;
646 use tempfile::NamedTempFile;
647
648 const FAKE_KEY: &str = r#"{"private_key": "private_key", "private_key_id": "private_key_id", "client_email":"client_email", "disable_oauth":true}"#;
649 const FAKE_KEY_WITH_BASE_URL: &str = r#"{"private_key": "private_key", "private_key_id": "private_key_id", "client_email":"client_email", "disable_oauth":true, "gcs_base_url": "https://base-url-from-credentials:4443"}"#;
650
651 #[test]
652 fn gcs_test_service_account_key_and_path() {
653 let mut tfile = NamedTempFile::new().unwrap();
654 write!(tfile, "{FAKE_KEY}").unwrap();
655 let _ = GoogleCloudStorageBuilder::new()
656 .with_service_account_key(FAKE_KEY)
657 .with_service_account_path(tfile.path().to_str().unwrap())
658 .with_bucket_name("foo")
659 .build()
660 .unwrap_err();
661 }
662
663 #[test]
664 fn gcs_test_config_from_map() {
665 let google_service_account = "object_store:fake_service_account".to_string();
666 let google_bucket_name = "object_store:fake_bucket".to_string();
667 let options = HashMap::from([
668 ("google_service_account", google_service_account.clone()),
669 ("google_bucket_name", google_bucket_name.clone()),
670 ]);
671
672 let builder = options
673 .iter()
674 .fold(GoogleCloudStorageBuilder::new(), |builder, (key, value)| {
675 builder.with_config(key.parse().unwrap(), value)
676 });
677
678 assert_eq!(
679 builder.service_account_path.unwrap(),
680 google_service_account.as_str()
681 );
682 assert_eq!(builder.bucket_name.unwrap(), google_bucket_name.as_str());
683 }
684
685 #[test]
686 fn gcs_test_config_aliases() {
687 for alias in [
689 "google_service_account",
690 "service_account",
691 "google_service_account_path",
692 "service_account_path",
693 ] {
694 let builder = GoogleCloudStorageBuilder::new()
695 .with_config(alias.parse().unwrap(), "/fake/path.json");
696 assert_eq!("/fake/path.json", builder.service_account_path.unwrap());
697 }
698
699 for alias in ["google_service_account_key", "service_account_key"] {
701 let builder =
702 GoogleCloudStorageBuilder::new().with_config(alias.parse().unwrap(), FAKE_KEY);
703 assert_eq!(FAKE_KEY, builder.service_account_key.unwrap());
704 }
705
706 for alias in [
708 "google_bucket",
709 "google_bucket_name",
710 "bucket",
711 "bucket_name",
712 ] {
713 let builder =
714 GoogleCloudStorageBuilder::new().with_config(alias.parse().unwrap(), "fake_bucket");
715 assert_eq!("fake_bucket", builder.bucket_name.unwrap());
716 }
717 }
718
719 #[tokio::test]
720 async fn gcs_test_proxy_url() {
721 let mut tfile = NamedTempFile::new().unwrap();
722 write!(tfile, "{FAKE_KEY}").unwrap();
723 let service_account_path = tfile.path();
724 let gcs = GoogleCloudStorageBuilder::new()
725 .with_service_account_path(service_account_path.to_str().unwrap())
726 .with_bucket_name("foo")
727 .with_proxy_url("https://example.com")
728 .build();
729 assert!(gcs.is_ok());
730
731 let err = GoogleCloudStorageBuilder::new()
732 .with_service_account_path(service_account_path.to_str().unwrap())
733 .with_bucket_name("foo")
734 .with_proxy_url("dxx:ddd\\example.com")
736 .build()
737 .unwrap_err()
738 .to_string();
739
740 assert_eq!("Generic HTTP client error: builder error", err);
741 }
742
743 #[test]
744 fn gcs_test_urls() {
745 let mut builder = GoogleCloudStorageBuilder::new();
746 builder.parse_url("gs://bucket/path").unwrap();
747 assert_eq!(builder.bucket_name.as_deref(), Some("bucket"));
748
749 builder.parse_url("gs://bucket.mydomain/path").unwrap();
750 assert_eq!(builder.bucket_name.as_deref(), Some("bucket.mydomain"));
751
752 builder.parse_url("mailto://bucket/path").unwrap_err();
753 }
754
755 #[test]
756 fn gcs_test_service_account_key_only() {
757 let _ = GoogleCloudStorageBuilder::new()
758 .with_service_account_key(FAKE_KEY)
759 .with_bucket_name("foo")
760 .build()
761 .unwrap();
762 }
763
764 #[test]
765 fn gcs_test_with_base_url() {
766 let no_base_url = GoogleCloudStorageBuilder::new()
767 .with_bucket_name("foo")
768 .build()
769 .unwrap();
770 assert_eq!(no_base_url.client.config().base_url, DEFAULT_GCS_BASE_URL);
771
772 let explicit_override = GoogleCloudStorageBuilder::new()
773 .with_bucket_name("foo")
774 .with_base_url("https://explicitly-overridden:4443")
775 .build()
776 .unwrap();
777 assert_eq!(
778 explicit_override.client.config().base_url,
779 "https://explicitly-overridden:4443"
780 );
781
782 let url_in_credentials = GoogleCloudStorageBuilder::new()
783 .with_bucket_name("foo")
784 .with_service_account_key(FAKE_KEY_WITH_BASE_URL)
785 .build()
786 .unwrap();
787 assert_eq!(
788 url_in_credentials.client.config().base_url,
789 "https://base-url-from-credentials:4443"
790 );
791
792 let explicit_override_and_credentials = GoogleCloudStorageBuilder::new()
793 .with_bucket_name("foo")
794 .with_base_url("https://explicitly-overridden:4443") .with_service_account_key(FAKE_KEY_WITH_BASE_URL)
796 .build()
797 .unwrap();
798 assert_eq!(
799 explicit_override_and_credentials.client.config().base_url,
800 "https://explicitly-overridden:4443"
801 );
802 }
803
804 #[test]
805 fn gcs_test_config_get_value() {
806 let google_service_account = "object_store:fake_service_account".to_string();
807 let google_bucket_name = "object_store:fake_bucket".to_string();
808 let builder = GoogleCloudStorageBuilder::new()
809 .with_config(GoogleConfigKey::ServiceAccount, &google_service_account)
810 .with_config(GoogleConfigKey::Bucket, &google_bucket_name);
811
812 assert_eq!(
813 builder
814 .get_config_value(&GoogleConfigKey::ServiceAccount)
815 .unwrap(),
816 google_service_account
817 );
818 assert_eq!(
819 builder.get_config_value(&GoogleConfigKey::Bucket).unwrap(),
820 google_bucket_name
821 );
822 }
823
824 #[test]
825 fn gcp_test_client_opts() {
826 let key = "GOOGLE_PROXY_URL";
827 if let Ok(config_key) = key.to_ascii_lowercase().parse() {
828 assert_eq!(
829 GoogleConfigKey::Client(ClientConfigKey::ProxyUrl),
830 config_key
831 );
832 } else {
833 panic!("{key} not propagated as ClientConfigKey");
834 }
835 }
836
837 #[test]
838 fn gcs_test_explicit_creds_skip_invalid_adc() {
839 let mut valid_key_file = NamedTempFile::new().unwrap();
841 write!(valid_key_file, "{FAKE_KEY}").unwrap();
842
843 let mut invalid_adc_file = NamedTempFile::new().unwrap();
845 invalid_adc_file
846 .write_all(br#"{"type": "external_account_authorized_user", "audience": "test"}"#)
847 .unwrap();
848
849 let result = GoogleCloudStorageBuilder::new()
852 .with_service_account_path(valid_key_file.path().to_str().unwrap())
853 .with_application_credentials(invalid_adc_file.path().to_str().unwrap())
854 .with_bucket_name("test-bucket")
855 .build();
856
857 assert!(
859 result.is_ok(),
860 "Build should succeed with explicit credentials despite invalid ADC: {:?}",
861 result.err()
862 );
863 }
864
865 #[test]
866 fn gcs_test_explicit_creds_with_service_account_key_skip_invalid_adc() {
867 let mut invalid_adc_file = NamedTempFile::new().unwrap();
869 invalid_adc_file
870 .write_all(br#"{"type": "external_account_authorized_user", "audience": "test"}"#)
871 .unwrap();
872
873 let result = GoogleCloudStorageBuilder::new()
875 .with_service_account_key(FAKE_KEY)
876 .with_application_credentials(invalid_adc_file.path().to_str().unwrap())
877 .with_bucket_name("test-bucket")
878 .build();
879
880 assert!(
882 result.is_ok(),
883 "Build should succeed with service account key despite invalid ADC: {:?}",
884 result.err()
885 );
886 }
887
888 #[test]
889 fn gcs_test_adc_error_propagated_without_explicit_creds() {
890 let mut invalid_adc_file = NamedTempFile::new().unwrap();
892 invalid_adc_file
893 .write_all(br#"{"type": "external_account_authorized_user", "audience": "test"}"#)
894 .unwrap();
895
896 let result = GoogleCloudStorageBuilder::new()
898 .with_application_credentials(invalid_adc_file.path().to_str().unwrap())
899 .with_bucket_name("test-bucket")
900 .build();
901
902 assert!(
904 result.is_err(),
905 "Build should fail without explicit credentials and invalid ADC"
906 );
907 let err_msg = result.unwrap_err().to_string();
908 assert!(
909 err_msg.contains("external_account_authorized_user"),
910 "Error should mention unsupported credential type: {}",
911 err_msg
912 );
913 }
914
915 #[test]
916 fn gcs_test_with_credentials_skip_invalid_adc() {
917 use crate::StaticCredentialProvider;
918
919 let mut invalid_adc_file = NamedTempFile::new().unwrap();
921 invalid_adc_file
922 .write_all(br#"{"type": "external_account_authorized_user", "audience": "test"}"#)
923 .unwrap();
924
925 let custom_creds = Arc::new(StaticCredentialProvider::new(GcpCredential {
927 bearer: "custom-token".to_string(),
928 }));
929
930 let result = GoogleCloudStorageBuilder::new()
932 .with_credentials(custom_creds)
933 .with_application_credentials(invalid_adc_file.path().to_str().unwrap())
934 .with_bucket_name("test-bucket")
935 .build();
936
937 assert!(
939 result.is_ok(),
940 "Build should succeed with custom credentials despite invalid ADC: {:?}",
941 result.err()
942 );
943 }
944}