From 463c060bf15f4b9e7360292ca9525332945b0ab6 Mon Sep 17 00:00:00 2001 From: copp1723 <200090596+copp1723@users.noreply.github.com> Date: Sun, 10 May 2026 21:57:46 -0500 Subject: [PATCH 1/7] test monero rpc pool coverage --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index 6fe5057a6..13b48823a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6520,6 +6520,7 @@ dependencies = [ "serde_json", "sqlx", "swap-serde", + "tempfile", "tokio", "tokio-rustls 0.26.4", "tor-rtcompat", From 024f69c61c7326238699f86f20d12042f8babac4 Mon Sep 17 00:00:00 2001 From: copp1723 <200090596+copp1723@users.noreply.github.com> Date: Sun, 10 May 2026 21:57:55 -0500 Subject: [PATCH 2/7] add monero rpc pool test tempdir dependency --- monero-rpc-pool/Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/monero-rpc-pool/Cargo.toml b/monero-rpc-pool/Cargo.toml index 4179a0cbe..acf0d56cf 100644 --- a/monero-rpc-pool/Cargo.toml +++ b/monero-rpc-pool/Cargo.toml @@ -67,3 +67,6 @@ swap-serde = { path = "../swap-serde" } # Optional dependencies (for features) cuprate-epee-encoding = { git = "https://github.com/Cuprate/cuprate.git", optional = true } reqwest = { version = "0.11", features = ["json"], optional = true } + +[dev-dependencies] +tempfile = "3" From a74f81bd7de45ae5add4b528bc876ea775e09f7c Mon Sep 17 00:00:00 2001 From: copp1723 <200090596+copp1723@users.noreply.github.com> Date: Sun, 10 May 2026 21:58:00 -0500 Subject: [PATCH 3/7] test monero rpc pool database health stats --- monero-rpc-pool/src/database.rs | 92 +++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/monero-rpc-pool/src/database.rs b/monero-rpc-pool/src/database.rs index eb3dfd8b1..c03a317f3 100644 --- a/monero-rpc-pool/src/database.rs +++ b/monero-rpc-pool/src/database.rs @@ -294,3 +294,95 @@ impl Database { Ok(addresses) } } + +#[cfg(test)] +mod tests { + use super::*; + + async fn temp_db() -> (tempfile::TempDir, Database) { + let temp_dir = tempfile::tempdir().expect("temporary database directory"); + let db = Database::new(temp_dir.path().to_path_buf()) + .await + .expect("database initialization"); + (temp_dir, db) + } + + #[test] + fn network_round_trips_between_enum_and_db_string() { + assert!(matches!( + parse_network("mainnet").unwrap(), + Network::Mainnet + )); + assert!(matches!( + parse_network("STAGENET").unwrap(), + Network::Stagenet + )); + assert!(matches!( + parse_network("testnet").unwrap(), + Network::Testnet + )); + assert!(parse_network("regtest").is_err()); + + assert_eq!(network_to_string(&Network::Mainnet), "mainnet"); + assert_eq!(network_to_string(&Network::Stagenet), "stagenet"); + assert_eq!(network_to_string(&Network::Testnet), "testnet"); + } + + #[tokio::test] + async fn migrations_seed_default_nodes_for_each_network() { + let (_temp_dir, db) = temp_db().await; + + let (mainnet_total, mainnet_reachable, mainnet_reliable) = + db.get_node_stats("mainnet").await.unwrap(); + let (stagenet_total, stagenet_reachable, stagenet_reliable) = + db.get_node_stats("stagenet").await.unwrap(); + + assert!(mainnet_total > 0); + assert!(stagenet_total > 0); + assert_eq!(mainnet_reachable, 0); + assert_eq!(mainnet_reliable, 0); + assert_eq!(stagenet_reachable, 0); + assert_eq!(stagenet_reliable, 0); + } + + #[tokio::test] + async fn health_checks_update_stats_and_reliable_nodes() { + let (_temp_dir, db) = temp_db().await; + + db.record_health_check("http", "stagenet.xmr-tw.org", 38081, true, Some(250.0)) + .await + .unwrap(); + db.record_health_check("http", "stagenet.xmr-tw.org", 38081, false, None) + .await + .unwrap(); + + let (_total, reachable, reliable) = db.get_node_stats("stagenet").await.unwrap(); + assert_eq!(reachable, 1); + assert_eq!(reliable, 0); + + let (successful, unsuccessful) = db.get_health_check_stats("stagenet").await.unwrap(); + assert_eq!((successful, unsuccessful), (1, 1)); + + let reliable_nodes = db.get_reliable_nodes("stagenet").await.unwrap(); + let node = reliable_nodes + .iter() + .find(|node| node.address.host == "stagenet.xmr-tw.org") + .expect("health-checked node should be considered"); + assert_eq!(node.health.success_count, 1); + assert_eq!(node.health.failure_count, 1); + assert_eq!(node.health.avg_latency_ms, Some(250.0)); + assert_eq!(node.success_rate(), 0.5); + } + + #[tokio::test] + async fn unknown_node_health_checks_are_ignored() { + let (_temp_dir, db) = temp_db().await; + + db.record_health_check("http", "missing.example", 18081, true, Some(10.0)) + .await + .unwrap(); + + let (successful, unsuccessful) = db.get_health_check_stats("mainnet").await.unwrap(); + assert_eq!((successful, unsuccessful), (0, 0)); + } +} From b1fd83b260672291fde9a4f035fdad2097bcd5ec Mon Sep 17 00:00:00 2001 From: copp1723 <200090596+copp1723@users.noreply.github.com> Date: Sun, 10 May 2026 21:58:06 -0500 Subject: [PATCH 4/7] test monero rpc pool app startup status --- monero-rpc-pool/src/lib.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/monero-rpc-pool/src/lib.rs b/monero-rpc-pool/src/lib.rs index 495814843..ae4ec17a7 100644 --- a/monero-rpc-pool/src/lib.rs +++ b/monero-rpc-pool/src/lib.rs @@ -185,3 +185,32 @@ pub async fn start_server_with_random_port( Ok((server_info, status_receiver, pool_handle)) } + +#[cfg(test)] +mod tests { + use super::*; + use monero_address::Network; + use tokio::time::{Duration, timeout}; + + #[tokio::test] + async fn create_app_publishes_initial_status() { + let temp_dir = tempfile::tempdir().expect("temporary database directory"); + let config = Config::new_with_port( + "127.0.0.1".to_string(), + 0, + temp_dir.path().to_path_buf(), + Network::Stagenet, + ); + + let (_app, mut receiver, _handle) = create_app_with_receiver(config).await.unwrap(); + let status = timeout(Duration::from_secs(1), receiver.recv()) + .await + .expect("initial status timeout") + .expect("initial status"); + + assert!(status.total_node_count > 0); + assert_eq!(status.healthy_node_count, 0); + assert_eq!(status.successful_health_checks, 0); + assert_eq!(status.unsuccessful_health_checks, 0); + } +} From bb9f4ea3f6761dce0d642caa0e8febdcfd6d62ce Mon Sep 17 00:00:00 2001 From: copp1723 <200090596+copp1723@users.noreply.github.com> Date: Sun, 10 May 2026 21:58:13 -0500 Subject: [PATCH 5/7] test monero rpc pool status broadcasts --- monero-rpc-pool/src/pool.rs | 72 +++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/monero-rpc-pool/src/pool.rs b/monero-rpc-pool/src/pool.rs index 4df648749..d97c39f8a 100644 --- a/monero-rpc-pool/src/pool.rs +++ b/monero-rpc-pool/src/pool.rs @@ -248,3 +248,75 @@ impl NodePool { Ok(selected_nodes) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::database::Database; + use monero_address::Network; + use tokio::time::{Duration, timeout}; + + async fn temp_pool() -> (tempfile::TempDir, NodePool, broadcast::Receiver) { + let temp_dir = tempfile::tempdir().expect("temporary database directory"); + let db = Database::new(temp_dir.path().to_path_buf()) + .await + .expect("database initialization"); + let (pool, receiver) = NodePool::new(db, Network::Stagenet); + (temp_dir, pool, receiver) + } + + #[test] + fn bandwidth_tracker_requires_enough_samples() { + let tracker = BandwidthTracker::new(); + for _ in 0..4 { + tracker.record_bytes(1024); + } + + assert_eq!(tracker.get_kb_per_sec(), 0.0); + } + + #[tokio::test] + async fn status_reports_health_counts_and_reliable_nodes() { + let (_temp_dir, pool, _receiver) = temp_pool().await; + + pool.record_success("http", "stagenet.xmr-tw.org", 38081, 120.0) + .await + .unwrap(); + pool.record_failure("http", "node.monerodevs.org", 38089) + .await + .unwrap(); + + let status = pool.get_current_status().await.unwrap(); + + assert!(status.total_node_count > 0); + assert_eq!(status.healthy_node_count, 1); + assert_eq!(status.successful_health_checks, 1); + assert_eq!(status.unsuccessful_health_checks, 1); + assert!( + status + .top_reliable_nodes + .iter() + .any(|node| node.url == "http://stagenet.xmr-tw.org:38081" + && node.success_rate == 1.0 + && node.avg_latency_ms == Some(120.0)) + ); + } + + #[tokio::test] + async fn publish_status_update_notifies_receivers() { + let (_temp_dir, pool, mut receiver) = temp_pool().await; + + pool.record_success("http", "stagenet.xmr-tw.org", 38081, 100.0) + .await + .unwrap(); + pool.publish_status_update().await.unwrap(); + + let status = timeout(Duration::from_secs(1), receiver.recv()) + .await + .expect("status broadcast timeout") + .expect("status broadcast"); + + assert_eq!(status.healthy_node_count, 1); + assert_eq!(status.successful_health_checks, 1); + } +} From b405deb3fd55accc92ee78d4cdeb006858996fda Mon Sep 17 00:00:00 2001 From: copp1723 <200090596+copp1723@users.noreply.github.com> Date: Sun, 10 May 2026 21:58:22 -0500 Subject: [PATCH 6/7] test monero rpc pool node types --- monero-rpc-pool/src/types.rs | 43 ++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/monero-rpc-pool/src/types.rs b/monero-rpc-pool/src/types.rs index 43fd54ab0..5f6d37863 100644 --- a/monero-rpc-pool/src/types.rs +++ b/monero-rpc-pool/src/types.rs @@ -97,3 +97,46 @@ impl NodeRecord { self.health.success_rate() } } + +#[cfg(test)] +mod tests { + use super::*; + use monero_address::Network; + + #[test] + fn node_address_formats_full_url_and_display() { + let address = NodeAddress::new("https".to_string(), "node.example".to_string(), 18089); + + assert_eq!(address.full_url(), "https://node.example:18089"); + assert_eq!(address.to_string(), "https://node.example:18089"); + } + + #[test] + fn node_health_success_rate_handles_empty_and_mixed_counts() { + let empty = NodeHealthStats::default(); + assert_eq!(empty.success_rate(), 0.0); + + let mixed = NodeHealthStats { + success_count: 3, + failure_count: 1, + ..Default::default() + }; + assert_eq!(mixed.success_rate(), 0.75); + } + + #[test] + fn node_record_delegates_url_and_success_rate() { + let record = NodeRecord::new( + NodeAddress::new("http".to_string(), "localhost".to_string(), 38081), + NodeMetadata::new(7, Network::Stagenet, Utc::now()), + NodeHealthStats { + success_count: 9, + failure_count: 1, + ..Default::default() + }, + ); + + assert_eq!(record.full_url(), "http://localhost:38081"); + assert_eq!(record.success_rate(), 0.9); + } +} From 06d0ebcd7ad440a1496dd9744f0c9727a50594fa Mon Sep 17 00:00:00 2001 From: copp1723 <200090596+copp1723@users.noreply.github.com> Date: Sun, 10 May 2026 22:00:33 -0500 Subject: [PATCH 7/7] test monero rpc pool proxy helpers --- monero-rpc-pool/src/proxy.rs | 91 ++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/monero-rpc-pool/src/proxy.rs b/monero-rpc-pool/src/proxy.rs index f3e2e7f2e..2ab38a624 100644 --- a/monero-rpc-pool/src/proxy.rs +++ b/monero-rpc-pool/src/proxy.rs @@ -830,3 +830,94 @@ pub async fn stats_handler(State(state): State) -> Response { .instrument(info_span!("stats_request")) .await } + +#[cfg(test)] +mod tests { + use super::*; + use axum::http::Request; + + async fn body_bytes(response: Response) -> Vec { + response + .into_body() + .collect() + .await + .expect("response body") + .to_bytes() + .to_vec() + } + + #[tokio::test] + async fn cloneable_request_buffers_body_and_extracts_jsonrpc_method() { + let request = Request::builder() + .uri("/json_rpc") + .body(Body::from(r#"{"jsonrpc":"2.0","method":"get_info"}"#)) + .unwrap(); + + let cloneable = CloneableRequest::from_request(request).await.unwrap(); + + assert_eq!(cloneable.uri(), "/json_rpc"); + assert_eq!(cloneable.jsonrpc_method(), Some("get_info".to_string())); + + let cloned_request = cloneable.to_request(); + let cloned_body = cloned_request + .into_body() + .collect() + .await + .unwrap() + .to_bytes(); + assert_eq!(cloned_body.as_ref(), cloneable.body.as_slice()); + } + + #[tokio::test] + async fn cloneable_request_marks_safe_block_downloads_as_clearnet() { + let blocks = CloneableRequest::from_request( + Request::builder() + .uri("/getblocks.bin") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + let rpc = CloneableRequest::from_request( + Request::builder() + .uri("/json_rpc") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert!(blocks.clearnet_whitelisted()); + assert!(!rpc.clearnet_whitelisted()); + } + + #[tokio::test] + async fn cloneable_response_detects_string_jsonrpc_errors() { + let response = Response::builder() + .status(StatusCode::OK) + .body(Body::from(r#"{"error":"daemon busy"}"#)) + .unwrap(); + + let cloneable = CloneableResponse::from_response(response).await.unwrap(); + + assert_eq!(cloneable.status(), StatusCode::OK); + assert_eq!( + cloneable.get_jsonrpc_error(), + Some("daemon busy".to_string()) + ); + assert_eq!(get_jsonrpc_error(b"not json"), None); + } + + #[tokio::test] + async fn handler_errors_render_status_and_details_json() { + let response = HandlerError::NoNodes.to_response(); + assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); + + let body = body_bytes(response).await; + let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); + + assert_eq!(json["error"]["code"], 503); + assert_eq!(json["error"]["message"], "No nodes available"); + assert_eq!(json["error"]["details"], "No nodes available"); + } +}