use std::path::{Path, PathBuf};
use super::{
GH_TOKEN_ENV_VAR, GH_USER_ENV_VAR, find_pinned_gh_identity,
resolve_gh_account_env_for_registry, resolve_gh_account_env_with,
};
use crate::core::trusty_tools_config::GithubConfig;
use crate::project::Project;
use crate::project::ProjectRegistry;
const GH_CONFIG_DIR: &str = "GH_CONFIG_DIR";
fn value_of(vars: &[(String, String)], name: &str) -> String {
let matches: Vec<&(String, String)> = vars.iter().filter(|(k, _)| k == name).collect();
assert_eq!(matches.len(), 1, "expected exactly one {name} in {vars:?}");
matches[0].1.clone()
}
fn write_hosts_yml(dir: &Path, account: &str) {
let yaml = format!("github.com:\n git_protocol: https\n user: {account}\n");
std::fs::write(dir.join("hosts.yml"), yaml).expect("write hosts.yml");
}
#[test]
fn resolve_gh_account_env_with_unset_is_none() {
let result = resolve_gh_account_env_with(None, None, |_| Ok("unused".to_string()));
assert!(result.is_none());
}
#[test]
fn resolve_gh_account_env_with_blank_is_none() {
let result = resolve_gh_account_env_with(Some(" "), None, |_| Ok("unused".to_string()));
assert!(result.is_none());
}
#[test]
fn resolve_gh_account_env_with_success_returns_token_and_user() {
let result = resolve_gh_account_env_with(Some("bobmatnyc"), None, |account| {
assert_eq!(account, "bobmatnyc");
Ok("ghp_fake_token".to_string())
});
let env = result.expect("some").expect("ok");
assert_eq!(
env.vars,
vec![
(GH_TOKEN_ENV_VAR.to_string(), "ghp_fake_token".to_string()),
(GH_USER_ENV_VAR.to_string(), "bobmatnyc".to_string()),
]
);
assert_eq!(env.warning, None);
}
#[test]
fn resolve_gh_account_env_with_failure_returns_err() {
let result = resolve_gh_account_env_with(Some("bob-duetto"), None, |_| {
Err("account 'bob-duetto' is not logged in".to_string())
});
let err = result.expect("some").expect_err("err");
assert!(err.contains("bob-duetto"), "err: {err}");
}
#[test]
fn resolve_gh_account_env_with_trims_account_name() {
let result = resolve_gh_account_env_with(Some(" bobmatnyc "), None, |account| {
assert_eq!(account, "bobmatnyc");
Ok("tok".to_string())
});
let env = result.expect("some").expect("ok");
assert!(
env.vars
.contains(&(GH_USER_ENV_VAR.to_string(), "bobmatnyc".to_string()))
);
}
#[test]
fn config_dir_is_selected_over_a_minted_token() {
let dir = tempfile::tempdir().expect("tempdir");
write_hosts_yml(dir.path(), "bobmatnyc");
let result = resolve_gh_account_env_with(Some("bobmatnyc"), Some(dir.path()), |_| {
panic!("the token path must not run when a config_dir is pinned")
});
let env = result.expect("some").expect("ok");
assert_eq!(
value_of(&env.vars, GH_CONFIG_DIR),
dir.path().to_string_lossy()
);
assert!(
!env.vars.iter().any(|(k, _)| k == GH_TOKEN_ENV_VAR),
"GH_TOKEN must never accompany GH_CONFIG_DIR: {:?}",
env.vars
);
assert_eq!(value_of(&env.vars, GH_USER_ENV_VAR), "bobmatnyc");
}
#[test]
fn config_dir_with_credential_has_no_warning() {
let dir = tempfile::tempdir().expect("tempdir");
write_hosts_yml(dir.path(), "bobmatnyc");
let env = resolve_gh_account_env_with(Some("bobmatnyc"), Some(dir.path()), |_| {
panic!("token path must not run")
})
.expect("some")
.expect("ok");
assert_eq!(env.warning, None);
}
#[test]
fn config_dir_without_credential_still_pins_and_warns() {
let dir = tempfile::tempdir().expect("tempdir");
let env = resolve_gh_account_env_with(Some("bobmatnyc"), Some(dir.path()), |_| {
panic!("the token path must not be used as a fallback for an empty config dir")
})
.expect("some")
.expect("ok");
assert_eq!(
value_of(&env.vars, GH_CONFIG_DIR),
dir.path().to_string_lossy(),
"an empty config dir must still be pinned — falling back is the fail-open defect"
);
assert!(!env.vars.iter().any(|(k, _)| k == GH_TOKEN_ENV_VAR));
let warning = env.warning.expect("an empty config dir must warn");
let shown = dir.path().display().to_string();
assert!(
warning.contains(&shown),
"warning must name the dir: {warning}"
);
assert!(
warning.contains("gh auth login"),
"warning must say what to run: {warning}"
);
}
#[test]
fn blank_config_dir_falls_through_to_the_token_path() {
let env = resolve_gh_account_env_with(Some("bobmatnyc"), Some(Path::new(" ")), |_| {
Ok("tok".to_string())
})
.expect("some")
.expect("ok");
assert_eq!(value_of(&env.vars, GH_TOKEN_ENV_VAR), "tok");
assert!(!env.vars.iter().any(|(k, _)| k == GH_CONFIG_DIR));
}
#[test]
fn config_dir_without_an_account_still_pins() {
let dir = tempfile::tempdir().expect("tempdir");
write_hosts_yml(dir.path(), "bobmatnyc");
let env = resolve_gh_account_env_with(None, Some(dir.path()), |_| panic!("no token path"))
.expect("some")
.expect("ok");
assert_eq!(
value_of(&env.vars, GH_CONFIG_DIR),
dir.path().to_string_lossy()
);
assert!(!env.vars.iter().any(|(k, _)| k == GH_USER_ENV_VAR));
}
fn project(name: &str, repo_url: &str, gh_account: Option<&str>) -> Project {
Project {
name: name.to_string(),
repo_url: repo_url.to_string(),
default_branch: "main".to_string(),
stack_hint: None,
tags: vec![],
description: None,
gh_user: None,
gh_account: gh_account.map(str::to_string),
github: None,
commit_name: None,
commit_email: None,
worktree: None,
}
}
fn project_with_config_dir(name: &str, repo_url: &str, config_dir: &Path) -> Project {
Project {
github: Some(GithubConfig {
config_dir: Some(config_dir.to_path_buf()),
..GithubConfig::default()
}),
..project(name, repo_url, None)
}
}
#[tokio::test]
async fn resolve_gh_account_env_for_registry_picks_up_registered_gh_account() {
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
registry
.register(project(
"widget",
"https://github.com/acme/widget",
Some("bobmatnyc"),
))
.await
.expect("register");
let found = find_pinned_gh_identity(®istry, "https://github.com/acme/widget")
.await
.expect("matched");
assert_eq!(found.account.as_deref(), Some("bobmatnyc"));
assert_eq!(found.config_dir, None);
let found_git_suffix = find_pinned_gh_identity(®istry, "https://github.com/acme/widget.git")
.await
.expect("matched");
assert_eq!(found_git_suffix.account.as_deref(), Some("bobmatnyc"));
}
#[tokio::test]
async fn find_pinned_gh_identity_reads_config_dir() {
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
let config_dir = PathBuf::from("/home/bob/.config/gh-bobmatnyc");
registry
.register(project_with_config_dir(
"widget",
"https://github.com/acme/widget",
&config_dir,
))
.await
.expect("register");
let found = find_pinned_gh_identity(®istry, "https://github.com/acme/widget")
.await
.expect("matched");
assert_eq!(found.config_dir.as_deref(), Some(config_dir.as_path()));
}
#[tokio::test]
async fn resolve_gh_account_env_for_registry_no_match_is_none() {
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
registry
.register(project(
"widget",
"https://github.com/acme/widget",
Some("bobmatnyc"),
))
.await
.expect("register");
let found = find_pinned_gh_identity(®istry, "https://github.com/acme/other-repo").await;
assert_eq!(found, None);
}
#[tokio::test]
async fn resolve_gh_account_env_for_registry_registered_without_gh_account_is_none() {
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
registry
.register(project("widget", "https://github.com/acme/widget", None))
.await
.expect("register");
let found = find_pinned_gh_identity(®istry, "https://github.com/acme/widget").await;
assert_eq!(found, None);
}
#[tokio::test]
async fn resolve_gh_account_env_for_registry_no_origin_is_empty() {
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
let workspace = tempfile::tempdir().expect("workspace tempdir");
let vars = resolve_gh_account_env_for_registry(®istry, workspace.path()).await;
assert!(vars.is_empty(), "vars: {vars:?}");
}
fn workspace_with_origin(dir: &Path, origin_url: &str) {
let init = std::process::Command::new("git")
.args(["init", "-q"])
.arg(dir)
.output()
.expect("git init");
assert!(init.status.success(), "git init failed");
let remote = std::process::Command::new("git")
.arg("-C")
.arg(dir)
.args(["remote", "add", "origin", origin_url])
.output()
.expect("git remote add");
assert!(remote.status.success(), "git remote add failed");
}
#[tokio::test]
async fn pinned_config_dir_reaches_the_spawn_env() {
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
let gh_home = tempfile::tempdir().expect("gh home tempdir");
write_hosts_yml(gh_home.path(), "bobmatnyc");
registry
.register(project_with_config_dir(
"widget",
"https://github.com/acme/widget",
gh_home.path(),
))
.await
.expect("register");
let workspace = tempfile::tempdir().expect("workspace tempdir");
workspace_with_origin(workspace.path(), "https://github.com/acme/widget.git");
let vars = resolve_gh_account_env_for_registry(®istry, workspace.path()).await;
assert_eq!(
value_of(&vars, GH_CONFIG_DIR),
gh_home.path().to_string_lossy(),
"a project pinned by github.config_dir must have it injected at spawn"
);
assert!(
!vars.iter().any(|(k, _)| k == GH_TOKEN_ENV_VAR),
"GH_TOKEN must not accompany GH_CONFIG_DIR: {vars:?}"
);
}
#[tokio::test]
async fn registered_project_pinning_nothing_injects_nothing() {
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
registry
.register(project("widget", "https://github.com/acme/widget", None))
.await
.expect("register");
let workspace = tempfile::tempdir().expect("workspace tempdir");
workspace_with_origin(workspace.path(), "https://github.com/acme/widget.git");
let vars = resolve_gh_account_env_for_registry(®istry, workspace.path()).await;
assert!(vars.is_empty(), "vars: {vars:?}");
}