use std::process::{Child, Command};
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
use super::support::{MockRdpServer, load_fixture};
fn daemon_test_mutex() -> &'static Mutex<()> {
static MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
MUTEX.get_or_init(|| Mutex::new(()))
}
fn ff_rdp_bin() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_BIN_EXE_ff-rdp"))
}
struct DaemonGuard {
child: Option<Child>,
home_dir: Option<std::path::PathBuf>,
}
impl DaemonGuard {
fn new(child: Child, home_dir: std::path::PathBuf) -> Self {
Self {
child: Some(child),
home_dir: Some(home_dir),
}
}
fn kill(&mut self) {
if let Some(mut c) = self.child.take() {
let _ = c.kill();
let _ = c.wait();
}
if let Some(ref home) = self.home_dir {
let _ = std::fs::remove_file(home.join(".ff-rdp/daemon.json"));
}
}
}
impl Drop for DaemonGuard {
fn drop(&mut self) {
self.kill();
}
}
fn isolated_home() -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("creating temp dir for isolated HOME");
std::fs::create_dir_all(dir.path().join(".ff-rdp")).expect("creating .ff-rdp in temp HOME");
dir
}
fn start_daemon(mock_port: u16, home_dir: &std::path::Path) -> DaemonGuard {
let child = Command::new(ff_rdp_bin())
.env("FF_RDP_HOME", home_dir)
.args([
"--host",
"127.0.0.1",
"--port",
&mock_port.to_string(),
"_daemon",
])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("failed to spawn daemon process");
DaemonGuard::new(child, home_dir.to_owned())
}
fn wait_for_daemon_ready(mock_port: u16, timeout: Duration, home_dir: &std::path::Path) -> u16 {
let start = Instant::now();
loop {
assert!(
start.elapsed() <= timeout,
"daemon did not become ready within {timeout:?}"
);
let registry_path = home_dir.join(".ff-rdp/daemon.json");
if let Ok(contents) = std::fs::read_to_string(®istry_path)
&& let Ok(info) = serde_json::from_str::<serde_json::Value>(&contents)
&& info["firefox_port"].as_u64() == Some(u64::from(mock_port))
&& let Some(proxy_port) = info["proxy_port"].as_u64()
{
return u16::try_from(proxy_port).expect("proxy_port fits in u16");
}
std::thread::sleep(Duration::from_millis(50));
}
}
fn daemon_args(mock_port: u16) -> Vec<String> {
vec![
"--host".to_owned(),
"127.0.0.1".to_owned(),
"--port".to_owned(),
mock_port.to_string(),
"--timeout".to_owned(),
"2000".to_owned(),
]
}
fn navigate_with_network_daemon_server() -> MockRdpServer {
MockRdpServer::new()
.on("listTabs", load_fixture("list_tabs_response.json"))
.on("getTarget", load_fixture("get_target_response.json"))
.on(
"startListeners",
load_fixture("start_listeners_response.json"),
)
.on("getWatcher", load_fixture("get_watcher_response.json"))
.on(
"watchResources",
load_fixture("watch_resources_response.json"),
)
.on_with_followups(
"navigateTo",
load_fixture("navigate_response.json"),
vec![
load_fixture("resources_available_network.json"),
load_fixture("resources_updated_network.json"),
],
)
}
fn network_daemon_server() -> MockRdpServer {
MockRdpServer::new()
.on("listTabs", load_fixture("list_tabs_response.json"))
.on("getTarget", load_fixture("get_target_response.json"))
.on(
"startListeners",
load_fixture("start_listeners_response.json"),
)
.on("getWatcher", load_fixture("get_watcher_response.json"))
.on_with_followups(
"watchResources",
load_fixture("watch_resources_response.json"),
vec![
load_fixture("resources_available_network.json"),
load_fixture("resources_updated_network.json"),
],
)
}
#[test]
fn daemon_navigate_with_network_captures_requests() {
let _guard = daemon_test_mutex().lock().expect("daemon test mutex");
let home = isolated_home();
let server = navigate_with_network_daemon_server();
let mock_port = server.port();
let mock_handle = std::thread::spawn(move || server.serve_one());
let mut daemon = start_daemon(mock_port, home.path());
let _proxy_port = wait_for_daemon_ready(mock_port, Duration::from_secs(5), home.path());
let mut args = daemon_args(mock_port);
args.extend([
"navigate".to_owned(),
"https://example.com".to_owned(),
"--with-network".to_owned(),
]);
let output = Command::new(ff_rdp_bin())
.env("FF_RDP_HOME", home.path())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
daemon.kill();
let _ = mock_handle.join();
assert!(
output.status.success(),
"daemon navigate --with-network must succeed; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let json: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("stdout must be valid JSON");
assert_eq!(
json["results"]["navigated"], "https://example.com",
"navigated URL must be present in results"
);
let network = &json["results"]["network"];
assert!(network.is_object(), "network should be a summary object");
assert_eq!(
network["total_requests"], 2,
"expected 2 network entries through daemon; got: {network}"
);
assert!(
network["total_transfer_bytes"].is_number(),
"total_transfer_bytes must be a number"
);
assert!(
network["by_cause_type"].is_object(),
"by_cause_type must be an object"
);
assert!(network["slowest"].is_array(), "slowest must be an array");
}
#[test]
fn daemon_network_shows_summary() {
let _guard = daemon_test_mutex().lock().expect("daemon test mutex");
let home = isolated_home();
let server = network_daemon_server();
let mock_port = server.port();
let mock_handle = std::thread::spawn(move || server.serve_one());
let mut daemon = start_daemon(mock_port, home.path());
let _proxy_port = wait_for_daemon_ready(mock_port, Duration::from_secs(5), home.path());
let poll_timeout = Duration::from_secs(5);
let poll_start = Instant::now();
let (json, stderr) = loop {
let mut args = daemon_args(mock_port);
args.push("network".to_owned());
let output = Command::new(ff_rdp_bin())
.env("FF_RDP_HOME", home.path())
.args(&args)
.output()
.expect("failed to spawn ff-rdp");
if output.status.success()
&& let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(&output.stdout)
&& parsed["results"]["total_requests"].as_u64().unwrap_or(0) > 0
{
break (parsed, String::from_utf8_lossy(&output.stderr).to_string());
}
assert!(
poll_start.elapsed() < poll_timeout,
"daemon did not buffer events within {poll_timeout:?}; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
std::thread::sleep(Duration::from_millis(50));
};
daemon.kill();
let _ = mock_handle.join();
assert!(
json["results"].is_object(),
"default network output should be summary (object), got: {}; stderr: {stderr}",
json["results"]
);
assert_eq!(
json["results"]["total_requests"], 2,
"expected 2 network entries drained from daemon buffer; got: {}",
json["results"]
);
assert!(
json["results"]["slowest"].is_array(),
"slowest must be an array"
);
assert!(
json["results"]["by_cause_type"].is_object(),
"by_cause_type must be an object"
);
}