test(ai-gateway): pin the crypto provider to ring and prove the dial installs it

The two tests that shipped with the fix both called ensure_crypto_provider
themselves, so deleting the call from connect_upstream left the whole suite
green, and swapping ring for aws-lc-rs did too.

Adds an integration test, which gets its own process, that dials wss:// at a
local plain-TCP listener through the public Responses WebSocket entrypoint and
asserts an Err plus an installed provider. Without the install in the dial it
panics with the original CryptoProvider message. A unit test now compares the
installed provider's cipher suites and key-exchange groups against ring's, so
the choice of backend is pinned rather than assumed.

Also names tls12 in the workspace rustls features: it already arrives through
reqwest and tokio-rustls, so the graph is unchanged, but a direct dependency
should say it needs TLS 1.2 rather than inherit it.
This commit is contained in:
mateo-berri 2026-09-03 02:27:00 -07:00
parent c79d1d12ae
commit 2c08e7abf8
3 changed files with 81 additions and 5 deletions

View file

@ -26,7 +26,7 @@ pythonize = "0.29.0"
rand = "0.8"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls", "http2", "stream"] }
rstest = "0.26.1"
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha2 = "0.10"

View file

@ -8,7 +8,7 @@
//! Installing from the dial rather than from a `main` also covers the `cdylib`
//! the Python bridge loads, the tests, and the benches, none of which have one.
//! ring is what reqwest already falls back to, so installing it changes no
//! working path, and an embedder that installed its own provider first keeps it.
//! working path, and whoever installs into this rustls build first still wins.
use std::sync::Once;
@ -38,13 +38,32 @@ where
#[cfg(test)]
mod tests {
use rustls::crypto::CryptoProvider;
use super::ensure_crypto_provider;
fn fingerprint(
provider: &CryptoProvider,
) -> (Vec<rustls::CipherSuite>, Vec<rustls::NamedGroup>) {
(
provider
.cipher_suites
.iter()
.map(|suite| suite.suite())
.collect(),
provider
.kx_groups
.iter()
.map(|group| group.name())
.collect(),
)
}
#[test]
fn client_config_builder_works_with_both_provider_features_enabled() {
ensure_crypto_provider();
assert!(rustls::crypto::CryptoProvider::get_default().is_some());
assert!(CryptoProvider::get_default().is_some());
let config = rustls::ClientConfig::builder()
.with_root_certificates(rustls::RootCertStore::empty())
@ -53,13 +72,29 @@ mod tests {
assert!(!config.crypto_provider().cipher_suites.is_empty());
}
#[test]
fn installs_ring_rather_than_aws_lc_rs() {
ensure_crypto_provider();
let installed = CryptoProvider::get_default().expect("a provider is installed");
assert_eq!(
fingerprint(installed),
fingerprint(&rustls::crypto::ring::default_provider())
);
assert_ne!(
fingerprint(installed),
fingerprint(&rustls::crypto::aws_lc_rs::default_provider())
);
}
#[test]
fn ensure_crypto_provider_is_idempotent() {
ensure_crypto_provider();
let first = rustls::crypto::CryptoProvider::get_default().cloned();
let first = CryptoProvider::get_default().cloned();
ensure_crypto_provider();
let second = rustls::crypto::CryptoProvider::get_default().cloned();
let second = CryptoProvider::get_default().cloned();
assert!(first.is_some());
assert!(std::sync::Arc::ptr_eq(&first.unwrap(), &second.unwrap()));

View file

@ -0,0 +1,41 @@
//! Guards the wiring, not just the helper: the dial itself has to install the
//! rustls provider, in a test binary where nothing else has installed one.
use std::collections::HashMap;
use std::time::Duration;
use litellm_ai_gateway::io::responses_ws::ResponsesWebSocketConnection;
use tokio::net::TcpListener;
#[tokio::test]
async fn dialing_wss_returns_an_error_instead_of_panicking() {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("bind a loopback port");
let port = listener
.local_addr()
.expect("read the bound address")
.port();
tokio::spawn(async move {
while let Ok((stream, _peer)) = listener.accept().await {
drop(stream);
}
});
let result = ResponsesWebSocketConnection::connect_url(
&format!("wss://127.0.0.1:{port}/"),
&HashMap::new(),
Some(Duration::from_secs(10)),
)
.await;
assert!(
result.is_err(),
"a plain TCP server cannot finish a TLS handshake"
);
assert!(
rustls::crypto::CryptoProvider::get_default().is_some(),
"the dial is what installs the process-wide provider"
);
}