#![cfg(feature = "pkce-auth")]
#![allow(unsafe_code)]
#![allow(clippy::await_holding_lock)]
use std::path::Path;
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{SystemTime, UNIX_EPOCH};
use cli_engine::auth::pkce::PkceAuthProvider;
use cli_engine::{Cli, CliConfig};
const APP_ID: &str = "itest";
const ENV_VAR: &str = "ITEST_CREDENTIAL_STORE";
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn lock() -> MutexGuard<'static, ()> {
ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
struct EnvGuard {
key: String,
prev: Option<String>,
}
impl EnvGuard {
fn set(key: &str, value: Option<&str>) -> Self {
let prev = std::env::var(key).ok();
unsafe {
match value {
Some(v) => std::env::set_var(key, v),
None => std::env::remove_var(key),
}
}
Self {
key: key.to_owned(),
prev,
}
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
unsafe {
match &self.prev {
Some(v) => std::env::set_var(&self.key, v),
None => std::env::remove_var(&self.key),
}
}
}
}
fn seed_credential_file(xdg: &Path) {
let dir = xdg.join(APP_ID).join("credentials");
std::fs::create_dir_all(&dir).expect("create credentials dir");
let expires_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock")
.as_secs()
+ 3600;
let json = format!(
"{{\"access_token\":\"itok\",\"expires_at\":{expires_at},\"refresh_token\":null,\"scopes\":[]}}"
);
std::fs::write(dir.join("primary-dev.json"), json).expect("write credential file");
}
fn write_config(xdg: &Path, store: &str) {
let dir = xdg.join(APP_ID);
std::fs::create_dir_all(&dir).expect("create config dir");
std::fs::write(
dir.join("config.toml"),
format!("[credentials]\nstore = \"{store}\"\n"),
)
.expect("write config");
}
fn build_cli() -> Cli {
let provider = Arc::new(
PkceAuthProvider::new(
"primary",
"https://example.com/auth",
"https://example.com/token",
"client-id",
&["openid"],
)
.with_app_id(APP_ID),
);
Cli::new(
CliConfig::new(APP_ID, "Integration CLI", APP_ID)
.with_auth_provider(provider)
.with_default_auth_provider("primary"),
)
}
#[tokio::test]
async fn config_file_selects_file_store() {
let _guard = lock();
let dir = tempfile::tempdir().expect("tempdir");
let _xdg = EnvGuard::set("XDG_CONFIG_HOME", Some(&dir.path().to_string_lossy()));
let _env = EnvGuard::set(ENV_VAR, None);
write_config(dir.path(), "file");
seed_credential_file(dir.path());
let out = build_cli()
.run(["itest", "auth", "status", "--env", "dev"])
.await;
assert_eq!(out.exit_code, 0, "expected success, got: {}", out.rendered);
assert!(
out.rendered.contains("dev"),
"status should report the env: {}",
out.rendered
);
assert!(
!out.rendered.contains("not logged in"),
"file store should find the seeded credential: {}",
out.rendered
);
}
#[tokio::test]
async fn default_keyring_mode_ignores_credential_file() {
let _guard = lock();
let dir = tempfile::tempdir().expect("tempdir");
let _xdg = EnvGuard::set("XDG_CONFIG_HOME", Some(&dir.path().to_string_lossy()));
let _env = EnvGuard::set(ENV_VAR, None);
seed_credential_file(dir.path());
let out = build_cli()
.run(["itest", "auth", "status", "--env", "dev"])
.await;
assert_ne!(out.exit_code, 0, "expected not-logged-in: {}", out.rendered);
assert!(
out.rendered.contains("not logged in"),
"keyring mode must not read the credential file: {}",
out.rendered
);
}
#[tokio::test]
async fn env_var_overrides_config() {
let _guard = lock();
let dir = tempfile::tempdir().expect("tempdir");
let _xdg = EnvGuard::set("XDG_CONFIG_HOME", Some(&dir.path().to_string_lossy()));
write_config(dir.path(), "keyring");
let _env = EnvGuard::set(ENV_VAR, Some("file"));
seed_credential_file(dir.path());
let out = build_cli()
.run(["itest", "auth", "status", "--env", "dev"])
.await;
assert_eq!(
out.exit_code, 0,
"env override should win: {}",
out.rendered
);
assert!(!out.rendered.contains("not logged in"), "{}", out.rendered);
}
#[tokio::test]
async fn flag_overrides_env() {
let _guard = lock();
let dir = tempfile::tempdir().expect("tempdir");
let _xdg = EnvGuard::set("XDG_CONFIG_HOME", Some(&dir.path().to_string_lossy()));
let _env = EnvGuard::set(ENV_VAR, Some("keyring"));
seed_credential_file(dir.path());
let out = build_cli()
.run([
"itest",
"--credential-store",
"file",
"auth",
"status",
"--env",
"dev",
])
.await;
assert_eq!(
out.exit_code, 0,
"flag override should win: {}",
out.rendered
);
assert!(!out.rendered.contains("not logged in"), "{}", out.rendered);
let reset = build_cli()
.run(["itest", "auth", "status", "--env", "dev"])
.await;
assert_ne!(reset.exit_code, 0, "{}", reset.rendered);
}
#[tokio::test]
async fn invalid_credential_store_flag_is_rejected() {
let _guard = lock();
let out = build_cli()
.run([
"itest",
"--credential-store",
"vault",
"auth",
"status",
"--env",
"dev",
])
.await;
assert_ne!(out.exit_code, 0, "invalid value should be a usage error");
let reset = build_cli()
.run(["itest", "auth", "status", "--env", "dev"])
.await;
assert_ne!(reset.exit_code, 0, "{}", reset.rendered);
}