#![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};
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)
}
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
}
#[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;
}
#[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;
}
#[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 {
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;
}
#[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}"
);
}
}