solidmcp 0.4.0

A high-level Rust toolkit for building Model Context Protocol (MCP) servers with type safety and minimal boilerplate. Supports tools, resources, and prompts with automatic JSON schema generation.
Documentation
//! Tests for updated dependency functionality and session management

#[cfg(test)]
mod tests {
    use crate::http::session::{extract_session_id_from_cookie, generate_session_id};
    use std::collections::HashSet;

    #[test]
    fn test_session_id_generation_with_updated_rand() {
        // Test that session ID generation works with the updated rand crate (0.9.x)
        let session_id = generate_session_id();

        // Session ID should be 32 characters long (from Alphanumeric)
        assert_eq!(session_id.len(), 32);

        // Should only contain alphanumeric characters (letters and digits)
        assert!(session_id.chars().all(|c| c.is_ascii_alphanumeric()));
    }

    #[test]
    fn test_session_id_uniqueness() {
        // Generate multiple session IDs to ensure they're unique
        let mut session_ids = HashSet::new();

        for _ in 0..1000 {
            let session_id = generate_session_id();
            assert!(
                session_ids.insert(session_id),
                "Session ID collision detected"
            );
        }

        assert_eq!(session_ids.len(), 1000);
    }

    #[test]
    fn test_session_id_randomness_distribution() {
        // Test that generated session IDs have good entropy distribution
        let mut char_counts = std::collections::HashMap::new();

        // Generate many session IDs and count character frequency
        for _ in 0..100 {
            let session_id = generate_session_id();
            for ch in session_id.chars() {
                *char_counts.entry(ch).or_insert(0) += 1;
            }
        }

        // Should have reasonable distribution across all hex characters
        assert!(char_counts.len() >= 10); // Should use most hex characters

        // No character should be extremely over-represented
        let total_chars: usize = char_counts.values().sum();
        for &count in char_counts.values() {
            let frequency = count as f64 / total_chars as f64;
            assert!(frequency < 0.15); // No character > 15% frequency
        }
    }

    #[test]
    fn test_cookie_parsing_with_various_formats() {
        // Test various cookie header formats (function expects mcp_session= not session_id=)
        let test_cases = vec![
            (
                Some("mcp_session=ABC123DEF456".to_string()),
                Some("ABC123DEF456".to_string()),
            ),
            (
                Some("other=value; mcp_session=XYZ789; another=test".to_string()),
                Some("XYZ789".to_string()),
            ),
            (Some("mcp_session=".to_string()), Some("".to_string())), // Empty value
            (Some("MCP_SESSION=should_not_match".to_string()), None), // Case sensitive
            (
                Some("mcp_session=value_with_equals=sign".to_string()),
                Some("value_with_equals=sign".to_string()),
            ),
            (
                Some("mcp_session=value; path=/; secure".to_string()),
                Some("value".to_string()),
            ),
            (
                Some("multiple=cookies; mcp_session=target_value; more=data".to_string()),
                Some("target_value".to_string()),
            ),
            (Some("no_session_cookie=present".to_string()), None),
            (Some("".to_string()), None), // Empty cookie header
            (None, None),                 // No cookie header
        ];

        for (cookie_header, expected) in test_cases {
            let result = extract_session_id_from_cookie(&cookie_header);
            assert_eq!(result, expected, "Failed for cookie: '{:?}'", cookie_header);
        }
    }

    #[test]
    fn test_session_id_generation_format() {
        let session_id = generate_session_id();

        // Should be exactly 32 characters from Alphanumeric distribution
        assert_eq!(session_id.len(), 32);

        // Should only contain alphanumeric characters
        assert!(session_id.chars().all(|c| c.is_ascii_alphanumeric()));
    }

    #[test]
    fn test_concurrent_session_id_generation() {
        use std::sync::Arc;
        use std::sync::Mutex;
        use std::thread;

        let session_ids = Arc::new(Mutex::new(HashSet::new()));
        let mut handles = vec![];

        // Generate session IDs concurrently from multiple threads
        for _ in 0..10 {
            let session_ids_clone = Arc::clone(&session_ids);
            let handle = thread::spawn(move || {
                for _ in 0..100 {
                    let session_id = generate_session_id();
                    let mut set = session_ids_clone.lock().unwrap();
                    assert!(
                        set.insert(session_id),
                        "Duplicate session ID in concurrent test"
                    );
                }
            });
            handles.push(handle);
        }

        // Wait for all threads to complete
        for handle in handles {
            handle.join().unwrap();
        }

        // Should have generated 1000 unique session IDs
        let final_set = session_ids.lock().unwrap();
        assert_eq!(final_set.len(), 1000);
    }

    #[test]
    fn test_session_id_format_compliance() {
        // Test that session IDs meet security requirements
        for _ in 0..100 {
            let session_id = generate_session_id();

            // Should be exactly 32 characters (alphanumeric)
            assert_eq!(session_id.len(), 32);

            // Should only contain alphanumeric characters
            assert!(session_id.chars().all(|c| c.is_ascii_alphanumeric()));

            // Should not be all the same character
            let first_char = session_id.chars().next().unwrap();
            assert!(!session_id.chars().all(|c| c == first_char));

            // Should have reasonable entropy (not too many repeated characters)
            let unique_chars: HashSet<char> = session_id.chars().collect();
            assert!(unique_chars.len() >= 5); // At least 5 different characters
        }
    }

    #[test]
    fn test_cookie_edge_cases_and_security() {
        // Test potential security issues with cookie parsing
        let long_value = format!("session_id={}", "A".repeat(10000));
        let malicious_cases = vec![
            "session_id=<script>alert('xss')</script>", // XSS attempt
            "session_id=../../etc/passwd",              // Path traversal attempt
            &long_value,                                // Extremely long value
            "session_id=\r\n\r\nHTTP/1.1 200 OK",       // HTTP response injection
            "session_id=\u{0000}\u{0000}\u{0000}\u{0000}", // Null bytes
            "session_id=unicode_🦀_chars",              // Unicode characters
        ];

        for cookie_header in malicious_cases {
            let cookie_opt = Some(cookie_header.to_string());
            let result = extract_session_id_from_cookie(&cookie_opt);

            // Should either reject malicious input or sanitize it
            if let Some(session_id) = result {
                // If accepted, should not contain dangerous characters
                assert!(!session_id.contains("<script>"));
                assert!(!session_id.contains("../"));
                assert!(!session_id.contains("\r\n"));
                assert!(!session_id.contains('\0'));

                // Should be reasonable length
                assert!(session_id.len() < 1000);
            }
        }
    }

    #[test]
    fn test_updated_rand_api_compatibility() {
        // Ensure we're using the updated rand 0.9 API correctly
        use rand::{random, Rng};

        // Test direct random generation
        let random_u32: u32 = random();
        let random_u64: u64 = random();

        // Should generate different values
        assert_ne!(random_u32 as u64, random_u64);

        // Test rng usage (updated API)
        let mut rng = rand::rng();
        let value1: u32 = rng.random();
        let value2: u32 = rng.random();

        // Should generate different values
        assert_ne!(value1, value2);

        // Test range generation
        let range_value = rng.random_range(0..100);
        assert!(range_value < 100);
    }

    #[test]
    fn test_session_lifecycle_integration() {
        // Test a complete session lifecycle
        let session_id = generate_session_id();

        // Simulate cookie creation
        let cookie_header = format!("mcp_session={}", session_id);
        let cookie_opt = Some(cookie_header);

        // Test extraction
        let extracted_id = extract_session_id_from_cookie(&cookie_opt);
        assert_eq!(extracted_id, Some(session_id.clone()));

        // Test that session ID format is correct
        assert_eq!(session_id.len(), 32);
        assert!(session_id.chars().all(|c| c.is_ascii_alphanumeric()));
    }

    #[tokio::test]
    async fn test_session_management_under_load() {
        // Test session management under concurrent load
        use std::sync::Arc;
        use std::sync::Mutex;
        use tokio::task;

        let session_map = Arc::new(Mutex::new(std::collections::HashMap::new()));
        let mut tasks = vec![];

        // Simulate multiple concurrent clients creating sessions
        for client_id in 0..50 {
            let session_map_clone = Arc::clone(&session_map);
            let task = task::spawn(async move {
                for _ in 0..10 {
                    let session_id = generate_session_id();
                    let created_at = std::time::SystemTime::now();

                    let mut map = session_map_clone.lock().unwrap();
                    map.insert(session_id, (client_id, created_at));
                }
            });
            tasks.push(task);
        }

        // Wait for all tasks to complete
        for task in tasks {
            task.await.unwrap();
        }

        // Should have created 500 unique sessions
        let final_map = session_map.lock().unwrap();
        assert_eq!(final_map.len(), 500);

        // All session IDs should be unique
        let session_ids: HashSet<_> = final_map.keys().collect();
        assert_eq!(session_ids.len(), 500);
    }
}