fix: isolate flaky auth tests from host ADC credentials (#208)

* fix: isolate flaky auth tests from host ADC credentials

Fixes #206

Both test_load_credentials_no_options and
test_get_token_env_var_empty_falls_through now override HOME to a
temp dir and clear GOOGLE_APPLICATION_CREDENTIALS, preventing the
well-known ADC path from matching on CI runners that have gcloud
credentials.

* refactor: use RAII EnvVarGuard for panic-safe env var cleanup in tests

- Introduce EnvVarGuard struct that saves/restores env vars on Drop
- Replace all manual save/restore patterns in auth tests
- Fix bug where test_get_token_env_var_empty_falls_through did not
  restore GOOGLE_WORKSPACE_CLI_TOKEN
- Ensures cleanup runs even if a test panics

* fix: use var_os/OsString in EnvVarGuard for non-UTF-8 safety

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
This commit is contained in:
Justin Poehnelt
2026-03-05 14:48:59 -07:00
committed by GitHub
parent 24a25a6ee6
commit 3d59b2e5a9
2 changed files with 60 additions and 24 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@googleworkspace/cli": patch
---
fix: isolate flaky auth tests from host ADC credentials
+55 -24
View File
@@ -351,14 +351,60 @@ mod tests {
use std::io::Write;
use tempfile::NamedTempFile;
/// RAII guard that saves the current value of an environment variable and
/// restores it when dropped. This ensures cleanup even if a test panics.
struct EnvVarGuard {
name: String,
original: Option<std::ffi::OsString>,
}
impl EnvVarGuard {
/// Save the current value of `name`, then set it to `value`.
fn set(name: &str, value: impl AsRef<std::ffi::OsStr>) -> Self {
let original = std::env::var_os(name);
std::env::set_var(name, value);
Self {
name: name.to_string(),
original,
}
}
/// Save the current value of `name`, then remove it.
fn remove(name: &str) -> Self {
let original = std::env::var_os(name);
std::env::remove_var(name);
Self {
name: name.to_string(),
original,
}
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
match &self.original {
Some(v) => std::env::set_var(&self.name, v),
None => std::env::remove_var(&self.name),
}
}
}
#[tokio::test]
#[serial_test::serial]
async fn test_load_credentials_no_options() {
// Isolate from host ADC: override HOME so adc_well_known_path()
// resolves to a non-existent directory, and clear the env var.
let tmp = tempfile::tempdir().unwrap();
let _home_guard = EnvVarGuard::set("HOME", tmp.path());
let _adc_guard = EnvVarGuard::remove("GOOGLE_APPLICATION_CREDENTIALS");
let err = load_credentials_inner(
None,
&PathBuf::from("/does/not/exist1"),
&PathBuf::from("/does/not/exist2"),
)
.await;
assert!(err.is_err());
assert!(err
.unwrap_err()
@@ -378,7 +424,7 @@ mod tests {
}"#;
file.write_all(json.as_bytes()).unwrap();
std::env::set_var(
let _adc_guard = EnvVarGuard::set(
"GOOGLE_APPLICATION_CREDENTIALS",
file.path().to_str().unwrap(),
);
@@ -390,8 +436,6 @@ mod tests {
)
.await;
std::env::remove_var("GOOGLE_APPLICATION_CREDENTIALS");
match res.unwrap() {
Credential::AuthorizedUser(secret) => {
assert_eq!(secret.client_id, "adc_id");
@@ -417,7 +461,7 @@ mod tests {
}"#;
file.write_all(json.as_bytes()).unwrap();
std::env::set_var(
let _adc_guard = EnvVarGuard::set(
"GOOGLE_APPLICATION_CREDENTIALS",
file.path().to_str().unwrap(),
);
@@ -429,8 +473,6 @@ mod tests {
)
.await;
std::env::remove_var("GOOGLE_APPLICATION_CREDENTIALS");
match res.unwrap() {
Credential::ServiceAccount(key) => {
assert_eq!(
@@ -445,7 +487,7 @@ mod tests {
#[tokio::test]
#[serial_test::serial]
async fn test_load_credentials_adc_env_var_missing_file() {
std::env::set_var("GOOGLE_APPLICATION_CREDENTIALS", "/does/not/exist.json");
let _adc_guard = EnvVarGuard::set("GOOGLE_APPLICATION_CREDENTIALS", "/does/not/exist.json");
// When GOOGLE_APPLICATION_CREDENTIALS points to a missing file, we error immediately
// rather than falling through — the user explicitly asked for this file.
@@ -456,8 +498,6 @@ mod tests {
)
.await;
std::env::remove_var("GOOGLE_APPLICATION_CREDENTIALS");
assert!(err.is_err());
let msg = err.unwrap_err().to_string();
assert!(
@@ -563,20 +603,10 @@ mod tests {
#[tokio::test]
#[serial_test::serial]
async fn test_get_token_from_env_var() {
// Save the old token
let old_token = std::env::var("GOOGLE_WORKSPACE_CLI_TOKEN").ok();
// Set the token env var
std::env::set_var("GOOGLE_WORKSPACE_CLI_TOKEN", "my-test-token");
let _token_guard = EnvVarGuard::set("GOOGLE_WORKSPACE_CLI_TOKEN", "my-test-token");
let result = get_token(&["https://www.googleapis.com/auth/drive"], None).await;
if let Some(t) = old_token {
std::env::set_var("GOOGLE_WORKSPACE_CLI_TOKEN", t);
} else {
std::env::remove_var("GOOGLE_WORKSPACE_CLI_TOKEN");
}
assert!(result.is_ok());
assert_eq!(result.unwrap(), "my-test-token");
}
@@ -586,8 +616,11 @@ mod tests {
async fn test_get_token_env_var_empty_falls_through() {
// An empty token should not short-circuit — it should be ignored
// and fall through to normal credential loading.
// We test with non-existent credential paths to ensure fallthrough.
std::env::set_var("GOOGLE_WORKSPACE_CLI_TOKEN", "");
// Isolate from host ADC so the well-known path doesn't match.
let tmp = tempfile::tempdir().unwrap();
let _home_guard = EnvVarGuard::set("HOME", tmp.path());
let _adc_guard = EnvVarGuard::remove("GOOGLE_APPLICATION_CREDENTIALS");
let _token_guard = EnvVarGuard::set("GOOGLE_WORKSPACE_CLI_TOKEN", "");
let result = load_credentials_inner(
None,
@@ -596,8 +629,6 @@ mod tests {
)
.await;
std::env::remove_var("GOOGLE_WORKSPACE_CLI_TOKEN");
// Should fall through to normal credential loading, which fails
// because we pointed at non-existent paths
assert!(result.is_err());