use super::*;
#[test]
fn account_clone_env_shadows_inherited_identity_and_sets_gh_token() {
const CLONE_URL: &str = "https://github.com/duettoresearch/poc-hotel-supply";
let env = account_clone_env_with(
"bob-duetto",
None,
|account| {
assert_eq!(account, "bob-duetto");
Ok("minted-token".to_string())
},
|token| {
assert_eq!(token, "minted-token");
Ok("bob-duetto".to_string())
},
|_, _| panic!("verify_scoped_login must not run when config_dir is None"),
)
.expect("resolver and verification both succeeded");
assert_eq!(
env.remove,
clone_env_removal_set(),
"must shadow the shared inherited-identity set PLUS the clone-local GH_HOST"
);
assert_eq!(
env.set,
vec![
("GH_TOKEN".to_string(), "minted-token".to_string()),
("GH_USER".to_string(), "bob-duetto".to_string()),
]
);
let base_path = std::path::Path::new("/tmp/does-not-need-to-exist/base");
let mut cmd = std::process::Command::new("git");
cmd.args(["clone", "--no-local", CLONE_URL])
.arg(base_path)
.env("GH_TOKEN", "someone-elses-ambient-token")
.env("GITHUB_TOKEN", "another-ambient-token")
.env("GH_HOST", "ghe.example.com");
env.apply(&mut cmd);
let envs: Vec<_> = cmd.get_envs().collect();
assert!(
envs.contains(&(
std::ffi::OsStr::new("GH_TOKEN"),
Some(std::ffi::OsStr::new("minted-token"))
)),
"GH_TOKEN must be the minted token, not the ambient one: {envs:?}"
);
assert!(
envs.contains(&(std::ffi::OsStr::new("GITHUB_TOKEN"), None)),
"the ambient GITHUB_TOKEN must be shadowed (removed): {envs:?}"
);
assert!(
envs.contains(&(std::ffi::OsStr::new("GH_HOST"), None)),
"the ambient GH_HOST must be shadowed (removed) — the clone URL is hardcoded to \
github.com: {envs:?}"
);
let argv: Vec<String> = cmd
.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect();
assert_eq!(
argv,
vec![
"clone",
"--no-local",
CLONE_URL,
base_path.to_str().unwrap()
],
"argv must be exactly the plain clone invocation, no credential appended"
);
assert!(
argv.iter().all(|a| !a.contains("minted-token")),
"the token must never appear in argv: {argv:?}"
);
assert!(
!CLONE_URL.contains("minted-token"),
"the token must never appear in the clone URL"
);
}
#[test]
fn account_clone_env_propagates_a_resolver_failure_naming_the_account() {
let err = account_clone_env_with(
"not-logged-in",
None,
|account| {
Err(format!(
"`gh auth token -u {account}` failed: not logged in"
))
},
|_| panic!("verify_login must not run when token resolution already failed"),
|_, _| panic!("verify_scoped_login must not run when config_dir is None"),
)
.expect_err("a resolver failure must propagate");
assert!(err.contains("not-logged-in"), "{err}");
}
#[test]
fn account_clone_env_refuses_a_token_minted_for_a_different_account() {
let err = account_clone_env_with(
"bob-duetto",
None,
|_| Ok("token-for-the-wrong-account".to_string()),
|token| {
assert_eq!(token, "token-for-the-wrong-account");
Ok("bobmatnyc".to_string())
},
|_, _| panic!("verify_scoped_login must not run when config_dir is None"),
)
.expect_err("a login mismatch must refuse");
assert!(err.contains("bob-duetto"), "{err}");
assert!(err.contains("bobmatnyc"), "{err}");
}
#[test]
fn account_clone_env_propagates_a_verification_failure() {
let err = account_clone_env_with(
"bob-duetto",
None,
|_| Ok("some-token".to_string()),
|_| Err("gh api user failed: network error".to_string()),
|_, _| panic!("verify_scoped_login must not run when config_dir is None"),
)
.expect_err("a verification failure must propagate, not be swallowed");
assert!(err.contains("bob-duetto"), "{err}");
}
#[test]
fn account_clone_env_accepts_a_token_that_matches() {
let env = account_clone_env_with(
"bob-duetto",
None,
|_| Ok("minted-token".to_string()),
|_| Ok("Bob-Duetto".to_string()),
|_, _| panic!("verify_scoped_login must not run when config_dir is None"),
)
.expect("a matching (case-insensitively) login must proceed");
assert_eq!(
env.set,
vec![
("GH_TOKEN".to_string(), "minted-token".to_string()),
("GH_USER".to_string(), "bob-duetto".to_string()),
]
);
}
#[test]
fn account_clone_env_uses_a_config_dir_and_verifies_it_scoped() {
let dir = std::path::PathBuf::from("/tmp/tm-state/gh-accounts/bob-duetto");
let env = account_clone_env_with(
"bob-duetto",
Some(&dir),
|_| panic!("resolve_token (the -u fallback) must not run when config_dir is Some"),
|_| panic!("verify_login (bare-token) must not run for the config_dir arm"),
|scoped_dir, host| {
assert_eq!(scoped_dir, dir.to_string_lossy());
assert_eq!(host, crate::core::trusty_tools_config::DEFAULT_GITHUB_HOST);
Ok("bob-duetto".to_string())
},
)
.expect("a matching scoped identity must proceed");
assert!(
env.set
.iter()
.any(|(k, v)| k == "GH_CONFIG_DIR" && v == &dir.to_string_lossy()),
"GH_CONFIG_DIR must be set to the account dir: {:?}",
env.set
);
assert!(
env.set.iter().all(|(k, _)| k != "GH_TOKEN"),
"the config_dir arm must never also carry a bare GH_TOKEN: {:?}",
env.set
);
}
#[test]
fn account_clone_env_refuses_a_config_dir_scoped_to_a_different_login() {
let dir = std::path::PathBuf::from("/tmp/tm-state/gh-accounts/bob-duetto");
let err = account_clone_env_with(
"bob-duetto",
Some(&dir),
|_| panic!("resolve_token must not run for the config_dir arm"),
|_| panic!("verify_login must not run for the config_dir arm"),
|_, _| Ok("someone-else".to_string()),
)
.expect_err("a scoped-identity mismatch must refuse");
assert!(err.contains("bob-duetto"), "{err}");
assert!(err.contains("someone-else"), "{err}");
assert!(
!err.contains("gh auth switch"),
"the refusal must never suggest gh auth switch: {err}"
);
}
#[test]
fn verify_token_login_with_times_out_and_names_the_bound() {
let err = verify_token_login_with(std::time::Duration::from_millis(20), || {
std::thread::sleep(std::time::Duration::from_millis(400));
Ok("someone".to_string())
})
.expect_err("a stalled run must time out");
assert!(err.contains("timed out"), "{err}");
}
#[test]
fn verify_token_login_with_returns_the_inner_result_when_fast() {
let login = verify_token_login_with(std::time::Duration::from_secs(5), || {
Ok("bobmatnyc".to_string())
})
.expect("a fast run must succeed");
assert_eq!(login, "bobmatnyc");
}
#[test]
fn verify_token_login_with_propagates_a_fast_failure_untouched() {
let err = verify_token_login_with(std::time::Duration::from_secs(5), || {
Err("gh api user failed: 401".to_string())
})
.expect_err("a fast failure must propagate");
assert!(err.contains("401"), "{err}");
assert!(!err.contains("timed out"), "{err}");
}