use travelagent_core::error::{Result, TrvError};
pub fn resolve_token() -> Result<String> {
if let Ok(token) = std::env::var("GITHUB_TOKEN")
&& !token.is_empty()
{
return Ok(token);
}
if let Ok(token) = std::env::var("GH_TOKEN")
&& !token.is_empty()
{
return Ok(token);
}
let output = std::process::Command::new("gh")
.args(["auth", "token"])
.output()
.map_err(|e| TrvError::AuthError(format!("Failed to run gh auth token: {e}")))?;
if output.status.success()
&& let Some(token) = parse_gh_auth_token(&String::from_utf8_lossy(&output.stdout))
{
return Ok(token);
}
Err(TrvError::AuthError(
"No GitHub token found. Set GITHUB_TOKEN, GH_TOKEN, or run 'gh auth login'.".into(),
))
}
pub(crate) fn parse_gh_auth_token(stdout: &str) -> Option<String> {
let token = stdout
.lines()
.map(str::trim)
.find(|line| !line.is_empty())
.unwrap_or("")
.trim_start_matches('\u{feff}') .trim()
.to_string();
if token.is_empty() { None } else { Some(token) }
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn resolves_github_token_env_var() {
let _lock = ENV_LOCK.lock().unwrap();
unsafe {
std::env::set_var("GITHUB_TOKEN", "test-gh-token-123");
std::env::remove_var("GH_TOKEN");
}
let token = resolve_token().unwrap();
assert_eq!(token, "test-gh-token-123");
unsafe {
std::env::remove_var("GITHUB_TOKEN");
}
}
#[test]
fn falls_back_to_gh_token() {
let _lock = ENV_LOCK.lock().unwrap();
unsafe {
std::env::remove_var("GITHUB_TOKEN");
std::env::set_var("GH_TOKEN", "fallback-gh-token");
}
let token = resolve_token().unwrap();
assert_eq!(token, "fallback-gh-token");
unsafe {
std::env::remove_var("GH_TOKEN");
}
}
#[test]
fn skips_empty_github_token() {
let _lock = ENV_LOCK.lock().unwrap();
unsafe {
std::env::set_var("GITHUB_TOKEN", "");
std::env::set_var("GH_TOKEN", "nonempty-token");
}
let token = resolve_token().unwrap();
assert_eq!(token, "nonempty-token");
unsafe {
std::env::remove_var("GITHUB_TOKEN");
std::env::remove_var("GH_TOKEN");
}
}
#[test]
fn github_token_takes_priority_over_gh_token() {
let _lock = ENV_LOCK.lock().unwrap();
unsafe {
std::env::set_var("GITHUB_TOKEN", "primary");
std::env::set_var("GH_TOKEN", "secondary");
}
let token = resolve_token().unwrap();
assert_eq!(token, "primary");
unsafe {
std::env::remove_var("GITHUB_TOKEN");
std::env::remove_var("GH_TOKEN");
}
}
#[test]
fn parse_gh_token_current_format() {
let stdout = "ghp_AbCdEfGhIjKlMnOpQrStUvWxYz0123456789\n";
assert_eq!(
parse_gh_auth_token(stdout).as_deref(),
Some("ghp_AbCdEfGhIjKlMnOpQrStUvWxYz0123456789")
);
}
#[test]
fn parse_gh_token_fine_grained() {
let stdout = "github_pat_11AAAA0000BBBB1111CCCC2222DDDD3333\n";
assert_eq!(
parse_gh_auth_token(stdout).as_deref(),
Some("github_pat_11AAAA0000BBBB1111CCCC2222DDDD3333")
);
}
#[test]
fn parse_gh_token_tolerates_older_format_without_trailing_newline() {
let stdout = "ghp_LegacyTokenNoNewline";
assert_eq!(
parse_gh_auth_token(stdout).as_deref(),
Some("ghp_LegacyTokenNoNewline")
);
}
#[test]
fn parse_gh_token_strips_bom_and_blank_lines() {
let stdout = "\n\n\u{feff}gho_WrappedToken123\n";
assert_eq!(
parse_gh_auth_token(stdout).as_deref(),
Some("gho_WrappedToken123")
);
}
#[test]
fn parse_gh_token_returns_none_for_empty_output() {
assert_eq!(parse_gh_auth_token(""), None);
assert_eq!(parse_gh_auth_token("\n\n \n"), None);
}
#[test]
fn returns_error_when_no_token_and_cli_unavailable() {
let _lock = ENV_LOCK.lock().unwrap();
unsafe {
std::env::remove_var("GITHUB_TOKEN");
std::env::remove_var("GH_TOKEN");
std::env::set_var("PATH", "/nonexistent");
}
let result = resolve_token();
assert!(result.is_err());
unsafe {
std::env::set_var("PATH", "/usr/bin:/usr/local/bin:/bin");
}
}
}