use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use tempfile::TempDir;
fn run_setup_piped(dir: &TempDir, stdin_bytes: &[u8]) -> (std::process::Output, PathBuf) {
let config_path = dir.path().join("config.json");
let output = spawn_entangle_piped(&["setup"], &config_path, stdin_bytes);
(output, config_path)
}
fn spawn_entangle_piped(
args: &[&str],
config_path: &Path,
stdin_bytes: &[u8],
) -> std::process::Output {
let mut child = Command::new(env!("CARGO_BIN_EXE_entangle"))
.args(args)
.env("ENTANGLE_CONFIG_PATH", config_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("failed to spawn entangle");
child
.stdin
.take()
.unwrap()
.write_all(stdin_bytes)
.expect("failed to write stdin");
child.wait_with_output().expect("failed to wait for child")
}
fn read_config_json(path: &Path) -> serde_json::Value {
let content = std::fs::read_to_string(path)
.unwrap_or_else(|e| panic!("could not read config at {}: {e}", path.display()));
serde_json::from_str(&content)
.unwrap_or_else(|e| panic!("config not valid JSON: {e}\ncontent: {content}"))
}
fn write_config_json(path: &Path, github: &str, tangled: &str, origin: &str) {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
let json = serde_json::json!({
"github_username": github,
"tangled_username": tangled,
"origin_preference": origin,
});
std::fs::write(path, serde_json::to_string_pretty(&json).unwrap()).unwrap();
}
fn stderr_str(o: &std::process::Output) -> String {
String::from_utf8_lossy(&o.stderr).into_owned()
}
fn stdout_str(o: &std::process::Output) -> String {
String::from_utf8_lossy(&o.stdout).into_owned()
}
#[test]
fn no_pty_does_not_create_config() {
let dir = TempDir::new().unwrap();
let (output, config_path) = run_setup_piped(&dir, b"cyrusae\natdot.fyi\ngithub\n");
assert!(
!config_path.exists(),
"config must not be written when dialoguer cannot get a terminal\
\nstdout: {}\nstderr: {}",
stdout_str(&output),
stderr_str(&output)
);
}
#[test]
fn no_pty_leaves_existing_config_unchanged() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.json");
write_config_json(&config_path, "cyrusae", "atdot.fyi", "github");
let original = std::fs::read_to_string(&config_path).unwrap();
spawn_entangle_piped(&["setup"], &config_path, b"new-name\n");
let after = std::fs::read_to_string(&config_path).unwrap();
assert_eq!(original, after, "pre-existing config must not be changed");
}
#[cfg(unix)]
mod pty_tests {
use super::*;
use rexpect::session::spawn_command;
const TIMEOUT_MS: Option<u64> = Some(10_000);
fn pty_setup(config_path: &Path) -> rexpect::session::PtySession {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_entangle"));
cmd.arg("setup");
cmd.env("ENTANGLE_CONFIG_PATH", config_path);
spawn_command(cmd, TIMEOUT_MS).expect("failed to spawn PTY session")
}
#[test]
fn fresh_setup_writes_full_config() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.json");
let mut p = pty_setup(&config_path);
p.exp_string("GitHub username").unwrap();
p.send_line("cyrusae").unwrap();
p.exp_string("Tangled username").unwrap();
p.send_line("atdot.fyi").unwrap();
p.exp_string("Origin preference").unwrap();
p.send_line("github").unwrap();
p.exp_string("saved").unwrap();
p.exp_eof().unwrap();
assert!(config_path.exists(), "config file not created");
let cfg = read_config_json(&config_path);
assert_eq!(cfg["github_username"], "cyrusae");
assert_eq!(cfg["tangled_username"], "atdot.fyi");
assert_eq!(cfg["origin_preference"], "github");
}
#[test]
fn fresh_setup_with_tangled_origin() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.json");
let mut p = pty_setup(&config_path);
p.exp_string("GitHub username").unwrap();
p.send_line("cyrusae").unwrap();
p.exp_string("Tangled username").unwrap();
p.send_line("atdot.fyi").unwrap();
p.exp_string("Origin preference").unwrap();
p.send_line("tangled").unwrap();
p.exp_string("saved").unwrap();
p.exp_eof().unwrap();
assert_eq!(
read_config_json(&config_path)["origin_preference"],
"tangled"
);
}
#[test]
fn fresh_setup_gh_alias_accepted() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.json");
let mut p = pty_setup(&config_path);
p.exp_string("GitHub username").unwrap();
p.send_line("cyrusae").unwrap();
p.exp_string("Tangled username").unwrap();
p.send_line("atdot.fyi").unwrap();
p.exp_string("Origin preference").unwrap();
p.send_line("gh").unwrap();
p.exp_string("saved").unwrap();
p.exp_eof().unwrap();
assert_eq!(
read_config_json(&config_path)["origin_preference"],
"github"
);
}
#[test]
fn fresh_setup_tngl_alias_accepted() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.json");
let mut p = pty_setup(&config_path);
p.exp_string("GitHub username").unwrap();
p.send_line("cyrusae").unwrap();
p.exp_string("Tangled username").unwrap();
p.send_line("atdot.fyi").unwrap();
p.exp_string("Origin preference").unwrap();
p.send_line("tngl").unwrap();
p.exp_string("saved").unwrap();
p.exp_eof().unwrap();
assert_eq!(
read_config_json(&config_path)["origin_preference"],
"tangled"
);
}
#[test]
fn fresh_setup_sanitises_username() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.json");
let mut p = pty_setup(&config_path);
p.exp_string("GitHub username").unwrap();
p.send_line("\"CyrusAE\"").unwrap(); p.exp_string("Tangled username").unwrap();
p.send_line("atdot.fyi").unwrap();
p.exp_string("Origin preference").unwrap();
p.send_line("github").unwrap();
p.exp_string("saved").unwrap();
p.exp_eof().unwrap();
assert_eq!(read_config_json(&config_path)["github_username"], "cyrusae");
}
#[test]
fn invalid_github_username_triggers_reprompt() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.json");
let mut p = pty_setup(&config_path);
p.exp_string("GitHub username").unwrap();
p.send_line("-invalid").unwrap();
p.exp_string("GitHub username").unwrap();
p.send_line("cyrusae").unwrap();
p.exp_string("Tangled username").unwrap();
p.send_line("atdot.fyi").unwrap();
p.exp_string("Origin preference").unwrap();
p.send_line("github").unwrap();
p.exp_string("saved").unwrap();
p.exp_eof().unwrap();
assert_eq!(read_config_json(&config_path)["github_username"], "cyrusae");
}
#[test]
fn invalid_tangled_username_triggers_reprompt() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.json");
let mut p = pty_setup(&config_path);
p.exp_string("GitHub username").unwrap();
p.send_line("cyrusae").unwrap();
p.exp_string("Tangled username").unwrap();
p.send_line("nodot").unwrap(); p.exp_string("Tangled username").unwrap(); p.send_line("atdot.fyi").unwrap();
p.exp_string("Origin preference").unwrap();
p.send_line("github").unwrap();
p.exp_string("saved").unwrap();
p.exp_eof().unwrap();
assert_eq!(
read_config_json(&config_path)["tangled_username"],
"atdot.fyi"
);
}
#[test]
fn invalid_origin_triggers_reprompt() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.json");
let mut p = pty_setup(&config_path);
p.exp_string("GitHub username").unwrap();
p.send_line("cyrusae").unwrap();
p.exp_string("Tangled username").unwrap();
p.send_line("atdot.fyi").unwrap();
p.exp_string("Origin preference").unwrap();
p.send_line("gitlab").unwrap(); p.exp_string("Origin preference").unwrap(); p.send_line("github").unwrap();
p.exp_string("saved").unwrap();
p.exp_eof().unwrap();
assert_eq!(
read_config_json(&config_path)["origin_preference"],
"github"
);
}
#[test]
fn setup_with_existing_config_keep_all() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.json");
write_config_json(&config_path, "cyrusae", "atdot.fyi", "github");
let mut p = pty_setup(&config_path);
p.exp_string("Keep it?").unwrap();
p.send_line("").unwrap();
p.exp_string("Keep it?").unwrap();
p.send_line("").unwrap();
p.exp_string("Keep it?").unwrap();
p.send_line("").unwrap();
p.exp_string("saved").unwrap();
p.exp_eof().unwrap();
let cfg = read_config_json(&config_path);
assert_eq!(cfg["github_username"], "cyrusae");
assert_eq!(cfg["tangled_username"], "atdot.fyi");
assert_eq!(cfg["origin_preference"], "github");
}
#[test]
fn setup_with_existing_config_change_one_field() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.json");
write_config_json(&config_path, "old-name", "atdot.fyi", "github");
let mut p = pty_setup(&config_path);
p.exp_string("Keep it?").unwrap();
p.send_line("n").unwrap();
p.exp_string("GitHub username").unwrap();
p.send_line("cyrusae").unwrap();
p.exp_string("Keep it?").unwrap();
p.send_line("").unwrap();
p.exp_string("Keep it?").unwrap();
p.send_line("").unwrap();
p.exp_string("saved").unwrap();
p.exp_eof().unwrap();
let cfg = read_config_json(&config_path);
assert_eq!(cfg["github_username"], "cyrusae", "should have changed");
assert_eq!(cfg["tangled_username"], "atdot.fyi", "should be unchanged");
assert_eq!(cfg["origin_preference"], "github", "should be unchanged");
}
#[test]
fn ctrl_c_on_first_prompt_does_not_write_config() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.json");
let mut p = pty_setup(&config_path);
p.exp_string("GitHub username").unwrap();
p.send_control('c').unwrap();
let _ = p.exp_eof();
assert!(
!config_path.exists(),
"config must not be written after Ctrl+C on first prompt"
);
}
#[test]
fn ctrl_c_mid_setup_leaves_existing_config_unchanged() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join("config.json");
write_config_json(&config_path, "cyrusae", "atdot.fyi", "github");
let original = std::fs::read_to_string(&config_path).unwrap();
let mut p = pty_setup(&config_path);
p.exp_string("GitHub username").unwrap();
p.send_line("cyrusae").unwrap();
p.exp_string("Tangled username").unwrap();
p.send_control('c').unwrap();
let _ = p.exp_eof();
let after = std::fs::read_to_string(&config_path).unwrap();
assert_eq!(
original, after,
"pre-existing config must be unchanged after Ctrl+C"
);
}
}