use std::future::Future;
use std::net::TcpListener;
use std::time::Duration;
use crate::{ApiClient, ApiClientBuilder};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HealthStatus {
Healthy,
Unhealthy,
Uncheckable,
}
pub trait TestServer {
type Error: std::error::Error + Send + Sync + 'static;
fn launch(&self, listener: TcpListener)
-> impl Future<Output = Result<(), Self::Error>> + Send;
fn is_healthy(
&self,
_client: &mut ApiClient,
) -> impl Future<Output = Result<HealthStatus, Self::Error>> + Send {
std::future::ready(Ok(HealthStatus::Uncheckable))
}
fn config(&self) -> TestServerConfig {
TestServerConfig::default()
}
}
#[derive(Debug, Clone)]
pub struct TestServerConfig {
pub api_client: Option<ApiClientBuilder>,
pub min_backoff_delay: Duration,
pub max_backoff_delay: Duration,
pub backoff_jitter: bool,
pub max_retry_attempts: usize,
}
impl Default for TestServerConfig {
fn default() -> Self {
Self {
api_client: None,
min_backoff_delay: Duration::from_millis(10),
max_backoff_delay: Duration::from_secs(1),
backoff_jitter: true,
max_retry_attempts: 10,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ApiClient;
use std::net::TcpListener;
use std::time::Duration;
use tokio::net::TcpListener as TokioTcpListener;
#[derive(Debug)]
struct MockServer {
config: TestServerConfig,
should_be_healthy: bool,
}
impl MockServer {
fn new() -> Self {
Self {
config: TestServerConfig::default(),
should_be_healthy: true,
}
}
fn with_health_status(mut self, healthy: bool) -> Self {
self.should_be_healthy = healthy;
self
}
}
impl TestServer for MockServer {
type Error = std::io::Error;
async fn launch(&self, listener: TcpListener) -> Result<(), Self::Error> {
listener.set_nonblocking(true)?;
let tokio_listener = TokioTcpListener::from_std(listener)?;
loop {
if let Ok((mut stream, _)) = tokio_listener.accept().await {
tokio::spawn(async move {
let response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
if let Err(e) =
tokio::io::AsyncWriteExt::write_all(&mut stream, response.as_bytes())
.await
{
eprintln!("Failed to write response: {e}");
}
});
}
}
}
async fn is_healthy(&self, _client: &mut ApiClient) -> Result<HealthStatus, Self::Error> {
Ok(if self.should_be_healthy {
HealthStatus::Healthy
} else {
HealthStatus::Unhealthy
})
}
fn config(&self) -> TestServerConfig {
self.config.clone()
}
}
#[test]
fn test_test_server_config_default() {
let config = TestServerConfig::default();
assert!(config.api_client.is_none());
assert_eq!(config.min_backoff_delay, Duration::from_millis(10));
assert_eq!(config.max_backoff_delay, Duration::from_secs(1));
assert!(config.backoff_jitter);
assert_eq!(config.max_retry_attempts, 10);
}
#[test]
fn test_test_server_config_custom() {
let min_delay = Duration::from_millis(50);
let max_delay = Duration::from_secs(5);
let client_builder = ApiClient::builder()
.with_host("test.example.com")
.with_port(8080);
let config = TestServerConfig {
api_client: Some(client_builder),
min_backoff_delay: min_delay,
max_backoff_delay: max_delay,
backoff_jitter: false,
max_retry_attempts: 5,
};
assert!(config.api_client.is_some());
assert_eq!(config.min_backoff_delay, min_delay);
assert_eq!(config.max_backoff_delay, max_delay);
assert!(!config.backoff_jitter);
assert_eq!(config.max_retry_attempts, 5);
}
#[tokio::test]
async fn test_mock_server_health_check_healthy() {
let server = MockServer::new().with_health_status(true);
let mut client = ApiClient::builder().build().expect("valid client");
let result = server.is_healthy(&mut client).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), HealthStatus::Healthy);
}
#[tokio::test]
async fn test_mock_server_health_check_unhealthy() {
let server = MockServer::new().with_health_status(false);
let mut client = ApiClient::builder().build().expect("valid client");
let result = server.is_healthy(&mut client).await;
assert!(result.is_ok());
assert_eq!(result.unwrap(), HealthStatus::Unhealthy);
}
#[test]
fn test_default_backoff_configuration() {
let config = TestServerConfig::default();
assert_eq!(config.min_backoff_delay, Duration::from_millis(10));
assert_eq!(config.max_backoff_delay, Duration::from_secs(1));
assert!(config.backoff_jitter);
assert_eq!(config.max_retry_attempts, 10);
}
#[test]
fn test_test_server_trait_bounds() {
fn assert_test_server<T: TestServer + Send + Sync + 'static>(_: T) {}
let server = MockServer::new();
assert_test_server(server);
}
#[test]
fn test_health_status_healthy_variant() {
let status = HealthStatus::Healthy;
assert_eq!(status, HealthStatus::Healthy);
}
#[test]
fn test_health_status_unhealthy_variant() {
let status = HealthStatus::Unhealthy;
assert_eq!(status, HealthStatus::Unhealthy);
}
#[test]
fn test_health_status_uncheckable_variant() {
let status = HealthStatus::Uncheckable;
assert_eq!(status, HealthStatus::Uncheckable);
}
#[test]
fn test_health_status_debug() {
let healthy = HealthStatus::Healthy;
let unhealthy = HealthStatus::Unhealthy;
let uncheckable = HealthStatus::Uncheckable;
assert!(format!("{healthy:?}").contains("Healthy"));
assert!(format!("{unhealthy:?}").contains("Unhealthy"));
assert!(format!("{uncheckable:?}").contains("Uncheckable"));
}
#[test]
fn test_health_status_clone() {
let original = HealthStatus::Healthy;
let cloned = original;
assert_eq!(original, cloned);
}
#[test]
fn test_health_status_copy() {
let status = HealthStatus::Unhealthy;
let copied = status;
assert_eq!(status, copied);
}
#[test]
fn test_health_status_equality() {
assert_eq!(HealthStatus::Healthy, HealthStatus::Healthy);
assert_eq!(HealthStatus::Unhealthy, HealthStatus::Unhealthy);
assert_eq!(HealthStatus::Uncheckable, HealthStatus::Uncheckable);
assert_ne!(HealthStatus::Healthy, HealthStatus::Unhealthy);
assert_ne!(HealthStatus::Healthy, HealthStatus::Uncheckable);
assert_ne!(HealthStatus::Unhealthy, HealthStatus::Uncheckable);
}
#[derive(Debug)]
struct MinimalServer;
impl TestServer for MinimalServer {
type Error = std::io::Error;
async fn launch(&self, _listener: TcpListener) -> Result<(), Self::Error> {
Ok(())
}
}
#[tokio::test]
async fn test_default_is_healthy_returns_uncheckable() {
let server = MinimalServer;
let mut client = ApiClient::builder().build().expect("valid client");
let result = server.is_healthy(&mut client).await;
assert!(result.is_ok());
assert_eq!(result.expect("should be Ok"), HealthStatus::Uncheckable);
}
#[test]
fn test_default_config_returns_defaults() {
let server = MinimalServer;
let config = server.config();
assert!(config.api_client.is_none());
assert_eq!(config.min_backoff_delay, Duration::from_millis(10));
assert_eq!(config.max_backoff_delay, Duration::from_secs(1));
assert!(config.backoff_jitter);
assert_eq!(config.max_retry_attempts, 10);
}
#[test]
fn test_test_server_config_debug() {
let config = TestServerConfig::default();
let debug_str = format!("{config:?}");
assert!(debug_str.contains("TestServerConfig"));
assert!(debug_str.contains("min_backoff_delay"));
assert!(debug_str.contains("max_backoff_delay"));
}
#[test]
fn test_test_server_config_clone() {
let original = TestServerConfig {
api_client: None,
min_backoff_delay: Duration::from_millis(100),
max_backoff_delay: Duration::from_secs(10),
backoff_jitter: false,
max_retry_attempts: 3,
};
let cloned = original.clone();
assert!(cloned.api_client.is_none());
assert_eq!(cloned.min_backoff_delay, Duration::from_millis(100));
assert_eq!(cloned.max_backoff_delay, Duration::from_secs(10));
assert!(!cloned.backoff_jitter);
assert_eq!(cloned.max_retry_attempts, 3);
}
#[test]
fn test_test_server_config_with_api_client_clone() {
let original = TestServerConfig {
api_client: Some(ApiClient::builder().with_host("test.local").with_port(3000)),
min_backoff_delay: Duration::from_millis(50),
max_backoff_delay: Duration::from_secs(2),
backoff_jitter: true,
max_retry_attempts: 5,
};
let cloned = original.clone();
assert!(cloned.api_client.is_some());
assert_eq!(cloned.min_backoff_delay, Duration::from_millis(50));
assert_eq!(cloned.max_backoff_delay, Duration::from_secs(2));
assert!(cloned.backoff_jitter);
assert_eq!(cloned.max_retry_attempts, 5);
}
}