//! Unit tests for Mycelium client functionality //! //! These tests validate the core Mycelium client operations including: //! - Node information retrieval //! - Peer management (listing, adding, removing) //! - Route inspection (selected and fallback routes) //! - Message operations (sending and receiving) //! //! Tests are designed to work with a real Mycelium node when available, //! but gracefully handle cases where the node is not accessible. use sal_mycelium::*; use std::time::Duration; /// Test configuration for Mycelium API const TEST_API_URL: &str = "http://localhost:8989"; const FALLBACK_API_URL: &str = "http://localhost:7777"; /// Helper function to check if a Mycelium node is available async fn is_mycelium_available(api_url: &str) -> bool { match get_node_info(api_url).await { Ok(_) => true, Err(_) => false, } } /// Helper function to get an available Mycelium API URL async fn get_available_api_url() -> Option { if is_mycelium_available(TEST_API_URL).await { Some(TEST_API_URL.to_string()) } else if is_mycelium_available(FALLBACK_API_URL).await { Some(FALLBACK_API_URL.to_string()) } else { None } } #[tokio::test] async fn test_get_node_info_success() { if let Some(api_url) = get_available_api_url().await { let result = get_node_info(&api_url).await; match result { Ok(node_info) => { // Validate that we got a JSON response with expected fields assert!(node_info.is_object(), "Node info should be a JSON object"); // Check for common Mycelium node info fields let obj = node_info.as_object().unwrap(); // These fields are typically present in Mycelium node info // We check if at least one of them exists to validate the response let has_expected_fields = obj.contains_key("nodeSubnet") || obj.contains_key("nodePubkey") || obj.contains_key("peers") || obj.contains_key("routes"); assert!( has_expected_fields, "Node info should contain expected Mycelium fields" ); println!("✓ Node info retrieved successfully: {:?}", node_info); } Err(e) => { // If we can connect but get an error, it might be a version mismatch // or API change - log it but don't fail the test println!("⚠ Node info request failed (API might have changed): {}", e); } } } else { println!("⚠ Skipping test_get_node_info_success: No Mycelium node available"); } } #[tokio::test] async fn test_get_node_info_invalid_url() { let invalid_url = "http://localhost:99999"; let result = get_node_info(invalid_url).await; assert!(result.is_err(), "Should fail with invalid URL"); let error = result.unwrap_err(); assert!( error.contains("Failed to send request") || error.contains("Request failed"), "Error should indicate connection failure: {}", error ); println!("✓ Correctly handled invalid URL: {}", error); } #[tokio::test] async fn test_list_peers() { if let Some(api_url) = get_available_api_url().await { let result = list_peers(&api_url).await; match result { Ok(peers) => { // Peers should be an array (even if empty) assert!(peers.is_array(), "Peers should be a JSON array"); println!( "✓ Peers listed successfully: {} peers found", peers.as_array().unwrap().len() ); } Err(e) => { println!( "⚠ List peers request failed (API might have changed): {}", e ); } } } else { println!("⚠ Skipping test_list_peers: No Mycelium node available"); } } #[tokio::test] async fn test_add_peer_validation() { if let Some(api_url) = get_available_api_url().await { // Test with an invalid peer address format let invalid_peer = "invalid-peer-address"; let result = add_peer(&api_url, invalid_peer).await; // This should either succeed (if the node accepts it) or fail with a validation error match result { Ok(response) => { println!("✓ Add peer response: {:?}", response); } Err(e) => { // Expected for invalid peer addresses println!("✓ Correctly rejected invalid peer address: {}", e); } } } else { println!("⚠ Skipping test_add_peer_validation: No Mycelium node available"); } } #[tokio::test] async fn test_list_selected_routes() { if let Some(api_url) = get_available_api_url().await { let result = list_selected_routes(&api_url).await; match result { Ok(routes) => { // Routes should be an array or object assert!( routes.is_array() || routes.is_object(), "Routes should be a JSON array or object" ); println!("✓ Selected routes retrieved successfully"); } Err(e) => { println!("⚠ List selected routes request failed: {}", e); } } } else { println!("⚠ Skipping test_list_selected_routes: No Mycelium node available"); } } #[tokio::test] async fn test_list_fallback_routes() { if let Some(api_url) = get_available_api_url().await { let result = list_fallback_routes(&api_url).await; match result { Ok(routes) => { // Routes should be an array or object assert!( routes.is_array() || routes.is_object(), "Routes should be a JSON array or object" ); println!("✓ Fallback routes retrieved successfully"); } Err(e) => { println!("⚠ List fallback routes request failed: {}", e); } } } else { println!("⚠ Skipping test_list_fallback_routes: No Mycelium node available"); } } #[tokio::test] async fn test_send_message_validation() { if let Some(api_url) = get_available_api_url().await { // Test message sending with invalid destination let invalid_destination = "invalid-destination"; let topic = "test_topic"; let message = "test message"; let deadline = Some(Duration::from_secs(1)); let result = send_message(&api_url, invalid_destination, topic, message, deadline).await; // This should fail with invalid destination match result { Ok(response) => { // Some implementations might accept any destination format println!("✓ Send message response: {:?}", response); } Err(e) => { // Expected for invalid destinations println!("✓ Correctly rejected invalid destination: {}", e); } } } else { println!("⚠ Skipping test_send_message_validation: No Mycelium node available"); } } #[tokio::test] async fn test_receive_messages_timeout() { if let Some(api_url) = get_available_api_url().await { let topic = "non_existent_topic"; let deadline = Some(Duration::from_secs(1)); // Short timeout let result = receive_messages(&api_url, topic, deadline).await; match result { Ok(messages) => { // Should return empty or no messages for non-existent topic println!("✓ Receive messages completed: {:?}", messages); } Err(e) => { // Timeout or no messages is acceptable println!("✓ Receive messages handled correctly: {}", e); } } } else { println!("⚠ Skipping test_receive_messages_timeout: No Mycelium node available"); } } #[tokio::test] async fn test_error_handling_malformed_url() { let malformed_url = "not-a-url"; let result = get_node_info(malformed_url).await; assert!(result.is_err(), "Should fail with malformed URL"); let error = result.unwrap_err(); assert!( error.contains("Failed to send request"), "Error should indicate request failure: {}", error ); println!("✓ Correctly handled malformed URL: {}", error); } #[tokio::test] async fn test_base64_encoding_in_messages() { // Test that our message functions properly handle base64 encoding // This is a unit test that doesn't require a running Mycelium node let topic = "test/topic"; let message = "Hello, Mycelium!"; // Test base64 encoding directly use base64::{engine::general_purpose, Engine as _}; let encoded_topic = general_purpose::STANDARD.encode(topic); let encoded_message = general_purpose::STANDARD.encode(message); assert!( !encoded_topic.is_empty(), "Encoded topic should not be empty" ); assert!( !encoded_message.is_empty(), "Encoded message should not be empty" ); // Verify we can decode back let decoded_topic = general_purpose::STANDARD.decode(&encoded_topic).unwrap(); let decoded_message = general_purpose::STANDARD.decode(&encoded_message).unwrap(); assert_eq!(String::from_utf8(decoded_topic).unwrap(), topic); assert_eq!(String::from_utf8(decoded_message).unwrap(), message); println!("✓ Base64 encoding/decoding works correctly"); }