use std::time::Duration;
const TIMEOUT_SECS: u64 = 3;
const SLOW_THRESHOLD_SECS: u64 = 2;
const HTTP_OK: u16 = 200;
const HTTP_NO_CONTENT: u16 = 204;
#[derive(Debug)]
pub(crate) enum NetworkStatus {
Good,
Slow(Duration),
Offline,
}
fn has_internet_connection() -> bool {
let tls_config = ureq::tls::TlsConfig::builder()
.provider(ureq::tls::TlsProvider::NativeTls)
.build();
let config = ureq::config::Config::builder()
.timeout_global(Some(Duration::from_secs(TIMEOUT_SECS)))
.tls_config(tls_config)
.build();
let agent = config.new_agent();
let endpoints = [
"https://dns.google/generate_204", "https://www.cloudflare.com/cdn-cgi/trace", "https://api.github.com/zen", ];
for endpoint in endpoints {
if let Ok(response) = agent.get(endpoint).call() {
let status = response.status();
if status == HTTP_OK || status == HTTP_NO_CONTENT {
return true;
}
}
}
false
}
pub(crate) fn check_network_status() -> NetworkStatus {
let start = std::time::Instant::now();
if has_internet_connection() {
let latency = start.elapsed();
if latency > Duration::from_secs(SLOW_THRESHOLD_SECS) {
NetworkStatus::Slow(latency)
} else {
NetworkStatus::Good
}
} else {
NetworkStatus::Offline
}
}
#[cfg(test)]
mod tests {
use std::{
io::{BufRead, BufReader, Write},
net::TcpListener,
sync::Mutex,
thread,
};
use super::*;
struct MockHttpServer {
listener: TcpListener,
port: u16,
response_status: u16,
response_delay_ms: u64,
}
impl MockHttpServer {
fn new(status: u16, delay_ms: u64) -> Self {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
MockHttpServer {
listener,
port,
response_status: status,
response_delay_ms: delay_ms,
}
}
fn spawn(self) -> thread::JoinHandle<()> {
thread::spawn(move || {
if let Some(mut stream) = self.listener.incoming().flatten().next() {
if self.response_delay_ms > 0 {
thread::sleep(Duration::from_millis(self.response_delay_ms));
}
let mut reader = BufReader::new(&stream);
let mut _request_line = String::new();
let _ = reader.read_line(&mut _request_line);
let response = match self.response_status {
204 => "HTTP/1.1 204 No Content\r\n\r\n",
200 => "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK",
404 => "HTTP/1.1 404 Not Found\r\nContent-Length: 9\r\n\r\nNot Found",
500 => {
"HTTP/1.1 500 Internal Server Error\r\nContent-Length: 5\r\n\r\nError"
},
_ => "HTTP/1.1 503 Service Unavailable\r\n\r\n",
};
let _ = stream.write_all(response.as_bytes());
}
})
}
}
#[test]
fn test_network_status_enum_variants() {
let good = NetworkStatus::Good;
let slow = NetworkStatus::Slow(Duration::from_secs(3));
let offline = NetworkStatus::Offline;
assert_eq!(format!("{:?}", good), "Good");
assert!(format!("{:?}", slow).contains("Slow"));
assert_eq!(format!("{:?}", offline), "Offline");
}
#[test]
fn test_network_status_slow_contains_duration() {
let test_duration = Duration::from_millis(2500);
let status = NetworkStatus::Slow(test_duration);
match status {
NetworkStatus::Slow(d) => assert_eq!(d, test_duration),
_ => panic!("Expected Slow variant"),
}
}
#[test]
#[ignore = "Requires internet connection"]
fn test_real_network_connectivity_check() {
let status = check_network_status();
match status {
NetworkStatus::Good | NetworkStatus::Slow(_) => {},
NetworkStatus::Offline => {
panic!("Expected network connectivity for this test");
},
}
}
#[test]
fn test_http_status_code_handling() {
let server = MockHttpServer::new(200, 0);
let port = server.port;
let _handle = server.spawn();
let config = ureq::config::Config::builder()
.timeout_global(Some(Duration::from_secs(1)))
.build();
let agent = config.new_agent();
let url = format!("http://127.0.0.1:{}", port);
let response = agent.get(&url).call();
assert!(response.is_ok());
assert_eq!(response.unwrap().status(), 200);
}
#[test]
fn test_http_no_content_status_handling() {
let server = MockHttpServer::new(204, 0);
let port = server.port;
let _handle = server.spawn();
let config = ureq::config::Config::builder()
.timeout_global(Some(Duration::from_secs(1)))
.build();
let agent = config.new_agent();
let url = format!("http://127.0.0.1:{}", port);
let response = agent.get(&url).call();
assert!(response.is_ok());
assert_eq!(response.unwrap().status(), 204);
}
#[test]
fn test_timeout_behavior() {
let server = MockHttpServer::new(200, 2000); let port = server.port;
let _handle = server.spawn();
let config = ureq::config::Config::builder()
.timeout_global(Some(Duration::from_secs(1)))
.build();
let agent = config.new_agent();
let url = format!("http://127.0.0.1:{}", port);
let start = std::time::Instant::now();
let response = agent.get(&url).call();
let elapsed = start.elapsed();
assert!(response.is_err());
assert!(elapsed.as_secs() <= 2); }
#[test]
fn test_latency_measurement_accuracy() {
let start = std::time::Instant::now();
thread::sleep(Duration::from_millis(100));
let elapsed = start.elapsed();
assert!(elapsed.as_millis() >= 100); assert!(elapsed.as_millis() < 200); }
#[test]
fn test_concurrent_status_checks_safety() {
let counter = std::sync::Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..3 {
let counter_clone = counter.clone();
let handle = thread::spawn(move || {
let _status = check_network_status();
let mut count = counter_clone.lock().unwrap();
*count += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(*counter.lock().unwrap(), 3);
}
#[test]
fn test_error_status_codes_not_accepted() {
let server = MockHttpServer::new(500, 0);
let port = server.port;
let _handle = server.spawn();
let config = ureq::config::Config::builder()
.timeout_global(Some(Duration::from_secs(1)))
.build();
let agent = config.new_agent();
let url = format!("http://127.0.0.1:{}", port);
let response = agent.get(&url).call();
if let Ok(resp) = response {
assert_ne!(resp.status(), HTTP_OK);
assert_ne!(resp.status(), HTTP_NO_CONTENT);
}
}
}