use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use bssh::cli::Cli;
use clap::Parser;
use tempfile::tempdir;
fn run_bssh(arguments: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_bssh"))
.env_remove("RUST_LOG")
.env_remove("BSSH_PDSH_COMPAT")
.args(arguments)
.output()
.expect("failed to run bssh")
}
fn as_str(path: &Path) -> &str {
path.to_str().expect("temporary path should be UTF-8")
}
#[test]
fn parses_separated_and_attached_log_file_options() {
let separated = Cli::try_parse_from(["bssh", "-E", "/tmp/bssh.log", "example.com"])
.expect("separated -E should parse");
let attached = Cli::try_parse_from(["bssh", "-E/tmp/bssh.log", "example.com"])
.expect("attached -Epath should parse");
let expected = Some(PathBuf::from("/tmp/bssh.log"));
assert_eq!(separated.log_file, expected);
assert_eq!(attached.log_file, expected);
}
#[test]
fn repeated_log_file_option_uses_the_last_value() {
let parsed = Cli::try_parse_from([
"bssh",
"-E",
"/tmp/first.log",
"-E/tmp/last.log",
"example.com",
])
.expect("repeated -E should parse like OpenSSH");
assert_eq!(parsed.log_file, Some(PathBuf::from("/tmp/last.log")));
}
#[test]
fn human_diagnostics_are_routed_and_repeated_runs_append() {
let directory = tempdir().expect("failed to create temporary directory");
let log = directory.path().join("bssh.log");
for _ in 0..2 {
let output = run_bssh(&["-E", as_str(&log), "-Q", "not-a-query"]);
assert!(!output.status.success(), "an unknown query should fail");
assert!(
output.stderr.is_empty(),
"bssh diagnostic leaked to stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let contents = std::fs::read_to_string(&log).expect("failed to read diagnostic log");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(&log)
.expect("failed to inspect diagnostic log")
.permissions()
.mode()
& 0o777;
assert_eq!(mode, 0o600, "new diagnostic logs must be private");
}
assert_eq!(
contents
.matches("Unknown query option: not-a-query")
.count(),
2
);
assert_eq!(
contents
.matches("Use 'bssh -Q help' to see available options")
.count(),
2
);
}
#[test]
fn tracing_and_top_level_errors_use_the_log_file() {
let directory = tempdir().expect("failed to create temporary directory");
let log = directory.path().join("bssh.log");
let missing_config = directory.path().join("missing.yaml");
let output = run_bssh(&[
"-vv",
"-E",
as_str(&log),
"--config",
as_str(&missing_config),
"-H",
"example.invalid",
"true",
]);
assert!(!output.status.success(), "missing config must fail");
assert!(
output.stderr.is_empty(),
"bssh diagnostic leaked to stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let contents = std::fs::read_to_string(&log).expect("failed to read diagnostic log");
assert!(contents.contains("Appending bssh diagnostics to log file"));
assert!(contents.contains("Config file not found"));
assert!(contents.contains(as_str(&missing_config)));
}
#[test]
fn log_open_failure_is_actionable_and_exits_nonzero() {
let directory = tempdir().expect("failed to create temporary directory");
let output = run_bssh(&["-E", as_str(directory.path()), "-Q", "help"]);
assert!(!output.status.success(), "a directory cannot be a log file");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("Cannot open log file"),
"stderr was: {stderr}"
);
assert!(
stderr.contains(as_str(directory.path())),
"stderr did not name the failed path: {stderr}"
);
}