oxihttp 0.1.3

OxiHTTP Pure-Rust HTTP facade for the COOLJAPAN ecosystem.
Documentation
//! HTTP/3 integration tests — client + server roundtrip via `oxihttp::h3`.
//!
//! These tests require:
//! - `feature = "h3"` on the `oxihttp` crate.
//! - A QUIC-capable rustls `CryptoProvider` (supplied by `oxiquic-crypto`).
//! - A self-signed Ed25519 certificate (generated by `oxitls::rcgen_bridge`).
//!
//! The server binds to `127.0.0.1:0` (OS-assigned ephemeral port); the client
//! reads the port and connects.  Tests are isolated and leave no global state.

#![cfg(feature = "h3")]

use std::net::SocketAddr;
use std::sync::Arc;

use bytes::Bytes;
use oxihttp::h3::{H3ConnectionBuilder, H3Request, H3Response, H3Server};
use oxiquic_crypto::quic_crypto_provider;
use oxitls::rcgen_bridge::generate_self_signed_ed25519;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use rustls::{ClientConfig, RootCertStore, ServerConfig};

// ─────────────────────────────────────────────────────────────────────────────
// Helper: build a TLS client/server config pair from a single self-signed cert
// ─────────────────────────────────────────────────────────────────────────────

/// Build matching TLS server + client configs for `localhost`.
///
/// Uses `quic_crypto_provider()` which carries the QUIC header/packet
/// protection suites required by QUIC (RFC 9001).
fn make_tls_pair() -> (ServerConfig, ClientConfig) {
    let ck = generate_self_signed_ed25519(&["localhost"]).expect("generate cert");
    let cert_der = CertificateDer::from(ck.cert_der.clone());
    let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(ck.pkcs8_der.clone()));

    let provider = Arc::new(quic_crypto_provider());

    let mut roots = RootCertStore::empty();
    roots.add(cert_der.clone()).expect("trust self-signed cert");

    let server = ServerConfig::builder_with_provider(provider.clone())
        .with_protocol_versions(&[&rustls::version::TLS13])
        .expect("TLS 1.3 server")
        .with_no_client_auth()
        .with_single_cert(vec![cert_der], key_der)
        .expect("server cert");

    let client = ClientConfig::builder_with_provider(provider)
        .with_protocol_versions(&[&rustls::version::TLS13])
        .expect("TLS 1.3 client")
        .with_root_certificates(roots)
        .with_no_client_auth();

    (server, client)
}

/// Spawn an `H3Server` and return its local address.
///
/// The server routes every incoming request through `handler`.  The JoinHandle
/// is detached (the server drives until the endpoint is closed or the test
/// process exits).
async fn spawn_h3_server<F, Fut>(server_tls: ServerConfig, handler: F) -> SocketAddr
where
    F: Fn(H3Request, Bytes) -> Fut + Send + Sync + 'static,
    Fut: std::future::Future<Output = H3Response> + Send + 'static,
{
    let server = H3Server::bind("127.0.0.1:0".parse().expect("valid addr"), server_tls)
        .await
        .expect("H3Server::bind");
    let addr = server.local_addr().expect("local addr");
    tokio::spawn(server.serve(handler));
    addr
}

// ─────────────────────────────────────────────────────────────────────────────
// Test: simple GET returns 200 with expected body
// ─────────────────────────────────────────────────────────────────────────────

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_h3_get_returns_200() {
    let (server_tls, client_tls) = make_tls_pair();

    let addr = spawn_h3_server(server_tls, |_req, _body| async move {
        H3Response::new(200).with_body(b"hello h3".to_vec())
    })
    .await;

    let mut conn = H3ConnectionBuilder::new("localhost")
        .with_tls_config(client_tls)
        .connect(addr)
        .await
        .expect("connect");

    let resp = conn.get("https://localhost/").await.expect("GET");
    assert_eq!(resp.status(), 200, "expected 200 OK");
    assert_eq!(resp.body_bytes(), b"hello h3", "unexpected body");

    let _ = conn.close().await;
}

// ─────────────────────────────────────────────────────────────────────────────
// Test: POST echoes body
// ─────────────────────────────────────────────────────────────────────────────

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_h3_post_echoes_body() {
    let (server_tls, client_tls) = make_tls_pair();

    let addr = spawn_h3_server(server_tls, |_req, body| async move {
        H3Response::new(200).with_body(body.to_vec())
    })
    .await;

    let mut conn = H3ConnectionBuilder::new("localhost")
        .with_tls_config(client_tls)
        .connect(addr)
        .await
        .expect("connect");

    let resp = conn
        .post("https://localhost/echo", Bytes::from("payload"))
        .await
        .expect("POST");
    assert_eq!(resp.status(), 200, "expected 200 OK");
    assert_eq!(resp.body_bytes(), b"payload", "body should echo");

    let _ = conn.close().await;
}

// ─────────────────────────────────────────────────────────────────────────────
// Test: custom request method and headers round-trip
// ─────────────────────────────────────────────────────────────────────────────

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_h3_custom_request_with_headers() {
    let (server_tls, client_tls) = make_tls_pair();

    let addr = spawn_h3_server(server_tls, |req, _body| async move {
        // Mirror the value of the incoming `x-custom` header back in the response.
        let custom_val = req
            .headers()
            .iter()
            .find(|(k, _)| k == "x-custom")
            .map(|(_, v)| v.clone())
            .unwrap_or_default();
        H3Response::new(200)
            .with_header("x-echo", custom_val)
            .with_body(b"ok".to_vec())
    })
    .await;

    let mut conn = H3ConnectionBuilder::new("localhost")
        .with_tls_config(client_tls)
        .connect(addr)
        .await
        .expect("connect");

    let req = H3Request::get("https://localhost/").with_header("x-custom", "test-value");
    let resp = conn.request(req, None).await.expect("request");

    assert_eq!(resp.status(), 200);
    assert_eq!(
        resp.header("x-echo"),
        Some("test-value"),
        "echo header mismatch"
    );

    let _ = conn.close().await;
}

// ─────────────────────────────────────────────────────────────────────────────
// Test: missing TLS config returns H3 error before connecting
// ─────────────────────────────────────────────────────────────────────────────

#[tokio::test]
async fn test_h3_missing_tls_config_returns_error() {
    let addr: SocketAddr = "127.0.0.1:9999".parse().expect("valid addr");
    let result = H3ConnectionBuilder::new("localhost").connect(addr).await;

    assert!(result.is_err(), "expected error when TLS config is missing");
    if let Err(err) = result {
        assert!(
            err.to_string().contains("TLS config") || err.to_string().contains("H3"),
            "unexpected error message: {err}"
        );
    }
}