mechanics-core 0.2.2

mechanics automation framework (core)
Documentation
use super::super::*;
use crate::internal::http::EndpointHttpHeaders;
use serde_json::json;

#[test]
fn endpoint_deserializes_retry_policy_from_json() {
    let endpoint: HttpEndpoint = serde_json::from_value(json!({
        "method": "get",
        "url_template": "https://example.com/{id}",
        "url_param_specs": { "id": {} },
        "retry_policy": {
            "max_attempts": 4,
            "base_backoff_ms": 25,
            "max_backoff_ms": 200,
            "max_retry_delay_ms": 500,
            "rate_limit_backoff_ms": 75,
            "retry_on_io_errors": true,
            "retry_on_timeout": true,
            "respect_retry_after": true,
            "retry_on_status": [429, 503]
        }
    }))
    .expect("endpoint with retry policy should deserialize");

    endpoint
        .validate_config()
        .expect("retry policy values should validate");
}

#[test]
fn endpoint_rejects_invalid_retry_policy() {
    let err = serde_json::from_value::<MechanicsConfig>(json!({
        "endpoints": {
            "bad": {
                "method": "get",
                "url_template": "https://example.com/{id}",
                "url_param_specs": { "id": {} },
                "retry_policy": {
                    "max_attempts": 0
                }
            }
        }
    }))
    .expect_err("invalid retry policy should fail config parsing");

    assert!(
        err.to_string()
            .contains("retry_policy.max_attempts must be > 0")
    );
}

#[test]
fn endpoint_rejects_unknown_retry_policy_fields() {
    let err = serde_json::from_value::<MechanicsConfig>(json!({
        "endpoints": {
            "bad": {
                "method": "get",
                "url_template": "https://example.com/{id}",
                "url_param_specs": { "id": {} },
                "retry_policy": {
                    "max_attempts": 2,
                    "unknown": true
                }
            }
        }
    }))
    .expect_err("unknown retry_policy fields should fail config parsing");

    assert!(err.to_string().contains("unknown field"));
}

#[test]
fn endpoint_rejects_zero_max_retry_delay() {
    let err = serde_json::from_value::<MechanicsConfig>(json!({
        "endpoints": {
            "bad": {
                "method": "get",
                "url_template": "https://example.com/{id}",
                "url_param_specs": { "id": {} },
                "retry_policy": {
                    "max_attempts": 2,
                    "max_retry_delay_ms": 0
                }
            }
        }
    }))
    .expect_err("zero max_retry_delay_ms should fail config parsing");

    assert!(
        err.to_string()
            .contains("retry_policy.max_retry_delay_ms must be > 0")
    );
}

#[test]
fn retry_policy_uses_retry_after_for_rate_limit() {
    let policy = EndpointRetryPolicy {
        max_attempts: 3,
        base_backoff_ms: 10,
        max_backoff_ms: 100,
        max_retry_delay_ms: 5_000,
        rate_limit_backoff_ms: 250,
        retry_on_io_errors: true,
        retry_on_timeout: true,
        respect_retry_after: true,
        retry_on_status: vec![429],
    };

    let mut headers = EndpointHttpHeaders::new();
    headers.insert("retry-after", "2");
    let delay = policy.retry_delay_for_status(429, &headers, 1);
    assert_eq!(delay, std::time::Duration::from_secs(2));
}

#[test]
fn retry_policy_falls_back_to_rate_limit_backoff_without_retry_after() {
    let policy = EndpointRetryPolicy {
        max_attempts: 3,
        base_backoff_ms: 10,
        max_backoff_ms: 100,
        max_retry_delay_ms: 5_000,
        rate_limit_backoff_ms: 250,
        retry_on_io_errors: true,
        retry_on_timeout: true,
        respect_retry_after: true,
        retry_on_status: vec![429],
    };

    let delay = policy.retry_delay_for_status(429, &EndpointHttpHeaders::new(), 1);
    assert_eq!(delay, std::time::Duration::from_millis(250));
}

#[test]
fn retry_policy_backoff_caps_to_max_delay() {
    let policy = EndpointRetryPolicy {
        max_attempts: 5,
        base_backoff_ms: 200,
        max_backoff_ms: 10_000,
        max_retry_delay_ms: 500,
        rate_limit_backoff_ms: 100,
        retry_on_io_errors: true,
        retry_on_timeout: true,
        respect_retry_after: true,
        retry_on_status: vec![500],
    };

    let delay = policy.retry_delay_for_status(500, &EndpointHttpHeaders::new(), 4);
    assert_eq!(delay, std::time::Duration::from_millis(500));
}