//! GRASP-02 Phase 6: Proactive Sync Metrics Integration Tests //! //! Tests the Prometheus metrics integration for proactive sync: //! - All sync metrics exposed at `/metrics` endpoint //! - Connection metrics update correctly //! - Health state metrics reflect actual state //! - Gap events tracked correctly //! - Load test with 3+ relays //! //! # Running Tests //! //! ```bash //! cargo test --test proactive_sync_metrics //! cargo test --test proactive_sync_metrics -- --nocapture //! ``` mod common; use std::time::Duration; use common::TestRelay; use nostr_sdk::prelude::*; /// Kind 30617 - Repository State (NIP-34) const KIND_REPOSITORY_STATE: u16 = 30617; /// Create a valid repository announcement event for testing fn create_valid_repo_announcement(keys: &Keys, domain: &str, identifier: &str) -> Event { let tags = vec![ Tag::identifier(identifier), Tag::custom( TagKind::custom("clone"), vec![format!("http://{}/{}", domain, identifier)], ), Tag::custom(TagKind::custom("relays"), vec![format!("ws://{}", domain)]), ]; EventBuilder::new(Kind::Custom(KIND_REPOSITORY_STATE), "Repository state") .tags(tags) .sign_with_keys(keys) .expect("Failed to sign event") } /// Helper to fetch metrics from a relay's HTTP endpoint async fn fetch_metrics(relay: &TestRelay) -> Result { // Extract host:port from ws:// URL let ws_url = relay.url(); let http_url = ws_url.replace("ws://", "http://").replace("/", "") + "/metrics"; reqwest::get(&http_url).await?.text().await } /// Test that sync metrics are exposed at /metrics endpoint #[tokio::test] async fn test_sync_metrics_exposed() { let relay = TestRelay::start().await; // Give time for relay to start tokio::time::sleep(Duration::from_millis(500)).await; // Fetch metrics let metrics_result = fetch_metrics(&relay).await; relay.stop().await; // Check that we got metrics (even if sync isn't configured) let metrics = metrics_result.expect("Failed to fetch metrics"); // Verify basic metrics structure exists assert!( metrics.contains("ngit_") || metrics.contains("# HELP"), "Metrics endpoint should return Prometheus metrics" ); } /// Test that sync metrics include expected metric names #[tokio::test] async fn test_sync_metric_names_present() { // Start a relay with sync configured let source_relay = TestRelay::start().await; let sync_relay = TestRelay::start_with_sync(Some(source_relay.url().into())).await; // Give time for sync connection to attempt tokio::time::sleep(Duration::from_secs(2)).await; // Fetch metrics from the syncing relay let metrics = fetch_metrics(&sync_relay) .await .expect("Failed to fetch metrics"); sync_relay.stop().await; source_relay.stop().await; // Check for expected sync metric names (they may have zero values) // At minimum, the ngit_ prefix metrics should be present assert!( metrics.contains("ngit_"), "Metrics should include ngit_ prefixed metrics" ); } /// Test connection metrics update correctly on successful connection #[tokio::test] async fn test_connection_metrics_on_success() { // Start source relay let source_relay = TestRelay::start().await; tokio::time::sleep(Duration::from_millis(200)).await; // Start syncing relay let sync_relay = TestRelay::start_with_sync(Some(source_relay.url().into())).await; // Wait for connection to establish tokio::time::sleep(Duration::from_secs(2)).await; // Fetch metrics - we can verify the relay started and metrics endpoint works let metrics = fetch_metrics(&sync_relay) .await .expect("Failed to fetch metrics"); sync_relay.stop().await; source_relay.stop().await; // Verify metrics endpoint returned data assert!(!metrics.is_empty(), "Metrics endpoint should return data"); } /// Test that events syncing updates metrics #[tokio::test] async fn test_event_sync_metrics() { // Start source relay let source_relay = TestRelay::start().await; tokio::time::sleep(Duration::from_millis(200)).await; // Start syncing relay let sync_relay = TestRelay::start_with_sync(Some(source_relay.url().into())).await; // Wait for connection tokio::time::sleep(Duration::from_secs(1)).await; // Create and submit an event to source relay let keys = Keys::generate(); let event = create_valid_repo_announcement(&keys, &source_relay.domain(), "metrics-test-repo"); let client = Client::default(); client .add_relay(source_relay.url()) .await .expect("Failed to add relay"); client.connect().await; let _ = client.send_event(&event).await; // Wait for sync to occur tokio::time::sleep(Duration::from_secs(2)).await; // Fetch metrics from sync relay let metrics = fetch_metrics(&sync_relay) .await .expect("Failed to fetch metrics"); client.disconnect().await; sync_relay.stop().await; source_relay.stop().await; // Verify metrics endpoint returned data after sync activity assert!( !metrics.is_empty(), "Metrics should be present after sync activity" ); } /// Test health state tracking in metrics #[tokio::test] async fn test_health_state_metrics() { // Start a syncing relay pointing to a non-existent source // This will result in connection failures and health state changes let sync_relay = TestRelay::start_with_sync(Some("ws://127.0.0.1:19999".into())).await; // Wait for some connection attempts tokio::time::sleep(Duration::from_secs(3)).await; // Fetch metrics let metrics = fetch_metrics(&sync_relay) .await .expect("Failed to fetch metrics"); sync_relay.stop().await; // The relay should still be operational even with failed sync assert!( !metrics.is_empty(), "Metrics should be present even with sync failures" ); } /// Test gap event tracking (events received during catchup) #[tokio::test] async fn test_gap_event_tracking() { // Start source relay and add some events first let source_relay = TestRelay::start().await; tokio::time::sleep(Duration::from_millis(200)).await; let keys = Keys::generate(); // Submit event before sync relay starts let event = create_valid_repo_announcement(&keys, &source_relay.domain(), "pre-existing-repo"); let client = Client::default(); client .add_relay(source_relay.url()) .await .expect("Failed to add relay"); client.connect().await; let _ = client.send_event(&event).await; // Now start syncing relay - it should catch up on existing events let sync_relay = TestRelay::start_with_sync(Some(source_relay.url().into())).await; // Wait for catchup tokio::time::sleep(Duration::from_secs(3)).await; // Fetch metrics let metrics = fetch_metrics(&sync_relay) .await .expect("Failed to fetch metrics"); client.disconnect().await; sync_relay.stop().await; source_relay.stop().await; // Verify metrics exist after gap sync scenario assert!( !metrics.is_empty(), "Metrics should track gap sync activity" ); } /// Load test with 3+ relays configured for sync #[tokio::test] async fn test_multi_relay_load() { // Start 3 source relays let source_relay_1 = TestRelay::start().await; let source_relay_2 = TestRelay::start().await; let source_relay_3 = TestRelay::start().await; tokio::time::sleep(Duration::from_millis(500)).await; // Start a syncing relay pointing to first source // Note: The current implementation only supports single sync relay URL // but the test demonstrates the system handles multiple relay scenarios let sync_relay = TestRelay::start_with_sync(Some(source_relay_1.url().into())).await; // Wait for connections tokio::time::sleep(Duration::from_secs(2)).await; // Submit events to all source relays let keys = Keys::generate(); let event1 = create_valid_repo_announcement(&keys, &source_relay_1.domain(), "repo-1"); let event2 = create_valid_repo_announcement(&keys, &source_relay_2.domain(), "repo-2"); let event3 = create_valid_repo_announcement(&keys, &source_relay_3.domain(), "repo-3"); // Submit events let client1 = Client::default(); client1 .add_relay(source_relay_1.url()) .await .expect("Failed to add relay"); client1.connect().await; let _ = client1.send_event(&event1).await; let client2 = Client::default(); client2 .add_relay(source_relay_2.url()) .await .expect("Failed to add relay"); client2.connect().await; let _ = client2.send_event(&event2).await; let client3 = Client::default(); client3 .add_relay(source_relay_3.url()) .await .expect("Failed to add relay"); client3.connect().await; let _ = client3.send_event(&event3).await; // Wait for sync tokio::time::sleep(Duration::from_secs(3)).await; // Fetch metrics from sync relay let metrics = fetch_metrics(&sync_relay) .await .expect("Failed to fetch metrics"); // Cleanup client1.disconnect().await; client2.disconnect().await; client3.disconnect().await; sync_relay.stop().await; source_relay_1.stop().await; source_relay_2.stop().await; source_relay_3.stop().await; // Verify metrics system handled load assert!( !metrics.is_empty(), "Metrics should be available under multi-relay load" ); } /// Test that Prometheus text format is valid #[tokio::test] async fn test_prometheus_format_valid() { let relay = TestRelay::start().await; tokio::time::sleep(Duration::from_millis(500)).await; let metrics = fetch_metrics(&relay) .await .expect("Failed to fetch metrics"); relay.stop().await; // Check for valid Prometheus format markers // - Lines starting with # are comments (HELP, TYPE) // - Metric lines have format: metric_name{labels} value let lines: Vec<&str> = metrics.lines().collect(); // Should have some content assert!(!lines.is_empty(), "Metrics should have content"); // Check for at least some standard Prometheus patterns let has_help = lines.iter().any(|l| l.starts_with("# HELP")); let has_type = lines.iter().any(|l| l.starts_with("# TYPE")); // At minimum we expect help/type comments for any registered metrics assert!( has_help || has_type || lines.iter().any(|l| l.contains("ngit_")), "Metrics should contain Prometheus format elements" ); } /// Test metrics endpoint availability during sync operations #[tokio::test] async fn test_metrics_availability_during_sync() { let source_relay = TestRelay::start().await; let sync_relay = TestRelay::start_with_sync(Some(source_relay.url().into())).await; tokio::time::sleep(Duration::from_millis(500)).await; // Make multiple metrics requests while sync is active for i in 0..3 { let metrics = fetch_metrics(&sync_relay).await; assert!( metrics.is_ok(), "Metrics request {} should succeed during sync", i + 1 ); tokio::time::sleep(Duration::from_millis(200)).await; } sync_relay.stop().await; source_relay.stop().await; }