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#[derive(Debug, Clone, Default)]
12pub struct TlsConfig {
13 pub ca_cert_path: Option<std::path::PathBuf>,
15 pub min_tls_version: Option<String>,
17 pub pin_speedtest_certs: bool,
19}
20
21impl TlsConfig {
22 #[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 #[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 #[must_use]
38 pub fn with_cert_pinning(mut self) -> Self {
39 self.pin_speedtest_certs = true;
40 self
41 }
42}
43
44pub 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#[derive(Debug, Clone)]
53pub struct Settings {
54 pub timeout_secs: u64,
56 pub source_ip: Option<String>,
58 pub user_agent: String,
60 pub retry_enabled: bool,
62 pub tls: TlsConfig,
64}
65
66impl 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 #[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 #[must_use]
114 pub fn with_retry_disabled(mut self) -> Self {
115 self.retry_enabled = false;
116 self
117 }
118}
119
120pub 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 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
155fn build_tls_config(tls: &TlsConfig) -> Result<ClientConfig, Error> {
157 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 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 if tls.pin_speedtest_certs {
177 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 let mut root_store = RootCertStore::empty();
188 root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
190
191 if let Some(ref ca_path) = tls.ca_cert_path {
192 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
203fn 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 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 !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#[derive(Debug)]
239struct PinningVerifier;
240
241impl PinningVerifier {
242 fn new() -> Self {
243 Self
244 }
245
246 fn is_valid_domain(host: &str) -> bool {
247 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 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 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 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
323fn 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 if let Some(status) = e.status() {
333 return status.as_u16() >= 500;
334 }
335 false
336}
337
338pub 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 if let Err(e) = &result {
367 let (delay, should_retry) = TestConfig::retry_delay(attempt);
368
369 #[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 return result.map_err(Error::NetworkError);
378 }
379 }
380
381 Err(Error::context("retry loop ended without result or error"))
383}
384
385pub 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 #[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 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 #[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 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 #[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 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 #[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 #[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 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 let result = load_custom_ca_cert(std::path::Path::new("/tmp"));
648 assert!(result.is_err());
649 }
650
651 #[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 match result {
664 Ok(_) => {}
665 Err(Error::Context { .. }) => {} 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 { .. }) => {} 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 #[test]
742 fn test_pinning_verifier_is_valid_domain_speedtest() {
743 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")); 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 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")); assert!(!PinningVerifier::is_valid_domain("ookla.net"));
767 }
768
769 #[test]
770 fn test_pinning_verifier_edge_cases() {
771 assert!(!PinningVerifier::is_valid_domain(""));
773 assert!(!PinningVerifier::is_valid_domain("speedtestXnet")); assert!(!PinningVerifier::is_valid_domain("attack.com")); }
776
777 #[test]
778 fn test_pinning_verifier_exact_domains() {
779 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 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 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")); }
803
804 #[test]
805 fn test_pinning_verifier_case_sensitivity() {
806 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")); }
812
813 #[test]
814 fn test_pinning_verifier_special_characters() {
815 assert!(!PinningVerifier::is_valid_domain("speedtest.net/")); assert!(!PinningVerifier::is_valid_domain("speedtest.net:443")); assert!(!PinningVerifier::is_valid_domain("speedtest.net/path")); }
820
821 #[test]
822 fn test_pinning_verifier_numeric_domains() {
823 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")); }
830
831 #[test]
832 fn test_pinning_verifier_new_returns_self() {
833 let verifier = PinningVerifier::new();
835 assert!(matches!(verifier, PinningVerifier));
836 }
837
838 #[test]
839 fn test_pinning_verifier_debug_trait() {
840 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 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 assert_eq!(schemes.len(), 8);
863 }
864
865 #[test]
871 fn test_pinning_verifier_verify_server_cert_rejects_invalid_domain() {
872 let verifier = PinningVerifier::new();
873
874 let dns_name = rustls::pki_types::DnsName::try_from("evil.com".to_string()).unwrap();
876 let server_name = ServerName::DnsName(dns_name);
877
878 let cert_der = CertificateDer::from(vec![]);
881
882 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 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 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 let cert_der = CertificateDer::from(vec![]);
922
923 let result =
924 verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
925
926 assert!(result.is_err());
928 let err = result.unwrap_err();
929 let err_msg = format!("{:?}", err);
930 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 let dns_name = rustls::pki_types::DnsName::try_from("speedtest.net".to_string()).unwrap();
940 let server_name = ServerName::DnsName(dns_name);
941
942 let cert_der = CertificateDer::from(vec![]);
945
946 let result =
947 verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
948
949 assert!(result.is_err());
952 let err = result.unwrap_err();
953 let err_msg = format!("{:?}", err);
954 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 let ip_addr = std::net::IpAddr::from([0, 0, 0, 0, 0, 0, 0, 1]); 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 let dns_name = rustls::pki_types::DnsName::try_from("ookla.com".to_string()).unwrap();
982 let server_name = ServerName::DnsName(dns_name);
983
984 let cert_der = CertificateDer::from(vec![]);
986
987 let result =
988 verifier.verify_server_cert(&cert_der, &[], &server_name, &[], UnixTime::now());
989
990 assert!(result.is_err());
992 }
993
994 #[test]
995 fn test_pinning_verifier_domain_validation_order() {
996 let verifier = PinningVerifier::new();
998
999 let dns_name = rustls::pki_types::DnsName::try_from("attacker.com".to_string()).unwrap();
1001 let server_name = ServerName::DnsName(dns_name);
1002
1003 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 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 let verifier = PinningVerifier::new();
1045
1046 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 let cert_der = CertificateDer::from(vec![]);
1053
1054 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 assert!(result.is_err());
1067 }
1068
1069 #[test]
1070 fn test_pinning_verifier_ocsp_response_ignored() {
1071 let verifier = PinningVerifier::new();
1073
1074 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 let cert_der = CertificateDer::from(vec![]);
1080
1081 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 assert!(result.is_err());
1094 }
1095
1096 #[test]
1097 fn test_pinning_verifier_all_valid_subdomains() {
1098 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 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 #[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 #[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 #[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 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 assert_eq!(counter.load(Ordering::SeqCst), 1);
1362 }
1363
1364 #[tokio::test]
1365 async fn test_with_retry_different_value_types() {
1366 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 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 #[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 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 #[tokio::test]
1439 async fn test_discover_client_ip_handles_network_failure() {
1440 let settings = Settings::default().with_retry_disabled();
1442 let client = create_client(&settings).unwrap();
1443
1444 let result = discover_client_ip(&client).await;
1446
1447 match result {
1449 Ok(ip) => {
1450 assert!(ip == "unknown" || common::is_valid_ipv4(&ip));
1452 }
1453 Err(e) => {
1454 assert!(matches!(e, Error::NetworkError(_)));
1456 }
1457 }
1458 }
1459
1460 #[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 #[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 settings.timeout_secs = 120;
1520 assert_eq!(cloned.timeout_secs, 60); }
1522
1523 #[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 #[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 #[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 #[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 #[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 #[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 #[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 }
1728
1729 #[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 assert!(PinningVerifier::is_valid_domain("123.speedtest.net")); assert!(!PinningVerifier::is_valid_domain("speedtest123.net")); assert!(!PinningVerifier::is_valid_domain("123speedtest.net")); }
1744
1745 #[test]
1746 fn test_pinning_verifier_unicode_in_subdomain() {
1747 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 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 assert!(PinningVerifier::is_valid_domain(&long_subdomain));
1778 }
1779
1780 #[test]
1781 fn test_pinning_verifier_concatenation_attack() {
1782 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 #[test]
1792 fn test_settings_retry_disabled_chain() {
1793 let settings = Settings::default().with_retry_disabled();
1794 assert!(!settings.retry_enabled);
1795
1796 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 #[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 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 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 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 match result {
1863 Ok(_) | Err(Error::NetworkError(_) | Error::Context { .. }) => {}
1864 Err(e) => panic!("Unexpected error: {e:?}"),
1865 }
1866 }
1867}