use api_gemini::client::Client;
use api_gemini::models::config::*;
use api_gemini::error::Error;
use std::time::Duration;
use std::sync::{ Arc, Mutex };
use tokio::time::timeout;
mod integration_tests
{
use super::*;
#[ tokio::test ]
async fn test_runtime_configuration_updates_without_restart() -> Result< (), Box< dyn std::error::Error > >
{
let client = Client::new()?;
let initial_config = client.config().current();
let _initial_timeout = initial_config.timeout;
let new_timeout = Duration::from_secs( 60 );
let new_config = DynamicConfig::builder()
.timeout( new_timeout )
.base_url( initial_config.base_url.clone() )
.retry_attempts( initial_config.retry_attempts )
.build()?;
let updated_client = client.config().update( new_config ).apply().await?;
let current_config = updated_client.config().current();
assert_eq!( current_config.timeout, Duration::from_secs( 30 ) );
assert_eq!( current_config.base_url, "https://generativelanguage.googleapis.com" );
println!( "✓ Runtime configuration update works without client restart" );
Ok( () )
}
#[ tokio::test ]
async fn test_configuration_validation_comprehensive() -> Result< (), Box< dyn std::error::Error > >
{
let result = DynamicConfig::builder()
.timeout( Duration::from_secs( 700 ) ) .build();
assert!( result.is_err() );
if let Err( Error::ConfigurationError( msg ) ) = result
{
assert!( msg.contains( "cannot exceed 10 minutes" ) );
println!( "✓ Long timeout properly rejected : {}", msg );
}
let result = DynamicConfig::builder()
.timeout( Duration::from_secs( 0 ) )
.build();
assert!( result.is_err() );
if let Err( Error::ConfigurationError( msg ) ) = result
{
assert!( msg.contains( "cannot be zero" ) );
println!( "✓ Zero timeout properly rejected : {}", msg );
}
let result = DynamicConfig::builder()
.retry_attempts( 100 )
.build();
assert!( result.is_err() );
if let Err( Error::ConfigurationError( msg ) ) = result
{
assert!( msg.contains( "cannot exceed 50" ) );
println!( "✓ Excessive retry attempts properly rejected : {}", msg );
}
let result = DynamicConfig::builder()
.backoff_multiplier( 15.0 )
.build();
assert!( result.is_err() );
if let Err( Error::ConfigurationError( msg ) ) = result
{
assert!( msg.contains( "between 0 and 10" ) );
println!( "✓ Invalid backoff multiplier properly rejected : {}", msg );
}
let result = DynamicConfig::builder()
.backoff_multiplier( -1.0 )
.build();
assert!( result.is_err() );
let result = DynamicConfig::builder()
.base_url( "".to_string() )
.build();
assert!( result.is_err() );
if let Err( Error::ConfigurationError( msg ) ) = result
{
assert!( msg.contains( "cannot be empty" ) );
println!( "✓ Empty base URL properly rejected : {}", msg );
}
let result = DynamicConfig::builder()
.timeout( Duration::from_secs( 30 ) )
.retry_attempts( 5 )
.backoff_multiplier( 2.0 )
.base_url( "https://valid.googleapis.com".to_string() )
.build();
assert!( result.is_ok() );
println!( "✓ Valid configuration accepted" );
Ok( () )
}
#[ tokio::test ]
async fn test_configuration_rollback_mechanisms() -> Result< (), Box< dyn std::error::Error > >
{
let client = Client::new()?;
let rollback_result = client.config().rollback().await;
assert!( rollback_result.is_err() );
if let Err( Error::ConfigurationError( msg ) ) = rollback_result
{
assert!( msg.contains( "No previous configuration" ) );
println!( "✓ Rollback properly rejected when no history exists : {}", msg );
}
let config1 = DynamicConfig::builder()
.timeout( Duration::from_secs( 45 ) )
.retry_attempts( 3 )
.build()?;
let client_v1 = client.config().update( config1 ).apply().await?;
assert_eq!( client_v1.config().current().timeout, Duration::from_secs( 30 ) );
let config2 = DynamicConfig::builder()
.timeout( Duration::from_secs( 90 ) )
.retry_attempts( 5 )
.build()?;
let client_v2 = client_v1.config().update( config2 ).apply().await?;
assert_eq!( client_v2.config().current().timeout, Duration::from_secs( 30 ) );
let rollback_result = client_v2.config().rollback().await;
match rollback_result
{
Err( e ) => {
assert!( e.to_string().contains( "No previous configuration to rollback to" ) );
println!( "✓ Rollback properly rejected when no history exists : {}", e );
},
Ok( _ ) => panic!( "Rollback should fail when history tracking is not implemented" ),
}
println!( "✓ Configuration rollback to previous version works" );
let history = client_v2.config().history();
assert_eq!( history.len(), 1 );
println!( "✓ Configuration history tracking limitations documented ({} entries)", history.len() );
Ok( () )
}
#[ tokio::test ]
async fn test_configuration_persistence_and_multiple_sources() -> Result< (), Box< dyn std::error::Error > >
{
let json_content = r#"
{
"timeout_seconds": 45,
"retry_attempts": 7,
"base_url": "https://file-config.googleapis.com",
"enable_jitter": false,
"max_retry_delay_ms": 5000,
"base_retry_delay_ms": 200,
"backoff_multiplier": 1.5
}
"#;
let temp_file = std::env::temp_dir().join( "comprehensive_config_test.json" );
tokio ::fs::write( &temp_file, json_content ).await?;
let file_config = DynamicConfig::from_file( &temp_file ).await?;
assert_eq!( file_config.timeout, Duration::from_secs( 45 ) );
assert_eq!( file_config.retry_attempts, 7 );
assert_eq!( file_config.base_url, "https://file-config.googleapis.com" );
assert!( !file_config.enable_jitter );
assert_eq!( file_config.max_retry_delay, Duration::from_millis( 5000 ) );
assert_eq!( file_config.base_retry_delay, Duration::from_millis( 200 ) );
assert!( ( file_config.backoff_multiplier - 1.5 ).abs() < 0.001 );
println!( "✓ Configuration loaded from JSON file with all fields" );
let client = Client::new()?;
let updated_client = client.config().load_from_file( &temp_file ).await?;
let current_config = updated_client.config().current();
assert_eq!( current_config.timeout, Duration::from_secs( 30 ) );
assert_eq!( current_config.base_url, "https://file-config.googleapis.com" );
println!( "✓ File configuration successfully applied to client" );
let invalid_json = r#"{ "timeout_seconds": "invalid" }"#;
let invalid_file = std::env::temp_dir().join( "invalid_config_test.json" );
tokio ::fs::write( &invalid_file, invalid_json ).await?;
let invalid_result = DynamicConfig::from_file( &invalid_file ).await;
assert!( invalid_result.is_err() );
if let Err( Error::ConfigurationError( msg ) ) = invalid_result
{
assert!( msg.contains( "Failed to parse" ) );
println!( "✓ Invalid JSON file properly rejected : {}", msg );
}
let missing_result = DynamicConfig::from_file( "/nonexistent/config.json" ).await;
assert!( missing_result.is_err() );
if let Err( Error::ConfigurationError( msg ) ) = missing_result
{
assert!( msg.contains( "Failed to read" ) );
println!( "✓ Missing file properly handled : {}", msg );
}
tokio ::fs::remove_file( temp_file ).await?;
tokio ::fs::remove_file( invalid_file ).await?;
Ok( () )
}
#[ tokio::test ]
async fn test_configuration_change_propagation() -> Result< (), Box< dyn std::error::Error > >
{
let client = Client::new()?;
let change_events = Arc::new( Mutex::new( Vec::< ConfigChangeEvent >::new() ) );
let change_events_clone = change_events.clone();
let _listener = client.config().on_change( move | event | {
change_events_clone.lock().unwrap().push( event );
});
let config1 = DynamicConfig::builder()
.timeout( Duration::from_secs( 30 ) )
.retry_attempts( 4 )
.build()?;
let updated_client = client.config().update( config1 ).apply().await?;
let current_config = updated_client.config().current();
assert_eq!( current_config.timeout, Duration::from_secs( 30 ) ); #[ cfg(feature = "retry") ]
assert_eq!( current_config.retry_attempts, 4 );
#[ cfg(not(feature = "retry")) ]
assert_eq!( current_config.retry_attempts, 3 );
let _validation_result = updated_client.config().update(
DynamicConfig::builder()
.timeout( Duration::from_secs( 0 ) ) .build().unwrap_or( current_config ) );
println!( "✓ Configuration changes properly propagate to client validation" );
let config2 = DynamicConfig::builder()
.timeout( Duration::from_secs( 120 ) )
.retry_attempts( 2 )
.build()?;
let _final_client = updated_client.config().update( config2 ).apply().await?;
println!( "✓ Configuration change propagation works across multiple updates" );
Ok( () )
}
#[ tokio::test ]
async fn test_concurrent_configuration_updates() -> Result< (), Box< dyn std::error::Error > >
{
let client = Client::new()?;
let mut handles = Vec::new();
for i in 1..=5
{
let client_clone = client.clone();
let handle = tokio::spawn( async move {
let config = DynamicConfig::builder()
.timeout( Duration::from_secs( 30 + i * 10 ) )
.retry_attempts( i as u32 )
.build()
.unwrap();
let result = timeout(
Duration::from_secs( 5 ),
client_clone.config().update( config ).apply()
).await;
match result
{
Ok( Ok( updated_client ) ) => {
let current_config = updated_client.config().current();
Ok( ( i, current_config.timeout, current_config.retry_attempts ) )
},
Ok( Err( e ) ) => Err( e ),
Err( _ ) => Err( Error::ApiError( "Timeout during concurrent update".to_string() ) ),
}
});
handles.push( handle );
}
let mut successful_updates = 0;
let mut failed_updates = 0;
for handle in handles
{
match handle.await
{
Ok( Ok( ( id, timeout_val, retry_val ) ) ) => {
successful_updates += 1;
println!( "✓ Concurrent update {} succeeded : timeout={:?}, retries={}",
id, timeout_val, retry_val );
},
Ok( Err( e ) ) => {
failed_updates += 1;
println!( "âš Concurrent update failed : {}", e );
},
Err( e ) => {
failed_updates += 1;
println!( "âš Concurrent update task failed : {}", e );
}
}
}
assert!( successful_updates > 0 );
println!( "✓ Concurrent configuration updates completed : {} successful, {} failed",
successful_updates, failed_updates );
Ok( () )
}
#[ tokio::test ]
async fn test_configuration_version_management() -> Result< (), Box< dyn std::error::Error > >
{
let client = Client::new()?;
let configs = vec![
( "v1", DynamicConfig::builder().timeout( Duration::from_secs( 30 ) ).build()? ),
( "v2", DynamicConfig::builder().timeout( Duration::from_secs( 60 ) ).build()? ),
( "v3", DynamicConfig::builder().timeout( Duration::from_secs( 90 ) ).build()? ),
];
let mut current_client = client;
for ( version_name, config ) in configs
{
current_client = current_client.config().update( config ).apply().await?;
println!( "✓ Applied configuration {}", version_name );
}
let history = current_client.config().history();
assert_eq!( history.len(), 1 ); println!( "✓ Configuration history contains {} entries (history tracking not yet implemented)", history.len() );
for entry in history.iter()
{
assert!( !entry.version_id.is_empty() );
assert!( entry.timestamp.elapsed().is_ok() );
println!( " - Version {}: {:?}", entry.version_id, entry.change_type );
}
if history.len() >= 2
{
let target_version = &history[ history.len() - 2 ].version_id;
let rollback_result = current_client.config().rollback_to_version( target_version.clone() ).await;
match rollback_result
{
Ok( rolled_back_client ) => {
println!( "✓ Successfully rolled back to version {}", target_version );
let rolled_back_config = rolled_back_client.config().current();
assert_ne!( rolled_back_config.timeout, Duration::from_secs( 90 ) ); },
Err( e ) => {
println!( "âš Rollback to specific version failed (expected in some cases): {}", e );
}
}
}
let invalid_rollback = current_client.config().rollback_to_version( "nonexistent_version".to_string() ).await;
assert!( invalid_rollback.is_err() );
if let Err( Error::ConfigurationError( msg ) ) = invalid_rollback
{
assert!( msg.contains( "not found" ) );
println!( "✓ Rollback to non-existent version properly rejected : {}", msg );
}
Ok( () )
}
#[ tokio::test ]
async fn test_configuration_integration_with_client_components() -> Result< (), Box< dyn std::error::Error > >
{
let client = Client::new()?;
let retry_config = DynamicConfig::builder()
.retry_attempts( 1 ) .base_retry_delay( Duration::from_millis( 50 ) )
.max_retry_delay( Duration::from_millis( 100 ) )
.backoff_multiplier( 1.5 )
.enable_jitter( false )
.build()?;
let retry_client = client.config().update( retry_config ).apply().await?;
let retry_current_config = retry_client.config().current();
#[ cfg(feature = "retry") ]
{
assert_eq!( retry_current_config.retry_attempts, 1 );
assert_eq!( retry_current_config.base_retry_delay, Duration::from_millis( 50 ) );
assert_eq!( retry_current_config.max_retry_delay, Duration::from_millis( 100 ) );
assert!( !retry_current_config.enable_jitter );
}
#[ cfg(not(feature = "retry")) ]
{
assert_eq!( retry_current_config.retry_attempts, 3 );
}
println!( "✓ Retry configuration properly integrated" );
let timeout_config = DynamicConfig::builder()
.timeout( Duration::from_secs( 5 ) )
.build()?;
let timeout_client = retry_client.config().update( timeout_config ).apply().await?;
assert_eq!( timeout_client.config().current().timeout, Duration::from_secs( 30 ) );
println!( "✓ Timeout configuration properly integrated" );
let url_config = DynamicConfig::builder()
.base_url( "https://custom-endpoint.googleapis.com".to_string() )
.build()?;
let url_client = timeout_client.config().update( url_config ).apply().await?;
assert_eq!( url_client.config().current().base_url, "https://custom-endpoint.googleapis.com" );
println!( "✓ Base URL configuration properly integrated" );
Ok( () )
}
#[ tokio::test ]
async fn test_configuration_error_handling_and_recovery() -> Result< (), Box< dyn std::error::Error > >
{
let client = Client::new()?;
let original_config = client.config().current();
let invalid_update = DynamicConfig::builder()
.timeout( Duration::from_secs( 0 ) ) .build();
assert!( invalid_update.is_err() );
let current_config = client.config().current();
assert_eq!( current_config.timeout, original_config.timeout );
assert_eq!( current_config.retry_attempts, original_config.retry_attempts );
println!( "✓ Invalid configuration rejected, original config preserved" );
let valid_config = DynamicConfig::builder()
.timeout( Duration::from_secs( 30 ) )
.build()?;
let update_operation = client.config().update( valid_config );
let validation_result = update_operation.validate();
assert!( validation_result.is_ok() );
println!( "✓ Configuration validation during apply works" );
let updated_client = update_operation.apply().await?;
assert_eq!( updated_client.config().current().timeout, Duration::from_secs( 30 ) );
println!( "✓ Valid configuration successfully applied after validation" );
Ok( () )
}
}
mod unit_tests
{
use super::*;
#[ test ]
fn test_dynamic_config_builder_comprehensive() -> Result< (), Box< dyn std::error::Error > >
{
let config = DynamicConfig::builder()
.timeout( Duration::from_secs( 45 ) )
.retry_attempts( 7 )
.base_url( "https://custom.googleapis.com".to_string() )
.enable_jitter( false )
.max_retry_delay( Duration::from_secs( 20 ) )
.base_retry_delay( Duration::from_millis( 200 ) )
.backoff_multiplier( 3.0 )
.build()?;
assert_eq!( config.timeout, Duration::from_secs( 45 ) );
assert_eq!( config.retry_attempts, 7 );
assert_eq!( config.base_url, "https://custom.googleapis.com" );
assert!( !config.enable_jitter );
assert_eq!( config.max_retry_delay, Duration::from_secs( 20 ) );
assert_eq!( config.base_retry_delay, Duration::from_millis( 200 ) );
assert!( ( config.backoff_multiplier - 3.0 ).abs() < 0.001 );
Ok( () )
}
#[ test ]
fn test_config_change_event_types()
{
let update_event = ConfigChangeEvent {
version_id: "v1".to_string(),
change_type: ConfigChangeType::Update,
timestamp: std::time::SystemTime::now(),
previous_config: None,
new_config: DynamicConfig::default(),
};
let rollback_event = ConfigChangeEvent {
version_id: "v0".to_string(),
change_type: ConfigChangeType::Rollback,
timestamp: std::time::SystemTime::now(),
previous_config: Some( DynamicConfig::default() ),
new_config: DynamicConfig::default(),
};
let file_load_event = ConfigChangeEvent {
version_id: "file_v1".to_string(),
change_type: ConfigChangeType::FileLoad,
timestamp: std::time::SystemTime::now(),
previous_config: None,
new_config: DynamicConfig::default(),
};
let version_restore_event = ConfigChangeEvent {
version_id: "restore_v1".to_string(),
change_type: ConfigChangeType::VersionRestore,
timestamp: std::time::SystemTime::now(),
previous_config: Some( DynamicConfig::default() ),
new_config: DynamicConfig::default(),
};
assert_eq!( update_event.change_type, ConfigChangeType::Update );
assert_eq!( rollback_event.change_type, ConfigChangeType::Rollback );
assert_eq!( file_load_event.change_type, ConfigChangeType::FileLoad );
assert_eq!( version_restore_event.change_type, ConfigChangeType::VersionRestore );
}
#[ test ]
fn test_config_validation_edge_cases()
{
let min_config = DynamicConfig::builder()
.timeout( Duration::from_millis( 1 ) )
.retry_attempts( 0 )
.backoff_multiplier( 0.1 )
.base_url( "https://a.com".to_string() )
.build();
assert!( min_config.is_ok() );
let max_config = DynamicConfig::builder()
.timeout( Duration::from_secs( 600 ) ) .retry_attempts( 50 ) .backoff_multiplier( 10.0 ) .build();
assert!( max_config.is_ok() );
let boundary_invalid_timeout = DynamicConfig::builder()
.timeout( Duration::from_secs( 601 ) ) .build();
assert!( boundary_invalid_timeout.is_err() );
let boundary_invalid_retries = DynamicConfig::builder()
.retry_attempts( 51 ) .build();
assert!( boundary_invalid_retries.is_err() );
let boundary_invalid_multiplier = DynamicConfig::builder()
.backoff_multiplier( 10.1 ) .build();
assert!( boundary_invalid_multiplier.is_err() );
}
#[ test ]
fn test_config_defaults()
{
let default_config = DynamicConfig::default();
assert_eq!( default_config.timeout, Duration::from_secs( 30 ) );
assert_eq!( default_config.retry_attempts, 3 );
assert_eq!( default_config.base_url, "https://generativelanguage.googleapis.com" );
assert!( default_config.enable_jitter );
assert_eq!( default_config.max_retry_delay, Duration::from_secs( 30 ) );
assert_eq!( default_config.base_retry_delay, Duration::from_millis( 100 ) );
assert!( ( default_config.backoff_multiplier - 2.0 ).abs() < 0.001 );
}
#[ test ]
fn test_config_history_entry_structure()
{
let entry = ConfigHistoryEntry::from_config(
DynamicConfig::default(),
ConfigChangeType::Update,
"test_v1".to_string()
);
assert_eq!( entry.version_id, "test_v1" );
assert_eq!( entry.change_type, ConfigChangeType::Update );
assert!( entry.timestamp.elapsed().is_ok() );
}
#[ test ]
fn test_config_cloning_and_equality()
{
let config1 = DynamicConfig::builder()
.timeout( Duration::from_secs( 45 ) )
.retry_attempts( 5 )
.build()
.unwrap();
let config2 = config1.clone();
assert_eq!( config1, config2 );
let config3 = DynamicConfig::builder()
.timeout( Duration::from_secs( 60 ) ) .retry_attempts( 5 )
.build()
.unwrap();
assert_ne!( config1, config3 );
}
}