cloudllm 0.15.9

A batteries-included Rust toolkit for building intelligent agents with LLM integration, multi-protocol tool support, multi-agent orchestration, and MentisDB-backed durable memory.
Documentation
//! Comprehensive test suite for the HTTP Client tool
//!
//! Tests cover:
//! - HTTP methods (GET, POST, PUT, DELETE, PATCH, HEAD)
//! - Headers and query parameters
//! - Authentication (basic auth, bearer tokens)
//! - Domain security (allowlist/blocklist)
//! - Timeout and size limits
//! - JSON parsing
//! - Error handling

use cloudllm::tools::HttpClient;

fn assert_domain_rejected(
    result: Result<cloudllm::tools::http_client::HttpResponse, impl std::fmt::Display>,
) {
    assert!(result.is_err());
    let message = result.unwrap_err().to_string();
    assert!(
        message.contains("blocked")
            || message.contains("not allowed")
            || message.contains("reserved/private range")
            || message.contains("Could not resolve host"),
        "unexpected domain rejection message: {}",
        message
    );
}

#[tokio::test]
async fn test_http_client_creation() {
    let _client = HttpClient::new();
    // Should create without errors
    let _default_client = HttpClient::default();
    // Both should work identically
}

#[tokio::test]
async fn test_query_parameter_building() {
    let mut client = HttpClient::new();
    client.allow_domain("example.com");
    client.with_query_param("key1", "value1");
    client.with_query_param("key2", "value2");
    client.with_query_param("key3", "value3");
    // Verify that multiple calls to with_query_param work
}

#[tokio::test]
async fn test_header_building() {
    let mut client = HttpClient::new();
    client.with_header("X-Custom", "header-value");
    client.with_header("Authorization", "Bearer token123");
    client.with_header("User-Agent", "MyAgent/1.0");
    client.with_header("Accept", "application/json");
    // Verify that multiple header additions work
}

#[tokio::test]
async fn test_basic_auth_encoding() {
    let mut client = HttpClient::new();
    client.with_basic_auth("user", "pass");
    // Verify basic auth was set (would be used in actual request)
}

#[tokio::test]
async fn test_timeout_configuration() {
    let mut client = HttpClient::new();
    let timeout = std::time::Duration::from_secs(10);
    client.with_timeout(timeout);
    client.with_timeout(std::time::Duration::from_secs(5));
    // Verify timeout configuration works
}

#[tokio::test]
async fn test_max_response_size_configuration() {
    let mut client = HttpClient::new();
    client.with_max_response_size(50 * 1024 * 1024); // 50MB
    client.with_max_response_size(100 * 1024 * 1024); // 100MB
                                                      // Verify size limit configuration works
}

#[tokio::test]
async fn test_domain_allowlist() {
    let mut client = HttpClient::new();
    client.allow_domain("api.example.com");
    client.allow_domain("api.partner.com");

    // Allowed domains should pass domain check
    // Result depends on network availability
    let _ = client.get("https://api.example.com/test").await;
    // Just verify no domain error was thrown before network operation
}

#[tokio::test]
async fn test_domain_blocklist() {
    let mut client = HttpClient::new();
    client.deny_domain("malicious.com");
    client.deny_domain("phishing.net");

    // Blocked domain should fail with domain error
    assert_domain_rejected(client.get("https://malicious.com/data").await);

    // Another blocked domain should also fail
    assert_domain_rejected(client.get("https://phishing.net/data").await);
}

#[tokio::test]
async fn test_domain_extraction_https() {
    let mut client = HttpClient::new();
    client.allow_domain("api.example.com");

    // Should accept HTTPS URLs with allowed domain
    let _ = client.get("https://api.example.com/path/to/resource").await;
}

#[tokio::test]
async fn test_domain_extraction_http() {
    let mut client = HttpClient::new();
    client.allow_domain("example.com");

    // Should accept HTTP URLs with allowed domain
    let _ = client.get("http://example.com/data").await;
}

#[tokio::test]
async fn test_domain_extraction_with_port() {
    let mut client = HttpClient::new();
    client.allow_domain("api.example.com");

    // Should extract domain correctly even with port
    let _ = client.get("https://api.example.com:8080/data").await;
}

#[tokio::test]
async fn test_domain_extraction_with_path() {
    let mut client = HttpClient::new();
    client.allow_domain("example.com");

    // Should extract domain correctly with path
    let _ = client.get("https://example.com/api/v1/users").await;
}

#[tokio::test]
async fn test_blocklist_takes_precedence() {
    let mut client = HttpClient::new();
    client.allow_domain("evil.com"); // Add to allowlist
    client.deny_domain("evil.com"); // But also block it

    // Blocklist should take precedence
    assert_domain_rejected(client.get("https://evil.com/data").await);
}

#[tokio::test]
async fn test_empty_allowlist_allows_all() {
    let client = HttpClient::new();
    // No domains in allowlist, so all should be checked only against blocklist

    let _ = client.get("https://any-domain.com/data").await;
    // No domain error should be thrown (request may fail due to connection)
}

#[tokio::test]
async fn test_http_response_success_check() {
    use cloudllm::tools::http_client::HttpResponse;

    let response_2xx = HttpResponse {
        status: 200,
        headers: std::collections::HashMap::new(),
        body: "OK".to_string(),
    };
    assert!(response_2xx.is_success());
    assert!(!response_2xx.is_client_error());
    assert!(!response_2xx.is_server_error());

    let response_201 = HttpResponse {
        status: 201,
        headers: std::collections::HashMap::new(),
        body: "Created".to_string(),
    };
    assert!(response_201.is_success());
}

#[tokio::test]
async fn test_http_response_client_error_check() {
    use cloudllm::tools::http_client::HttpResponse;

    let response_404 = HttpResponse {
        status: 404,
        headers: std::collections::HashMap::new(),
        body: "Not Found".to_string(),
    };
    assert!(!response_404.is_success());
    assert!(response_404.is_client_error());
    assert!(!response_404.is_server_error());

    let response_400 = HttpResponse {
        status: 400,
        headers: std::collections::HashMap::new(),
        body: "Bad Request".to_string(),
    };
    assert!(response_400.is_client_error());
}

#[tokio::test]
async fn test_http_response_server_error_check() {
    use cloudllm::tools::http_client::HttpResponse;

    let response_500 = HttpResponse {
        status: 500,
        headers: std::collections::HashMap::new(),
        body: "Internal Server Error".to_string(),
    };
    assert!(!response_500.is_success());
    assert!(!response_500.is_client_error());
    assert!(response_500.is_server_error());

    let response_502 = HttpResponse {
        status: 502,
        headers: std::collections::HashMap::new(),
        body: "Bad Gateway".to_string(),
    };
    assert!(response_502.is_server_error());
}

#[tokio::test]
async fn test_http_response_json_parsing() {
    use cloudllm::tools::http_client::HttpResponse;

    let json_body = r#"{"name": "Alice", "age": 30, "active": true}"#;
    let response = HttpResponse {
        status: 200,
        headers: std::collections::HashMap::new(),
        body: json_body.to_string(),
    };

    let parsed = response.json().unwrap();
    assert_eq!(parsed["name"], "Alice");
    assert_eq!(parsed["age"], 30);
    assert_eq!(parsed["active"], true);
}

#[tokio::test]
async fn test_http_response_json_parsing_array() {
    use cloudllm::tools::http_client::HttpResponse;

    let json_body = r#"[{"id": 1}, {"id": 2}, {"id": 3}]"#;
    let response = HttpResponse {
        status: 200,
        headers: std::collections::HashMap::new(),
        body: json_body.to_string(),
    };

    let parsed = response.json().unwrap();
    assert!(parsed.is_array());
    assert_eq!(parsed.as_array().unwrap().len(), 3);
}

#[tokio::test]
async fn test_http_response_json_parsing_failure() {
    use cloudllm::tools::http_client::HttpResponse;

    let invalid_json = "this is not json";
    let response = HttpResponse {
        status: 200,
        headers: std::collections::HashMap::new(),
        body: invalid_json.to_string(),
    };

    let parsed = response.json();
    assert!(parsed.is_err());
}

#[tokio::test]
async fn test_query_params_special_characters() {
    let mut client = HttpClient::new();
    client.allow_domain("example.com");
    client.with_query_param("search", "hello world");
    client.with_query_param("filter", "status=active&type=premium");

    // URL encoding should handle special characters
    // This test verifies the parameter building logic
}

#[tokio::test]
async fn test_builder_pattern_chainable() {
    let mut client = HttpClient::new();
    let _ = client
        .allow_domain("api.example.com")
        .with_header("Authorization", "Bearer token")
        .with_query_param("key", "value")
        .with_timeout(std::time::Duration::from_secs(10));

    // Verify chainable builder pattern works
}

#[tokio::test]
async fn test_multiple_domains_allowlist() {
    let mut client = HttpClient::new();
    client.allow_domain("api1.com");
    client.allow_domain("api2.com");
    client.allow_domain("api3.com");

    // All three should be allowed
    // (will fail with connection errors, not domain errors)
    let _ = client.get("https://api1.com/data").await;
    let _ = client.get("https://api2.com/data").await;
    let _ = client.get("https://api3.com/data").await;
}

#[tokio::test]
async fn test_multiple_domains_blocklist() {
    let mut client = HttpClient::new();
    client.deny_domain("bad1.com");
    client.deny_domain("bad2.com");
    client.deny_domain("bad3.com");

    // All three should be blocked
    let result1 = client.get("https://bad1.com/data").await;
    assert!(result1.is_err());

    let result2 = client.get("https://bad2.com/data").await;
    assert!(result2.is_err());

    let result3 = client.get("https://bad3.com/data").await;
    assert!(result3.is_err());
}

#[tokio::test]
async fn test_invalid_url_domain_extraction() {
    let mut client = HttpClient::new();
    client.allow_domain("example.com");

    // Malformed URL without scheme
    let result = client.get("example.com/data").await;
    assert!(result.is_err()); // Should fail on domain extraction

    // Malformed URL with only scheme
    let result = client.get("https://").await;
    assert!(result.is_err());
}

#[tokio::test]
async fn test_http_client_clone() {
    let mut client = HttpClient::new();
    client.allow_domain("example.com");
    client.with_header("X-Test", "value");

    let _cloned = client.clone();
    // Cloned client should have same settings
}

#[tokio::test]
async fn test_http_response_status_code_boundaries() {
    use cloudllm::tools::http_client::HttpResponse;

    // Test boundary values
    let status_199 = HttpResponse {
        status: 199,
        headers: std::collections::HashMap::new(),
        body: String::new(),
    };
    assert!(!status_199.is_success());

    let status_200 = HttpResponse {
        status: 200,
        headers: std::collections::HashMap::new(),
        body: String::new(),
    };
    assert!(status_200.is_success());

    let status_299 = HttpResponse {
        status: 299,
        headers: std::collections::HashMap::new(),
        body: String::new(),
    };
    assert!(status_299.is_success());

    let status_300 = HttpResponse {
        status: 300,
        headers: std::collections::HashMap::new(),
        body: String::new(),
    };
    assert!(!status_300.is_success());
}

#[tokio::test]
async fn test_default_timeout_is_30_seconds() {
    let _client = HttpClient::new();
    // Default should be 30 seconds (not directly testable without inspecting internals)
    let mut client_custom = HttpClient::new();
    client_custom.with_timeout(std::time::Duration::from_secs(5));
}

#[tokio::test]
async fn test_default_max_response_size_is_10mb() {
    let _client = HttpClient::new();
    // Default is 10MB, can verify by setting smaller size
    let mut client_small = HttpClient::new();
    client_small.with_max_response_size(1024); // 1KB
}