use std::net::IpAddr;
use ant_core::node::binary::NoopProgress;
use ant_core::node::daemon::server;
use ant_core::node::registry::NodeRegistry;
use ant_core::node::types::{AddNodeOpts, BinarySource, DaemonConfig, DaemonStatus, NodeInfo};
fn test_config(dir: &tempfile::TempDir) -> DaemonConfig {
DaemonConfig {
listen_addr: IpAddr::V4(std::net::Ipv4Addr::LOCALHOST),
port: Some(0), registry_path: dir.path().join("registry.json"),
log_path: dir.path().join("daemon.log"),
port_file_path: dir.path().join("daemon.port"),
pid_file_path: dir.path().join("daemon.pid"),
}
}
#[tokio::test]
async fn start_daemon_get_status_stop() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(&dir);
let registry = NodeRegistry::load(&config.registry_path).unwrap();
let shutdown = tokio_util::sync::CancellationToken::new();
let addr = server::start(config, registry, shutdown.clone())
.await
.unwrap();
let url = format!("http://{addr}/api/v1/status");
let resp = reqwest::get(&url).await.unwrap();
assert!(resp.status().is_success());
let status: DaemonStatus = resp.json().await.unwrap();
assert!(status.running);
assert!(status.pid.is_some());
assert_eq!(status.nodes_total, 0);
assert_eq!(status.nodes_running, 0);
assert_eq!(status.nodes_stopped, 0);
assert_eq!(status.nodes_errored, 0);
assert!(status.uptime_secs.unwrap() < 5);
shutdown.cancel();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
#[tokio::test]
async fn openapi_spec_is_valid_json() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(&dir);
let registry = NodeRegistry::load(&config.registry_path).unwrap();
let shutdown = tokio_util::sync::CancellationToken::new();
let addr = server::start(config, registry, shutdown.clone())
.await
.unwrap();
let url = format!("http://{addr}/api/v1/openapi.json");
let resp = reqwest::get(&url).await.unwrap();
assert!(resp.status().is_success());
let spec: serde_json::Value = resp.json().await.unwrap();
assert_eq!(spec["openapi"], "3.1.0");
assert_eq!(spec["info"]["title"], "Ant Daemon API");
assert!(spec["paths"]["/api/v1/status"].is_object());
shutdown.cancel();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
#[tokio::test]
async fn port_and_pid_files_written() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(&dir);
let port_file = config.port_file_path.clone();
let pid_file = config.pid_file_path.clone();
let registry = NodeRegistry::load(&config.registry_path).unwrap();
let shutdown = tokio_util::sync::CancellationToken::new();
let addr = server::start(config, registry, shutdown.clone())
.await
.unwrap();
let port_contents = std::fs::read_to_string(&port_file).unwrap();
let port: u16 = port_contents.trim().parse().unwrap();
assert_eq!(port, addr.port());
let pid_contents = std::fs::read_to_string(&pid_file).unwrap();
let pid: u32 = pid_contents.trim().parse().unwrap();
assert_eq!(pid, std::process::id());
shutdown.cancel();
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
assert!(
!port_file.exists(),
"Port file should be removed after shutdown"
);
assert!(
!pid_file.exists(),
"PID file should be removed after shutdown"
);
}
#[tokio::test]
async fn console_returns_html() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(&dir);
let registry = NodeRegistry::load(&config.registry_path).unwrap();
let shutdown = tokio_util::sync::CancellationToken::new();
let addr = server::start(config, registry, shutdown.clone())
.await
.unwrap();
let url = format!("http://{addr}/console");
let resp = reqwest::get(&url).await.unwrap();
assert!(resp.status().is_success());
let content_type = resp
.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap();
assert!(
content_type.contains("text/html"),
"Expected text/html, got {content_type}"
);
let body = resp.text().await.unwrap();
assert!(body.contains("Node Console"));
assert!(body.contains("/api/v1"));
shutdown.cancel();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
fn create_fake_binary(dir: &std::path::Path) -> std::path::PathBuf {
#[cfg(unix)]
{
let binary_path = dir.join("fake-antnode");
std::fs::write(&binary_path, "#!/bin/sh\necho \"antnode 0.1.0-test\"\n").unwrap();
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&binary_path, std::fs::Permissions::from_mode(0o755)).unwrap();
binary_path
}
#[cfg(windows)]
{
let binary_path = dir.join("fake-antnode.cmd");
std::fs::write(&binary_path, "@echo off\r\necho antnode 0.1.0-test\r\n").unwrap();
binary_path
}
}
#[tokio::test]
async fn get_node_detail_returns_full_config() {
let dir = tempfile::tempdir().unwrap();
let config = test_config(&dir);
let reg_path = config.registry_path.clone();
let binary = create_fake_binary(dir.path());
let opts = AddNodeOpts {
count: 1,
rewards_address: "0x1234567890abcdef1234567890abcdef12345678".to_string(),
data_dir_path: Some(dir.path().join("data")),
log_dir_path: Some(dir.path().join("logs")),
binary_source: BinarySource::LocalPath(binary),
..Default::default()
};
ant_core::node::add_nodes(opts, ®_path, &NoopProgress)
.await
.unwrap();
let registry = NodeRegistry::load(®_path).unwrap();
let shutdown = tokio_util::sync::CancellationToken::new();
let addr = server::start(config, registry, shutdown.clone())
.await
.unwrap();
let url = format!("http://{addr}/api/v1/nodes/1");
let resp = reqwest::get(&url).await.unwrap();
assert!(resp.status().is_success());
let detail: NodeInfo = resp.json().await.unwrap();
assert_eq!(detail.config.id, 1);
assert_eq!(detail.config.service_name, "node1");
assert_eq!(
detail.config.rewards_address,
"0x1234567890abcdef1234567890abcdef12345678"
);
assert!(detail.config.data_dir.exists());
assert_eq!(detail.status, ant_core::node::types::NodeStatus::Stopped);
assert!(detail.pid.is_none());
assert!(detail.uptime_secs.is_none());
let raw: serde_json::Value = reqwest::get(&url).await.unwrap().json().await.unwrap();
assert!(raw.get("id").is_some(), "should have flattened id");
assert!(
raw.get("service_name").is_some(),
"should have flattened service_name"
);
assert!(
raw.get("data_dir").is_some(),
"should have flattened data_dir"
);
assert!(
raw.get("rewards_address").is_some(),
"should have flattened rewards_address"
);
assert!(raw.get("status").is_some());
let resp_404 = reqwest::get(format!("http://{addr}/api/v1/nodes/999"))
.await
.unwrap();
assert_eq!(resp_404.status(), 404);
shutdown.cancel();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}