use crate::auth::secure_string::SecureString;
use tracing::debug;
const TOKEN_ENV_VARS: &[(&str, &str)] = &[
("SECUREGIT_TOKEN", "SecureGit"),
("GITHUB_TOKEN", "GitHub"),
("GH_TOKEN", "GitHub CLI"),
("GITLAB_TOKEN", "GitLab"),
("GITLAB_CI_JOB_TOKEN", "GitLab CI"),
("BITBUCKET_TOKEN", "Bitbucket"),
];
pub fn discover_token() -> Option<(SecureString, &'static str)> {
for (var, source) in TOKEN_ENV_VARS {
if let Ok(val) = std::env::var(var) {
if !val.is_empty() {
debug!("Found token from {} ({})", source, var);
return Some((SecureString::from_string(val), source));
}
}
}
None
}
pub fn token_for_host(host: &str) -> Option<SecureString> {
let env_token = match host {
h if h.contains("github") => std::env::var("GITHUB_TOKEN")
.or_else(|_| std::env::var("GH_TOKEN"))
.or_else(|_| std::env::var("SECUREGIT_TOKEN"))
.ok()
.filter(|s| !s.is_empty())
.map(SecureString::from_string),
h if h.contains("gitlab") => std::env::var("GITLAB_TOKEN")
.or_else(|_| std::env::var("GITLAB_CI_JOB_TOKEN"))
.or_else(|_| std::env::var("SECUREGIT_TOKEN"))
.ok()
.filter(|s| !s.is_empty())
.map(SecureString::from_string),
h if h.contains("bitbucket") => std::env::var("BITBUCKET_TOKEN")
.or_else(|_| std::env::var("SECUREGIT_TOKEN"))
.ok()
.filter(|s| !s.is_empty())
.map(SecureString::from_string),
_ => std::env::var("SECUREGIT_TOKEN")
.ok()
.filter(|s| !s.is_empty())
.map(SecureString::from_string),
};
if env_token.is_some() {
return env_token;
}
crate::auth::store::get_token(host)
}
pub fn token_for_server(
server: &crate::platform::server_registry::ServerConfig,
) -> Option<SecureString> {
let env_key = format!(
"SECUREGIT_SERVER_{}_TOKEN",
server.name.to_uppercase().replace('-', "_")
);
if let Ok(val) = std::env::var(&env_key) {
if !val.is_empty() {
debug!(
"Found token for server '{}' from env var {}",
server.name, env_key
);
return Some(SecureString::from_string(val));
}
}
let store_key = format!("server:{}", server.name);
if let Some(token) = crate::auth::store::get_token(&store_key) {
debug!("Found stored token for server '{}'", server.name);
return Some(token);
}
if let Ok(url) = url::Url::parse(&server.api_url) {
if let Some(host) = url.host_str() {
debug!(
"Falling back to host-based token resolution for server '{}' (host: {})",
server.name, host
);
return token_for_host(host);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
fn clear_token_env_vars() {
for var in &[
"SECUREGIT_TOKEN",
"GITHUB_TOKEN",
"GH_TOKEN",
"GITLAB_TOKEN",
"GITLAB_CI_JOB_TOKEN",
"BITBUCKET_TOKEN",
] {
std::env::remove_var(var);
}
}
#[test]
#[serial]
fn test_resolve_token_github_env() {
clear_token_env_vars();
std::env::set_var("GITHUB_TOKEN", "ghp_test_github_token");
let result = discover_token();
assert!(result.is_some(), "Should discover GITHUB_TOKEN");
let (token, source) = result.unwrap();
assert_eq!(token.as_str(), "ghp_test_github_token");
assert_eq!(source, "GitHub");
clear_token_env_vars();
}
#[test]
#[serial]
fn test_resolve_token_gitlab_env() {
clear_token_env_vars();
std::env::set_var("GITLAB_TOKEN", "glpat-test_gitlab_token");
let result = discover_token();
assert!(result.is_some(), "Should discover GITLAB_TOKEN");
let (token, source) = result.unwrap();
assert_eq!(token.as_str(), "glpat-test_gitlab_token");
assert_eq!(source, "GitLab");
clear_token_env_vars();
}
#[test]
#[serial]
fn test_resolve_token_missing() {
clear_token_env_vars();
let result = discover_token();
assert!(
result.is_none(),
"Should return None when no env vars are set"
);
}
#[test]
#[serial]
fn test_resolve_token_empty_value_skipped() {
clear_token_env_vars();
std::env::set_var("GITHUB_TOKEN", "");
let result = discover_token();
assert!(result.is_none(), "Empty env var should be skipped");
clear_token_env_vars();
}
#[test]
#[serial]
fn test_discover_token_priority_order() {
clear_token_env_vars();
std::env::set_var("SECUREGIT_TOKEN", "securegit-first");
std::env::set_var("GITHUB_TOKEN", "github-second");
let result = discover_token();
assert!(result.is_some());
let (token, source) = result.unwrap();
assert_eq!(token.as_str(), "securegit-first");
assert_eq!(source, "SecureGit");
clear_token_env_vars();
}
#[test]
#[serial]
fn test_token_for_host_github_env() {
clear_token_env_vars();
std::env::set_var("GITHUB_TOKEN", "ghp_for_github_host");
let result = token_for_host("github.com");
assert!(result.is_some());
assert_eq!(result.unwrap().as_str(), "ghp_for_github_host");
clear_token_env_vars();
}
#[test]
#[serial]
fn test_token_for_host_gitlab_env() {
clear_token_env_vars();
std::env::set_var("GITLAB_TOKEN", "glpat-for_gitlab_host");
let result = token_for_host("gitlab.com");
assert!(result.is_some());
assert_eq!(result.unwrap().as_str(), "glpat-for_gitlab_host");
clear_token_env_vars();
}
#[test]
#[serial]
fn test_token_for_host_bitbucket_env() {
clear_token_env_vars();
std::env::set_var("BITBUCKET_TOKEN", "bb_for_bitbucket_host");
let result = token_for_host("bitbucket.org");
assert!(result.is_some());
assert_eq!(result.unwrap().as_str(), "bb_for_bitbucket_host");
clear_token_env_vars();
}
#[test]
#[serial]
fn test_token_for_host_securegit_fallback() {
clear_token_env_vars();
std::env::set_var("SECUREGIT_TOKEN", "securegit-universal");
let result = token_for_host("custom-git.example.com");
assert!(result.is_some());
assert_eq!(result.unwrap().as_str(), "securegit-universal");
clear_token_env_vars();
}
#[test]
#[serial]
fn test_token_for_host_gh_token_fallback() {
clear_token_env_vars();
std::env::set_var("GH_TOKEN", "gh_cli_token");
let result = token_for_host("github.com");
assert!(result.is_some());
assert_eq!(result.unwrap().as_str(), "gh_cli_token");
clear_token_env_vars();
}
#[test]
#[serial]
fn test_token_for_host_missing_returns_none() {
clear_token_env_vars();
let result = token_for_host("nonexistent-host.example.com");
let _ = result;
}
fn clear_server_env_vars() {
clear_token_env_vars();
for var in &[
"SECUREGIT_SERVER_MY_GITLAB_TOKEN",
"SECUREGIT_SERVER_TEST_SERVER_TOKEN",
] {
std::env::remove_var(var);
}
}
fn make_server_config(
name: &str,
api_url: &str,
) -> crate::platform::server_registry::ServerConfig {
crate::platform::server_registry::ServerConfig {
name: name.to_string(),
platform: crate::platform::server_registry::ServerPlatform::GitLab,
api_url: api_url.to_string(),
web_url: None,
push_enabled: false,
}
}
#[test]
#[serial]
fn test_token_for_server_env_var_priority() {
clear_server_env_vars();
std::env::set_var("SECUREGIT_SERVER_MY_GITLAB_TOKEN", "env-server-token");
let server = make_server_config("my-gitlab", "https://gitlab.example.com/api/v4");
let result = token_for_server(&server);
assert!(result.is_some());
assert_eq!(result.unwrap().as_str(), "env-server-token");
clear_server_env_vars();
}
#[test]
#[serial]
fn test_token_for_server_host_fallback() {
clear_server_env_vars();
std::env::set_var("GITLAB_TOKEN", "gitlab-host-fallback");
let server = make_server_config("test-server", "https://gitlab.example.com/api/v4");
let result = token_for_server(&server);
assert!(result.is_some());
assert_eq!(result.unwrap().as_str(), "gitlab-host-fallback");
clear_server_env_vars();
}
#[test]
#[serial]
fn test_token_for_server_env_beats_host_fallback() {
clear_server_env_vars();
std::env::set_var("SECUREGIT_SERVER_TEST_SERVER_TOKEN", "specific-env");
std::env::set_var("GITLAB_TOKEN", "generic-gitlab");
let server = make_server_config("test-server", "https://gitlab.example.com/api/v4");
let result = token_for_server(&server);
assert!(result.is_some());
assert_eq!(result.unwrap().as_str(), "specific-env");
clear_server_env_vars();
}
}