use super::support::{MockRdpServer, load_fixture};
fn ff_rdp_bin() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_BIN_EXE_ff-rdp"))
}
fn base_args(port: u16) -> Vec<String> {
vec![
"--host".to_owned(),
"127.0.0.1".to_owned(),
"--port".to_owned(),
port.to_string(),
"--no-daemon".to_owned(),
]
}
#[test]
fn no_daemon_flag_bypasses_daemon_and_connects_directly() {
let list_tabs_response = load_fixture("list_tabs_response.json");
let server = MockRdpServer::new().on("listTabs", list_tabs_response);
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let mut args = base_args(port);
args.push("tabs".to_owned());
let output = std::process::Command::new(ff_rdp_bin())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
handle.join().expect("mock server thread panicked");
assert!(
output.status.success(),
"--no-daemon tabs must succeed; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
serde_json::from_str::<serde_json::Value>(stdout.trim())
.expect("--no-daemon output must be valid JSON");
}
#[test]
fn no_daemon_flag_accepted_as_global_flag() {
let list_tabs_response = load_fixture("list_tabs_response.json");
let server = MockRdpServer::new().on("listTabs", list_tabs_response);
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let output = std::process::Command::new(ff_rdp_bin())
.args([
"--host",
"127.0.0.1",
"--port",
&port.to_string(),
"--no-daemon",
"tabs",
])
.output()
.expect("failed to spawn ff-rdp");
handle.join().expect("mock server thread panicked");
assert!(
output.status.success(),
"--no-daemon as global flag must succeed; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn daemon_subcommand_is_recognised_and_fails_gracefully_without_firefox() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind random port");
let port = listener.local_addr().expect("local_addr").port();
drop(listener);
let output = std::process::Command::new(ff_rdp_bin())
.args([
"--host",
"127.0.0.1",
"--port",
&port.to_string(),
"_daemon",
])
.output()
.expect("failed to spawn ff-rdp");
assert!(
!output.status.success(),
"_daemon without Firefox must exit non-zero"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.to_lowercase().contains("unrecognized subcommand"),
"_daemon must be a recognised subcommand; stderr: {stderr}"
);
assert!(
!stderr.to_lowercase().contains("unknown subcommand"),
"_daemon must be a recognised subcommand; stderr: {stderr}"
);
}
fn plant_corrupt_registry(home_dir: &std::path::Path) {
let dir = home_dir.join(".ff-rdp");
std::fs::create_dir_all(&dir).expect("create .ff-rdp");
std::fs::write(dir.join("daemon.json"), b"not valid json").expect("write corrupt registry");
}
#[test]
fn registry_not_found_warning_silent_when_direct_fallback_succeeds() {
let home = tempfile::tempdir().expect("tempdir");
plant_corrupt_registry(home.path());
let server = MockRdpServer::new()
.on("listTabs", load_fixture("list_tabs_response.json"))
.on("getTarget", load_fixture("get_target_response.json"))
.on_with_followup(
"evaluateJSAsync",
load_fixture("eval_immediate_response.json"),
load_fixture("eval_result_ready_state_complete.json"),
);
let port = server.port();
let handle = std::thread::spawn(move || server.serve_one());
let output = std::process::Command::new(ff_rdp_bin())
.env("FF_RDP_HOME", home.path())
.args([
"--host",
"127.0.0.1",
"--port",
&port.to_string(),
"eval",
"document.readyState",
])
.output()
.expect("failed to spawn ff-rdp");
handle.join().expect("mock server thread panicked");
assert!(
output.status.success(),
"expected success when direct fallback works; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("warning:"),
"happy path must be silent (no daemon warnings on stderr); got: {stderr}"
);
}
#[test]
fn registry_not_found_warning_visible_when_direct_fallback_also_fails() {
let home = tempfile::tempdir().expect("tempdir");
plant_corrupt_registry(home.path());
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let port = listener.local_addr().expect("addr").port();
drop(listener);
let output = std::process::Command::new(ff_rdp_bin())
.env("FF_RDP_HOME", home.path())
.args([
"--host",
"127.0.0.1",
"--port",
&port.to_string(),
"--timeout",
"500",
"eval",
"1",
])
.output()
.expect("failed to spawn ff-rdp");
assert!(
!output.status.success(),
"expected failure when both daemon and direct paths break"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("warning:"),
"broken path must surface the deferred daemon warning; got: {stderr}"
);
assert!(
stderr.contains("could not connect to Firefox"),
"broken path must also report the direct connection failure; got: {stderr}"
);
}
#[test]
fn help_shows_daemon_flags() {
let output = std::process::Command::new(ff_rdp_bin())
.args(["--help"])
.output()
.expect("failed to spawn ff-rdp");
assert!(
output.status.success(),
"--help must exit 0; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("--no-daemon"),
"--help output must contain --no-daemon; got:\n{stdout}"
);
assert!(
stdout.contains("--daemon-timeout"),
"--help output must contain --daemon-timeout; got:\n{stdout}"
);
}