xecute-cli 0.1.8

Project and resource generator for Xecute
//! HTTP behavior tests for limits, health probes, CORS, errors, and concurrency.

use super::support::{app, request};
use crate::{
    configs::{Config, CorsConfig},
    server::router,
};
use axum::{
    Json, Router,
    body::{Body, to_bytes},
    http::{HeaderValue, Method, StatusCode, header},
    routing::get,
    routing::post,
};
use std::sync::Arc;
use tower::ServiceExt;

#[tokio::test]
async fn successful_and_error_responses_have_ids_and_security_headers() {
    let app = app(&Config::default());
    for (path, status) in [
        ("/items", StatusCode::OK),
        ("/missing?secret=private", StatusCode::NOT_FOUND),
    ] {
        let response = app
            .clone()
            .oneshot(request(path, "127.0.0.1"))
            .await
            .unwrap();
        assert_eq!(response.status(), status);
        let id = response.headers()["x-request-id"]
            .to_str()
            .unwrap()
            .to_owned();
        assert!(uuid::Uuid::parse_str(&id).is_ok());
        assert_eq!(response.headers()["x-content-type-options"], "nosniff");
        if status.is_client_error() {
            let body = to_bytes(response.into_body(), 4096).await.unwrap();
            let body: serde_json::Value = serde_json::from_slice(&body).unwrap();
            assert_eq!(body["error"]["request_id"], id);
            assert_eq!(body["error"]["code"], "not_found");
        }
    }
}

#[tokio::test]
async fn rate_limits_socket_peers_and_ignores_spoofed_forwarding_headers() {
    let app = app(&Config {
        rate_limit_requests: 1,
        ..Config::default()
    });
    assert_eq!(
        app.clone()
            .oneshot(request("/items", "127.0.0.1"))
            .await
            .unwrap()
            .status(),
        StatusCode::OK
    );
    let mut spoofed = request("/items", "127.0.0.1");
    spoofed
        .headers_mut()
        .insert("x-forwarded-for", HeaderValue::from_static("203.0.113.9"));
    let response = app.clone().oneshot(spoofed).await.unwrap();
    assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
    assert!(response.headers().contains_key(header::RETRY_AFTER));
    assert!(response.headers().contains_key("x-request-id"));
    assert_eq!(
        app.clone()
            .oneshot(request("/items", "127.0.0.2"))
            .await
            .unwrap()
            .status(),
        StatusCode::OK
    );
    for path in ["/health/live", "/health/ready"] {
        assert_eq!(
            app.clone()
                .oneshot(request(path, "127.0.0.1"))
                .await
                .unwrap()
                .status(),
            StatusCode::OK
        );
    }
}

#[tokio::test]
async fn timeout_and_panic_return_safe_json_errors() {
    let config = Config {
        request_timeout: std::time::Duration::from_millis(5),
        ..Config::default()
    };
    let app = router(
        Router::new()
            .route(
                "/slow",
                get(|| async {
                    tokio::time::sleep(std::time::Duration::from_secs(1)).await;
                    StatusCode::OK
                }),
            )
            .route(
                "/panic",
                get(|| async {
                    panic!("private internal details");
                    #[allow(unreachable_code)]
                    StatusCode::OK
                }),
            ),
        &config,
    );
    for (path, status, code) in [
        ("/slow", StatusCode::GATEWAY_TIMEOUT, "request_timeout"),
        (
            "/panic",
            StatusCode::INTERNAL_SERVER_ERROR,
            "internal_error",
        ),
    ] {
        let response = app
            .clone()
            .oneshot(request(path, "127.0.0.1"))
            .await
            .unwrap();
        assert_eq!(response.status(), status);
        let body = to_bytes(response.into_body(), 4096).await.unwrap();
        let body: serde_json::Value = serde_json::from_slice(&body).unwrap();
        assert_eq!(body["error"]["code"], code);
        assert!(!body.to_string().contains("private internal details"));
    }
}

#[tokio::test]
async fn rejects_declared_and_streamed_oversized_bodies() {
    let app = router(
        Router::new().route("/echo", post(|body: axum::body::Bytes| async move { body })),
        &Config {
            body_limit: 4,
            ..Config::default()
        },
    );
    for declared in [false, true] {
        let mut req = request("/echo", "127.0.0.1");
        *req.method_mut() = Method::POST;
        *req.body_mut() = Body::from("12345");
        if declared {
            req.headers_mut()
                .insert(header::CONTENT_LENGTH, HeaderValue::from_static("5"));
        }
        let response = app.clone().oneshot(req).await.unwrap();
        assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
        assert_eq!(response.headers()[header::CONTENT_TYPE], "application/json");
    }
}

#[tokio::test]
async fn cors_allows_only_configured_origins() {
    let app = app(&Config {
        cors: CorsConfig {
            origins: vec![HeaderValue::from_static("https://example.com")],
            ..CorsConfig::default()
        },
        ..Config::default()
    });
    for origin in ["https://example.com", "https://untrusted.example"] {
        let mut req = request("/items", "127.0.0.1");
        *req.method_mut() = Method::OPTIONS;
        req.headers_mut()
            .insert(header::ORIGIN, origin.parse().unwrap());
        req.headers_mut().insert(
            header::ACCESS_CONTROL_REQUEST_METHOD,
            HeaderValue::from_static("GET"),
        );
        let response = app.clone().oneshot(req).await.unwrap();
        assert_eq!(
            response
                .headers()
                .contains_key(header::ACCESS_CONTROL_ALLOW_ORIGIN),
            origin == "https://example.com"
        );
        assert!(response.headers().contains_key("x-request-id"));
    }
    let app = self::app(&Config::default());
    let mut req = request("/items", "127.0.0.1");
    req.headers_mut().insert(
        header::ORIGIN,
        HeaderValue::from_static("https://example.com"),
    );
    assert!(
        !app.oneshot(req)
            .await
            .unwrap()
            .headers()
            .contains_key(header::ACCESS_CONTROL_ALLOW_ORIGIN)
    );
}

#[tokio::test]
async fn concurrency_rejection_preserves_health_and_permits_are_released() {
    let entered = Arc::new(tokio::sync::Notify::new());
    let release = Arc::new(tokio::sync::Notify::new());
    let handler_entered = entered.clone();
    let handler_release = release.clone();
    let app = router(
        Router::new().route(
            "/work",
            get(move || {
                let entered = handler_entered.clone();
                let release = handler_release.clone();
                async move {
                    entered.notify_one();
                    release.notified().await;
                    StatusCode::OK
                }
            }),
        ),
        &Config {
            max_in_flight: 1,
            ..Config::default()
        },
    );
    let first = tokio::spawn(app.clone().oneshot(request("/work", "127.0.0.1")));
    tokio::time::timeout(std::time::Duration::from_secs(1), entered.notified())
        .await
        .unwrap();
    let response = app
        .clone()
        .oneshot(request("/work", "127.0.0.2"))
        .await
        .unwrap();
    assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
    assert!(response.headers().contains_key(header::RETRY_AFTER));
    assert_eq!(
        app.clone()
            .oneshot(request("/health/live", "127.0.0.2"))
            .await
            .unwrap()
            .status(),
        StatusCode::OK
    );
    release.notify_one();
    assert_eq!(first.await.unwrap().unwrap().status(), StatusCode::OK);
    release.notify_one();
    assert_eq!(
        app.oneshot(request("/work", "127.0.0.2"))
            .await
            .unwrap()
            .status(),
        StatusCode::OK
    );
}

#[tokio::test]
async fn json_rejections_and_method_errors_use_the_error_envelope() {
    let app = router(
        Router::new().route(
            "/json",
            post(|Json(value): Json<serde_json::Value>| async move { Json(value) }),
        ),
        &Config::default(),
    );
    let mut req = request("/json", "127.0.0.1");
    *req.method_mut() = Method::POST;
    *req.body_mut() = Body::from("{broken");
    req.headers_mut().insert(
        header::CONTENT_TYPE,
        HeaderValue::from_static("application/json"),
    );
    let response = app.clone().oneshot(req).await.unwrap();
    assert_eq!(response.status(), StatusCode::BAD_REQUEST);
    let body = to_bytes(response.into_body(), 4096).await.unwrap();
    assert_eq!(
        serde_json::from_slice::<serde_json::Value>(&body).unwrap()["error"]["code"],
        "bad_request"
    );
    let response = app.oneshot(request("/json", "127.0.0.1")).await.unwrap();
    assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
    assert!(response.headers().contains_key(header::ALLOW));
}