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_with, resolve_gh_account_env_with,
};
use crate::core::gh_account_dir::gh_account_dir_tests::{TableCheck, TableProbe, migrated_dir};
use crate::core::gh_org_accounts::{OrgAccounts, OrgAccountsError};
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()
}
async fn for_registry(registry: &ProjectRegistry, cwd: &Path) -> Vec<(String, String)> {
resolve_gh_account_env_for_registry_with(registry, cwd, || Ok(OrgAccounts::default())).await
}
async fn agreed_pin(registry: &ProjectRegistry, origin: &str) -> Option<super::PinnedGhIdentity> {
find_pinned_gh_identity(registry, origin)
.await
.expect("the registry reads and its pins agree")
}
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 = agreed_pin(®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 = agreed_pin(®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 = agreed_pin(®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 = agreed_pin(®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 = agreed_pin(®istry, "https://github.com/acme/widget").await;
assert_eq!(found, None);
}
#[tokio::test]
async fn find_pinned_gh_identity_skips_an_unpinned_duplicate() {
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
for i in 0..16 {
registry
.register(project(
&format!("widget-{i:02}"),
"https://github.com/acme/widget.git",
None,
))
.await
.expect("register");
}
registry
.register(project(
"widget",
"https://github.com/acme/widget",
Some("bobmatnyc"),
))
.await
.expect("register");
let found = agreed_pin(®istry, "https://github.com/acme/widget")
.await
.expect("the pinned record must be found past its unpinned duplicates");
assert_eq!(found.account.as_deref(), Some("bobmatnyc"));
}
#[tokio::test]
async fn find_pinned_gh_identity_prefers_the_config_dir_pin_for_one_login() {
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
let config_dir = PathBuf::from("/home/bob/.config/gh-bob-duetto");
registry
.register(project(
"jev",
"https://github.com/acme/widget",
Some("bob-duetto"),
))
.await
.expect("register");
registry
.register(Project {
gh_account: Some("bob-duetto".to_string()),
..project_with_config_dir(
"jev-matching",
"https://github.com/acme/widget",
&config_dir,
)
})
.await
.expect("register");
let found = agreed_pin(®istry, "https://github.com/acme/widget")
.await
.expect("one login must resolve to a pin");
assert_eq!(found.account.as_deref(), Some("bob-duetto"));
assert_eq!(found.config_dir.as_deref(), Some(config_dir.as_path()));
}
#[tokio::test]
async fn find_pinned_gh_identity_inherits_the_login_for_a_no_login_config_dir_pin() {
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
let config_dir = PathBuf::from("/home/bob/.config/gh-bob-duetto");
registry
.register(Project {
gh_account: None,
..project_with_config_dir("jev", "https://github.com/acme/widget", &config_dir)
})
.await
.expect("register");
registry
.register(Project {
gh_account: Some("bob-duetto".to_string()),
github: Some(GithubConfig {
config_dir: Some(config_dir.clone()),
account: Some("bob-duetto".to_string()),
..GithubConfig::default()
}),
..project("widget", "https://github.com/acme/widget", None)
})
.await
.expect("register");
let found = agreed_pin(®istry, "https://github.com/acme/widget")
.await
.expect("a missing login is not a disagreement");
assert_eq!(found.account.as_deref(), Some("bob-duetto"));
assert_eq!(found.config_dir.as_deref(), Some(config_dir.as_path()));
}
#[tokio::test]
async fn find_pinned_gh_identity_refuses_disagreeing_pins() {
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
for (name, account) in [("widget-a", "bobmatnyc"), ("widget-b", "bob-duetto")] {
registry
.register(project(
name,
"https://github.com/acme/widget",
Some(account),
))
.await
.expect("register");
}
let found = find_pinned_gh_identity(®istry, "https://github.com/acme/widget").await;
let reason = found.expect_err("disagreeing pins must not be resolved by position");
assert!(reason.contains("different gh accounts"), "{reason}");
}
#[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 = for_registry(®istry, workspace.path()).await;
assert!(vars.is_empty(), "vars: {vars:?}");
}
#[tokio::test]
async fn a_local_only_repo_spawns_with_gh_disabled() {
use crate::core::remote_mode::{LOCAL_ONLY_GH_CONFIG_DIR, LOCAL_ONLY_GH_TOKEN};
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
let workspace = tempfile::tempdir().expect("workspace tempdir");
let init = std::process::Command::new("git")
.args(["init", "-q"])
.arg(workspace.path())
.output()
.expect("git init");
assert!(init.status.success(), "git init failed");
let vars = for_registry(®istry, workspace.path()).await;
assert_eq!(value_of(&vars, GH_CONFIG_DIR), LOCAL_ONLY_GH_CONFIG_DIR);
assert_eq!(value_of(&vars, "GH_TOKEN"), LOCAL_ONLY_GH_TOKEN);
assert_eq!(value_of(&vars, "GH_ENTERPRISE_TOKEN"), LOCAL_ONLY_GH_TOKEN);
assert_eq!(value_of(&vars, "GIT_TERMINAL_PROMPT"), "0");
let unset = crate::core::gh_identity::inherited_identity_to_clear(&vars);
assert!(unset.iter().any(|k| k == "GITHUB_TOKEN"), "{unset:?}");
}
#[cfg(unix)]
#[tokio::test]
async fn an_unreadable_origin_spawns_with_the_nobody_token() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
let workspace = tempfile::tempdir().expect("workspace tempdir");
workspace_with_origin(workspace.path(), "https://github.com/acme-8934/widget.git");
let git_dir = workspace.path().join(".git");
std::fs::set_permissions(&git_dir, std::fs::Permissions::from_mode(0o000)).expect("chmod");
let vars = for_registry(®istry, workspace.path()).await;
std::fs::set_permissions(&git_dir, std::fs::Permissions::from_mode(0o755)).expect("restore");
for var in [GH_TOKEN_ENV_VAR, "GH_ENTERPRISE_TOKEN"] {
assert_eq!(value_of(&vars, var), super::REFUSED_GH_TOKEN, "{var}");
}
}
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 = 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_eq!(value_of(&vars, GH_TOKEN_ENV_VAR), super::REFUSED_GH_TOKEN);
}
#[tokio::test]
async fn disagreeing_pins_spawn_with_the_nobody_token() {
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
for (name, account) in [("widget-a", "octo-a"), ("widget-b", "octo-b")] {
let origin = "https://github.com/acme-8914/widget";
registry
.register(project(name, origin, Some(account)))
.await
.expect("register");
}
let workspace = tempfile::tempdir().expect("workspace tempdir");
workspace_with_origin(workspace.path(), "https://github.com/acme-8914/widget.git");
let vars = for_registry(®istry, workspace.path()).await;
for var in [GH_TOKEN_ENV_VAR, "GH_ENTERPRISE_TOKEN"] {
assert_eq!(value_of(&vars, var), super::REFUSED_GH_TOKEN, "{var}");
}
}
#[tokio::test]
async fn an_unreadable_registry_spawns_with_the_nobody_token() {
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
let origin = "https://github.com/acme-8914/widget";
registry
.register(project("widget", origin, Some("octo-a")))
.await
.expect("register");
std::fs::write(dir.path().join("projects.json"), "{ not json").expect("corrupt");
let workspace = tempfile::tempdir().expect("workspace tempdir");
workspace_with_origin(workspace.path(), "https://github.com/acme-8914/widget.git");
let vars = for_registry(®istry, workspace.path()).await;
assert_eq!(value_of(&vars, GH_TOKEN_ENV_VAR), super::REFUSED_GH_TOKEN);
}
#[tokio::test]
async fn an_operator_config_dir_pin_never_leaves_the_session_on_the_active_account() {
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(), "octo-8914-op");
let origin = "https://github.com/acme-8914/widget";
registry
.register(Project {
gh_account: Some("octo-8914-op".into()),
..project_with_config_dir("widget", origin, gh_home.path())
})
.await
.expect("register");
let workspace = tempfile::tempdir().expect("workspace tempdir");
workspace_with_origin(workspace.path(), "https://github.com/acme-8914/widget.git");
let vars = for_registry(®istry, workspace.path()).await;
assert_eq!(
value_of(&vars, GH_TOKEN_ENV_VAR),
super::REFUSED_GH_TOKEN,
"a tokenless config dir resolves to the keyring's active account: {vars:?}"
);
assert_eq!(
value_of(&vars, GH_CONFIG_DIR),
gh_home.path().to_string_lossy()
);
}
#[tokio::test]
async fn a_tm_account_dir_pin_never_leaves_the_session_on_the_active_account() {
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
let account_dir = crate::core::paths::FrameworkPaths::default()
.root
.join("gh-accounts")
.join("octo-8914-test");
registry
.register(Project {
gh_account: Some("octo-8914-test".into()),
..project_with_config_dir(
"widget",
"https://github.com/acme-8914/widget",
&account_dir,
)
})
.await
.expect("register");
let workspace = tempfile::tempdir().expect("workspace tempdir");
workspace_with_origin(workspace.path(), "https://github.com/acme-8914/widget.git");
let vars = for_registry(®istry, workspace.path()).await;
assert_eq!(
value_of(&vars, GH_TOKEN_ENV_VAR),
super::REFUSED_GH_TOKEN,
"a bare tm account dir resolves to the keyring's active account: {vars:?}"
);
assert_eq!(
value_of(&vars, GH_CONFIG_DIR),
account_dir.to_string_lossy()
);
}
#[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 = for_registry(®istry, workspace.path()).await;
assert!(vars.is_empty(), "vars: {vars:?}");
}
#[tokio::test]
async fn an_org_mapped_origin_spawns_as_the_mapped_account() {
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
let workspace = tempfile::tempdir().expect("workspace tempdir");
workspace_with_origin(workspace.path(), "https://github.com/Acme-9091/widget.git");
let raw = "[accounts]\nacme-9091 = \"octo-9091-mapped\"\n";
let load = || OrgAccounts::from_toml(raw, Path::new("/home/u/.trusty-mpm/config.toml"));
let vars = resolve_gh_account_env_for_registry_with(®istry, workspace.path(), load).await;
assert_eq!(value_of(&vars, GH_USER_ENV_VAR), "octo-9091-mapped");
assert_eq!(value_of(&vars, GH_TOKEN_ENV_VAR), super::REFUSED_GH_TOKEN);
}
#[tokio::test]
async fn a_broken_accounts_table_spawns_with_the_nobody_token() {
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
let workspace = tempfile::tempdir().expect("workspace tempdir");
workspace_with_origin(workspace.path(), "https://github.com/acme-9091/widget.git");
let load = || -> Result<OrgAccounts, OrgAccountsError> {
OrgAccounts::from_toml(
"[accounts]\nacme-9091 = octo\n",
Path::new("/home/u/.trusty-mpm/config.toml"),
)
};
let vars = resolve_gh_account_env_for_registry_with(®istry, workspace.path(), load).await;
for var in [GH_TOKEN_ENV_VAR, "GH_ENTERPRISE_TOKEN"] {
assert_eq!(value_of(&vars, var), super::REFUSED_GH_TOKEN, "{var}");
}
}
#[tokio::test]
async fn a_github_account_only_pin_spawns_as_that_login_without_the_map() {
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
let origin = "https://github.com/acme-9091/widget";
registry
.register(Project {
github: Some(GithubConfig {
account: Some("octo-9091-gh".into()),
..Default::default()
}),
..project("widget", origin, None)
})
.await
.expect("register");
let workspace = tempfile::tempdir().expect("workspace tempdir");
workspace_with_origin(workspace.path(), "https://github.com/acme-9091/widget.git");
let vars = resolve_gh_account_env_for_registry_with(®istry, workspace.path(), || {
panic!("a pinned origin must not read the [accounts] table")
})
.await;
assert_eq!(value_of(&vars, GH_USER_ENV_VAR), "octo-9091-gh");
assert_eq!(value_of(&vars, GH_TOKEN_ENV_VAR), super::REFUSED_GH_TOKEN);
}
#[tokio::test]
async fn a_token_env_only_pin_spawns_as_before_without_the_map() {
for config_dir in [None, Some(PathBuf::from(" "))] {
let dir = tempfile::tempdir().expect("tempdir");
let registry = ProjectRegistry::load(dir.path()).await.expect("load");
let origin = "https://github.com/acme-9091/widget";
registry
.register(Project {
github: Some(GithubConfig {
token_env: Some("TM_9091_TOKEN".into()),
config_dir: config_dir.clone(),
..Default::default()
}),
..project("widget", origin, None)
})
.await
.expect("register");
let workspace = tempfile::tempdir().expect("workspace tempdir");
workspace_with_origin(workspace.path(), "https://github.com/acme-9091/widget.git");
let vars = resolve_gh_account_env_for_registry_with(®istry, workspace.path(), || {
panic!("a pinned origin must not read the [accounts] table")
})
.await;
assert!(vars.is_empty(), "{config_dir:?}: {vars:?}");
}
}
const SPAWN_ORIGIN: &str = "https://github.com/duettoresearch/jev-matching";
const GHES_ORIGIN: &str = "https://ghe.corp/duettoresearch/jev-matching";
fn account_only_pin() -> super::PinnedGhIdentity {
super::PinnedGhIdentity {
account: Some("octo-pinned".into()),
..Default::default()
}
}
fn spawn_with(
origin: &str,
dir: &Path,
probe: &TableProbe,
check: &TableCheck,
) -> super::GhSpawnEnv {
let sources = crate::core::gh_account_dir::AccountDirSources {
own_config_dir: Some(dir.to_path_buf()),
..Default::default()
};
let prover = crate::core::gh_account_proof::AccountProver {
sources: &sources,
probe,
check,
cache: None,
};
let aliases = crate::session_manager::ssh_host_alias::SshHostAliases::empty();
super::pinned_spawn_env(&account_only_pin(), origin, |login| {
super::spawn_proof(&prover, login, origin, &aliases)
})
.expect("a pin must produce an env")
.expect("the spawn env never errs")
}
fn github_spawn(who: Result<&str, &str>) -> super::GhSpawnEnv {
let root = tempfile::tempdir().expect("tempdir");
let dir = migrated_dir(root.path());
let probe = TableProbe::default().answer(&dir, "github.com", "octo-pinned", Ok("tok-bob"));
let check = TableCheck::default().answer("https://api.github.com", "tok-bob", who);
spawn_with(SPAWN_ORIGIN, &dir, &probe, &check)
}
#[test]
fn an_account_only_spawn_pin_gets_the_proven_token() {
let env = github_spawn(Ok("octo-pinned"));
assert_eq!(value_of(&env.vars, "GH_TOKEN"), "tok-bob");
assert_eq!(
value_of(&env.vars, "GH_ENTERPRISE_TOKEN"),
super::REFUSED_GH_TOKEN
);
assert_eq!(value_of(&env.vars, "GH_USER"), "octo-pinned");
assert!(
!env.vars.iter().any(|(k, _)| k == GH_CONFIG_DIR),
"{:?}",
env.vars
);
assert!(env.warning.is_none(), "{:?}", env.warning);
}
#[test]
fn an_account_only_spawn_pin_with_no_proven_token_fails_closed() {
let env = super::pinned_spawn_env(&account_only_pin(), SPAWN_ORIGIN, |_| {
Err("/x: the token gh returned for 'octo-pinned' authenticates as 'octo-other'".into())
})
.expect("a pinned account must never resolve to no identity")
.expect("the refusal rides the env, not an error");
for var in ["GH_TOKEN", "GH_ENTERPRISE_TOKEN"] {
assert_eq!(value_of(&env.vars, var), super::REFUSED_GH_TOKEN, "{var}");
}
let warning = env.warning.expect("the refusal must be logged");
assert!(
warning.contains("authenticates as 'octo-other'")
&& warning.contains(&format!(
"tm projects register <name> --repo-url {SPAWN_ORIGIN} --gh-account octo-pinned \
--gh-config-dir <dir>"
)),
"got: {warning}"
);
}
#[test]
fn a_config_dir_spawn_pin_is_proven_not_used_bare() {
let dir = tempfile::tempdir().expect("tempdir");
write_hosts_yml(dir.path(), "octo-pinned");
let pinned = super::PinnedGhIdentity {
account: Some("octo-pinned".into()),
config_dir: Some(dir.path().to_path_buf()),
..Default::default()
};
let env = super::pinned_spawn_env(&pinned, SPAWN_ORIGIN, |login| {
assert_eq!(login, "octo-pinned");
Err("not proven".into())
})
.expect("a pin must produce an env")
.expect("the spawn env never errs");
assert_eq!(
value_of(&env.vars, GH_TOKEN_ENV_VAR),
super::REFUSED_GH_TOKEN
);
}
#[test]
fn a_spawn_pin_refuses_a_token_for_another_account() {
let env = github_spawn(Ok("octo-other"));
assert_eq!(value_of(&env.vars, "GH_TOKEN"), super::REFUSED_GH_TOKEN);
let warning = env.warning.expect("the refusal must be logged");
assert!(
warning.contains("authenticates as 'octo-other'") && !warning.contains("tok-"),
"got: {warning}"
);
}
#[test]
fn a_spawn_pin_refuses_when_the_user_check_fails() {
let env = github_spawn(Err(
"GET https://api.github.com/user did not answer in time",
));
assert_eq!(value_of(&env.vars, "GH_TOKEN"), super::REFUSED_GH_TOKEN);
let warning = env.warning.expect("the refusal must be logged");
assert!(warning.contains("did not answer in time"), "got: {warning}");
}
#[test]
fn a_ghes_spawn_pin_puts_the_token_in_gh_enterprise_token() {
let root = tempfile::tempdir().expect("tempdir");
let dir = migrated_dir(root.path());
let probe = TableProbe::default().answer(&dir, "ghe.corp", "octo-pinned", Ok("tok-ghe"));
let check =
TableCheck::default().answer("https://ghe.corp/api/v3", "tok-ghe", Ok("octo-pinned"));
let env = spawn_with(GHES_ORIGIN, &dir, &probe, &check);
assert_eq!(value_of(&env.vars, "GH_ENTERPRISE_TOKEN"), "tok-ghe");
assert_eq!(value_of(&env.vars, "GH_TOKEN"), super::REFUSED_GH_TOKEN);
assert!(env.warning.is_none(), "{:?}", env.warning);
}
#[test]
fn a_ghes_spawn_pin_refusal_blanks_both_token_vars() {
let root = tempfile::tempdir().expect("tempdir");
let dir = migrated_dir(root.path());
let probe = TableProbe::default().answer(&dir, "ghe.corp", "octo-pinned", Ok("tok-ghe"));
let check = TableCheck::default().answer("https://ghe.corp/api/v3", "tok-ghe", Ok("other"));
let env = spawn_with(GHES_ORIGIN, &dir, &probe, &check);
for var in ["GH_TOKEN", "GH_ENTERPRISE_TOKEN"] {
assert_eq!(value_of(&env.vars, var), super::REFUSED_GH_TOKEN, "{var}");
}
}
#[tokio::test]
async fn a_panicked_spawn_env_task_fails_closed() {
let joined = tokio::task::spawn_blocking(|| -> Vec<(String, String)> {
panic!("identity task panicked")
})
.await;
let vars = super::joined_spawn_vars(
joined,
&account_only_pin(),
SPAWN_ORIGIN,
Path::new("/work/jev-matching"),
);
for var in ["GH_TOKEN", "GH_ENTERPRISE_TOKEN"] {
assert_eq!(value_of(&vars, var), super::REFUSED_GH_TOKEN, "{var}");
}
assert_eq!(value_of(&vars, "GH_USER"), "octo-pinned");
}
#[derive(Clone, Default)]
struct LogBuffer(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
impl std::io::Write for LogBuffer {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn logged_spawn(who: Result<&str, &str>) -> (Vec<(String, String)>, String) {
let buffer = LogBuffer::default();
let writer = buffer.clone();
let subscriber = tracing_subscriber::fmt()
.with_writer(move || writer.clone())
.with_ansi(false)
.with_max_level(tracing::Level::TRACE)
.finish();
let vars = tracing::subscriber::with_default(subscriber, || {
super::log_spawn_env(Some(Ok(github_spawn(who))), Path::new("/work/jev-matching"))
});
let bytes = buffer
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
(vars, String::from_utf8(bytes).expect("utf-8 log"))
}
#[test]
fn spawn_env_debug_redacts_every_token() {
let env = github_spawn(Ok("octo-pinned"));
assert_eq!(value_of(&env.vars, "GH_TOKEN"), "tok-bob");
let shown = format!("{env:?}");
assert!(!shown.contains("tok-bob"), "shown: {shown}");
assert!(
shown.contains("GH_USER") && shown.contains("octo-pinned"),
"shown: {shown}"
);
}
#[test]
fn a_spawn_proves_an_ssh_aliased_origin_on_the_daemons_host() {
let aliased = "git@github-duetto:duettoresearch/jev-matching.git";
let aliases = crate::session_manager::ssh_host_alias::SshHostAliases::parse(
"Host github-duetto\n HostName github.com\n",
);
let daemon_slug =
crate::session_manager::worktree_repo_slug::parse_repo_slug(aliased, &aliases)
.expect("the daemon resolves the alias");
assert_eq!(daemon_slug, "duettoresearch/jev-matching");
let root = tempfile::tempdir().expect("tempdir");
let dir = migrated_dir(root.path());
let probe = TableProbe::default().answer(&dir, "github.com", "octo-pinned", Ok("tok-bob"));
let check =
TableCheck::default().answer("https://api.github.com", "tok-bob", Ok("octo-pinned"));
let sources = crate::core::gh_account_dir::AccountDirSources {
own_config_dir: Some(dir.clone()),
..Default::default()
};
let prover = crate::core::gh_account_proof::AccountProver {
sources: &sources,
probe: &probe,
check: &check,
cache: None,
};
let proven = super::spawn_proof(&prover, "octo-pinned", aliased, &aliases)
.expect("the aliased origin proves on github.com");
assert_eq!(proven.host, "github.com");
let unresolved = crate::session_manager::ssh_host_alias::SshHostAliases::empty();
let err = super::spawn_proof(&prover, "octo-pinned", aliased, &unresolved)
.expect_err("an alias nothing renames names no host");
assert!(err.contains("'github-duetto'"), "got: {err}");
assert_eq!(probe.calls().len(), 1, "the refusal never runs gh");
}
#[test]
fn a_spawn_pin_logs_no_token_in_any_arm() {
let (vars, log) = logged_spawn(Ok("octo-pinned"));
assert_eq!(value_of(&vars, "GH_TOKEN"), "tok-bob");
assert!(!log.contains("tok-"), "a token reached the log: {log}");
for who in [
Ok("octo-other"),
Err("GET https://api.github.com/user answered HTTP 401"),
] {
let (vars, log) = logged_spawn(who);
assert_eq!(value_of(&vars, "GH_TOKEN"), super::REFUSED_GH_TOKEN);
assert!(
log.contains("pinned to gh account 'octo-pinned'"),
"the refusal must be logged: {log}"
);
assert!(!log.contains("tok-"), "a token reached the log: {log}");
}
}
#[test]
fn proof_origin_refusals_never_quote_the_origin_token() {
const TOKEN: &str = "ghp_9124ProofOriginSyntheticToken0";
let aliases = crate::session_manager::ssh_host_alias::SshHostAliases::empty();
for origin in [
format!("https://octo:{TOKEN}@github.com/"),
format!("ssh://octo:{TOKEN}@github-alias/acme/widget.git"),
] {
let err = crate::session_manager::worktree_reclaim_gh::proof_origin(&origin, &aliases)
.expect_err("the origin names no provable host");
assert!(!err.contains(TOKEN), "the token reached the refusal");
assert!(err.contains("://***@"), "{err}");
}
}