//! TLS posture coverage for the Rust client. //! //! tonic 0.13.1's `ClientTlsConfig` exposes no hook for a custom rustls //! `ServerCertVerifier` (the verifier is built internally inside the //! crate-private `TlsConnector`), so the Rust client cannot implement the //! "accept any server certificate" lenient default the other clients use. //! Rust is therefore the documented **pin-only exception**: TLS without a //! pinned CA is rejected up front with a clear, actionable error, and //! supplying a CA file is the supported path. These tests pin that contract. use std::time::Duration; use zb_mom_ww_mxgateway_client::{ClientOptions, Error, GalaxyClient, GatewayClient}; /// Drive `connect` to its error without requiring `GatewayClient: Debug` /// (the success arm is dropped explicitly so `unwrap_err` is unnecessary). async fn connect_err(options: ClientOptions) -> Error { match GatewayClient::connect(options).await { Ok(_client) => panic!("connect unexpectedly succeeded against a dead TLS address"), Err(error) => error, } } #[tokio::test] async fn tls_without_ca_is_rejected_with_actionable_error_by_default() { let options = ClientOptions::new("https://127.0.0.1:1") .with_plaintext(false) .with_connect_timeout(Duration::from_millis(200)); let error = connect_err(options).await; let Error::InvalidEndpoint { detail, .. } = error else { panic!("expected InvalidEndpoint, got {error:?}"); }; // The message must point the caller at the supported remedy (pin a CA) // and name the opt-in escape hatch. assert!( detail.contains("ca_file") || detail.contains("CA"), "error should instruct the user to pass a CA file: {detail}" ); assert!( detail.contains("require_certificate_validation"), "error should mention the require_certificate_validation opt-in: {detail}" ); } #[tokio::test] async fn tls_with_require_certificate_validation_does_not_short_circuit() { // With strict verification opted in, the no-CA guard must not fire; the // connect attempt instead proceeds to the transport (and fails to reach // the dead address) rather than returning the "CA required" guard error. let options = ClientOptions::new("https://127.0.0.1:1") .with_plaintext(false) .with_require_certificate_validation(true) .with_connect_timeout(Duration::from_millis(200)); let error = connect_err(options).await; assert!( !matches!(&error, Error::InvalidEndpoint { detail, .. } if detail.contains("require_certificate_validation")), "strict verification must bypass the no-CA guard, got {error:?}" ); } #[tokio::test] async fn tls_with_ca_file_is_permitted_and_proceeds_past_the_guard() { // Pinning a CA is the supported TLS path: the no-CA guard must not fire. // We hand it a readable PEM file; construction proceeds past the guard // and only fails later at the transport (dead address / handshake). let ca_path = std::env::temp_dir().join("mxgw-rust-tls-ca-fixture.pem"); std::fs::write(&ca_path, SELF_SIGNED_CA_PEM).unwrap(); let options = ClientOptions::new("https://127.0.0.1:1") .with_plaintext(false) .with_ca_file(&ca_path) .with_connect_timeout(Duration::from_millis(200)); let error = connect_err(options).await; let _ = std::fs::remove_file(&ca_path); assert!( !matches!(&error, Error::InvalidEndpoint { detail, .. } if detail.contains("require_certificate_validation")), "pinning a CA must bypass the no-CA guard, got {error:?}" ); } /// Drive `GalaxyClient::connect` to its error (mirrors `connect_err` above). async fn galaxy_connect_err(options: ClientOptions) -> Error { match GalaxyClient::connect(options).await { Ok(_client) => { panic!("GalaxyClient::connect unexpectedly succeeded against a dead TLS address") } Err(error) => error, } } #[tokio::test] async fn galaxy_tls_without_ca_is_rejected_with_actionable_error_by_default() { // GalaxyClient::connect must apply the same TLS guard as GatewayClient — // TLS without a pinned CA (and without require_certificate_validation) // returns a clear, actionable InvalidEndpoint error. let options = ClientOptions::new("https://127.0.0.1:1") .with_plaintext(false) .with_connect_timeout(Duration::from_millis(200)); let error = galaxy_connect_err(options).await; let Error::InvalidEndpoint { detail, .. } = error else { panic!("expected InvalidEndpoint, got {error:?}"); }; assert!( detail.contains("ca_file") || detail.contains("CA"), "error should instruct the user to pass a CA file: {detail}" ); assert!( detail.contains("require_certificate_validation"), "error should mention the require_certificate_validation opt-in: {detail}" ); } /// A throwaway self-signed CA certificate (PEM). Only needs to parse as a /// PEM trust root so the CA-pinning path is exercised past the guard. const SELF_SIGNED_CA_PEM: &str = "-----BEGIN CERTIFICATE----- MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw DgYDVQQKEwdBY21lIENvMB4XDTE3MTAyMDE5NDMwNloXDTE4MTAyMDE5NDMwNlow EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABD0d 7VNhbWvZLWPuj/RtHFjvtJBEwOkhbN/BnnE8rnZR8+sbwnc/KhCk3FhnpHZnQz7B 5aETbbIgmuvewdjvSBSjYzBhMA4GA1UdDwEB/wQEAwICpDATBgNVHSUEDDAKBggr BgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdEQQiMCCCDmxvY2FsaG9zdDo1 NDUzgg4xMjcuMC4wLjE6NTQ1MzAKBggqhkjOPQQDAgNIADBFAiEA2zpJEPQyz6/l Wf86aX6PepsntZv2GYlA5UpabfT2EZICICpJ5h/iI+i341gBmLiAFQOyTDT+/wQc 6MF9+Yw1Yy0t -----END CERTIFICATE----- ";