1use crate::common;
2use crate::error::Error;
3use crate::test_config::TestConfig;
4use reqwest::Client;
5use rustls::client::WebPkiServerVerifier;
6use rustls::client::danger::ServerCertVerifier;
7use rustls::crypto::CryptoProvider;
8use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
9use rustls::{ClientConfig, DigitallySignedStruct, RootCertStore, SignatureScheme};
10use std::sync::Arc;
11
12#[derive(Debug, Clone, Default)]
14pub struct TlsConfig {
15 pub ca_cert_path: Option<std::path::PathBuf>,
17 pub min_tls_version: Option<String>,
19 pub pin_speedtest_certs: bool,
21}
22
23impl TlsConfig {
24 #[must_use]
26 pub fn with_ca_cert(mut self, path: std::path::PathBuf) -> Self {
27 self.ca_cert_path = Some(path);
28 self
29 }
30
31 #[must_use]
33 pub fn with_min_tls_version(mut self, version: impl Into<String>) -> Self {
34 self.min_tls_version = Some(version.into());
35 self
36 }
37
38 #[must_use]
40 pub fn with_cert_pinning(mut self) -> Self {
41 self.pin_speedtest_certs = true;
42 self
43 }
44}
45
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";
49
50#[derive(Debug, Clone)]
55pub struct Settings {
56 pub timeout_secs: u64,
58 pub source_ip: Option<String>,
60 pub user_agent: String,
62 pub retry_enabled: bool,
64 pub tls: TlsConfig,
66}
67
68impl From<&crate::config::Config> for Settings {
76 fn from(config: &crate::config::Config) -> Self {
77 Self {
78 timeout_secs: config.timeout(),
79 source_ip: config.source().map(String::from),
80 user_agent: config
81 .custom_user_agent()
82 .map(String::from)
83 .unwrap_or_else(|| DEFAULT_USER_AGENT.to_string()),
84 retry_enabled: true,
85 tls: TlsConfig {
86 ca_cert_path: config.ca_cert_path(),
87 min_tls_version: config.tls_version().map(String::from),
88 pin_speedtest_certs: config.pin_certs(),
89 },
90 }
91 }
92}
93
94impl Default for Settings {
95 fn default() -> Self {
96 Self {
97 timeout_secs: 10,
98 source_ip: None,
99 user_agent: DEFAULT_USER_AGENT.to_string(),
100 retry_enabled: true,
101 tls: TlsConfig::default(),
102 }
103 }
104}
105
106impl Settings {
107 #[must_use]
109 pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
110 self.user_agent = user_agent.into();
111 self
112 }
113
114 #[must_use]
116 pub fn with_retry_disabled(mut self) -> Self {
117 self.retry_enabled = false;
118 self
119 }
120}
121
122pub fn create_client(settings: &Settings) -> Result<Client, Error> {
129 let mut builder = Client::builder()
130 .timeout(std::time::Duration::from_secs(settings.timeout_secs))
131 .http1_only()
132 .no_gzip()
133 .use_rustls_tls()
134 .user_agent(&settings.user_agent);
135
136 if let Some(ref source_ip) = settings.source_ip {
137 let addr: std::net::SocketAddr = source_ip
138 .parse()
139 .map_err(|e| Error::with_source("Invalid source IP", e))?;
140 builder = builder.local_address(addr.ip());
141 }
142
143 if settings.tls.ca_cert_path.is_some()
145 || settings.tls.min_tls_version.is_some()
146 || settings.tls.pin_speedtest_certs
147 {
148 let tls_config = build_tls_config(&settings.tls)?;
149 builder = builder.use_preconfigured_tls(tls_config);
150 }
151
152 let client = builder.build().map_err(Error::NetworkError)?;
153
154 Ok(client)
155}
156
157fn build_tls_config(tls: &TlsConfig) -> Result<ClientConfig, Error> {
159 let versions: &[&rustls::SupportedProtocolVersion] = match tls.min_tls_version.as_deref() {
161 Some("1.2") => &[&rustls::version::TLS12],
162 Some("1.3") => &[&rustls::version::TLS13],
163 Some(v) => {
164 eprintln!("Warning: Unknown TLS version '{}', using defaults", v);
165 rustls::DEFAULT_VERSIONS
166 }
167 None => rustls::DEFAULT_VERSIONS,
168 };
169
170 if tls.pin_speedtest_certs && tls.ca_cert_path.is_some() {
171 eprintln!(
172 "Warning: Both --ca-cert and --pin-certs are set. Custom CA verification will be used before the speedtest.net domain restriction."
173 );
174 }
175
176 let provider = Arc::new(rustls::crypto::aws_lc_rs::default_provider());
177 let builder = ClientConfig::builder_with_provider(Arc::clone(&provider))
178 .with_protocol_versions(versions)
179 .map_err(|e| Error::context(format!("Invalid TLS configuration: {e}")))?;
180
181 let root_store = match tls.ca_cert_path.as_deref() {
182 Some(ca_path) => load_custom_ca_cert(ca_path)?,
183 None => default_root_store(),
184 };
185
186 if tls.pin_speedtest_certs {
187 let verifier = PinningVerifier::try_new(root_store, provider)?;
188 return Ok(builder
189 .dangerous()
190 .with_custom_certificate_verifier(Arc::new(verifier))
191 .with_no_client_auth());
192 }
193
194 Ok(builder
195 .with_root_certificates(root_store)
196 .with_no_client_auth())
197}
198
199fn default_root_store() -> RootCertStore {
200 let mut root_store = RootCertStore::empty();
201 root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
202 root_store
203}
204
205fn load_custom_ca_cert(path: &std::path::Path) -> Result<RootCertStore, Error> {
207 let pem_data = std::fs::read(path)
208 .map_err(|e| Error::context(format!("Failed to read CA cert: {}", e)))?;
209
210 let mut store = RootCertStore::empty();
211
212 let mut cursor = std::io::Cursor::new(&pem_data);
214 let mut found_cert = false;
215 for cert_result in rustls_pemfile::certs(&mut cursor) {
216 match cert_result {
217 Ok(cert) => {
218 store
219 .add(cert)
220 .map_err(|e| Error::context(format!("Failed to add cert: {}", e)))?;
221 found_cert = true;
222 }
223 Err(e) => {
224 eprintln!("Warning: Failed to parse PEM cert: {}", e);
225 }
226 }
227 }
228
229 if !found_cert {
231 store
232 .add(CertificateDer::from(pem_data))
233 .map_err(|e| Error::context(format!("Failed to parse cert: {}", e)))?;
234 }
235
236 Ok(store)
237}
238
239#[derive(Debug)]
242struct PinningVerifier {
243 inner: Arc<WebPkiServerVerifier>,
244}
245
246impl PinningVerifier {
247 #[cfg(test)]
248 fn new() -> Self {
249 Self::try_new(
250 default_root_store(),
251 Arc::new(rustls::crypto::aws_lc_rs::default_provider()),
252 )
253 .expect("default TLS verifier should build")
254 }
255
256 fn try_new(root_store: RootCertStore, provider: Arc<CryptoProvider>) -> Result<Self, Error> {
257 let inner = WebPkiServerVerifier::builder_with_provider(Arc::new(root_store), provider)
258 .build()
259 .map_err(|e| Error::context(format!("Failed to build TLS verifier: {e:?}")))?;
260 Ok(Self { inner })
261 }
262
263 fn is_valid_domain(host: &str) -> bool {
264 host == "speedtest.net"
266 || host == "ookla.com"
267 || host.ends_with(".speedtest.net")
268 || host.ends_with(".ookla.com")
269 }
270}
271
272impl ServerCertVerifier for PinningVerifier {
273 fn verify_server_cert(
274 &self,
275 end_entity: &CertificateDer<'_>,
276 _intermediate_certs: &[CertificateDer<'_>],
277 server_name: &ServerName<'_>,
278 _ocsp_response: &[u8],
279 _now: UnixTime,
280 ) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
281 let hostname = match server_name {
283 ServerName::DnsName(name) => name.as_ref(),
284 _ => {
285 return Err(rustls::Error::General(
286 "Unsupported server name type".to_string(),
287 ));
288 }
289 };
290
291 if !Self::is_valid_domain(hostname) {
292 return Err(rustls::Error::General(format!(
293 "'{}' is not a speedtest.net domain",
294 hostname
295 )));
296 }
297
298 self.inner.verify_server_cert(
299 end_entity,
300 _intermediate_certs,
301 server_name,
302 _ocsp_response,
303 _now,
304 )
305 }
306
307 fn verify_tls12_signature(
308 &self,
309 message: &[u8],
310 cert: &CertificateDer<'_>,
311 dss: &DigitallySignedStruct,
312 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
313 self.inner.verify_tls12_signature(message, cert, dss)
314 }
315
316 fn verify_tls13_signature(
317 &self,
318 message: &[u8],
319 cert: &CertificateDer<'_>,
320 dss: &DigitallySignedStruct,
321 ) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
322 self.inner.verify_tls13_signature(message, cert, dss)
323 }
324
325 fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
326 self.inner.supported_verify_schemes()
327 }
328}
329
330fn is_transient_error(e: &reqwest::Error) -> bool {
332 if e.is_timeout() {
333 return true;
334 }
335 if e.is_connect() {
336 return true;
337 }
338 if let Some(status) = e.status() {
340 return status.as_u16() >= 500;
341 }
342 false
343}
344
345pub async fn with_retry<R, F, Fut>(mut request: F) -> Result<R, Error>
358where
359 F: FnMut() -> Fut,
360 Fut: std::future::Future<Output = Result<R, reqwest::Error>>,
361{
362 let config = TestConfig::default();
363 let max_attempts = config.http_retry_attempts;
364
365 for attempt in 0..max_attempts {
366 let result = request().await;
367
368 if let Ok(r) = result {
369 return Ok(r);
370 }
371
372 if let Err(e) = &result {
374 let (delay, should_retry) = TestConfig::retry_delay(attempt);
375
376 #[allow(clippy::collapsible_if)]
378 if should_retry && is_transient_error(e) && attempt < max_attempts - 1 {
379 tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
380 continue;
381 }
382
383 return result.map_err(Error::NetworkError);
385 }
386 }
387
388 Err(Error::context("retry loop ended without result or error"))
390}
391
392pub async fn discover_client_ip(client: &Client) -> Result<String, Error> {
398 if let Ok(response) = client
399 .get("https://www.speedtest.net/api/ip.php")
400 .send()
401 .await
402 {
403 if let Ok(text) = response.text().await {
404 let trimmed = text.trim().to_string();
405 if common::is_valid_ipv4(&trimmed) {
406 return Ok(trimmed);
407 }
408 }
409 }
410
411 if let Ok(response) = client
412 .get("https://www.speedtest.net/api/ios-config.php")
413 .send()
414 .await
415 {
416 if let Ok(text) = response.text().await {
417 if let Some(ip) = parse_ip_from_xml(&text) {
418 return Ok(ip);
419 }
420 }
421 }
422
423 Ok("unknown".to_string())
424}
425
426fn parse_ip_from_xml(xml: &str) -> Option<String> {
427 #[derive(serde::Deserialize)]
430 struct Settings {
431 client: ClientElement,
432 }
433 #[derive(serde::Deserialize)]
434 struct ClientElement {
435 #[serde(rename = "@ip")]
436 ip: Option<String>,
437 }
438
439 let settings: Settings = match quick_xml::de::from_str(xml) {
442 Ok(s) => s,
443 Err(_) => return None,
444 };
445 let ip = settings.client.ip?;
446 if common::is_valid_ipv4(&ip) {
447 Some(ip)
448 } else {
449 None
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456 use std::sync::Arc;
457 use std::sync::atomic::{AtomicUsize, Ordering};
458
459 #[test]
462 fn test_tls_config_with_ca_cert() {
463 let config = TlsConfig::default();
464 assert!(config.ca_cert_path.is_none());
465
466 let config = config.with_ca_cert(std::path::PathBuf::from("/path/to/cert.pem"));
467 assert_eq!(
468 config.ca_cert_path,
469 Some(std::path::PathBuf::from("/path/to/cert.pem"))
470 );
471 }
472
473 #[test]
474 fn test_tls_config_with_min_tls_version() {
475 let config = TlsConfig::default();
476 assert!(config.min_tls_version.is_none());
477
478 let config = config.with_min_tls_version("1.2");
479 assert_eq!(config.min_tls_version, Some("1.2".to_string()));
480
481 let config = TlsConfig::default().with_min_tls_version("1.3");
482 assert_eq!(config.min_tls_version, Some("1.3".to_string()));
483 }
484
485 #[test]
486 fn test_tls_config_with_cert_pinning() {
487 let config = TlsConfig::default();
488 assert!(!config.pin_speedtest_certs);
489
490 let config = config.with_cert_pinning();
491 assert!(config.pin_speedtest_certs);
492 }
493
494 #[test]
495 fn test_tls_config_builder_chaining() {
496 let config = TlsConfig::default()
498 .with_ca_cert(std::path::PathBuf::from("/custom/ca.pem"))
499 .with_min_tls_version("1.3")
500 .with_cert_pinning();
501
502 assert_eq!(
503 config.ca_cert_path,
504 Some(std::path::PathBuf::from("/custom/ca.pem"))
505 );
506 assert_eq!(config.min_tls_version, Some("1.3".to_string()));
507 assert!(config.pin_speedtest_certs);
508 }
509
510 #[test]
513 fn test_settings_default_values() {
514 let settings = Settings::default();
515 assert_eq!(settings.timeout_secs, 10);
516 assert!(settings.source_ip.is_none());
517 assert_eq!(settings.user_agent, DEFAULT_USER_AGENT);
518 assert!(settings.retry_enabled);
519 assert!(settings.tls.ca_cert_path.is_none());
521 assert!(settings.tls.min_tls_version.is_none());
522 assert!(!settings.tls.pin_speedtest_certs);
523 }
524
525 #[test]
526 fn test_settings_with_user_agent() {
527 let settings = Settings::default().with_user_agent("Custom Agent/1.0");
528 assert_eq!(settings.user_agent, "Custom Agent/1.0");
529 }
530
531 #[test]
532 fn test_settings_with_user_agent_chaining() {
533 let settings = Settings::default()
534 .with_user_agent("Test Agent")
535 .with_retry_disabled();
536 assert_eq!(settings.user_agent, "Test Agent");
537 assert!(!settings.retry_enabled);
538 }
539
540 #[test]
541 fn test_settings_with_retry_disabled() {
542 let settings = Settings::default();
543 assert!(settings.retry_enabled);
544
545 let settings = settings.with_retry_disabled();
546 assert!(!settings.retry_enabled);
547 }
548
549 #[test]
550 fn test_settings_debug_trait() {
551 let settings = Settings::default();
552 let debug_str = format!("{:?}", settings);
553 assert!(debug_str.contains("timeout_secs"));
554 assert!(debug_str.contains("user_agent"));
555 }
556
557 #[test]
558 fn test_settings_clone() {
559 let settings = Settings::default();
560 let cloned = settings.clone();
561 assert_eq!(settings.user_agent, cloned.user_agent);
562 assert_eq!(settings.timeout_secs, cloned.timeout_secs);
563 }
564
565 #[test]
576 #[ignore]
577 fn test_build_tls_config_unknown_tls_version() {
578 let tls = TlsConfig {
579 min_tls_version: Some("99.0".to_string()),
580 ..Default::default()
581 };
582 let result = build_tls_config(&tls);
583 assert!(result.is_ok());
584 }
585
586 #[test]
587 #[ignore]
588 fn test_build_tls_config_tls12() {
589 let tls = TlsConfig {
590 min_tls_version: Some("1.2".to_string()),
591 ..Default::default()
592 };
593 let result = build_tls_config(&tls);
594 assert!(result.is_ok());
595 }
596
597 #[test]
598 #[ignore]
599 fn test_build_tls_config_tls13() {
600 let tls = TlsConfig {
601 min_tls_version: Some("1.3".to_string()),
602 ..Default::default()
603 };
604 let result = build_tls_config(&tls);
605 assert!(result.is_ok());
606 }
607
608 #[test]
609 #[ignore]
610 fn test_build_tls_config_pinning_takes_precedence() {
611 let tls = TlsConfig {
612 ca_cert_path: Some(std::path::PathBuf::from("/path/to/ca.pem")),
613 pin_speedtest_certs: true,
614 ..Default::default()
615 };
616 let result = build_tls_config(&tls);
617 assert!(result.is_ok());
618 }
619
620 #[test]
621 #[ignore]
622 fn test_build_tls_config_pinning_only() {
623 let tls = TlsConfig {
624 pin_speedtest_certs: true,
625 ..Default::default()
626 };
627 let result = build_tls_config(&tls);
628 assert!(result.is_ok());
629 }
630
631 #[test]
632 #[ignore]
633 fn test_build_tls_config_no_options() {
634 let tls = TlsConfig::default();
635 let result = build_tls_config(&tls);
636 assert!(result.is_ok());
637 }
638
639 #[test]
642 fn test_load_custom_ca_cert_file_not_found() {
643 let result = load_custom_ca_cert(std::path::Path::new("/nonexistent/cert.pem"));
644 assert!(result.is_err());
645 let err = result.unwrap_err();
646 let err_msg = format!("{:?}", err);
648 assert!(err_msg.contains("nonexistent") || err_msg.contains("Failed to read CA cert"));
649 }
650
651 #[test]
652 fn test_load_custom_ca_cert_invalid_path() {
653 let result = load_custom_ca_cert(std::path::Path::new("/tmp"));
655 assert!(result.is_err());
656 }
657
658 #[test]
663 fn test_create_client_source_ip_v4() {
664 let settings = Settings {
665 source_ip: Some("192.168.1.100".to_string()),
666 ..Default::default()
667 };
668 let result = create_client(&settings);
669 match result {
671 Ok(_) => {}
672 Err(Error::Context { .. }) => {} Err(e) => panic!("Unexpected error type for valid IPv4: {e:?}"),
674 }
675 }
676
677 #[test]
678 fn test_create_client_source_ip_v6() {
679 let settings = Settings {
680 source_ip: Some("::1".to_string()),
681 ..Default::default()
682 };
683 let result = create_client(&settings);
684 match result {
685 Ok(_) => {}
686 Err(Error::NetworkError(_) | Error::Context { .. }) => {} Err(e) => panic!("Unexpected error type: {e:?}"),
688 }
689 }
690
691 #[test]
692 #[ignore]
693 fn test_create_client_with_ca_cert() {
694 let settings = Settings {
695 tls: TlsConfig {
696 ca_cert_path: Some(std::path::PathBuf::from("/nonexistent/ca.pem")),
697 ..Default::default()
698 },
699 ..Default::default()
700 };
701 let result = create_client(&settings);
702 assert!(result.is_err());
703 }
704
705 #[test]
706 #[ignore]
707 fn test_create_client_with_pinning() {
708 let settings = Settings {
709 tls: TlsConfig {
710 pin_speedtest_certs: true,
711 ..Default::default()
712 },
713 ..Default::default()
714 };
715 let result = create_client(&settings);
716 assert!(result.is_ok());
717 }
718
719 #[test]
720 fn test_create_client_with_retry_disabled() {
721 let settings = Settings::default().with_retry_disabled();
722 let result = create_client(&settings);
723 assert!(result.is_ok());
724 }
725
726 #[test]
727 fn test_create_client_timeout_30() {
728 let settings = Settings {
729 timeout_secs: 30,
730 ..Default::default()
731 };
732 let result = create_client(&settings);
733 assert!(result.is_ok());
734 }
735
736 #[test]
737 fn test_create_client_timeout_60() {
738 let settings = Settings {
739 timeout_secs: 60,
740 ..Default::default()
741 };
742 let result = create_client(&settings);
743 assert!(result.is_ok());
744 }
745
746 #[test]
749 fn test_pinning_verifier_is_valid_domain_speedtest() {
750 assert!(PinningVerifier::is_valid_domain("speedtest.net"));
752 assert!(PinningVerifier::is_valid_domain("www.speedtest.net"));
753 assert!(PinningVerifier::is_valid_domain("api.speedtest.net"));
754 assert!(PinningVerifier::is_valid_domain("foo.bar.speedtest.net"));
755 assert!(PinningVerifier::is_valid_domain("fake.speedtest.net")); assert!(!PinningVerifier::is_valid_domain("evilsite.net"));
759 assert!(!PinningVerifier::is_valid_domain("speedtest.com"));
760 assert!(!PinningVerifier::is_valid_domain("notspeedtest.net"));
761 }
762
763 #[test]
764 fn test_pinning_verifier_is_valid_domain_ookla() {
765 assert!(PinningVerifier::is_valid_domain("ookla.com"));
767 assert!(PinningVerifier::is_valid_domain("www.ookla.com"));
768 assert!(PinningVerifier::is_valid_domain("api.ookla.com"));
769 assert!(PinningVerifier::is_valid_domain("foo.bar.ookla.com"));
770 assert!(PinningVerifier::is_valid_domain("fake.ookla.com")); assert!(!PinningVerifier::is_valid_domain("ookla.net"));
774 }
775
776 #[test]
777 fn test_pinning_verifier_edge_cases() {
778 assert!(!PinningVerifier::is_valid_domain(""));
780 assert!(!PinningVerifier::is_valid_domain("speedtestXnet")); assert!(!PinningVerifier::is_valid_domain("attack.com")); }
783
784 #[test]
785 fn test_pinning_verifier_exact_domains() {
786 assert!(PinningVerifier::is_valid_domain("speedtest.net"));
788 assert!(PinningVerifier::is_valid_domain("ookla.com"));
789 }
790
791 #[test]
792 fn test_pinning_verifier_subdomains() {
793 assert!(PinningVerifier::is_valid_domain("www.speedtest.net"));
795 assert!(PinningVerifier::is_valid_domain("api.speedtest.net"));
796 assert!(PinningVerifier::is_valid_domain("a.b.c.speedtest.net"));
797 assert!(PinningVerifier::is_valid_domain("www.ookla.com"));
798 assert!(PinningVerifier::is_valid_domain("api.www.ookla.com"));
799 }
800
801 #[test]
802 fn test_pinning_verifier_invalid_suffixes() {
803 assert!(!PinningVerifier::is_valid_domain("xspeedtest.net")); assert!(!PinningVerifier::is_valid_domain("fake-speedtest.net")); assert!(!PinningVerifier::is_valid_domain("speedtest.net.evil.com")); assert!(!PinningVerifier::is_valid_domain("ookla.com.evil.com")); assert!(!PinningVerifier::is_valid_domain("fooookla.com")); }
810
811 #[test]
812 fn test_pinning_verifier_case_sensitivity() {
813 assert!(!PinningVerifier::is_valid_domain("Speedtest.net")); assert!(!PinningVerifier::is_valid_domain("SPEEDTEST.NET")); assert!(!PinningVerifier::is_valid_domain("www.Speedtest.net")); assert!(!PinningVerifier::is_valid_domain("OOKLA.COM")); }
819
820 #[test]
821 fn test_pinning_verifier_special_characters() {
822 assert!(!PinningVerifier::is_valid_domain("speedtest.net/")); assert!(!PinningVerifier::is_valid_domain("speedtest.net:443")); assert!(!PinningVerifier::is_valid_domain("speedtest.net/path")); }
827
828 #[test]
829 fn test_pinning_verifier_numeric_domains() {
830 assert!(PinningVerifier::is_valid_domain("123.speedtest.net")); assert!(PinningVerifier::is_valid_domain("1.2.3.speedtest.net")); assert!(!PinningVerifier::is_valid_domain("speedtest123.net")); assert!(!PinningVerifier::is_valid_domain("123speedtest.net")); }
837
838 #[test]
839 fn test_pinning_verifier_new_returns_self() {
840 let verifier = PinningVerifier::new();
842 assert!(!verifier.supported_verify_schemes().is_empty());
843 }
844
845 #[test]
846 fn test_pinning_verifier_debug_trait() {
847 let verifier = PinningVerifier::new();
849 let debug_str = format!("{:?}", verifier);
850 assert!(debug_str.contains("PinningVerifier"));
851 }
852
853 #[test]
854 fn test_pinning_verifier_supported_verify_schemes() {
855 let verifier = PinningVerifier::new();
856 let schemes = verifier.supported_verify_schemes();
857
858 assert!(schemes.contains(&SignatureScheme::RSA_PKCS1_SHA256));
860 assert!(schemes.contains(&SignatureScheme::RSA_PKCS1_SHA384));
861 assert!(schemes.contains(&SignatureScheme::RSA_PKCS1_SHA512));
862 assert!(schemes.contains(&SignatureScheme::ECDSA_NISTP256_SHA256));
863 assert!(schemes.contains(&SignatureScheme::ECDSA_NISTP384_SHA384));
864 assert!(schemes.contains(&SignatureScheme::RSA_PSS_SHA256));
865 assert!(schemes.contains(&SignatureScheme::RSA_PSS_SHA384));
866 assert!(schemes.contains(&SignatureScheme::RSA_PSS_SHA512));
867
868 assert!(schemes.len() >= 8);
869 }
870
871 #[test]
877 fn test_pinning_verifier_verify_server_cert_rejects_invalid_domain() {
878 let verifier = PinningVerifier::new();
879
880 let dns_name = rustls::pki_types::DnsName::try_from("evil.com".to_string()).unwrap();
882 let server_name = ServerName::DnsName(dns_name);
883
884 let cert_der = CertificateDer::from(vec![]);
887
888 let result =
890 verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
891
892 assert!(result.is_err());
893 let err = result.unwrap_err();
894 let err_msg = format!("{:?}", err);
895 assert!(err_msg.contains("evil.com") || err_msg.contains("not a speedtest.net domain"));
896 }
897
898 #[test]
899 fn test_pinning_verifier_verify_server_cert_rejects_unsupported_name_type() {
900 let verifier = PinningVerifier::new();
901
902 let ip_addr = std::net::IpAddr::from([127, 0, 0, 1]);
904 let server_name = ServerName::IpAddress(ip_addr.into());
905
906 let cert_der = CertificateDer::from(vec![]);
907
908 let result =
909 verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
910
911 assert!(result.is_err());
912 let err = result.unwrap_err();
913 let err_msg = format!("{:?}", err);
914 assert!(err_msg.contains("Unsupported server name type"));
915 }
916
917 #[test]
918 fn test_pinning_verifier_verify_server_cert_rejects_invalid_certificate() {
919 let verifier = PinningVerifier::new();
920
921 let dns_name =
923 rustls::pki_types::DnsName::try_from("www.speedtest.net".to_string()).unwrap();
924 let server_name = ServerName::DnsName(dns_name);
925
926 let cert_der = CertificateDer::from(vec![]);
928
929 let result =
930 verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
931
932 assert!(result.is_err());
934 let err = result.unwrap_err();
935 let err_msg = format!("{:?}", err);
936 assert!(!err_msg.contains("not a speedtest.net domain"));
938 }
939
940 #[test]
941 fn test_pinning_verifier_domain_checked_before_cert_parse_speedtest() {
942 let verifier = PinningVerifier::new();
943
944 let dns_name = rustls::pki_types::DnsName::try_from("speedtest.net".to_string()).unwrap();
946 let server_name = ServerName::DnsName(dns_name);
947
948 let cert_der = CertificateDer::from(vec![]);
951
952 let result =
953 verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
954
955 assert!(result.is_err());
958 let err = result.unwrap_err();
959 let err_msg = format!("{:?}", err);
960 assert!(!err_msg.contains("not a speedtest.net domain"));
962 }
963
964 #[test]
965 fn test_pinning_verifier_ipv6_address_rejected() {
966 let verifier = PinningVerifier::new();
967
968 let ip_addr = std::net::IpAddr::from([0, 0, 0, 0, 0, 0, 0, 1]); let server_name = ServerName::IpAddress(ip_addr.into());
971
972 let cert_der = CertificateDer::from(vec![]);
973
974 let result =
975 verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
976
977 assert!(result.is_err());
978 }
979
980 #[test]
981 fn test_pinning_verifier_domain_checked_before_cert_parse_ookla() {
982 let verifier = PinningVerifier::new();
983
984 let dns_name = rustls::pki_types::DnsName::try_from("ookla.com".to_string()).unwrap();
986 let server_name = ServerName::DnsName(dns_name);
987
988 let cert_der = CertificateDer::from(vec![]);
990
991 let result =
992 verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
993
994 assert!(result.is_err());
996 }
997
998 #[test]
999 fn test_pinning_verifier_domain_validation_order() {
1000 let verifier = PinningVerifier::new();
1002
1003 let dns_name = rustls::pki_types::DnsName::try_from("attacker.com".to_string()).unwrap();
1005 let server_name = ServerName::DnsName(dns_name);
1006
1007 let cert_der = CertificateDer::from(vec![]);
1010
1011 let result =
1012 verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
1013
1014 assert!(result.is_err());
1015 let err = result.unwrap_err();
1016 let err_msg = format!("{:?}", err);
1017 assert!(
1018 err_msg.contains("not a speedtest.net domain"),
1019 "Expected domain validation error, got: {}",
1020 err_msg
1021 );
1022 }
1023
1024 #[test]
1025 fn test_pinning_verifier_verify_server_cert_rejects_different_tld() {
1026 let verifier = PinningVerifier::new();
1027
1028 let dns_name =
1030 rustls::pki_types::DnsName::try_from("speedtest.net.org".to_string()).unwrap();
1031 let server_name = ServerName::DnsName(dns_name);
1032
1033 let cert_der = CertificateDer::from(vec![]);
1034
1035 let result =
1036 verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
1037
1038 assert!(result.is_err());
1039 let err = result.unwrap_err();
1040 let err_msg = format!("{:?}", err);
1041 assert!(err_msg.contains("not a speedtest.net domain"));
1042 }
1043
1044 #[test]
1045 fn test_pinning_verifier_intermediate_certs_ignored() {
1046 let verifier = PinningVerifier::new();
1049
1050 let dns_name =
1052 rustls::pki_types::DnsName::try_from("www.speedtest.net".to_string()).unwrap();
1053 let server_name = ServerName::DnsName(dns_name);
1054
1055 let cert_der = CertificateDer::from(vec![]);
1057
1058 let intermediate_cert = CertificateDer::from(vec![0u8; 10]);
1060
1061 let result = verifier.verify_server_cert(
1062 &cert_der,
1063 &[intermediate_cert],
1064 &server_name,
1065 &[],
1066 UnixTime::now(),
1067 );
1068
1069 assert!(result.is_err());
1071 }
1072
1073 #[test]
1074 fn test_pinning_verifier_ocsp_response_ignored() {
1075 let verifier = PinningVerifier::new();
1077
1078 let dns_name = rustls::pki_types::DnsName::try_from("api.ookla.com".to_string()).unwrap();
1080 let server_name = ServerName::DnsName(dns_name);
1081
1082 let cert_der = CertificateDer::from(vec![]);
1084
1085 let ocsp_response = vec![0x30, 0x03, 0x01, 0x00];
1087
1088 let result = verifier.verify_server_cert(
1089 &cert_der,
1090 &[],
1091 &server_name,
1092 &ocsp_response,
1093 UnixTime::now(),
1094 );
1095
1096 assert!(result.is_err());
1098 }
1099
1100 #[test]
1101 fn test_pinning_verifier_all_valid_subdomains() {
1102 let valid_subdomains = [
1104 "www.speedtest.net",
1105 "api.speedtest.net",
1106 "test.speedtest.net",
1107 "staging.speedtest.net",
1108 "prod.speedtest.net",
1109 "cdn.speedtest.net",
1110 "a.speedtest.net",
1111 "z.speedtest.net",
1112 "a1b2c3.speedtest.net",
1113 "my-site.speedtest.net",
1114 "www.ookla.com",
1115 "api.ookla.com",
1116 "test.ookla.com",
1117 ];
1118
1119 for domain in valid_subdomains {
1120 assert!(
1121 PinningVerifier::is_valid_domain(domain),
1122 "Domain '{}' should be valid",
1123 domain
1124 );
1125 }
1126 }
1127
1128 #[test]
1129 fn test_pinning_verifier_all_invalid_domains() {
1130 let invalid_domains = [
1132 "evilsite.net",
1133 "speedtest.net.evil.com",
1134 "ookla.com.evil.com",
1135 "speedtest.com",
1136 "ookla.net",
1137 "notspeedtest.net",
1138 "notookla.com",
1139 "fake-speedtest.net",
1140 "fake-ookla.com",
1141 "attacker.speedtest.net.fake.com",
1142 "attacker.ookla.com.fake.com",
1143 ];
1144
1145 for domain in invalid_domains {
1146 assert!(
1147 !PinningVerifier::is_valid_domain(domain),
1148 "Domain '{}' should be invalid",
1149 domain
1150 );
1151 }
1152 }
1153
1154 #[test]
1157 fn test_parse_ip_from_xml() {
1158 let xml = r#"<settings><client country="CA" ip="173.35.57.235" isp="Rogers"/></settings>"#;
1159 assert_eq!(parse_ip_from_xml(xml), Some("173.35.57.235".to_string()));
1160 }
1161
1162 #[test]
1163 fn test_parse_ip_from_xml_full_response() {
1164 let xml = r#"<?xml version="1.0"?>
1165<settings>
1166 <config downloadThreadCountV3="4"/>
1167 <client country="CA" ip="173.35.57.235" isp="Rogers"/>
1168</settings>"#;
1169 assert_eq!(parse_ip_from_xml(xml), Some("173.35.57.235".to_string()));
1170 }
1171
1172 #[test]
1173 fn test_parse_ip_from_xml_invalid() {
1174 assert!(parse_ip_from_xml("not xml").is_none());
1175 assert!(parse_ip_from_xml("<html></html>").is_none());
1176 assert!(parse_ip_from_xml("<settings><client ip=\"invalid\"/></settings>").is_none());
1177 }
1178
1179 #[test]
1180 fn test_create_client_invalid_source_ip() {
1181 let source = crate::config::ConfigSource::default();
1182 let config = crate::config::Config::from_source(&source);
1183 let mut settings = Settings::from(&config);
1184 settings.source_ip = Some("invalid-ip".to_string());
1185 let result = create_client(&settings);
1186 assert!(result.is_err());
1187 assert!(matches!(result.unwrap_err(), Error::Context { .. }));
1188 }
1189
1190 #[test]
1191 fn test_create_client_valid_config() {
1192 let source = crate::config::ConfigSource::default();
1193 let config = crate::config::Config::from_source(&source);
1194 let settings = Settings::from(&config);
1195 let result = create_client(&settings);
1196 assert!(result.is_ok());
1197 }
1198
1199 #[test]
1200 fn test_create_client_with_source_ip() {
1201 let source = crate::config::ConfigSource {
1202 network: crate::config::NetworkSource {
1203 source: Some("0.0.0.0".into()),
1204 ..Default::default()
1205 },
1206 ..Default::default()
1207 };
1208 let config = crate::config::Config::from_source(&source);
1209 let settings = Settings::from(&config);
1210 let result = create_client(&settings);
1211 match result {
1212 Ok(_) | Err(Error::NetworkError(_) | Error::Context { .. }) => {}
1213 Err(e) => panic!("Unexpected error type: {e:?}"),
1214 }
1215 }
1216
1217 #[test]
1218 fn test_create_client_custom_timeout() {
1219 let source = crate::config::ConfigSource {
1220 network: crate::config::NetworkSource {
1221 timeout: 30,
1222 ..Default::default()
1223 },
1224 ..Default::default()
1225 };
1226 let config = crate::config::Config::from_source(&source);
1227 let settings = Settings::from(&config);
1228 let result = create_client(&settings);
1229 assert!(result.is_ok());
1230 }
1231
1232 #[test]
1235 fn test_settings_from_config_with_source_ip() {
1236 let source = crate::config::ConfigSource {
1237 network: crate::config::NetworkSource {
1238 source: Some("192.168.1.50".to_string()),
1239 ..Default::default()
1240 },
1241 ..Default::default()
1242 };
1243 let config = crate::config::Config::from_source(&source);
1244 let settings = Settings::from(&config);
1245 assert_eq!(settings.source_ip, Some("192.168.1.50".to_string()));
1246 }
1247
1248 #[test]
1249 fn test_settings_from_config_with_ca_cert() {
1250 let source = crate::config::ConfigSource {
1251 network: crate::config::NetworkSource {
1252 ca_cert: Some("/path/to/ca.pem".to_string()),
1253 ..Default::default()
1254 },
1255 ..Default::default()
1256 };
1257 let config = crate::config::Config::from_source(&source);
1258 let settings = Settings::from(&config);
1259 assert_eq!(
1260 settings.tls.ca_cert_path,
1261 Some(std::path::PathBuf::from("/path/to/ca.pem"))
1262 );
1263 }
1264
1265 #[test]
1266 fn test_settings_from_config_with_tls_version() {
1267 let source = crate::config::ConfigSource {
1268 network: crate::config::NetworkSource {
1269 tls_version: Some("1.2".to_string()),
1270 ..Default::default()
1271 },
1272 ..Default::default()
1273 };
1274 let config = crate::config::Config::from_source(&source);
1275 let settings = Settings::from(&config);
1276 assert_eq!(settings.tls.min_tls_version, Some("1.2".to_string()));
1277 }
1278
1279 #[test]
1280 fn test_settings_from_config_with_pinning() {
1281 let source = crate::config::ConfigSource {
1282 network: crate::config::NetworkSource {
1283 pin_certs: Some(true),
1284 ..Default::default()
1285 },
1286 ..Default::default()
1287 };
1288 let config = crate::config::Config::from_source(&source);
1289 let settings = Settings::from(&config);
1290 assert!(settings.tls.pin_speedtest_certs);
1291 }
1292
1293 #[test]
1294 fn test_settings_from_config_timeout() {
1295 let source = crate::config::ConfigSource {
1296 network: crate::config::NetworkSource {
1297 timeout: 45,
1298 ..Default::default()
1299 },
1300 ..Default::default()
1301 };
1302 let config = crate::config::Config::from_source(&source);
1303 let settings = Settings::from(&config);
1304 assert_eq!(settings.timeout_secs, 45);
1305 }
1306
1307 #[test]
1308 fn test_settings_from_config_default_user_agent() {
1309 let config = crate::config::Config::from_source(&crate::config::ConfigSource::default());
1310 let settings = Settings::from(&config);
1311 assert_eq!(settings.user_agent, DEFAULT_USER_AGENT);
1312 }
1313
1314 #[test]
1315 fn test_settings_from_config_retry_enabled_by_default() {
1316 let config = crate::config::Config::from_source(&crate::config::ConfigSource::default());
1317 let settings = Settings::from(&config);
1318 assert!(settings.retry_enabled);
1319 }
1320
1321 #[tokio::test]
1324 async fn test_with_retry_immediate_success() {
1325 let counter = Arc::new(AtomicUsize::new(0));
1326 let count = Arc::clone(&counter);
1327
1328 let result = with_retry(|| {
1329 let c = Arc::clone(&count);
1330 async move {
1331 c.fetch_add(1, Ordering::SeqCst);
1332 Ok::<_, reqwest::Error>(42)
1333 }
1334 })
1335 .await;
1336
1337 assert!(result.is_ok());
1338 assert_eq!(result.unwrap(), 42);
1339 assert_eq!(counter.load(Ordering::SeqCst), 1);
1340 }
1341
1342 #[tokio::test]
1343 async fn test_with_retry_with_mock_request() {
1344 let result = with_retry(|| async { Ok::<_, reqwest::Error>(100) }).await;
1346 assert!(result.is_ok());
1347 assert_eq!(result.unwrap(), 100);
1348 }
1349
1350 #[tokio::test]
1351 async fn test_with_retry_counter_increment() {
1352 let counter = Arc::new(AtomicUsize::new(0));
1353 let count = Arc::clone(&counter);
1354
1355 let _result = with_retry(|| {
1356 let c = Arc::clone(&count);
1357 async move {
1358 c.fetch_add(1, Ordering::SeqCst);
1359 Ok::<_, reqwest::Error>(1)
1360 }
1361 })
1362 .await;
1363
1364 assert_eq!(counter.load(Ordering::SeqCst), 1);
1366 }
1367
1368 #[tokio::test]
1369 async fn test_with_retry_different_value_types() {
1370 let result_str = with_retry(|| async { Ok::<_, reqwest::Error>("hello") }).await;
1372 assert!(result_str.is_ok());
1373 assert_eq!(result_str.unwrap(), "hello");
1374
1375 let result_u64 = with_retry(|| async { Ok::<_, reqwest::Error>(999u64) }).await;
1376 assert!(result_u64.is_ok());
1377 assert_eq!(result_u64.unwrap(), 999);
1378
1379 let result_vec = with_retry(|| async { Ok::<_, reqwest::Error>(vec![1, 2, 3]) }).await;
1380 assert!(result_vec.is_ok());
1381 assert_eq!(result_vec.unwrap(), vec![1, 2, 3]);
1382 }
1383
1384 #[tokio::test]
1385 async fn test_with_retry_multiple_sequential_calls() {
1386 for i in 0..3 {
1388 let result = with_retry(|| async { Ok::<_, reqwest::Error>(i) }).await;
1389 assert!(result.is_ok());
1390 assert_eq!(result.unwrap(), i);
1391 }
1392 }
1393
1394 #[test]
1397 fn test_parse_ip_from_xml_missing_client_element() {
1398 let xml = r#"<settings><server ip="127.0.0.1"/></settings>"#;
1399 assert!(parse_ip_from_xml(xml).is_none());
1400 }
1401
1402 #[test]
1403 fn test_parse_ip_from_xml_empty_ip() {
1404 let xml = r#"<settings><client ip=""/></settings>"#;
1405 assert!(parse_ip_from_xml(xml).is_none());
1406 }
1407
1408 #[test]
1409 fn test_parse_ip_from_xml_whitespace_ip() {
1410 let xml = r#"<settings><client ip=" " /></settings>"#;
1411 assert!(parse_ip_from_xml(xml).is_none());
1412 }
1413
1414 #[test]
1415 fn test_parse_ip_from_xml_ipv6_format() {
1416 let xml = r#"<settings><client ip="::1"/></settings>"#;
1417 assert!(parse_ip_from_xml(xml).is_none());
1419 }
1420
1421 #[test]
1422 fn test_parse_ip_from_xml_special_characters() {
1423 let xml = r#"<settings><client country="US" ip="192.168.1.1" isp="ISP"/></settings>"#;
1424 assert_eq!(parse_ip_from_xml(xml), Some("192.168.1.1".to_string()));
1425 }
1426
1427 #[test]
1428 fn test_parse_ip_from_xml_garbage_after_xml() {
1429 let xml = r#"<settings><client ip="1.2.3.4" /></settings>GARBAGE"#;
1430 assert_eq!(parse_ip_from_xml(xml), Some("1.2.3.4".to_string()));
1431 }
1432
1433 #[test]
1434 fn test_parse_ip_from_xml_malformed_xml() {
1435 assert!(parse_ip_from_xml("<settings><client").is_none());
1436 assert!(parse_ip_from_xml("</settings>").is_none());
1437 assert!(parse_ip_from_xml("").is_none());
1438 }
1439
1440 #[tokio::test]
1443 async fn test_discover_client_ip_handles_network_failure() {
1444 let settings = Settings::default().with_retry_disabled();
1446 let client = create_client(&settings).unwrap();
1447
1448 let result = discover_client_ip(&client).await;
1450
1451 match result {
1453 Ok(ip) => {
1454 assert!(ip == "unknown" || common::is_valid_ipv4(&ip));
1456 }
1457 Err(e) => {
1458 assert!(matches!(e, Error::NetworkError(_)));
1460 }
1461 }
1462 }
1463
1464 #[test]
1467 fn test_tls_config_debug() {
1468 let config = TlsConfig::default();
1469 let debug_str = format!("{:?}", config);
1470 assert!(debug_str.contains("TlsConfig"));
1471 }
1472
1473 #[test]
1474 fn test_tls_config_clone() {
1475 let config = TlsConfig::default()
1476 .with_ca_cert(std::path::PathBuf::from("/test.pem"))
1477 .with_min_tls_version("1.3")
1478 .with_cert_pinning();
1479 let cloned = config.clone();
1480 assert_eq!(cloned.ca_cert_path, config.ca_cert_path);
1481 assert_eq!(cloned.min_tls_version, config.min_tls_version);
1482 assert_eq!(cloned.pin_speedtest_certs, config.pin_speedtest_certs);
1483 }
1484
1485 #[test]
1486 fn test_tls_config_default_trait() {
1487 let config = TlsConfig::default();
1488 assert!(config.ca_cert_path.is_none());
1489 assert!(config.min_tls_version.is_none());
1490 assert!(!config.pin_speedtest_certs);
1491 }
1492
1493 #[test]
1496 fn test_settings_with_source_ip() {
1497 let settings = Settings {
1498 source_ip: Some("10.0.0.1".to_string()),
1499 ..Default::default()
1500 };
1501 let cloned = settings.clone();
1502 assert_eq!(cloned.source_ip, Some("10.0.0.1".to_string()));
1503 }
1504
1505 #[test]
1506 fn test_settings_builder_full_chain() {
1507 let settings = Settings::default()
1508 .with_user_agent("Test/1.0")
1509 .with_retry_disabled();
1510 assert_eq!(settings.user_agent, "Test/1.0");
1511 assert!(!settings.retry_enabled);
1512 }
1513
1514 #[test]
1515 fn test_settings_clone_is_independent() {
1516 let mut settings = Settings {
1517 timeout_secs: 60,
1518 ..Default::default()
1519 };
1520 let cloned = settings.clone();
1521 assert_eq!(cloned.timeout_secs, 60);
1522 settings.timeout_secs = 120;
1524 assert_eq!(cloned.timeout_secs, 60); }
1526
1527 #[test]
1530 fn test_create_client_with_source_ip_none() {
1531 let settings = Settings::default();
1532 let result = create_client(&settings);
1533 assert!(result.is_ok());
1534 }
1535
1536 #[test]
1537 fn test_create_client_with_custom_user_agent() {
1538 let settings = Settings::default().with_user_agent("TestAgent/1.0");
1539 let result = create_client(&settings);
1540 assert!(result.is_ok());
1541 }
1542
1543 #[test]
1544 fn test_create_client_timeout_zero() {
1545 let settings = Settings {
1546 timeout_secs: 0,
1547 ..Default::default()
1548 };
1549 let result = create_client(&settings);
1550 assert!(result.is_ok());
1551 }
1552
1553 #[test]
1554 fn test_create_client_timeout_large() {
1555 let settings = Settings {
1556 timeout_secs: 300,
1557 ..Default::default()
1558 };
1559 let result = create_client(&settings);
1560 assert!(result.is_ok());
1561 }
1562
1563 #[test]
1566 fn test_error_context_message() {
1567 let err = Error::context("test error");
1568 let msg = format!("{:?}", err);
1569 assert!(msg.contains("test error"));
1570 }
1571
1572 #[test]
1573 fn test_error_context_with_source() {
1574 let inner = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
1575 let err = Error::with_source("operation failed", inner);
1576 let msg = format!("{:?}", err);
1577 assert!(msg.contains("operation failed") || msg.contains("file not found"));
1578 }
1579
1580 #[test]
1581 fn test_error_server_not_found() {
1582 let err = Error::ServerNotFound("no servers available".into());
1583 let msg = format!("{:?}", err);
1584 assert!(msg.contains("no servers available") || msg.contains("ServerNotFound"));
1585 }
1586
1587 #[test]
1588 fn test_error_download_failure() {
1589 let err = Error::DownloadFailure("test download failed".into());
1590 let msg = format!("{:?}", err);
1591 assert!(msg.contains("test download failed") || msg.contains("DownloadFailure"));
1592 }
1593
1594 #[test]
1595 fn test_error_upload_failure() {
1596 let err = Error::UploadFailure("test upload failed".into());
1597 let msg = format!("{:?}", err);
1598 assert!(msg.contains("test upload failed") || msg.contains("UploadFailure"));
1599 }
1600
1601 #[test]
1602 fn test_error_context_debug() {
1603 let err = Error::context("context debug");
1604 let debug_str = format!("{:?}", err);
1605 assert!(debug_str.contains("Context"));
1606 assert!(debug_str.contains("context debug"));
1607 }
1608
1609 #[test]
1610 fn test_error_context_display() {
1611 let err = Error::context("context display");
1612 assert_eq!(format!("{}", err), "context display");
1613 }
1614
1615 #[test]
1616 fn test_error_download_failure_display() {
1617 let err = Error::DownloadFailure("download failed".into());
1618 let display = format!("{}", err);
1619 assert!(display.contains("download failed"));
1620 }
1621
1622 #[test]
1623 fn test_error_upload_failure_display() {
1624 let err = Error::UploadFailure("upload failed".into());
1625 let display = format!("{}", err);
1626 assert!(display.contains("upload failed"));
1627 }
1628
1629 #[test]
1630 fn test_error_server_not_found_display() {
1631 let err = Error::ServerNotFound("server not found".into());
1632 let display = format!("{}", err);
1633 assert!(display.contains("Server not found"));
1634 assert!(display.contains("server not found"));
1635 }
1636
1637 #[test]
1640 fn test_settings_default_timeout_10() {
1641 let settings = Settings::default();
1642 assert_eq!(settings.timeout_secs, 10);
1643 }
1644
1645 #[test]
1646 fn test_settings_default_retry_true() {
1647 let settings = Settings::default();
1648 assert!(settings.retry_enabled);
1649 }
1650
1651 #[test]
1652 fn test_settings_with_timeout() {
1653 let settings = Settings {
1654 timeout_secs: 120,
1655 ..Default::default()
1656 };
1657 assert_eq!(settings.timeout_secs, 120);
1658 }
1659
1660 #[test]
1663 fn test_tls_config_default_values() {
1664 let tls = TlsConfig::default();
1665 assert!(tls.ca_cert_path.is_none());
1666 assert!(tls.min_tls_version.is_none());
1667 assert!(!tls.pin_speedtest_certs);
1668 }
1669
1670 #[test]
1671 fn test_tls_config_multiple_options() {
1672 let tls = TlsConfig::default()
1673 .with_ca_cert("/path/to/ca.pem".into())
1674 .with_min_tls_version("1.2");
1675 assert!(tls.ca_cert_path.is_some());
1676 assert!(tls.min_tls_version.is_some());
1677 }
1678
1679 #[test]
1682 fn test_settings_chained_modifications() {
1683 let settings = Settings::default()
1684 .with_user_agent("Test/1.0")
1685 .with_retry_disabled()
1686 .with_user_agent("Test/2.0");
1687 assert_eq!(settings.user_agent, "Test/2.0");
1688 assert!(!settings.retry_enabled);
1689 }
1690
1691 #[test]
1694 fn test_default_user_agent_is_valid() {
1695 assert!(!DEFAULT_USER_AGENT.is_empty());
1696 assert!(DEFAULT_USER_AGENT.contains("Mozilla"));
1697 assert!(DEFAULT_USER_AGENT.contains("Chrome"));
1698 }
1699
1700 #[test]
1701 fn test_default_user_agent_in_settings() {
1702 let settings = Settings::default();
1703 assert_eq!(settings.user_agent, DEFAULT_USER_AGENT);
1704 }
1705
1706 #[test]
1709 fn test_create_client_all_defaults() {
1710 let settings = Settings::default();
1711 let result = create_client(&settings);
1712 assert!(result.is_ok());
1713 }
1714
1715 #[test]
1716 fn test_create_client_minimal_tls_config() {
1717 let settings = Settings {
1718 tls: TlsConfig::default(),
1719 ..Default::default()
1720 };
1721 let result = create_client(&settings);
1722 assert!(result.is_ok());
1723 }
1724
1725 #[test]
1726 fn test_create_client_http1_only() {
1727 let settings = Settings::default();
1728 let result = create_client(&settings);
1729 assert!(result.is_ok());
1730 }
1732
1733 #[test]
1736 fn test_pinning_verifier_single_char_subdomain() {
1737 assert!(PinningVerifier::is_valid_domain("a.speedtest.net"));
1738 assert!(PinningVerifier::is_valid_domain("z.ookla.com"));
1739 }
1740
1741 #[test]
1742 fn test_pinning_verifier_numbers_in_subdomain() {
1743 assert!(PinningVerifier::is_valid_domain("123.speedtest.net")); assert!(!PinningVerifier::is_valid_domain("speedtest123.net")); assert!(!PinningVerifier::is_valid_domain("123speedtest.net")); }
1748
1749 #[test]
1750 fn test_pinning_verifier_unicode_in_subdomain() {
1751 assert!(PinningVerifier::is_valid_domain("münchen.speedtest.net"));
1753 }
1754
1755 #[test]
1756 fn test_pinning_verifier_empty_cert_with_valid_domain() {
1757 let verifier = PinningVerifier::new();
1758 let dns_name =
1759 rustls::pki_types::DnsName::try_from("cdn.speedtest.net".to_string()).unwrap();
1760 let server_name = ServerName::DnsName(dns_name);
1761 let cert_der = CertificateDer::from(vec![]);
1762
1763 let result =
1765 verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
1766 assert!(result.is_err());
1767 }
1768
1769 #[test]
1770 fn test_pinning_verifier_subdomain_with_dashes() {
1771 assert!(PinningVerifier::is_valid_domain(
1772 "my-custom-subdomain.speedtest.net"
1773 ));
1774 assert!(PinningVerifier::is_valid_domain("api-v2.ookla.com"));
1775 }
1776
1777 #[test]
1778 fn test_pinning_verifier_long_subdomain() {
1779 let long_subdomain = "a".repeat(63) + ".speedtest.net";
1780 assert!(PinningVerifier::is_valid_domain(&long_subdomain));
1782 }
1783
1784 #[test]
1785 fn test_pinning_verifier_concatenation_attack() {
1786 assert!(!PinningVerifier::is_valid_domain("speedtestXnet"));
1788 assert!(!PinningVerifier::is_valid_domain("speedtestXcom"));
1789 assert!(!PinningVerifier::is_valid_domain("ooklaXcom"));
1790 assert!(!PinningVerifier::is_valid_domain("ooklaXnet"));
1791 }
1792
1793 #[test]
1796 fn test_settings_retry_disabled_chain() {
1797 let settings = Settings::default().with_retry_disabled();
1798 assert!(!settings.retry_enabled);
1799
1800 assert_eq!(settings.timeout_secs, 10);
1802 assert_eq!(settings.user_agent, DEFAULT_USER_AGENT);
1803 }
1804
1805 #[test]
1806 fn test_settings_user_agent_chain() {
1807 let settings = Settings::default()
1808 .with_user_agent("Custom/1.0")
1809 .with_user_agent("Custom/2.0");
1810 assert_eq!(settings.user_agent, "Custom/2.0");
1811 }
1812
1813 #[test]
1816 fn test_create_client_source_ip_loopback_v4() {
1817 let settings = Settings {
1818 source_ip: Some("127.0.0.1".to_string()),
1819 ..Default::default()
1820 };
1821 let result = create_client(&settings);
1822 match result {
1824 Ok(_) | Err(Error::NetworkError(_) | Error::Context { .. }) => {}
1825 Err(e) => panic!("Unexpected error: {e:?}"),
1826 }
1827 }
1828
1829 #[test]
1830 fn test_create_client_source_ip_loopback_v6() {
1831 let settings = Settings {
1832 source_ip: Some("::1".to_string()),
1833 ..Default::default()
1834 };
1835 let result = create_client(&settings);
1836 match result {
1838 Ok(_) | Err(Error::NetworkError(_) | Error::Context { .. }) => {}
1839 Err(e) => panic!("Unexpected error: {e:?}"),
1840 }
1841 }
1842
1843 #[test]
1844 fn test_create_client_source_ip_unspecified() {
1845 let settings = Settings {
1846 source_ip: Some("0.0.0.0".to_string()),
1847 ..Default::default()
1848 };
1849 let result = create_client(&settings);
1850 match result {
1852 Ok(_) | Err(Error::NetworkError(_) | Error::Context { .. }) => {}
1853 Err(e) => panic!("Unexpected error: {e:?}"),
1854 }
1855 }
1856
1857 #[test]
1858 fn test_create_client_source_ip_with_tls() {
1859 let settings = Settings {
1860 source_ip: Some("127.0.0.1".to_string()),
1861 tls: TlsConfig::default(),
1862 ..Default::default()
1863 };
1864 let result = create_client(&settings);
1865 match result {
1867 Ok(_) | Err(Error::NetworkError(_) | Error::Context { .. }) => {}
1868 Err(e) => panic!("Unexpected error: {e:?}"),
1869 }
1870 }
1871}