Skip to main content

netspeed_cli/
http.rs

1use crate::common;
2use crate::error::Error;
3use crate::test_config::TestConfig;
4use reqwest::Client;
5use rustls::client::danger::ServerCertVerifier;
6use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
7use rustls::{ClientConfig, DigitallySignedStruct, RootCertStore, SignatureScheme};
8use std::sync::Arc;
9
10/// TLS certificate configuration options.
11#[derive(Debug, Clone, Default)]
12pub struct TlsConfig {
13    /// Path to a custom CA certificate file (PEM format).
14    pub ca_cert_path: Option<std::path::PathBuf>,
15    /// Minimum TLS version to use (e.g., "1.2", "1.3").
16    pub min_tls_version: Option<String>,
17    /// Enable certificate pinning for speedtest.net servers.
18    pub pin_speedtest_certs: bool,
19}
20
21impl TlsConfig {
22    /// Set a custom CA certificate file for TLS verification.
23    #[must_use]
24    pub fn with_ca_cert(mut self, path: std::path::PathBuf) -> Self {
25        self.ca_cert_path = Some(path);
26        self
27    }
28
29    /// Set minimum TLS version.
30    #[must_use]
31    pub fn with_min_tls_version(mut self, version: impl Into<String>) -> Self {
32        self.min_tls_version = Some(version.into());
33        self
34    }
35
36    /// Enable certificate pinning for speedtest.net.
37    #[must_use]
38    pub fn with_cert_pinning(mut self) -> Self {
39        self.pin_speedtest_certs = true;
40        self
41    }
42}
43
44/// Default browser-like user agent for speedtest.net compatibility.
45/// Can be overridden via config file with custom_user_agent option.
46pub const DEFAULT_USER_AGENT: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
47
48/// HTTP client settings - decoupled from Config struct.
49///
50/// This allows creating HTTP clients without depending on the full Config,
51/// improving modularity and testability.
52#[derive(Debug, Clone)]
53pub struct Settings {
54    /// Timeout in seconds for HTTP requests.
55    pub timeout_secs: u64,
56    /// Optional source IP address to bind to.
57    pub source_ip: Option<String>,
58    /// User agent string for HTTP requests.
59    pub user_agent: String,
60    /// Enable automatic retry on transient failures.
61    pub retry_enabled: bool,
62    /// TLS certificate configuration.
63    pub tls: TlsConfig,
64}
65
66/// Build [`Settings`] from a [`crate::config::Config`] reference.
67///
68/// Centralizes the Config→HTTP bridging so callers don't duplicate
69/// the mapping. Resolves custom_user_agent from file config or default.
70///
71/// This impl lives in `http.rs` (not `config.rs`) to preserve layering:
72/// dependency flows http → config, not config → http.
73impl From<&crate::config::Config> for Settings {
74    fn from(config: &crate::config::Config) -> Self {
75        Self {
76            timeout_secs: config.timeout(),
77            source_ip: config.source().map(String::from),
78            user_agent: config
79                .custom_user_agent()
80                .map(String::from)
81                .unwrap_or_else(|| DEFAULT_USER_AGENT.to_string()),
82            retry_enabled: true,
83            tls: TlsConfig {
84                ca_cert_path: config.ca_cert_path(),
85                min_tls_version: config.tls_version().map(String::from),
86                pin_speedtest_certs: config.pin_certs(),
87            },
88        }
89    }
90}
91
92impl Default for Settings {
93    fn default() -> Self {
94        Self {
95            timeout_secs: 10,
96            source_ip: None,
97            user_agent: DEFAULT_USER_AGENT.to_string(),
98            retry_enabled: true,
99            tls: TlsConfig::default(),
100        }
101    }
102}
103
104impl Settings {
105    /// Set a custom user agent (e.g., from config file).
106    #[must_use]
107    pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
108        self.user_agent = user_agent.into();
109        self
110    }
111
112    /// Disable retry logic (useful for tests or when caller handles retries).
113    #[must_use]
114    pub fn with_retry_disabled(mut self) -> Self {
115        self.retry_enabled = false;
116        self
117    }
118}
119
120/// Create an HTTP client with the given settings.
121///
122/// # Errors
123///
124/// Returns [`Error::Context`] if the source IP is invalid.
125/// Returns [`Error::NetworkError`] if the client fails to build.
126pub fn create_client(settings: &Settings) -> Result<Client, Error> {
127    let mut builder = Client::builder()
128        .timeout(std::time::Duration::from_secs(settings.timeout_secs))
129        .http1_only()
130        .no_gzip()
131        .use_rustls_tls()
132        .user_agent(&settings.user_agent);
133
134    if let Some(ref source_ip) = settings.source_ip {
135        let addr: std::net::SocketAddr = source_ip
136            .parse()
137            .map_err(|e| Error::with_source("Invalid source IP", e))?;
138        builder = builder.local_address(addr.ip());
139    }
140
141    // Apply TLS configuration if any options are set
142    if settings.tls.ca_cert_path.is_some()
143        || settings.tls.min_tls_version.is_some()
144        || settings.tls.pin_speedtest_certs
145    {
146        let tls_config = build_tls_config(&settings.tls)?;
147        builder = builder.use_preconfigured_tls(tls_config);
148    }
149
150    let client = builder.build().map_err(Error::NetworkError)?;
151
152    Ok(client)
153}
154
155/// Build a rustls client configuration based on the TLS settings.
156fn build_tls_config(tls: &TlsConfig) -> Result<ClientConfig, Error> {
157    // Determine protocol versions based on min_tls_version setting
158    let versions: &[&rustls::SupportedProtocolVersion] = match tls.min_tls_version.as_deref() {
159        Some("1.2") => &[&rustls::version::TLS12],
160        Some("1.3") => &[&rustls::version::TLS13],
161        Some(v) => {
162            eprintln!("Warning: Unknown TLS version '{}', using defaults", v);
163            rustls::DEFAULT_VERSIONS
164        }
165        None => rustls::DEFAULT_VERSIONS,
166    };
167
168    // Warn if both CA cert and pinning are configured (pinning takes precedence)
169    if tls.pin_speedtest_certs && tls.ca_cert_path.is_some() {
170        eprintln!(
171            "Warning: Both --ca-cert and --pin-certs are set. Certificate pinning takes precedence and --ca-cert will be ignored."
172        );
173    }
174
175    // Build configuration based on whether custom CA or pinning is enabled
176    if tls.pin_speedtest_certs {
177        // For pinning, use the dangerous builder with custom verifier
178        // Note: This only validates domain names, not actual certificate hashes
179        // For true pinning, additional SPKI hash verification would be needed
180        return Ok(ClientConfig::builder_with_protocol_versions(versions)
181            .dangerous()
182            .with_custom_certificate_verifier(Arc::new(PinningVerifier::new()))
183            .with_no_client_auth());
184    }
185
186    // Standard configuration with webpki-roots (Mozilla's root certs)
187    let mut root_store = RootCertStore::empty();
188    // webpki-roots 0.26 provides TLS_SERVER_ROOTS which can be extended into the store
189    root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
190
191    if let Some(ref ca_path) = tls.ca_cert_path {
192        // Load custom CA certificate instead of webpki-roots
193        return Ok(ClientConfig::builder_with_protocol_versions(versions)
194            .with_root_certificates(load_custom_ca_cert(ca_path)?)
195            .with_no_client_auth());
196    }
197
198    Ok(ClientConfig::builder_with_protocol_versions(versions)
199        .with_root_certificates(root_store)
200        .with_no_client_auth())
201}
202
203/// Load a custom CA certificate from a PEM or DER file.
204fn load_custom_ca_cert(path: &std::path::Path) -> Result<RootCertStore, Error> {
205    let pem_data = std::fs::read(path)
206        .map_err(|e| Error::context(format!("Failed to read CA cert: {}", e)))?;
207
208    let mut store = RootCertStore::empty();
209
210    // Try PEM first (returns iterator in newer versions)
211    let mut cursor = std::io::Cursor::new(&pem_data);
212    let mut found_cert = false;
213    for cert_result in rustls_pemfile::certs(&mut cursor) {
214        match cert_result {
215            Ok(cert) => {
216                store
217                    .add(cert)
218                    .map_err(|e| Error::context(format!("Failed to add cert: {}", e)))?;
219                found_cert = true;
220            }
221            Err(e) => {
222                eprintln!("Warning: Failed to parse PEM cert: {}", e);
223            }
224        }
225    }
226
227    // If no PEM certs found, try DER format
228    if !found_cert {
229        store
230            .add(CertificateDer::from(pem_data))
231            .map_err(|e| Error::context(format!("Failed to parse cert: {}", e)))?;
232    }
233
234    Ok(store)
235}
236
237/// Certificate verifier for speedtest.net pinning.
238#[derive(Debug)]
239struct PinningVerifier;
240
241impl PinningVerifier {
242    fn new() -> Self {
243        Self
244    }
245
246    fn is_valid_domain(host: &str) -> bool {
247        // Check exact domains first, then subdomains ending with the suffix
248        host == "speedtest.net"
249            || host == "ookla.com"
250            || host.ends_with(".speedtest.net")
251            || host.ends_with(".ookla.com")
252    }
253}
254
255impl ServerCertVerifier for PinningVerifier {
256    fn verify_server_cert(
257        &self,
258        end_entity: &CertificateDer<'_>,
259        _intermediate_certs: &[CertificateDer<'_>],
260        server_name: &ServerName<'_>,
261        _ocsp_response: &[u8],
262        _now: UnixTime,
263    ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
264        // Extract hostname from server name
265        let hostname = match server_name {
266            ServerName::DnsName(name) => name.as_ref(),
267            _ => {
268                return Err(rustls::Error::General(
269                    "Unsupported server name type".to_string(),
270                ));
271            }
272        };
273
274        // Check if the domain is allowed (domain pinning)
275        // Note: This only validates domain names, not actual certificate hashes
276        // An attacker with a valid speedtest.net certificate could still MITM
277        if !Self::is_valid_domain(hostname) {
278            return Err(rustls::Error::General(format!(
279                "'{}' is not a speedtest.net domain",
280                hostname
281            )));
282        }
283
284        // Verify the certificate can be parsed by webpki
285        webpki::EndEntityCert::try_from(end_entity.as_ref())
286            .map_err(|_| rustls::Error::General("Invalid certificate structure".to_string()))?;
287
288        Ok(rustls::client::danger::ServerCertVerified::assertion())
289    }
290
291    fn verify_tls12_signature(
292        &self,
293        _message: &[u8],
294        _cert: &CertificateDer<'_>,
295        _dss: &DigitallySignedStruct,
296    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
297        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
298    }
299
300    fn verify_tls13_signature(
301        &self,
302        _message: &[u8],
303        _cert: &CertificateDer<'_>,
304        _dss: &DigitallySignedStruct,
305    ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
306        Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
307    }
308
309    fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
310        vec![
311            SignatureScheme::RSA_PKCS1_SHA256,
312            SignatureScheme::RSA_PKCS1_SHA384,
313            SignatureScheme::RSA_PKCS1_SHA512,
314            SignatureScheme::ECDSA_NISTP256_SHA256,
315            SignatureScheme::ECDSA_NISTP384_SHA384,
316            SignatureScheme::RSA_PSS_SHA256,
317            SignatureScheme::RSA_PSS_SHA384,
318            SignatureScheme::RSA_PSS_SHA512,
319        ]
320    }
321}
322
323/// Represents a transient HTTP error that may benefit from retry.
324fn is_transient_error(e: &reqwest::Error) -> bool {
325    if e.is_timeout() {
326        return true;
327    }
328    if e.is_connect() {
329        return true;
330    }
331    // Server errors (5xx) are transient
332    if let Some(status) = e.status() {
333        return status.as_u16() >= 500;
334    }
335    false
336}
337
338/// Execute an HTTP request with automatic retry on transient failures.
339///
340/// This function wraps a request closure with exponential backoff retry logic.
341/// It will retry on timeouts, connection errors, and 5xx server errors.
342///
343/// # Arguments
344///
345/// * `request` - Closure that creates and executes the request
346///
347/// # Errors
348///
349/// Returns the final error after all retry attempts are exhausted.
350pub async fn with_retry<R, F, Fut>(mut request: F) -> Result<R, Error>
351where
352    F: FnMut() -> Fut,
353    Fut: std::future::Future<Output = Result<R, reqwest::Error>>,
354{
355    let config = TestConfig::default();
356    let max_attempts = config.http_retry_attempts;
357
358    for attempt in 0..max_attempts {
359        let result = request().await;
360
361        if let Ok(r) = result {
362            return Ok(r);
363        }
364
365        // Get the error reference (we can't clone reqwest::Error)
366        if let Err(e) = &result {
367            let (delay, should_retry) = TestConfig::retry_delay(attempt);
368
369            // Check if error is transient and we should retry
370            #[allow(clippy::collapsible_if)]
371            if should_retry && is_transient_error(e) && attempt < max_attempts - 1 {
372                tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
373                continue;
374            }
375
376            // Non-transient error or exhausted retries - return the error
377            return result.map_err(Error::NetworkError);
378        }
379    }
380
381    // This should not be reached, but handle it defensively
382    Err(Error::context("retry loop ended without result or error"))
383}
384
385/// Discover the client's public IP address via speedtest.net.
386///
387/// # Errors
388///
389/// Returns [`Error::NetworkError`] if all IP discovery endpoints fail.
390pub async fn discover_client_ip(client: &Client) -> Result<String, Error> {
391    if let Ok(response) = client
392        .get("https://www.speedtest.net/api/ip.php")
393        .send()
394        .await
395    {
396        if let Ok(text) = response.text().await {
397            let trimmed = text.trim().to_string();
398            if common::is_valid_ipv4(&trimmed) {
399                return Ok(trimmed);
400            }
401        }
402    }
403
404    if let Ok(response) = client
405        .get("https://www.speedtest.net/api/ios-config.php")
406        .send()
407        .await
408    {
409        if let Ok(text) = response.text().await {
410            if let Some(ip) = parse_ip_from_xml(&text) {
411                return Ok(ip);
412            }
413        }
414    }
415
416    Ok("unknown".to_string())
417}
418
419fn parse_ip_from_xml(xml: &str) -> Option<String> {
420    // Use structured XML deserialization instead of manual string scanning
421    // to handle edge cases (comments, CDATA, nested elements) correctly.
422    #[derive(serde::Deserialize)]
423    struct Settings {
424        client: ClientElement,
425    }
426    #[derive(serde::Deserialize)]
427    struct ClientElement {
428        #[serde(rename = "@ip")]
429        ip: Option<String>,
430    }
431
432    // XML parse failures are expected (malformed responses, unexpected structure)
433    // and are not actionable — the caller falls back to returning "unknown".
434    let settings: Settings = match quick_xml::de::from_str(xml) {
435        Ok(s) => s,
436        Err(_) => return None,
437    };
438    let ip = settings.client.ip?;
439    if common::is_valid_ipv4(&ip) {
440        Some(ip)
441    } else {
442        None
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use super::*;
449    use std::sync::Arc;
450    use std::sync::atomic::{AtomicUsize, Ordering};
451
452    // ==================== TlsConfig Builder Method Tests ====================
453
454    #[test]
455    fn test_tls_config_with_ca_cert() {
456        let config = TlsConfig::default();
457        assert!(config.ca_cert_path.is_none());
458
459        let config = config.with_ca_cert(std::path::PathBuf::from("/path/to/cert.pem"));
460        assert_eq!(
461            config.ca_cert_path,
462            Some(std::path::PathBuf::from("/path/to/cert.pem"))
463        );
464    }
465
466    #[test]
467    fn test_tls_config_with_min_tls_version() {
468        let config = TlsConfig::default();
469        assert!(config.min_tls_version.is_none());
470
471        let config = config.with_min_tls_version("1.2");
472        assert_eq!(config.min_tls_version, Some("1.2".to_string()));
473
474        let config = TlsConfig::default().with_min_tls_version("1.3");
475        assert_eq!(config.min_tls_version, Some("1.3".to_string()));
476    }
477
478    #[test]
479    fn test_tls_config_with_cert_pinning() {
480        let config = TlsConfig::default();
481        assert!(!config.pin_speedtest_certs);
482
483        let config = config.with_cert_pinning();
484        assert!(config.pin_speedtest_certs);
485    }
486
487    #[test]
488    fn test_tls_config_builder_chaining() {
489        // Test that builder methods can be chained
490        let config = TlsConfig::default()
491            .with_ca_cert(std::path::PathBuf::from("/custom/ca.pem"))
492            .with_min_tls_version("1.3")
493            .with_cert_pinning();
494
495        assert_eq!(
496            config.ca_cert_path,
497            Some(std::path::PathBuf::from("/custom/ca.pem"))
498        );
499        assert_eq!(config.min_tls_version, Some("1.3".to_string()));
500        assert!(config.pin_speedtest_certs);
501    }
502
503    // ==================== Settings Tests ====================
504
505    #[test]
506    fn test_settings_default_values() {
507        let settings = Settings::default();
508        assert_eq!(settings.timeout_secs, 10);
509        assert!(settings.source_ip.is_none());
510        assert_eq!(settings.user_agent, DEFAULT_USER_AGENT);
511        assert!(settings.retry_enabled);
512        // Check TlsConfig fields individually since PartialEq isn't derived
513        assert!(settings.tls.ca_cert_path.is_none());
514        assert!(settings.tls.min_tls_version.is_none());
515        assert!(!settings.tls.pin_speedtest_certs);
516    }
517
518    #[test]
519    fn test_settings_with_user_agent() {
520        let settings = Settings::default().with_user_agent("Custom Agent/1.0");
521        assert_eq!(settings.user_agent, "Custom Agent/1.0");
522    }
523
524    #[test]
525    fn test_settings_with_user_agent_chaining() {
526        let settings = Settings::default()
527            .with_user_agent("Test Agent")
528            .with_retry_disabled();
529        assert_eq!(settings.user_agent, "Test Agent");
530        assert!(!settings.retry_enabled);
531    }
532
533    #[test]
534    fn test_settings_with_retry_disabled() {
535        let settings = Settings::default();
536        assert!(settings.retry_enabled);
537
538        let settings = settings.with_retry_disabled();
539        assert!(!settings.retry_enabled);
540    }
541
542    #[test]
543    fn test_settings_debug_trait() {
544        let settings = Settings::default();
545        let debug_str = format!("{:?}", settings);
546        assert!(debug_str.contains("timeout_secs"));
547        assert!(debug_str.contains("user_agent"));
548    }
549
550    #[test]
551    fn test_settings_clone() {
552        let settings = Settings::default();
553        let cloned = settings.clone();
554        assert_eq!(settings.user_agent, cloned.user_agent);
555        assert_eq!(settings.timeout_secs, cloned.timeout_secs);
556    }
557
558    // ==================== is_transient_error Tests ====================
559    // Note: is_transient_error requires a real reqwest::Error which is difficult to construct.
560    // The function is tested indirectly via integration tests with actual network failures.
561
562    // ==================== build_tls_config Tests ====================
563    // Note: These tests require a configured rustls CryptoProvider.
564    // They are skipped in unit tests but tested via integration tests.
565    // The build_tls_config function's behavior is tested indirectly via
566    // create_client tests with TLS options.
567
568    #[test]
569    #[ignore]
570    fn test_build_tls_config_unknown_tls_version() {
571        let tls = TlsConfig {
572            min_tls_version: Some("99.0".to_string()),
573            ..Default::default()
574        };
575        let result = build_tls_config(&tls);
576        assert!(result.is_ok());
577    }
578
579    #[test]
580    #[ignore]
581    fn test_build_tls_config_tls12() {
582        let tls = TlsConfig {
583            min_tls_version: Some("1.2".to_string()),
584            ..Default::default()
585        };
586        let result = build_tls_config(&tls);
587        assert!(result.is_ok());
588    }
589
590    #[test]
591    #[ignore]
592    fn test_build_tls_config_tls13() {
593        let tls = TlsConfig {
594            min_tls_version: Some("1.3".to_string()),
595            ..Default::default()
596        };
597        let result = build_tls_config(&tls);
598        assert!(result.is_ok());
599    }
600
601    #[test]
602    #[ignore]
603    fn test_build_tls_config_pinning_takes_precedence() {
604        let tls = TlsConfig {
605            ca_cert_path: Some(std::path::PathBuf::from("/path/to/ca.pem")),
606            pin_speedtest_certs: true,
607            ..Default::default()
608        };
609        let result = build_tls_config(&tls);
610        assert!(result.is_ok());
611    }
612
613    #[test]
614    #[ignore]
615    fn test_build_tls_config_pinning_only() {
616        let tls = TlsConfig {
617            pin_speedtest_certs: true,
618            ..Default::default()
619        };
620        let result = build_tls_config(&tls);
621        assert!(result.is_ok());
622    }
623
624    #[test]
625    #[ignore]
626    fn test_build_tls_config_no_options() {
627        let tls = TlsConfig::default();
628        let result = build_tls_config(&tls);
629        assert!(result.is_ok());
630    }
631
632    // ==================== load_custom_ca_cert Tests ====================
633
634    #[test]
635    fn test_load_custom_ca_cert_file_not_found() {
636        let result = load_custom_ca_cert(std::path::Path::new("/nonexistent/cert.pem"));
637        assert!(result.is_err());
638        let err = result.unwrap_err();
639        // Error should mention the path
640        let err_msg = format!("{:?}", err);
641        assert!(err_msg.contains("nonexistent") || err_msg.contains("Failed to read CA cert"));
642    }
643
644    #[test]
645    fn test_load_custom_ca_cert_invalid_path() {
646        // Test with a directory path instead of file
647        let result = load_custom_ca_cert(std::path::Path::new("/tmp"));
648        assert!(result.is_err());
649    }
650
651    // ==================== create_client Tests ====================
652    // Note: Some TLS-related tests are ignored due to rustls CryptoProvider requirements.
653    // These are tested via integration tests.
654
655    #[test]
656    fn test_create_client_source_ip_v4() {
657        let settings = Settings {
658            source_ip: Some("192.168.1.100".to_string()),
659            ..Default::default()
660        };
661        let result = create_client(&settings);
662        // IPv4 source IP should work
663        match result {
664            Ok(_) => {}
665            Err(Error::Context { .. }) => {} // Invalid IP format returns Context
666            Err(e) => panic!("Unexpected error type for valid IPv4: {e:?}"),
667        }
668    }
669
670    #[test]
671    fn test_create_client_source_ip_v6() {
672        let settings = Settings {
673            source_ip: Some("::1".to_string()),
674            ..Default::default()
675        };
676        let result = create_client(&settings);
677        match result {
678            Ok(_) => {}
679            Err(Error::NetworkError(_) | Error::Context { .. }) => {} // Network errors acceptable
680            Err(e) => panic!("Unexpected error type: {e:?}"),
681        }
682    }
683
684    #[test]
685    #[ignore]
686    fn test_create_client_with_ca_cert() {
687        let settings = Settings {
688            tls: TlsConfig {
689                ca_cert_path: Some(std::path::PathBuf::from("/nonexistent/ca.pem")),
690                ..Default::default()
691            },
692            ..Default::default()
693        };
694        let result = create_client(&settings);
695        assert!(result.is_err());
696    }
697
698    #[test]
699    #[ignore]
700    fn test_create_client_with_pinning() {
701        let settings = Settings {
702            tls: TlsConfig {
703                pin_speedtest_certs: true,
704                ..Default::default()
705            },
706            ..Default::default()
707        };
708        let result = create_client(&settings);
709        assert!(result.is_ok());
710    }
711
712    #[test]
713    fn test_create_client_with_retry_disabled() {
714        let settings = Settings::default().with_retry_disabled();
715        let result = create_client(&settings);
716        assert!(result.is_ok());
717    }
718
719    #[test]
720    fn test_create_client_timeout_30() {
721        let settings = Settings {
722            timeout_secs: 30,
723            ..Default::default()
724        };
725        let result = create_client(&settings);
726        assert!(result.is_ok());
727    }
728
729    #[test]
730    fn test_create_client_timeout_60() {
731        let settings = Settings {
732            timeout_secs: 60,
733            ..Default::default()
734        };
735        let result = create_client(&settings);
736        assert!(result.is_ok());
737    }
738
739    // ==================== PinningVerifier Tests ====================
740
741    #[test]
742    fn test_pinning_verifier_is_valid_domain_speedtest() {
743        // Valid subdomains - .ends_with() matches exact domains too
744        assert!(PinningVerifier::is_valid_domain("speedtest.net"));
745        assert!(PinningVerifier::is_valid_domain("www.speedtest.net"));
746        assert!(PinningVerifier::is_valid_domain("api.speedtest.net"));
747        assert!(PinningVerifier::is_valid_domain("foo.bar.speedtest.net"));
748        assert!(PinningVerifier::is_valid_domain("fake.speedtest.net")); // also valid (subdomain)
749
750        // Invalid domains - must NOT end with .speedtest.net
751        assert!(!PinningVerifier::is_valid_domain("evilsite.net"));
752        assert!(!PinningVerifier::is_valid_domain("speedtest.com"));
753        assert!(!PinningVerifier::is_valid_domain("notspeedtest.net"));
754    }
755
756    #[test]
757    fn test_pinning_verifier_is_valid_domain_ookla() {
758        // Valid subdomains - .ends_with() matches exact domains too
759        assert!(PinningVerifier::is_valid_domain("ookla.com"));
760        assert!(PinningVerifier::is_valid_domain("www.ookla.com"));
761        assert!(PinningVerifier::is_valid_domain("api.ookla.com"));
762        assert!(PinningVerifier::is_valid_domain("foo.bar.ookla.com"));
763        assert!(PinningVerifier::is_valid_domain("fake.ookla.com")); // also valid (subdomain)
764
765        // Invalid domains - must NOT end with .ookla.com
766        assert!(!PinningVerifier::is_valid_domain("ookla.net"));
767    }
768
769    #[test]
770    fn test_pinning_verifier_edge_cases() {
771        // Edge cases for security
772        assert!(!PinningVerifier::is_valid_domain(""));
773        assert!(!PinningVerifier::is_valid_domain("speedtestXnet")); // no dot prefix
774        assert!(!PinningVerifier::is_valid_domain("attack.com")); // unrelated domain
775    }
776
777    #[test]
778    fn test_pinning_verifier_exact_domains() {
779        // Test exact domain matches (should be valid)
780        assert!(PinningVerifier::is_valid_domain("speedtest.net"));
781        assert!(PinningVerifier::is_valid_domain("ookla.com"));
782    }
783
784    #[test]
785    fn test_pinning_verifier_subdomains() {
786        // Test various subdomain depths
787        assert!(PinningVerifier::is_valid_domain("www.speedtest.net"));
788        assert!(PinningVerifier::is_valid_domain("api.speedtest.net"));
789        assert!(PinningVerifier::is_valid_domain("a.b.c.speedtest.net"));
790        assert!(PinningVerifier::is_valid_domain("www.ookla.com"));
791        assert!(PinningVerifier::is_valid_domain("api.www.ookla.com"));
792    }
793
794    #[test]
795    fn test_pinning_verifier_invalid_suffixes() {
796        // These should NOT match because they don't end with the exact suffix
797        assert!(!PinningVerifier::is_valid_domain("xspeedtest.net")); // prefix attack
798        assert!(!PinningVerifier::is_valid_domain("fake-speedtest.net")); // prefix attack
799        assert!(!PinningVerifier::is_valid_domain("speedtest.net.evil.com")); // suffix confusion
800        assert!(!PinningVerifier::is_valid_domain("ookla.com.evil.com")); // suffix confusion
801        assert!(!PinningVerifier::is_valid_domain("fooookla.com")); // prefix attack
802    }
803
804    #[test]
805    fn test_pinning_verifier_case_sensitivity() {
806        // Domain matching should be case-sensitive (DNS is case-insensitive but we check exact match)
807        assert!(!PinningVerifier::is_valid_domain("Speedtest.net")); // uppercase
808        assert!(!PinningVerifier::is_valid_domain("SPEEDTEST.NET")); // all caps
809        assert!(!PinningVerifier::is_valid_domain("www.Speedtest.net")); // mixed case
810        assert!(!PinningVerifier::is_valid_domain("OOKLA.COM")); // all caps ookla
811    }
812
813    #[test]
814    fn test_pinning_verifier_special_characters() {
815        // Invalid domain formats
816        assert!(!PinningVerifier::is_valid_domain("speedtest.net/")); // trailing slash
817        assert!(!PinningVerifier::is_valid_domain("speedtest.net:443")); // port number
818        assert!(!PinningVerifier::is_valid_domain("speedtest.net/path")); // path
819    }
820
821    #[test]
822    fn test_pinning_verifier_numeric_domains() {
823        // Numeric subdomains are valid
824        assert!(PinningVerifier::is_valid_domain("123.speedtest.net")); // valid numeric subdomain
825        assert!(PinningVerifier::is_valid_domain("1.2.3.speedtest.net")); // valid numeric subdomain
826        // Numeric prefix on base domain is invalid
827        assert!(!PinningVerifier::is_valid_domain("speedtest123.net")); // not valid
828        assert!(!PinningVerifier::is_valid_domain("123speedtest.net")); // not valid
829    }
830
831    #[test]
832    fn test_pinning_verifier_new_returns_self() {
833        // Test that new() creates an instance
834        let verifier = PinningVerifier::new();
835        assert!(matches!(verifier, PinningVerifier));
836    }
837
838    #[test]
839    fn test_pinning_verifier_debug_trait() {
840        // Test that Debug can be derived and used
841        let verifier = PinningVerifier::new();
842        let debug_str = format!("{:?}", verifier);
843        assert_eq!(debug_str, "PinningVerifier");
844    }
845
846    #[test]
847    fn test_pinning_verifier_supported_verify_schemes() {
848        let verifier = PinningVerifier::new();
849        let schemes = verifier.supported_verify_schemes();
850
851        // Should support these signature schemes
852        assert!(schemes.contains(&SignatureScheme::RSA_PKCS1_SHA256));
853        assert!(schemes.contains(&SignatureScheme::RSA_PKCS1_SHA384));
854        assert!(schemes.contains(&SignatureScheme::RSA_PKCS1_SHA512));
855        assert!(schemes.contains(&SignatureScheme::ECDSA_NISTP256_SHA256));
856        assert!(schemes.contains(&SignatureScheme::ECDSA_NISTP384_SHA384));
857        assert!(schemes.contains(&SignatureScheme::RSA_PSS_SHA256));
858        assert!(schemes.contains(&SignatureScheme::RSA_PSS_SHA384));
859        assert!(schemes.contains(&SignatureScheme::RSA_PSS_SHA512));
860
861        // Should have exactly 8 schemes
862        assert_eq!(schemes.len(), 8);
863    }
864
865    // Note: Signature verification tests are omitted because DigitallySignedStruct
866    // has a private constructor in rustls. The signature verification methods always
867    // return HandshakeSignatureValid::assertion() in PinningVerifier, which is tested
868    // implicitly by successful TLS handshakes with valid speedtest.net certificates.
869
870    #[test]
871    fn test_pinning_verifier_verify_server_cert_rejects_invalid_domain() {
872        let verifier = PinningVerifier::new();
873
874        // Create a DnsName for an invalid domain
875        let dns_name = rustls::pki_types::DnsName::try_from("evil.com".to_string()).unwrap();
876        let server_name = ServerName::DnsName(dns_name);
877
878        // Create a minimal valid certificate structure
879        // Using a real but minimal test certificate
880        let cert_der = CertificateDer::from(vec![]);
881
882        // This should fail because the domain is not valid
883        let result =
884            verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
885
886        assert!(result.is_err());
887        let err = result.unwrap_err();
888        let err_msg = format!("{:?}", err);
889        assert!(err_msg.contains("evil.com") || err_msg.contains("not a speedtest.net domain"));
890    }
891
892    #[test]
893    fn test_pinning_verifier_verify_server_cert_rejects_unsupported_name_type() {
894        let verifier = PinningVerifier::new();
895
896        // Test with an IpAddress server name (unsupported)
897        let ip_addr = std::net::IpAddr::from([127, 0, 0, 1]);
898        let server_name = ServerName::IpAddress(ip_addr.into());
899
900        let cert_der = CertificateDer::from(vec![]);
901
902        let result =
903            verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
904
905        assert!(result.is_err());
906        let err = result.unwrap_err();
907        let err_msg = format!("{:?}", err);
908        assert!(err_msg.contains("Unsupported server name type"));
909    }
910
911    #[test]
912    fn test_pinning_verifier_verify_server_cert_rejects_invalid_certificate() {
913        let verifier = PinningVerifier::new();
914
915        // Valid domain but invalid certificate structure
916        let dns_name =
917            rustls::pki_types::DnsName::try_from("www.speedtest.net".to_string()).unwrap();
918        let server_name = ServerName::DnsName(dns_name);
919
920        // Empty certificate should fail webpki parsing
921        let cert_der = CertificateDer::from(vec![]);
922
923        let result =
924            verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
925
926        // Should fail on cert parsing, not domain validation
927        assert!(result.is_err());
928        let err = result.unwrap_err();
929        let err_msg = format!("{:?}", err);
930        // The error should be about certificate structure, not domain
931        assert!(err_msg.contains("Invalid certificate structure"));
932    }
933
934    #[test]
935    fn test_pinning_verifier_domain_checked_before_cert_parse_speedtest() {
936        let verifier = PinningVerifier::new();
937
938        // Valid speedtest.net domain
939        let dns_name = rustls::pki_types::DnsName::try_from("speedtest.net".to_string()).unwrap();
940        let server_name = ServerName::DnsName(dns_name);
941
942        // Empty certificate - the test verifies domain validation happens first
943        // Certificate structure validation is tested in a separate test
944        let cert_der = CertificateDer::from(vec![]);
945
946        let result =
947            verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
948
949        // Should fail on certificate structure validation, not domain validation
950        // This proves domain was checked before cert parsing
951        assert!(result.is_err());
952        let err = result.unwrap_err();
953        let err_msg = format!("{:?}", err);
954        // The error should be about certificate structure, not domain
955        assert!(
956            err_msg.contains("Invalid certificate structure") || err_msg.contains("EndEntityCert")
957        );
958    }
959
960    #[test]
961    fn test_pinning_verifier_ipv6_address_rejected() {
962        let verifier = PinningVerifier::new();
963
964        // Test with an IPv6 address server name
965        let ip_addr = std::net::IpAddr::from([0, 0, 0, 0, 0, 0, 0, 1]); // ::1
966        let server_name = ServerName::IpAddress(ip_addr.into());
967
968        let cert_der = CertificateDer::from(vec![]);
969
970        let result =
971            verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
972
973        assert!(result.is_err());
974    }
975
976    #[test]
977    fn test_pinning_verifier_domain_checked_before_cert_parse_ookla() {
978        let verifier = PinningVerifier::new();
979
980        // Valid ookla.com domain
981        let dns_name = rustls::pki_types::DnsName::try_from("ookla.com".to_string()).unwrap();
982        let server_name = ServerName::DnsName(dns_name);
983
984        // Empty certificate - domain validation is the key being tested here
985        let cert_der = CertificateDer::from(vec![]);
986
987        let result =
988            verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
989
990        // Should fail on certificate structure (proves domain was checked first)
991        assert!(result.is_err());
992    }
993
994    #[test]
995    fn test_pinning_verifier_domain_validation_order() {
996        // This test verifies that domain validation happens BEFORE certificate parsing
997        let verifier = PinningVerifier::new();
998
999        // Invalid domain should fail immediately, without attempting cert parsing
1000        let dns_name = rustls::pki_types::DnsName::try_from("attacker.com".to_string()).unwrap();
1001        let server_name = ServerName::DnsName(dns_name);
1002
1003        // Even with a potentially "valid" looking empty cert structure,
1004        // domain validation should fail first
1005        let cert_der = CertificateDer::from(vec![]);
1006
1007        let result =
1008            verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
1009
1010        assert!(result.is_err());
1011        let err = result.unwrap_err();
1012        let err_msg = format!("{:?}", err);
1013        assert!(
1014            err_msg.contains("not a speedtest.net domain"),
1015            "Expected domain validation error, got: {}",
1016            err_msg
1017        );
1018    }
1019
1020    #[test]
1021    fn test_pinning_verifier_verify_server_cert_rejects_different_tld() {
1022        let verifier = PinningVerifier::new();
1023
1024        // Test with speedtest.net.org (should be rejected)
1025        let dns_name =
1026            rustls::pki_types::DnsName::try_from("speedtest.net.org".to_string()).unwrap();
1027        let server_name = ServerName::DnsName(dns_name);
1028
1029        let cert_der = CertificateDer::from(vec![]);
1030
1031        let result =
1032            verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
1033
1034        assert!(result.is_err());
1035        let err = result.unwrap_err();
1036        let err_msg = format!("{:?}", err);
1037        assert!(err_msg.contains("not a speedtest.net domain"));
1038    }
1039
1040    #[test]
1041    fn test_pinning_verifier_intermediate_certs_ignored() {
1042        // Test that intermediate certificates are ignored in validation
1043        // The implementation only validates the end-entity certificate
1044        let verifier = PinningVerifier::new();
1045
1046        // Valid domain
1047        let dns_name =
1048            rustls::pki_types::DnsName::try_from("www.speedtest.net".to_string()).unwrap();
1049        let server_name = ServerName::DnsName(dns_name);
1050
1051        // Empty certificate - will fail on structure but that's expected
1052        let cert_der = CertificateDer::from(vec![]);
1053
1054        // Add intermediate certificates (should be ignored)
1055        let intermediate_cert = CertificateDer::from(vec![0u8; 10]);
1056
1057        let result = verifier.verify_server_cert(
1058            &cert_der,
1059            &[intermediate_cert],
1060            &server_name,
1061            &[],
1062            UnixTime::now(),
1063        );
1064
1065        // Should fail on cert structure, not because of intermediate certs
1066        assert!(result.is_err());
1067    }
1068
1069    #[test]
1070    fn test_pinning_verifier_ocsp_response_ignored() {
1071        // Test that OCSP response data is ignored
1072        let verifier = PinningVerifier::new();
1073
1074        // Valid domain
1075        let dns_name = rustls::pki_types::DnsName::try_from("api.ookla.com".to_string()).unwrap();
1076        let server_name = ServerName::DnsName(dns_name);
1077
1078        // Empty certificate
1079        let cert_der = CertificateDer::from(vec![]);
1080
1081        // Add OCSP response data (should be ignored)
1082        let ocsp_response = vec![0x30, 0x03, 0x01, 0x00];
1083
1084        let result = verifier.verify_server_cert(
1085            &cert_der,
1086            &[],
1087            &server_name,
1088            &ocsp_response,
1089            UnixTime::now(),
1090        );
1091
1092        // Should fail on cert structure, not because of OCSP
1093        assert!(result.is_err());
1094    }
1095
1096    #[test]
1097    fn test_pinning_verifier_all_valid_subdomains() {
1098        // Test all valid subdomain patterns
1099        let valid_subdomains = [
1100            "www.speedtest.net",
1101            "api.speedtest.net",
1102            "test.speedtest.net",
1103            "staging.speedtest.net",
1104            "prod.speedtest.net",
1105            "cdn.speedtest.net",
1106            "a.speedtest.net",
1107            "z.speedtest.net",
1108            "a1b2c3.speedtest.net",
1109            "my-site.speedtest.net",
1110            "www.ookla.com",
1111            "api.ookla.com",
1112            "test.ookla.com",
1113        ];
1114
1115        for domain in valid_subdomains {
1116            assert!(
1117                PinningVerifier::is_valid_domain(domain),
1118                "Domain '{}' should be valid",
1119                domain
1120            );
1121        }
1122    }
1123
1124    #[test]
1125    fn test_pinning_verifier_all_invalid_domains() {
1126        // Test all invalid domain patterns
1127        let invalid_domains = [
1128            "evilsite.net",
1129            "speedtest.net.evil.com",
1130            "ookla.com.evil.com",
1131            "speedtest.com",
1132            "ookla.net",
1133            "notspeedtest.net",
1134            "notookla.com",
1135            "fake-speedtest.net",
1136            "fake-ookla.com",
1137            "attacker.speedtest.net.fake.com",
1138            "attacker.ookla.com.fake.com",
1139        ];
1140
1141        for domain in invalid_domains {
1142            assert!(
1143                !PinningVerifier::is_valid_domain(domain),
1144                "Domain '{}' should be invalid",
1145                domain
1146            );
1147        }
1148    }
1149
1150    // ==================== Existing Tests ====================
1151
1152    #[test]
1153    fn test_parse_ip_from_xml() {
1154        let xml = r#"<settings><client country="CA" ip="173.35.57.235" isp="Rogers"/></settings>"#;
1155        assert_eq!(parse_ip_from_xml(xml), Some("173.35.57.235".to_string()));
1156    }
1157
1158    #[test]
1159    fn test_parse_ip_from_xml_full_response() {
1160        let xml = r#"<?xml version="1.0"?>
1161<settings>
1162 <config downloadThreadCountV3="4"/>
1163 <client country="CA" ip="173.35.57.235" isp="Rogers"/>
1164</settings>"#;
1165        assert_eq!(parse_ip_from_xml(xml), Some("173.35.57.235".to_string()));
1166    }
1167
1168    #[test]
1169    fn test_parse_ip_from_xml_invalid() {
1170        assert!(parse_ip_from_xml("not xml").is_none());
1171        assert!(parse_ip_from_xml("<html></html>").is_none());
1172        assert!(parse_ip_from_xml("<settings><client ip=\"invalid\"/></settings>").is_none());
1173    }
1174
1175    #[test]
1176    fn test_create_client_invalid_source_ip() {
1177        let source = crate::config::ConfigSource::default();
1178        let config = crate::config::Config::from_source(&source);
1179        let mut settings = Settings::from(&config);
1180        settings.source_ip = Some("invalid-ip".to_string());
1181        let result = create_client(&settings);
1182        assert!(result.is_err());
1183        assert!(matches!(result.unwrap_err(), Error::Context { .. }));
1184    }
1185
1186    #[test]
1187    fn test_create_client_valid_config() {
1188        let source = crate::config::ConfigSource::default();
1189        let config = crate::config::Config::from_source(&source);
1190        let settings = Settings::from(&config);
1191        let result = create_client(&settings);
1192        assert!(result.is_ok());
1193    }
1194
1195    #[test]
1196    fn test_create_client_with_source_ip() {
1197        let source = crate::config::ConfigSource {
1198            network: crate::config::NetworkSource {
1199                source: Some("0.0.0.0".into()),
1200                ..Default::default()
1201            },
1202            ..Default::default()
1203        };
1204        let config = crate::config::Config::from_source(&source);
1205        let settings = Settings::from(&config);
1206        let result = create_client(&settings);
1207        match result {
1208            Ok(_) | Err(Error::NetworkError(_) | Error::Context { .. }) => {}
1209            Err(e) => panic!("Unexpected error type: {e:?}"),
1210        }
1211    }
1212
1213    #[test]
1214    fn test_create_client_custom_timeout() {
1215        let source = crate::config::ConfigSource {
1216            network: crate::config::NetworkSource {
1217                timeout: 30,
1218                ..Default::default()
1219            },
1220            ..Default::default()
1221        };
1222        let config = crate::config::Config::from_source(&source);
1223        let settings = Settings::from(&config);
1224        let result = create_client(&settings);
1225        assert!(result.is_ok());
1226    }
1227
1228    // ==================== Settings from Config Tests ====================
1229
1230    #[test]
1231    fn test_settings_from_config_with_source_ip() {
1232        let source = crate::config::ConfigSource {
1233            network: crate::config::NetworkSource {
1234                source: Some("192.168.1.50".to_string()),
1235                ..Default::default()
1236            },
1237            ..Default::default()
1238        };
1239        let config = crate::config::Config::from_source(&source);
1240        let settings = Settings::from(&config);
1241        assert_eq!(settings.source_ip, Some("192.168.1.50".to_string()));
1242    }
1243
1244    #[test]
1245    fn test_settings_from_config_with_ca_cert() {
1246        let source = crate::config::ConfigSource {
1247            network: crate::config::NetworkSource {
1248                ca_cert: Some("/path/to/ca.pem".to_string()),
1249                ..Default::default()
1250            },
1251            ..Default::default()
1252        };
1253        let config = crate::config::Config::from_source(&source);
1254        let settings = Settings::from(&config);
1255        assert_eq!(
1256            settings.tls.ca_cert_path,
1257            Some(std::path::PathBuf::from("/path/to/ca.pem"))
1258        );
1259    }
1260
1261    #[test]
1262    fn test_settings_from_config_with_tls_version() {
1263        let source = crate::config::ConfigSource {
1264            network: crate::config::NetworkSource {
1265                tls_version: Some("1.2".to_string()),
1266                ..Default::default()
1267            },
1268            ..Default::default()
1269        };
1270        let config = crate::config::Config::from_source(&source);
1271        let settings = Settings::from(&config);
1272        assert_eq!(settings.tls.min_tls_version, Some("1.2".to_string()));
1273    }
1274
1275    #[test]
1276    fn test_settings_from_config_with_pinning() {
1277        let source = crate::config::ConfigSource {
1278            network: crate::config::NetworkSource {
1279                pin_certs: Some(true),
1280                ..Default::default()
1281            },
1282            ..Default::default()
1283        };
1284        let config = crate::config::Config::from_source(&source);
1285        let settings = Settings::from(&config);
1286        assert!(settings.tls.pin_speedtest_certs);
1287    }
1288
1289    #[test]
1290    fn test_settings_from_config_timeout() {
1291        let source = crate::config::ConfigSource {
1292            network: crate::config::NetworkSource {
1293                timeout: 45,
1294                ..Default::default()
1295            },
1296            ..Default::default()
1297        };
1298        let config = crate::config::Config::from_source(&source);
1299        let settings = Settings::from(&config);
1300        assert_eq!(settings.timeout_secs, 45);
1301    }
1302
1303    #[test]
1304    fn test_settings_from_config_default_user_agent() {
1305        let config = crate::config::Config::from_source(&crate::config::ConfigSource::default());
1306        let settings = Settings::from(&config);
1307        assert_eq!(settings.user_agent, DEFAULT_USER_AGENT);
1308    }
1309
1310    #[test]
1311    fn test_settings_from_config_retry_enabled_by_default() {
1312        let config = crate::config::Config::from_source(&crate::config::ConfigSource::default());
1313        let settings = Settings::from(&config);
1314        assert!(settings.retry_enabled);
1315    }
1316
1317    // ==================== with_retry Tests ====================
1318
1319    #[tokio::test]
1320    async fn test_with_retry_immediate_success() {
1321        let counter = Arc::new(AtomicUsize::new(0));
1322        let count = Arc::clone(&counter);
1323
1324        let result = with_retry(|| {
1325            let c = Arc::clone(&count);
1326            async move {
1327                c.fetch_add(1, Ordering::SeqCst);
1328                Ok::<_, reqwest::Error>(42)
1329            }
1330        })
1331        .await;
1332
1333        assert!(result.is_ok());
1334        assert_eq!(result.unwrap(), 42);
1335        assert_eq!(counter.load(Ordering::SeqCst), 1);
1336    }
1337
1338    #[tokio::test]
1339    async fn test_with_retry_with_mock_request() {
1340        // Test with_retry with a request that succeeds
1341        let result = with_retry(|| async { Ok::<_, reqwest::Error>(100) }).await;
1342        assert!(result.is_ok());
1343        assert_eq!(result.unwrap(), 100);
1344    }
1345
1346    #[tokio::test]
1347    async fn test_with_retry_counter_increment() {
1348        let counter = Arc::new(AtomicUsize::new(0));
1349        let count = Arc::clone(&counter);
1350
1351        let _result = with_retry(|| {
1352            let c = Arc::clone(&count);
1353            async move {
1354                c.fetch_add(1, Ordering::SeqCst);
1355                Ok::<_, reqwest::Error>(1)
1356            }
1357        })
1358        .await;
1359
1360        // Verify the counter was incremented exactly once (single attempt)
1361        assert_eq!(counter.load(Ordering::SeqCst), 1);
1362    }
1363
1364    #[tokio::test]
1365    async fn test_with_retry_different_value_types() {
1366        // Test with_retry with different success value types
1367        let result_str = with_retry(|| async { Ok::<_, reqwest::Error>("hello") }).await;
1368        assert!(result_str.is_ok());
1369        assert_eq!(result_str.unwrap(), "hello");
1370
1371        let result_u64 = with_retry(|| async { Ok::<_, reqwest::Error>(999u64) }).await;
1372        assert!(result_u64.is_ok());
1373        assert_eq!(result_u64.unwrap(), 999);
1374
1375        let result_vec = with_retry(|| async { Ok::<_, reqwest::Error>(vec![1, 2, 3]) }).await;
1376        assert!(result_vec.is_ok());
1377        assert_eq!(result_vec.unwrap(), vec![1, 2, 3]);
1378    }
1379
1380    #[tokio::test]
1381    async fn test_with_retry_multiple_sequential_calls() {
1382        // Test calling with_retry multiple times
1383        for i in 0..3 {
1384            let result = with_retry(|| async { Ok::<_, reqwest::Error>(i) }).await;
1385            assert!(result.is_ok());
1386            assert_eq!(result.unwrap(), i);
1387        }
1388    }
1389
1390    // ==================== parse_ip_from_xml Additional Tests ====================
1391
1392    #[test]
1393    fn test_parse_ip_from_xml_missing_client_element() {
1394        let xml = r#"<settings><server ip="127.0.0.1"/></settings>"#;
1395        assert!(parse_ip_from_xml(xml).is_none());
1396    }
1397
1398    #[test]
1399    fn test_parse_ip_from_xml_empty_ip() {
1400        let xml = r#"<settings><client ip=""/></settings>"#;
1401        assert!(parse_ip_from_xml(xml).is_none());
1402    }
1403
1404    #[test]
1405    fn test_parse_ip_from_xml_whitespace_ip() {
1406        let xml = r#"<settings><client ip="  " /></settings>"#;
1407        assert!(parse_ip_from_xml(xml).is_none());
1408    }
1409
1410    #[test]
1411    fn test_parse_ip_from_xml_ipv6_format() {
1412        let xml = r#"<settings><client ip="::1"/></settings>"#;
1413        // IPv6 should not match valid IPv4 check
1414        assert!(parse_ip_from_xml(xml).is_none());
1415    }
1416
1417    #[test]
1418    fn test_parse_ip_from_xml_special_characters() {
1419        let xml = r#"<settings><client country="US" ip="192.168.1.1" isp="ISP"/></settings>"#;
1420        assert_eq!(parse_ip_from_xml(xml), Some("192.168.1.1".to_string()));
1421    }
1422
1423    #[test]
1424    fn test_parse_ip_from_xml_garbage_after_xml() {
1425        let xml = r#"<settings><client ip="1.2.3.4" /></settings>GARBAGE"#;
1426        assert_eq!(parse_ip_from_xml(xml), Some("1.2.3.4".to_string()));
1427    }
1428
1429    #[test]
1430    fn test_parse_ip_from_xml_malformed_xml() {
1431        assert!(parse_ip_from_xml("<settings><client").is_none());
1432        assert!(parse_ip_from_xml("</settings>").is_none());
1433        assert!(parse_ip_from_xml("").is_none());
1434    }
1435
1436    // ==================== discover_client_ip Tests ====================
1437
1438    #[tokio::test]
1439    async fn test_discover_client_ip_handles_network_failure() {
1440        // Test with a client that's not properly configured (will fail to connect)
1441        let settings = Settings::default().with_retry_disabled();
1442        let client = create_client(&settings).unwrap();
1443
1444        // This test verifies the function handles network failures gracefully
1445        let result = discover_client_ip(&client).await;
1446
1447        // Should return "unknown" on failure, not panic
1448        match result {
1449            Ok(ip) => {
1450                // If it succeeds, verify the format
1451                assert!(ip == "unknown" || common::is_valid_ipv4(&ip));
1452            }
1453            Err(e) => {
1454                // Network errors are acceptable
1455                assert!(matches!(e, Error::NetworkError(_)));
1456            }
1457        }
1458    }
1459
1460    // ==================== TlsConfig Additional Tests ====================
1461
1462    #[test]
1463    fn test_tls_config_debug() {
1464        let config = TlsConfig::default();
1465        let debug_str = format!("{:?}", config);
1466        assert!(debug_str.contains("TlsConfig"));
1467    }
1468
1469    #[test]
1470    fn test_tls_config_clone() {
1471        let config = TlsConfig::default()
1472            .with_ca_cert(std::path::PathBuf::from("/test.pem"))
1473            .with_min_tls_version("1.3")
1474            .with_cert_pinning();
1475        let cloned = config.clone();
1476        assert_eq!(cloned.ca_cert_path, config.ca_cert_path);
1477        assert_eq!(cloned.min_tls_version, config.min_tls_version);
1478        assert_eq!(cloned.pin_speedtest_certs, config.pin_speedtest_certs);
1479    }
1480
1481    #[test]
1482    fn test_tls_config_default_trait() {
1483        let config = TlsConfig::default();
1484        assert!(config.ca_cert_path.is_none());
1485        assert!(config.min_tls_version.is_none());
1486        assert!(!config.pin_speedtest_certs);
1487    }
1488
1489    // ==================== Settings Additional Tests ====================
1490
1491    #[test]
1492    fn test_settings_with_source_ip() {
1493        let settings = Settings {
1494            source_ip: Some("10.0.0.1".to_string()),
1495            ..Default::default()
1496        };
1497        let cloned = settings.clone();
1498        assert_eq!(cloned.source_ip, Some("10.0.0.1".to_string()));
1499    }
1500
1501    #[test]
1502    fn test_settings_builder_full_chain() {
1503        let settings = Settings::default()
1504            .with_user_agent("Test/1.0")
1505            .with_retry_disabled();
1506        assert_eq!(settings.user_agent, "Test/1.0");
1507        assert!(!settings.retry_enabled);
1508    }
1509
1510    #[test]
1511    fn test_settings_clone_is_independent() {
1512        let mut settings = Settings {
1513            timeout_secs: 60,
1514            ..Default::default()
1515        };
1516        let cloned = settings.clone();
1517        assert_eq!(cloned.timeout_secs, 60);
1518        // Modify original should not affect clone (deep clone of primitives)
1519        settings.timeout_secs = 120;
1520        assert_eq!(cloned.timeout_secs, 60); // Clone should be independent
1521    }
1522
1523    // ==================== create_client Additional Tests ====================
1524
1525    #[test]
1526    fn test_create_client_with_source_ip_none() {
1527        let settings = Settings::default();
1528        let result = create_client(&settings);
1529        assert!(result.is_ok());
1530    }
1531
1532    #[test]
1533    fn test_create_client_with_custom_user_agent() {
1534        let settings = Settings::default().with_user_agent("TestAgent/1.0");
1535        let result = create_client(&settings);
1536        assert!(result.is_ok());
1537    }
1538
1539    #[test]
1540    fn test_create_client_timeout_zero() {
1541        let settings = Settings {
1542            timeout_secs: 0,
1543            ..Default::default()
1544        };
1545        let result = create_client(&settings);
1546        assert!(result.is_ok());
1547    }
1548
1549    #[test]
1550    fn test_create_client_timeout_large() {
1551        let settings = Settings {
1552            timeout_secs: 300,
1553            ..Default::default()
1554        };
1555        let result = create_client(&settings);
1556        assert!(result.is_ok());
1557    }
1558
1559    // ==================== Error Context Tests ====================
1560
1561    #[test]
1562    fn test_error_context_message() {
1563        let err = Error::context("test error");
1564        let msg = format!("{:?}", err);
1565        assert!(msg.contains("test error"));
1566    }
1567
1568    #[test]
1569    fn test_error_context_with_source() {
1570        let inner = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
1571        let err = Error::with_source("operation failed", inner);
1572        let msg = format!("{:?}", err);
1573        assert!(msg.contains("operation failed") || msg.contains("file not found"));
1574    }
1575
1576    #[test]
1577    fn test_error_server_not_found() {
1578        let err = Error::ServerNotFound("no servers available".into());
1579        let msg = format!("{:?}", err);
1580        assert!(msg.contains("no servers available") || msg.contains("ServerNotFound"));
1581    }
1582
1583    #[test]
1584    fn test_error_download_failure() {
1585        let err = Error::DownloadFailure("test download failed".into());
1586        let msg = format!("{:?}", err);
1587        assert!(msg.contains("test download failed") || msg.contains("DownloadFailure"));
1588    }
1589
1590    #[test]
1591    fn test_error_upload_failure() {
1592        let err = Error::UploadFailure("test upload failed".into());
1593        let msg = format!("{:?}", err);
1594        assert!(msg.contains("test upload failed") || msg.contains("UploadFailure"));
1595    }
1596
1597    #[test]
1598    fn test_error_context_debug() {
1599        let err = Error::context("context debug");
1600        let debug_str = format!("{:?}", err);
1601        assert!(debug_str.contains("Context"));
1602        assert!(debug_str.contains("context debug"));
1603    }
1604
1605    #[test]
1606    fn test_error_context_display() {
1607        let err = Error::context("context display");
1608        assert_eq!(format!("{}", err), "context display");
1609    }
1610
1611    #[test]
1612    fn test_error_download_failure_display() {
1613        let err = Error::DownloadFailure("download failed".into());
1614        let display = format!("{}", err);
1615        assert!(display.contains("download failed"));
1616    }
1617
1618    #[test]
1619    fn test_error_upload_failure_display() {
1620        let err = Error::UploadFailure("upload failed".into());
1621        let display = format!("{}", err);
1622        assert!(display.contains("upload failed"));
1623    }
1624
1625    #[test]
1626    fn test_error_server_not_found_display() {
1627        let err = Error::ServerNotFound("server not found".into());
1628        let display = format!("{}", err);
1629        assert!(display.contains("Server not found"));
1630        assert!(display.contains("server not found"));
1631    }
1632
1633    // ==================== HTTP Client Settings Default Tests ====================
1634
1635    #[test]
1636    fn test_settings_default_timeout_10() {
1637        let settings = Settings::default();
1638        assert_eq!(settings.timeout_secs, 10);
1639    }
1640
1641    #[test]
1642    fn test_settings_default_retry_true() {
1643        let settings = Settings::default();
1644        assert!(settings.retry_enabled);
1645    }
1646
1647    #[test]
1648    fn test_settings_with_timeout() {
1649        let settings = Settings {
1650            timeout_secs: 120,
1651            ..Default::default()
1652        };
1653        assert_eq!(settings.timeout_secs, 120);
1654    }
1655
1656    // ==================== TlsConfig Defaults Tests ====================
1657
1658    #[test]
1659    fn test_tls_config_default_values() {
1660        let tls = TlsConfig::default();
1661        assert!(tls.ca_cert_path.is_none());
1662        assert!(tls.min_tls_version.is_none());
1663        assert!(!tls.pin_speedtest_certs);
1664    }
1665
1666    #[test]
1667    fn test_tls_config_multiple_options() {
1668        let tls = TlsConfig::default()
1669            .with_ca_cert("/path/to/ca.pem".into())
1670            .with_min_tls_version("1.2");
1671        assert!(tls.ca_cert_path.is_some());
1672        assert!(tls.min_tls_version.is_some());
1673    }
1674
1675    // ==================== Settings Chain Tests ====================
1676
1677    #[test]
1678    fn test_settings_chained_modifications() {
1679        let settings = Settings::default()
1680            .with_user_agent("Test/1.0")
1681            .with_retry_disabled()
1682            .with_user_agent("Test/2.0");
1683        assert_eq!(settings.user_agent, "Test/2.0");
1684        assert!(!settings.retry_enabled);
1685    }
1686
1687    // ==================== DEFAULT_USER_AGENT Tests ====================
1688
1689    #[test]
1690    fn test_default_user_agent_is_valid() {
1691        assert!(!DEFAULT_USER_AGENT.is_empty());
1692        assert!(DEFAULT_USER_AGENT.contains("Mozilla"));
1693        assert!(DEFAULT_USER_AGENT.contains("Chrome"));
1694    }
1695
1696    #[test]
1697    fn test_default_user_agent_in_settings() {
1698        let settings = Settings::default();
1699        assert_eq!(settings.user_agent, DEFAULT_USER_AGENT);
1700    }
1701
1702    // ==================== create_client builder variations ====================
1703
1704    #[test]
1705    fn test_create_client_all_defaults() {
1706        let settings = Settings::default();
1707        let result = create_client(&settings);
1708        assert!(result.is_ok());
1709    }
1710
1711    #[test]
1712    fn test_create_client_minimal_tls_config() {
1713        let settings = Settings {
1714            tls: TlsConfig::default(),
1715            ..Default::default()
1716        };
1717        let result = create_client(&settings);
1718        assert!(result.is_ok());
1719    }
1720
1721    #[test]
1722    fn test_create_client_http1_only() {
1723        let settings = Settings::default();
1724        let result = create_client(&settings);
1725        assert!(result.is_ok());
1726        // HTTP/1.1 only is configured (verified by no_gzip and http1_only)
1727    }
1728
1729    // ==================== PinningVerifier additional tests ====================
1730
1731    #[test]
1732    fn test_pinning_verifier_single_char_subdomain() {
1733        assert!(PinningVerifier::is_valid_domain("a.speedtest.net"));
1734        assert!(PinningVerifier::is_valid_domain("z.ookla.com"));
1735    }
1736
1737    #[test]
1738    fn test_pinning_verifier_numbers_in_subdomain() {
1739        // Numbers in subdomain are valid
1740        assert!(PinningVerifier::is_valid_domain("123.speedtest.net")); // valid subdomain
1741        assert!(!PinningVerifier::is_valid_domain("speedtest123.net")); // not valid, doesn't end with .speedtest.net
1742        assert!(!PinningVerifier::is_valid_domain("123speedtest.net")); // prefix attack
1743    }
1744
1745    #[test]
1746    fn test_pinning_verifier_unicode_in_subdomain() {
1747        // Unicode subdomains should still match the ends_with check
1748        assert!(PinningVerifier::is_valid_domain("münchen.speedtest.net"));
1749    }
1750
1751    #[test]
1752    fn test_pinning_verifier_empty_cert_with_valid_domain() {
1753        let verifier = PinningVerifier::new();
1754        let dns_name =
1755            rustls::pki_types::DnsName::try_from("cdn.speedtest.net".to_string()).unwrap();
1756        let server_name = ServerName::DnsName(dns_name);
1757        let cert_der = CertificateDer::from(vec![]);
1758
1759        // Domain is valid, but cert structure is invalid
1760        let result =
1761            verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
1762        assert!(result.is_err());
1763    }
1764
1765    #[test]
1766    fn test_pinning_verifier_subdomain_with_dashes() {
1767        assert!(PinningVerifier::is_valid_domain(
1768            "my-custom-subdomain.speedtest.net"
1769        ));
1770        assert!(PinningVerifier::is_valid_domain("api-v2.ookla.com"));
1771    }
1772
1773    #[test]
1774    fn test_pinning_verifier_long_subdomain() {
1775        let long_subdomain = "a".repeat(63) + ".speedtest.net";
1776        // This should be valid as it ends with .speedtest.net
1777        assert!(PinningVerifier::is_valid_domain(&long_subdomain));
1778    }
1779
1780    #[test]
1781    fn test_pinning_verifier_concatenation_attack() {
1782        // These should all be rejected as they don't end with valid suffixes
1783        assert!(!PinningVerifier::is_valid_domain("speedtestXnet"));
1784        assert!(!PinningVerifier::is_valid_domain("speedtestXcom"));
1785        assert!(!PinningVerifier::is_valid_domain("ooklaXcom"));
1786        assert!(!PinningVerifier::is_valid_domain("ooklaXnet"));
1787    }
1788
1789    // ==================== Settings additional chain tests ====================
1790
1791    #[test]
1792    fn test_settings_retry_disabled_chain() {
1793        let settings = Settings::default().with_retry_disabled();
1794        assert!(!settings.retry_enabled);
1795
1796        // Ensure other defaults are still set
1797        assert_eq!(settings.timeout_secs, 10);
1798        assert_eq!(settings.user_agent, DEFAULT_USER_AGENT);
1799    }
1800
1801    #[test]
1802    fn test_settings_user_agent_chain() {
1803        let settings = Settings::default()
1804            .with_user_agent("Custom/1.0")
1805            .with_user_agent("Custom/2.0");
1806        assert_eq!(settings.user_agent, "Custom/2.0");
1807    }
1808
1809    // ==================== create_client edge cases ====================
1810
1811    #[test]
1812    fn test_create_client_source_ip_loopback_v4() {
1813        let settings = Settings {
1814            source_ip: Some("127.0.0.1".to_string()),
1815            ..Default::default()
1816        };
1817        let result = create_client(&settings);
1818        // Loopback should work
1819        match result {
1820            Ok(_) | Err(Error::NetworkError(_) | Error::Context { .. }) => {}
1821            Err(e) => panic!("Unexpected error: {e:?}"),
1822        }
1823    }
1824
1825    #[test]
1826    fn test_create_client_source_ip_loopback_v6() {
1827        let settings = Settings {
1828            source_ip: Some("::1".to_string()),
1829            ..Default::default()
1830        };
1831        let result = create_client(&settings);
1832        // Loopback should work
1833        match result {
1834            Ok(_) | Err(Error::NetworkError(_) | Error::Context { .. }) => {}
1835            Err(e) => panic!("Unexpected error: {e:?}"),
1836        }
1837    }
1838
1839    #[test]
1840    fn test_create_client_source_ip_unspecified() {
1841        let settings = Settings {
1842            source_ip: Some("0.0.0.0".to_string()),
1843            ..Default::default()
1844        };
1845        let result = create_client(&settings);
1846        // Unspecified should work
1847        match result {
1848            Ok(_) | Err(Error::NetworkError(_) | Error::Context { .. }) => {}
1849            Err(e) => panic!("Unexpected error: {e:?}"),
1850        }
1851    }
1852
1853    #[test]
1854    fn test_create_client_source_ip_with_tls() {
1855        let settings = Settings {
1856            source_ip: Some("127.0.0.1".to_string()),
1857            tls: TlsConfig::default(),
1858            ..Default::default()
1859        };
1860        let result = create_client(&settings);
1861        // Should work with default TLS or gracefully handle errors
1862        match result {
1863            Ok(_) | Err(Error::NetworkError(_) | Error::Context { .. }) => {}
1864            Err(e) => panic!("Unexpected error: {e:?}"),
1865        }
1866    }
1867}