use std::path::Path;
use std::process::{Command, Output};
use tempfile::TempDir;
fn aristo_bin() -> &'static str {
env!("CARGO_BIN_EXE_aristo")
}
fn run(home: &Path, cwd: &Path, args: &[&str]) -> Output {
let mut c = Command::new(aristo_bin());
c.env_clear();
if let Ok(p) = std::env::var("PATH") {
c.env("PATH", p);
}
#[cfg(target_os = "macos")]
if let Ok(p) = std::env::var("DYLD_FALLBACK_LIBRARY_PATH") {
c.env("DYLD_FALLBACK_LIBRARY_PATH", p);
}
c.env("HOME", home);
c.env("XDG_CONFIG_HOME", home.join("xdg"));
c.env("ARISTO_NO_BROWSER", "1");
c.current_dir(cwd);
c.args(args);
c.output().expect("run aristo")
}
fn stdout(out: &Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
fn git_workspace(parent: &Path, name: &str, owner_repo: &str) -> std::path::PathBuf {
let ws = parent.join(name);
let git = ws.join(".git");
std::fs::create_dir_all(&git).unwrap();
std::fs::write(
git.join("config"),
format!("[remote \"origin\"]\n url = https://github.com/{owner_repo}\n"),
)
.unwrap();
ws
}
fn seed(home: &Path, entries: &[(&str, &str, &str)]) {
let p = home.join("xdg/aristo/credentials");
std::fs::create_dir_all(p.parent().unwrap()).unwrap();
let mut body = String::from("version = 2\n");
for (i, (server, repo, token)) in entries.iter().enumerate() {
body.push_str(&format!(
"\n[[entries]]\nserver = \"{server}\"\nrepo = \"{repo}\"\ntoken = \"{token}\"\n\
minted_at = \"2026-09-15T00:0{i}:00Z\"\n"
));
}
std::fs::write(&p, body).unwrap();
}
fn fresh_home(tmp: &TempDir) -> std::path::PathBuf {
let home = tmp.path().join("home");
std::fs::create_dir_all(&home).unwrap();
home
}
fn plain_dir(tmp: &TempDir, name: &str) -> std::path::PathBuf {
let d = tmp.path().join(name);
std::fs::create_dir_all(&d).unwrap();
d
}
#[test]
fn two_repos_by_flag_resolve_independently() {
let tmp = TempDir::new().unwrap();
let home = fresh_home(&tmp);
let cwd = plain_dir(&tmp, "work");
seed(
&home,
&[
("https://a.example.com", "org/repoA", "tok_A"),
("https://b.example.com", "org/repoB", "tok_B"),
],
);
let ta = run(&home, &cwd, &["auth", "token", "--repo", "org/repoA"]);
assert_eq!(stdout(&ta).trim(), "tok_A");
let tb = run(&home, &cwd, &["auth", "token", "--repo", "org/repoB"]);
assert_eq!(stdout(&tb).trim(), "tok_B");
let st = run(&home, &cwd, &["auth", "status"]);
let s = stdout(&st);
assert!(s.contains("org/repoA"), "status: {s}");
assert!(s.contains("org/repoB"), "status: {s}");
assert!(s.contains("a.example.com"), "status: {s}");
assert!(s.contains("b.example.com"), "status: {s}");
assert!(
!s.contains("tok_A") && !s.contains("tok_B"),
"status leaked a token: {s}"
);
}
#[test]
fn auth_token_resolves_by_cwd_repo() {
let tmp = TempDir::new().unwrap();
let home = fresh_home(&tmp);
let ws_a = git_workspace(tmp.path(), "a", "org/repoA");
let ws_b = git_workspace(tmp.path(), "b", "org/repoB");
seed(
&home,
&[
("https://code.aretta.ai", "org/repoA", "tok_A"),
("https://code.aretta.ai", "org/repoB", "tok_B"),
],
);
assert_eq!(
stdout(&run(&home, &ws_a, &["auth", "token"])).trim(),
"tok_A"
);
assert_eq!(
stdout(&run(&home, &ws_b, &["auth", "token"])).trim(),
"tok_B"
);
let plain = plain_dir(&tmp, "plain");
let miss = run(&home, &plain, &["auth", "token"]);
assert!(!miss.status.success());
let err = String::from_utf8_lossy(&miss.stderr);
assert!(err.contains("--repo"), "{err}");
}
#[test]
fn logout_repo_scoped_removes_only_that_entry() {
let tmp = TempDir::new().unwrap();
let home = fresh_home(&tmp);
let cwd = plain_dir(&tmp, "work");
seed(
&home,
&[
("https://code.aretta.ai", "org/repoA", "tok_A"),
("https://code.aretta.ai", "org/repoB", "tok_B"),
],
);
let out = run(&home, &cwd, &["auth", "logout", "--repo", "org/repoA"]);
assert!(out.status.success(), "{}", stdout(&out));
assert!(stdout(&out).contains("logged out"), "{}", stdout(&out));
let st = stdout(&run(&home, &cwd, &["auth", "status"]));
assert!(st.contains("org/repoB"), "status: {st}");
assert!(!st.contains("org/repoA"), "repoA should be gone: {st}");
assert_eq!(
stdout(&run(&home, &cwd, &["auth", "token", "--repo", "org/repoB"])).trim(),
"tok_B"
);
let miss = run(&home, &cwd, &["auth", "token", "--repo", "org/repoA"]);
assert!(
!miss.status.success(),
"repoA token should error after logout"
);
}
#[test]
fn logout_outside_a_checkout_needs_repo_or_all() {
let tmp = TempDir::new().unwrap();
let home = fresh_home(&tmp);
let cwd = plain_dir(&tmp, "work");
seed(&home, &[("https://code.aretta.ai", "org/repoA", "tok_A")]);
let out = run(&home, &cwd, &["auth", "logout"]);
assert_eq!(out.status.code(), Some(2));
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("--repo") && err.contains("--all"), "{err}");
assert!(stdout(&run(&home, &cwd, &["auth", "status"])).contains("org/repoA"));
}
#[test]
fn logout_all_clears_every_entry() {
let tmp = TempDir::new().unwrap();
let home = fresh_home(&tmp);
let cwd = plain_dir(&tmp, "work");
seed(
&home,
&[
("https://code.aretta.ai", "org/repoA", "tok_A"),
("https://code.aretta.ai", "org/repoB", "tok_B"),
],
);
let out = run(&home, &cwd, &["auth", "logout", "--all"]);
assert!(out.status.success());
assert!(stdout(&out).contains("logged out"), "{}", stdout(&out));
let st = stdout(&run(&home, &cwd, &["auth", "status"]));
assert!(st.contains("not signed in"), "status: {st}");
}
#[test]
fn status_verdict_follows_the_checkout() {
let tmp = TempDir::new().unwrap();
let home = fresh_home(&tmp);
let ws_a = git_workspace(tmp.path(), "a", "org/repoA");
let ws_fork = git_workspace(tmp.path(), "fork", "alice/repoB");
seed(
&home,
&[
("https://code.aretta.ai", "org/repoA", "tok_A"),
("https://code.aretta.ai", "org/repoB", "tok_B"),
],
);
let st_a = stdout(&run(&home, &ws_a, &["auth", "status"]));
assert!(
st_a.contains(
"this checkout (org/repoA) resolves to: server https://code.aretta.ai, repo org/repoA"
),
"status: {st_a}"
);
let st = stdout(&run(&home, &ws_fork, &["auth", "status"]));
assert!(
st.contains("this checkout (alice/repoB) resolves to: no stored credential"),
"status: {st}"
);
assert!(
st.contains("aristo auth login --server https://<org>.aretta.ai --repo alice/repoB"),
"status: {st}"
);
}
#[test]
fn status_outside_a_github_checkout_says_so_and_resolves_nothing() {
let tmp = TempDir::new().unwrap();
let home = fresh_home(&tmp);
let plain = plain_dir(&tmp, "plain");
seed(&home, &[("https://code.aretta.ai", "org/repoA", "tok_A")]);
let st = stdout(&run(&home, &plain, &["auth", "status"]));
assert!(st.contains("not a GitHub checkout"), "status: {st}");
assert!(st.contains("no .git/config"), "status: {st}");
assert!(
st.contains("resolves to: no stored credential (1 on file"),
"no single-entry fallback: {st}"
);
}