1use crate::build_errors::Error as BuilderError;
16use crate::constants::GOOGLE_CLOUD_QUOTA_PROJECT_VAR;
17use crate::errors::{self, CredentialsError};
18use crate::{BuildResult, Result};
19use http::{Extensions, HeaderMap};
20use serde_json::Value;
21use std::future::Future;
22use std::sync::Arc;
23use std::sync::atomic::{AtomicU64, Ordering};
24
25pub mod anonymous;
26pub mod api_key_credentials;
27pub mod external_account;
28pub(crate) mod external_account_sources;
29#[cfg(google_cloud_unstable_id_token)]
30pub mod idtoken;
31pub mod impersonated;
32pub(crate) mod internal;
33pub mod mds;
34pub mod service_account;
35pub mod subject_token;
36pub mod user_account;
37pub(crate) const QUOTA_PROJECT_KEY: &str = "x-goog-user-project";
38
39#[cfg(test)]
40pub(crate) const DEFAULT_UNIVERSE_DOMAIN: &str = "googleapis.com";
41
42#[derive(Clone, Debug, PartialEq, Default)]
52pub struct EntityTag(u64);
53
54static ENTITY_TAG_GENERATOR: AtomicU64 = AtomicU64::new(0);
55impl EntityTag {
56 pub fn new() -> Self {
57 let value = ENTITY_TAG_GENERATOR.fetch_add(1, Ordering::SeqCst);
58 Self(value)
59 }
60}
61
62#[derive(Clone, PartialEq, Debug)]
69pub enum CacheableResource<T> {
70 NotModified,
71 New { entity_tag: EntityTag, data: T },
72}
73
74#[derive(Clone, Debug)]
107pub struct Credentials {
108 inner: Arc<dyn dynamic::CredentialsProvider>,
117}
118
119impl<T> std::convert::From<T> for Credentials
120where
121 T: crate::credentials::CredentialsProvider + Send + Sync + 'static,
122{
123 fn from(value: T) -> Self {
124 Self {
125 inner: Arc::new(value),
126 }
127 }
128}
129
130impl Credentials {
131 pub async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
132 self.inner.headers(extensions).await
133 }
134
135 pub async fn universe_domain(&self) -> Option<String> {
136 self.inner.universe_domain().await
137 }
138}
139
140pub trait CredentialsProvider: std::fmt::Debug {
180 fn headers(
202 &self,
203 extensions: Extensions,
204 ) -> impl Future<Output = Result<CacheableResource<HeaderMap>>> + Send;
205
206 fn universe_domain(&self) -> impl Future<Output = Option<String>> + Send;
208}
209
210pub(crate) mod dynamic {
211 use super::Result;
212 use super::{CacheableResource, Extensions, HeaderMap};
213
214 #[async_trait::async_trait]
216 pub trait CredentialsProvider: Send + Sync + std::fmt::Debug {
217 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>>;
239
240 async fn universe_domain(&self) -> Option<String> {
242 Some("googleapis.com".to_string())
243 }
244 }
245
246 #[async_trait::async_trait]
248 impl<T> CredentialsProvider for T
249 where
250 T: super::CredentialsProvider + Send + Sync,
251 {
252 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
253 T::headers(self, extensions).await
254 }
255 async fn universe_domain(&self) -> Option<String> {
256 T::universe_domain(self).await
257 }
258 }
259}
260
261#[derive(Debug)]
316pub struct Builder {
317 quota_project_id: Option<String>,
318 scopes: Option<Vec<String>>,
319}
320
321impl Default for Builder {
322 fn default() -> Self {
334 Self {
335 quota_project_id: None,
336 scopes: None,
337 }
338 }
339}
340
341impl Builder {
342 pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
364 self.quota_project_id = Some(quota_project_id.into());
365 self
366 }
367
368 pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
388 where
389 I: IntoIterator<Item = S>,
390 S: Into<String>,
391 {
392 self.scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
393 self
394 }
395
396 pub fn build(self) -> BuildResult<Credentials> {
407 let json_data = match load_adc()? {
408 AdcContents::Contents(contents) => {
409 Some(serde_json::from_str(&contents).map_err(BuilderError::parsing)?)
410 }
411 AdcContents::FallbackToMds => None,
412 };
413 let quota_project_id = std::env::var(GOOGLE_CLOUD_QUOTA_PROJECT_VAR)
414 .ok()
415 .or(self.quota_project_id);
416 build_credentials(json_data, quota_project_id, self.scopes)
417 }
418}
419
420#[derive(Debug, PartialEq)]
421enum AdcPath {
422 FromEnv(String),
423 WellKnown(String),
424}
425
426#[derive(Debug, PartialEq)]
427enum AdcContents {
428 Contents(String),
429 FallbackToMds,
430}
431
432fn extract_credential_type(json: &Value) -> BuildResult<&str> {
433 json.get("type")
434 .ok_or_else(|| BuilderError::parsing("no `type` field found."))?
435 .as_str()
436 .ok_or_else(|| BuilderError::parsing("`type` field is not a string."))
437}
438
439macro_rules! config_builder {
447 ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
448 let builder = $builder_instance;
449 let builder = $quota_project_id_option
450 .into_iter()
451 .fold(builder, |b, qp| b.with_quota_project_id(qp));
452
453 let builder = $scopes_option
454 .into_iter()
455 .fold(builder, |b, s| $apply_scopes_closure(b, s));
456
457 builder.build()
458 }};
459}
460
461fn build_credentials(
462 json: Option<Value>,
463 quota_project_id: Option<String>,
464 scopes: Option<Vec<String>>,
465) -> BuildResult<Credentials> {
466 match json {
467 None => config_builder!(
468 mds::Builder::from_adc(),
469 quota_project_id,
470 scopes,
471 |b: mds::Builder, s: Vec<String>| b.with_scopes(s)
472 ),
473 Some(json) => {
474 let cred_type = extract_credential_type(&json)?;
475 match cred_type {
476 "authorized_user" => {
477 config_builder!(
478 user_account::Builder::new(json),
479 quota_project_id,
480 scopes,
481 |b: user_account::Builder, s: Vec<String>| b.with_scopes(s)
482 )
483 }
484 "service_account" => config_builder!(
485 service_account::Builder::new(json),
486 quota_project_id,
487 scopes,
488 |b: service_account::Builder, s: Vec<String>| b
489 .with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
490 ),
491 "impersonated_service_account" => {
492 config_builder!(
493 impersonated::Builder::new(json),
494 quota_project_id,
495 scopes,
496 |b: impersonated::Builder, s: Vec<String>| b.with_scopes(s)
497 )
498 }
499 "external_account" => config_builder!(
500 external_account::Builder::new(json),
501 quota_project_id,
502 scopes,
503 |b: external_account::Builder, s: Vec<String>| b.with_scopes(s)
504 ),
505 _ => Err(BuilderError::unknown_type(cred_type)),
506 }
507 }
508 }
509}
510
511fn path_not_found(path: String) -> BuilderError {
512 BuilderError::loading(format!(
513 "{path}. {}",
514 concat!(
515 "This file name was found in the `GOOGLE_APPLICATION_CREDENTIALS` ",
516 "environment variable. Verify this environment variable points to ",
517 "a valid file."
518 )
519 ))
520}
521
522fn load_adc() -> BuildResult<AdcContents> {
523 match adc_path() {
524 None => Ok(AdcContents::FallbackToMds),
525 Some(AdcPath::FromEnv(path)) => match std::fs::read_to_string(&path) {
526 Ok(contents) => Ok(AdcContents::Contents(contents)),
527 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(path_not_found(path)),
528 Err(e) => Err(BuilderError::loading(e)),
529 },
530 Some(AdcPath::WellKnown(path)) => match std::fs::read_to_string(path) {
531 Ok(contents) => Ok(AdcContents::Contents(contents)),
532 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(AdcContents::FallbackToMds),
533 Err(e) => Err(BuilderError::loading(e)),
534 },
535 }
536}
537
538fn adc_path() -> Option<AdcPath> {
542 if let Ok(path) = std::env::var("GOOGLE_APPLICATION_CREDENTIALS") {
543 return Some(AdcPath::FromEnv(path));
544 }
545 Some(AdcPath::WellKnown(adc_well_known_path()?))
546}
547
548#[cfg(target_os = "windows")]
552fn adc_well_known_path() -> Option<String> {
553 std::env::var("APPDATA")
554 .ok()
555 .map(|root| root + "/gcloud/application_default_credentials.json")
556}
557
558#[cfg(not(target_os = "windows"))]
562fn adc_well_known_path() -> Option<String> {
563 std::env::var("HOME")
564 .ok()
565 .map(|root| root + "/.config/gcloud/application_default_credentials.json")
566}
567
568#[cfg_attr(test, mutants::skip)]
579#[doc(hidden)]
580pub mod testing {
581 use super::CacheableResource;
582 use crate::Result;
583 use crate::credentials::Credentials;
584 use crate::credentials::dynamic::CredentialsProvider;
585 use http::{Extensions, HeaderMap};
586 use std::sync::Arc;
587
588 pub fn error_credentials(retryable: bool) -> Credentials {
592 Credentials {
593 inner: Arc::from(ErrorCredentials(retryable)),
594 }
595 }
596
597 #[derive(Debug, Default)]
598 struct ErrorCredentials(bool);
599
600 #[async_trait::async_trait]
601 impl CredentialsProvider for ErrorCredentials {
602 async fn headers(&self, _extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
603 Err(super::CredentialsError::from_msg(self.0, "test-only"))
604 }
605
606 async fn universe_domain(&self) -> Option<String> {
607 None
608 }
609 }
610}
611
612#[cfg(test)]
613pub(crate) mod tests {
614 use super::*;
615 use base64::Engine;
616 use gax::backoff_policy::BackoffPolicy;
617 use gax::retry_policy::RetryPolicy;
618 use gax::retry_result::RetryResult;
619 use gax::retry_state::RetryState;
620 use gax::retry_throttler::RetryThrottler;
621 use mockall::mock;
622 use reqwest::header::AUTHORIZATION;
623 use rsa::BigUint;
624 use rsa::RsaPrivateKey;
625 use rsa::pkcs8::{EncodePrivateKey, LineEnding};
626 use scoped_env::ScopedEnv;
627 use std::error::Error;
628 use std::sync::LazyLock;
629 use test_case::test_case;
630 use tokio::time::Duration;
631
632 pub(crate) fn find_source_error<'a, T: Error + 'static>(
633 error: &'a (dyn Error + 'static),
634 ) -> Option<&'a T> {
635 let mut source = error.source();
636 while let Some(err) = source {
637 if let Some(target_err) = err.downcast_ref::<T>() {
638 return Some(target_err);
639 }
640 source = err.source();
641 }
642 None
643 }
644
645 mock! {
646 #[derive(Debug)]
647 pub RetryPolicy {}
648 impl RetryPolicy for RetryPolicy {
649 fn on_error(
650 &self,
651 state: &RetryState,
652 error: gax::error::Error,
653 ) -> RetryResult;
654 }
655 }
656
657 mock! {
658 #[derive(Debug)]
659 pub BackoffPolicy {}
660 impl BackoffPolicy for BackoffPolicy {
661 fn on_failure(&self, state: &RetryState) -> std::time::Duration;
662 }
663 }
664
665 mockall::mock! {
666 #[derive(Debug)]
667 pub RetryThrottler {}
668 impl RetryThrottler for RetryThrottler {
669 fn throttle_retry_attempt(&self) -> bool;
670 fn on_retry_failure(&mut self, error: &RetryResult);
671 fn on_success(&mut self);
672 }
673 }
674
675 type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
676
677 pub(crate) fn get_mock_auth_retry_policy(attempts: usize) -> MockRetryPolicy {
678 let mut retry_policy = MockRetryPolicy::new();
679 retry_policy
680 .expect_on_error()
681 .returning(move |state, error| {
682 if state.attempt_count >= attempts as u32 {
683 return RetryResult::Exhausted(error);
684 }
685 let is_transient = error
686 .source()
687 .and_then(|e| e.downcast_ref::<CredentialsError>())
688 .is_some_and(|ce| ce.is_transient());
689 if is_transient {
690 RetryResult::Continue(error)
691 } else {
692 RetryResult::Permanent(error)
693 }
694 });
695 retry_policy
696 }
697
698 pub(crate) fn get_mock_backoff_policy() -> MockBackoffPolicy {
699 let mut backoff_policy = MockBackoffPolicy::new();
700 backoff_policy
701 .expect_on_failure()
702 .return_const(Duration::from_secs(0));
703 backoff_policy
704 }
705
706 pub(crate) fn get_mock_retry_throttler() -> MockRetryThrottler {
707 let mut throttler = MockRetryThrottler::new();
708 throttler.expect_on_retry_failure().return_const(());
709 throttler
710 .expect_throttle_retry_attempt()
711 .return_const(false);
712 throttler.expect_on_success().return_const(());
713 throttler
714 }
715
716 pub(crate) fn get_headers_from_cache(
717 headers: CacheableResource<HeaderMap>,
718 ) -> Result<HeaderMap> {
719 match headers {
720 CacheableResource::New { data, .. } => Ok(data),
721 CacheableResource::NotModified => Err(CredentialsError::from_msg(
722 false,
723 "Expecting headers to be present",
724 )),
725 }
726 }
727
728 pub(crate) fn get_token_from_headers(headers: CacheableResource<HeaderMap>) -> Option<String> {
729 match headers {
730 CacheableResource::New { data, .. } => data
731 .get(AUTHORIZATION)
732 .and_then(|token_value| token_value.to_str().ok())
733 .and_then(|s| s.split_whitespace().nth(1))
734 .map(|s| s.to_string()),
735 CacheableResource::NotModified => None,
736 }
737 }
738
739 pub(crate) fn get_token_type_from_headers(
740 headers: CacheableResource<HeaderMap>,
741 ) -> Option<String> {
742 match headers {
743 CacheableResource::New { data, .. } => data
744 .get(AUTHORIZATION)
745 .and_then(|token_value| token_value.to_str().ok())
746 .and_then(|s| s.split_whitespace().next())
747 .map(|s| s.to_string()),
748 CacheableResource::NotModified => None,
749 }
750 }
751
752 pub static RSA_PRIVATE_KEY: LazyLock<RsaPrivateKey> = LazyLock::new(|| {
753 let p_str: &str = "141367881524527794394893355677826002829869068195396267579403819572502936761383874443619453704612633353803671595972343528718438130450055151198231345212263093247511629886734453413988207866331439612464122904648042654465604881130663408340669956544709445155137282157402427763452856646879397237752891502149781819597";
754 let q_str: &str = "179395413952110013801471600075409598322058038890563483332288896635704255883613060744402506322679437982046475766067250097809676406576067239936945362857700460740092421061356861438909617220234758121022105150630083703531219941303688818533566528599328339894969707615478438750812672509434761181735933851075292740309";
755 let e_str: &str = "65537";
756
757 let p = BigUint::parse_bytes(p_str.as_bytes(), 10).expect("Failed to parse prime P");
758 let q = BigUint::parse_bytes(q_str.as_bytes(), 10).expect("Failed to parse prime Q");
759 let public_exponent =
760 BigUint::parse_bytes(e_str.as_bytes(), 10).expect("Failed to parse public exponent");
761
762 RsaPrivateKey::from_primes(vec![p, q], public_exponent)
763 .expect("Failed to create RsaPrivateKey from primes")
764 });
765
766 pub static PKCS8_PK: LazyLock<String> = LazyLock::new(|| {
767 RSA_PRIVATE_KEY
768 .to_pkcs8_pem(LineEnding::LF)
769 .expect("Failed to encode key to PKCS#8 PEM")
770 .to_string()
771 });
772
773 pub fn b64_decode_to_json(s: String) -> serde_json::Value {
774 let decoded = String::from_utf8(
775 base64::engine::general_purpose::URL_SAFE_NO_PAD
776 .decode(s)
777 .unwrap(),
778 )
779 .unwrap();
780 serde_json::from_str(&decoded).unwrap()
781 }
782
783 #[cfg(target_os = "windows")]
784 #[test]
785 #[serial_test::serial]
786 fn adc_well_known_path_windows() {
787 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
788 let _appdata = ScopedEnv::set("APPDATA", "C:/Users/foo");
789 assert_eq!(
790 adc_well_known_path(),
791 Some("C:/Users/foo/gcloud/application_default_credentials.json".to_string())
792 );
793 assert_eq!(
794 adc_path(),
795 Some(AdcPath::WellKnown(
796 "C:/Users/foo/gcloud/application_default_credentials.json".to_string()
797 ))
798 );
799 }
800
801 #[cfg(target_os = "windows")]
802 #[test]
803 #[serial_test::serial]
804 fn adc_well_known_path_windows_no_appdata() {
805 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
806 let _appdata = ScopedEnv::remove("APPDATA");
807 assert_eq!(adc_well_known_path(), None);
808 assert_eq!(adc_path(), None);
809 }
810
811 #[cfg(not(target_os = "windows"))]
812 #[test]
813 #[serial_test::serial]
814 fn adc_well_known_path_posix() {
815 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
816 let _home = ScopedEnv::set("HOME", "/home/foo");
817 assert_eq!(
818 adc_well_known_path(),
819 Some("/home/foo/.config/gcloud/application_default_credentials.json".to_string())
820 );
821 assert_eq!(
822 adc_path(),
823 Some(AdcPath::WellKnown(
824 "/home/foo/.config/gcloud/application_default_credentials.json".to_string()
825 ))
826 );
827 }
828
829 #[cfg(not(target_os = "windows"))]
830 #[test]
831 #[serial_test::serial]
832 fn adc_well_known_path_posix_no_home() {
833 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
834 let _appdata = ScopedEnv::remove("HOME");
835 assert_eq!(adc_well_known_path(), None);
836 assert_eq!(adc_path(), None);
837 }
838
839 #[test]
840 #[serial_test::serial]
841 fn adc_path_from_env() {
842 let _creds = ScopedEnv::set(
843 "GOOGLE_APPLICATION_CREDENTIALS",
844 "/usr/bar/application_default_credentials.json",
845 );
846 assert_eq!(
847 adc_path(),
848 Some(AdcPath::FromEnv(
849 "/usr/bar/application_default_credentials.json".to_string()
850 ))
851 );
852 }
853
854 #[test]
855 #[serial_test::serial]
856 fn load_adc_no_well_known_path_fallback_to_mds() {
857 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
858 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
861 }
862
863 #[test]
864 #[serial_test::serial]
865 fn load_adc_no_file_at_well_known_path_fallback_to_mds() {
866 let dir = tempfile::TempDir::new().unwrap();
868 let path = dir.path().to_str().unwrap();
869 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
870 let _e2 = ScopedEnv::set("HOME", path); let _e3 = ScopedEnv::set("APPDATA", path); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
873 }
874
875 #[test]
876 #[serial_test::serial]
877 fn load_adc_no_file_at_env_is_error() {
878 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", "file-does-not-exist.json");
879 let err = load_adc().unwrap_err();
880 assert!(err.is_loading(), "{err:?}");
881 let msg = format!("{err:?}");
882 assert!(msg.contains("file-does-not-exist.json"), "{err:?}");
883 assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
884 }
885
886 #[test]
887 #[serial_test::serial]
888 fn load_adc_success() {
889 let file = tempfile::NamedTempFile::new().unwrap();
890 let path = file.into_temp_path();
891 std::fs::write(&path, "contents").expect("Unable to write to temporary file.");
892 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
893
894 assert_eq!(
895 load_adc().unwrap(),
896 AdcContents::Contents("contents".to_string())
897 );
898 }
899
900 #[test_case(true; "retryable")]
901 #[test_case(false; "non-retryable")]
902 #[tokio::test]
903 async fn error_credentials(retryable: bool) {
904 let credentials = super::testing::error_credentials(retryable);
905 assert!(
906 credentials.universe_domain().await.is_none(),
907 "{credentials:?}"
908 );
909 let err = credentials.headers(Extensions::new()).await.err().unwrap();
910 assert_eq!(err.is_transient(), retryable, "{err:?}");
911 let err = credentials.headers(Extensions::new()).await.err().unwrap();
912 assert_eq!(err.is_transient(), retryable, "{err:?}");
913 }
914
915 #[tokio::test]
916 #[serial_test::serial]
917 async fn create_access_token_credentials_fallback_to_mds_with_quota_project_override() {
918 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
919 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::set(GOOGLE_CLOUD_QUOTA_PROJECT_VAR, "env-quota-project");
922
923 let mds = Builder::default()
924 .with_quota_project_id("test-quota-project")
925 .build()
926 .unwrap();
927 let fmt = format!("{mds:?}");
928 assert!(fmt.contains("MDSCredentials"));
929 assert!(
930 fmt.contains("env-quota-project"),
931 "Expected 'env-quota-project', got: {fmt}"
932 );
933 }
934
935 #[tokio::test]
936 #[serial_test::serial]
937 async fn create_access_token_credentials_with_quota_project_from_builder() {
938 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
939 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
942
943 let creds = Builder::default()
944 .with_quota_project_id("test-quota-project")
945 .build()
946 .unwrap();
947 let fmt = format!("{creds:?}");
948 assert!(
949 fmt.contains("test-quota-project"),
950 "Expected 'test-quota-project', got: {fmt}"
951 );
952 }
953
954 #[tokio::test]
955 #[serial_test::serial]
956 async fn create_access_token_service_account_credentials_with_scopes() -> TestResult {
957 let _e1 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
958 let mut service_account_key = serde_json::json!({
959 "type": "service_account",
960 "project_id": "test-project-id",
961 "private_key_id": "test-private-key-id",
962 "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
963 "client_email": "test-client-email",
964 "universe_domain": "test-universe-domain"
965 });
966
967 let scopes =
968 ["https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate"];
969
970 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
971
972 let file = tempfile::NamedTempFile::new().unwrap();
973 let path = file.into_temp_path();
974 std::fs::write(&path, service_account_key.to_string())
975 .expect("Unable to write to temporary file.");
976 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
977
978 let sac = Builder::default()
979 .with_quota_project_id("test-quota-project")
980 .with_scopes(scopes)
981 .build()
982 .unwrap();
983
984 let headers = sac.headers(Extensions::new()).await?;
985 let token = get_token_from_headers(headers).unwrap();
986 let parts: Vec<_> = token.split('.').collect();
987 assert_eq!(parts.len(), 3);
988 let claims = b64_decode_to_json(parts.get(1).unwrap().to_string());
989
990 let fmt = format!("{sac:?}");
991 assert!(fmt.contains("ServiceAccountCredentials"));
992 assert!(fmt.contains("test-quota-project"));
993 assert_eq!(claims["scope"], scopes.join(" "));
994
995 Ok(())
996 }
997}