use anyhow::{Context, Result, bail};
use tokio::process::Command;
use crate::tickets::api::config::GithubConfig;
#[derive(Debug, PartialEq, Eq)]
pub(super) enum AuthSource {
Token,
GhCli,
}
pub(super) fn select_auth(cfg: &GithubConfig) -> AuthSource {
match cfg.token.as_deref() {
Some(t) if !t.trim().is_empty() => AuthSource::Token,
_ => AuthSource::GhCli,
}
}
pub(super) fn gh_token_args(cfg: &GithubConfig) -> Vec<String> {
let mut args = vec!["auth".to_string(), "token".to_string()];
if let Some(host) = cfg.gh_cli_host.as_deref().filter(|s| !s.trim().is_empty()) {
args.push("--hostname".to_string());
args.push(host.to_string());
}
if let Some(user) = cfg.gh_cli_user.as_deref().filter(|s| !s.trim().is_empty()) {
args.push("--user".to_string());
args.push(user.to_string());
}
args
}
pub(super) async fn resolve_token(cfg: &GithubConfig) -> Result<String> {
match select_auth(cfg) {
AuthSource::Token => Ok(cfg.token.as_deref().unwrap_or_default().trim().to_string()),
AuthSource::GhCli => gh_auth_token(cfg).await,
}
}
async fn gh_auth_token(cfg: &GithubConfig) -> Result<String> {
let args = gh_token_args(cfg);
let output = Command::new("gh")
.args(&args)
.output()
.await
.context("failed to spawn 'gh auth token' — is the gh CLI installed and on PATH?")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!(
"gh auth token failed (exit {}): {} — set GITHUB_TOKEN or run 'gh auth login'",
output.status,
stderr.trim()
);
}
let token = String::from_utf8_lossy(&output.stdout).trim().to_string();
if token.is_empty() {
bail!("gh auth token returned empty output — run 'gh auth login' to authenticate");
}
Ok(token)
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg_with(token: Option<&str>, host: Option<&str>, user: Option<&str>) -> GithubConfig {
GithubConfig {
token: token.map(String::from),
owner: Some("o".into()),
repo: Some("r".into()),
gh_cli_host: host.map(String::from),
gh_cli_user: user.map(String::from),
}
}
#[test]
fn select_auth_prefers_token() {
let cfg = cfg_with(Some("ghp_abc"), None, None);
assert_eq!(select_auth(&cfg), AuthSource::Token);
}
#[test]
fn select_auth_falls_back_to_gh_when_absent() {
let cfg = cfg_with(None, None, None);
assert_eq!(select_auth(&cfg), AuthSource::GhCli);
}
#[test]
fn select_auth_falls_back_to_gh_when_blank() {
let cfg = cfg_with(Some(" "), None, None);
assert_eq!(select_auth(&cfg), AuthSource::GhCli);
}
#[test]
fn gh_token_args_bare() {
let cfg = cfg_with(None, None, None);
assert_eq!(gh_token_args(&cfg), vec!["auth", "token"]);
}
#[test]
fn gh_token_args_with_host_and_user() {
let cfg = cfg_with(None, Some("ghe.example.com"), Some("octocat"));
assert_eq!(
gh_token_args(&cfg),
vec![
"auth",
"token",
"--hostname",
"ghe.example.com",
"--user",
"octocat"
]
);
}
#[test]
fn gh_token_args_ignores_blank_host_and_user() {
let cfg = cfg_with(None, Some(" "), Some(""));
assert_eq!(gh_token_args(&cfg), vec!["auth", "token"]);
}
#[tokio::test(flavor = "current_thread")]
async fn resolve_token_returns_trimmed_explicit_token() {
let cfg = cfg_with(Some(" ghp_padded "), None, None);
let tok = resolve_token(&cfg).await.expect("explicit token resolves");
assert_eq!(tok, "ghp_padded");
}
}