use api_gemini::
{
client ::Client,
models ::*,
error ::Error,
};
use serde_json::json;
use core::time::Duration;
use tokio::time::timeout;
use futures::StreamExt;
fn create_integration_client() -> Client
{
Client::new().unwrap_or_else( |err| {
panic!(
"\n❌ INTEGRATION TEST FAILURE: No valid API key found!\n\
\n🔑 Required: Set GEMINI_API_KEY environment variable or create secret/gemini_api_key file\n\
\n📋 Integration tests run by default and CANNOT be silently skipped\n\
\n🚫 Error details : {err:?}\n\
\n💡 To run only unit tests: cargo test --no-default-features\n"
)
})
}
#[ tokio::test ]
async fn integration_test_models_list_real_api()
{
let client = create_integration_client();
let result = client.models().list().await;
assert!( result.is_ok(), "Failed to list models with real API: {:?}", result.err() );
let models = result.unwrap();
assert!( !models.models.is_empty(), "No models returned from real API" );
let gemini_models: Vec< _ > = models.models
.iter()
.filter( | m | m.name.contains( "gemini" ) )
.collect();
assert!( !gemini_models.is_empty(), "No Gemini models found in real API response" );
}
#[ tokio::test ]
async fn integration_test_model_get_real_api()
{
let client = create_integration_client();
let result = client.models().get( "models/gemini-flash-latest" ).await;
assert!( result.is_ok(), "Failed to get model info with real API: {:?}", result.err() );
let model = result.unwrap();
assert_eq!( model.name, "models/gemini-flash-latest" );
assert!( model.supported_generation_methods.is_some() );
assert!( model.input_token_limit.is_some() );
}
#[ tokio::test ]
async fn integration_test_content_generation_real_api()
{
let client = create_integration_client();
let request = GenerateContentRequest
{
contents: vec![ Content
{
role: "user".to_string(),
parts: vec![ Part
{
text: Some( "Say exactly 'Integration test successful' and nothing else.".to_string() ),
..Default::default()
} ],
} ],
..Default::default()
};
let result = client
.models()
.by_name( "gemini-flash-latest" )
.generate_content( &request )
.await;
assert!( result.is_ok(), "Failed to generate content with real API: {:?}", result.err() );
let response = result.unwrap();
assert!( !response.candidates.is_empty(), "No candidates returned from real API" );
assert!( !response.candidates[0].content.parts.is_empty(), "No content parts in candidate from real API" );
let text = response.candidates[0].content.parts[0].text.as_ref().unwrap();
assert!( text.contains( "Integration test successful" ), "Real API response doesn't match expected : {text}" );
}
#[ tokio::test ]
async fn integration_test_embeddings_real_api()
{
let client = create_integration_client();
let request = EmbedContentRequest
{
content: Content
{
role: "user".to_string(),
parts: vec![ Part
{
text: Some( "This is a test text for embedding generation.".to_string() ),
..Default::default()
} ],
},
task_type: Some( "RETRIEVAL_DOCUMENT".to_string() ),
title: Some( "Test Document".to_string() ),
output_dimensionality: None,
};
let result = client
.models()
.by_name( "text-embedding-004" )
.embed_content( &request )
.await;
assert!( result.is_ok(), "Failed to embed content with real API: {:?}", result.err() );
let response = result.unwrap();
assert!( !response.embedding.values.is_empty(), "No embedding values returned from real API" );
assert!( response.embedding.values.len() > 100, "Embedding dimension too small : {}", response.embedding.values.len() );
}
#[ cfg( feature = "streaming" ) ]
#[ tokio::test ]
async fn integration_test_streaming_real_api()
{
let client = create_integration_client();
let request = GenerateContentRequest
{
contents: vec![ Content
{
role: "user".to_string(),
parts: vec![ Part
{
text: Some( "Count from 1 to 5, one number per line.".to_string() ),
..Default::default()
} ],
} ],
..Default::default()
};
let result = client
.models()
.by_name( "gemini-flash-latest" )
.generate_content_stream( &request )
.await;
assert!( result.is_ok(), "Failed to create stream with real API: {:?}", result.err() );
let stream = result.unwrap();
let mut stream = std::pin::pin!( stream );
let mut chunks_received = 0;
let mut content_parts = Vec::new();
while let Some( chunk_result ) = stream.next().await
{
assert!( chunk_result.is_ok(), "Stream chunk error from real API: {:?}", chunk_result.err() );
let chunk = chunk_result.unwrap();
if let Some( candidates ) = &chunk.candidates
{
if let Some( candidate ) = candidates.first()
{
let content = &candidate.content;
for part in &content.parts
{
if let Some( text ) = &part.text
{
content_parts.push( text.clone() );
}
}
}
}
chunks_received += 1;
if chunks_received > 100
{
break;
}
}
assert!( chunks_received > 0, "No chunks received from streaming API - streaming must return at least one chunk" );
assert!( !content_parts.is_empty(), "No content in streaming chunks from real API" );
}
#[ cfg( feature = "chat" ) ]
#[ tokio::test ]
async fn integration_test_chat_completion_real_api()
{
let client = create_integration_client();
let request = ChatCompletionRequest
{
messages: vec![ ChatMessage
{
role: "user".to_string(),
content: "What is 2+2? Answer with just the number.".to_string(),
} ],
model: "gemini-flash-latest".to_string(),
max_tokens: Some( 50 ),
temperature: Some( 0.1 ),
..Default::default()
};
let result = client.chat().complete( &request ).await;
assert!( result.is_ok(), "Failed chat completion with real API: {:?}", result.err() );
let response = result.unwrap();
assert!( !response.choices.is_empty(), "No choices returned from real chat API" );
assert!( response.choices[0].message.content.contains( '4' ), "Chat response incorrect : {}", response.choices[0].message.content );
}
#[ tokio::test ]
async fn integration_test_invalid_model_real_api()
{
let client = create_integration_client();
let request = GenerateContentRequest
{
contents: vec![ Content
{
role: "user".to_string(),
parts : vec![ Part { text : Some( "Test".to_string() ), ..Default::default() } ],
} ],
..Default::default()
};
let result = client
.models()
.by_name( "invalid-model-name-12345" )
.generate_content( &request )
.await;
assert!( result.is_err(), "Invalid model should fail with real API" );
match result.err().unwrap()
{
Error::ApiError( _ ) | Error::InvalidArgument( _ ) => {}, other => panic!( "Expected API error for invalid model, got : {other:?}" ),
}
}
#[ tokio::test ]
async fn integration_test_empty_content_real_api()
{
let client = create_integration_client();
let request = GenerateContentRequest
{
contents: vec![ Content
{
role: "user".to_string(),
parts : vec![ Part { text : Some( String::new() ), ..Default::default() } ],
} ],
..Default::default()
};
let result = client
.models()
.by_name( "gemini-flash-latest" )
.generate_content( &request )
.await;
match result
{
Ok( response ) => assert!( !response.candidates.is_empty() ),
Err( Error::ApiError( _ ) | Error::InvalidArgument( _ ) ) => {}, Err( other ) => panic!( "Unexpected error for empty content : {other:?}" ),
}
}
#[ tokio::test ]
async fn integration_test_http_timeout_real_network()
{
let _client_check = create_integration_client();
use workspace_tools as workspace;
let ws = workspace::workspace().expect( "Failed to resolve workspace" );
let api_key = ws.load_secret_key( "GEMINI_API_KEY", "-secrets.sh" )
.or_else( |_| std::env::var( "GEMINI_API_KEY" ) )
.expect( "❌ GEMINI_API_KEY not found in workspace secrets or environment" );
let client = Client::builder()
.api_key( api_key )
.build()
.expect( "Failed to build client" );
let request = GenerateContentRequest
{
contents: vec![ Content
{
role: "user".to_string(),
parts : vec![ Part { text : Some( "Quick test".to_string() ), ..Default::default() } ],
} ],
..Default::default()
};
let result = timeout(
Duration::from_millis( 1 ), client.models().by_name( "gemini-flash-latest" ).generate_content( &request )
).await;
assert!( result.is_err(), "Expected timeout with real network request" );
}
#[ tokio::test ]
async fn integration_test_http_large_request_real_api()
{
let client = create_integration_client();
let large_text = "Test ".repeat( 1000 );
let request = GenerateContentRequest
{
contents: vec![ Content
{
role: "user".to_string(),
parts : vec![ Part { text : Some( large_text ), ..Default::default() } ],
} ],
..Default::default()
};
let result = client
.models()
.by_name( "gemini-flash-latest" )
.generate_content( &request )
.await;
assert!( result.is_ok(), "Failed to handle large request with real API: {:?}", result.err() );
}
#[ cfg( feature = "retry" ) ]
#[ tokio::test ]
async fn integration_test_retry_logic_real_network()
{
use workspace_tools as workspace;
let ws = workspace::workspace().expect( "Failed to resolve workspace" );
let api_key = ws.load_secret_key( "GEMINI_API_KEY", "-secrets.sh" )
.or_else( |_| std::env::var( "GEMINI_API_KEY" ) )
.expect( "❌ GEMINI_API_KEY not found in workspace secrets or environment" );
let client = Client::builder()
.api_key( api_key )
.max_retries( 3 )
.base_delay( Duration::from_millis( 100 ) )
.build()
.expect( "Failed to build retry client" );
let request = GenerateContentRequest
{
contents: vec![ Content
{
role: "user".to_string(),
parts : vec![ Part { text : Some( "Test retry logic".to_string() ), ..Default::default() } ],
} ],
..Default::default()
};
let result = client
.models()
.by_name( "gemini-flash-latest" )
.generate_content( &request )
.await;
assert!( result.is_ok(), "Retry-enabled client failed with real API: {:?}", result.err() );
}
#[ cfg( feature = "circuit_breaker" ) ]
#[ tokio::test ]
async fn integration_test_circuit_breaker_real_api()
{
let _client_check = create_integration_client();
use workspace_tools as workspace;
let ws = workspace::workspace().expect( "Failed to resolve workspace" );
let api_key = ws.load_secret_key( "GEMINI_API_KEY", "-secrets.sh" )
.or_else( |_| std::env::var( "GEMINI_API_KEY" ) )
.expect( "❌ GEMINI_API_KEY not found in workspace secrets or environment" );
let client = Client::builder()
.api_key( api_key )
.enable_circuit_breaker( true )
.circuit_breaker_failure_threshold( 5 )
.circuit_breaker_timeout( Duration::from_secs( 60 ) )
.build()
.expect( "Failed to build circuit breaker client" );
let request = GenerateContentRequest
{
contents: vec![ Content
{
role: "user".to_string(),
parts : vec![ Part { text : Some( "Test circuit breaker".to_string() ), ..Default::default() } ],
} ],
..Default::default()
};
let result = client
.models()
.by_name( "gemini-flash-latest" )
.generate_content( &request )
.await;
assert!( result.is_ok(), "Circuit breaker client failed with real API: {:?}", result.err() );
}
#[ cfg( feature = "rate_limiting" ) ]
#[ tokio::test ]
async fn integration_test_rate_limiting_real_api()
{
use workspace_tools as workspace;
let ws = workspace::workspace().expect( "Failed to resolve workspace" );
let api_key = ws.load_secret_key( "GEMINI_API_KEY", "-secrets.sh" )
.or_else( |_| std::env::var( "GEMINI_API_KEY" ) )
.expect( "❌ GEMINI_API_KEY not found in workspace secrets or environment" );
let client = Client::builder()
.api_key( api_key )
.enable_rate_limiting( true )
.rate_limit_requests_per_second( 1.0 ) .build()
.expect( "Failed to build rate limiting client" );
let request = GenerateContentRequest
{
contents: vec![ Content
{
role: "user".to_string(),
parts : vec![ Part { text : Some( "Test rate limiting".to_string() ), ..Default::default() } ],
} ],
..Default::default()
};
let start = std::time::Instant::now();
let result1 = client.models().by_name( "gemini-flash-latest" ).generate_content( &request ).await;
let result2 = client.models().by_name( "gemini-flash-latest" ).generate_content( &request ).await;
let elapsed = start.elapsed();
assert!( result1.is_ok(), "First rate-limited request failed : {:?}", result1.err() );
assert!( result2.is_ok(), "Second rate-limited request failed : {:?}", result2.err() );
if elapsed < Duration::from_millis( 500 )
{
println!( "⚠️ Rate limiting might not be fully implemented - requests completed in {elapsed:?}" );
}
}
#[ cfg( feature = "caching" ) ]
#[ tokio::test ]
async fn integration_test_request_caching_real_api()
{
use workspace_tools as workspace;
let ws = workspace::workspace().expect( "Failed to resolve workspace" );
let api_key = ws.load_secret_key( "GEMINI_API_KEY", "-secrets.sh" )
.or_else( |_| std::env::var( "GEMINI_API_KEY" ) )
.expect( "❌ GEMINI_API_KEY not found in workspace secrets or environment" );
let client = Client::builder()
.api_key( api_key )
.enable_request_cache( true )
.cache_ttl( Duration::from_secs( 60 ) )
.build()
.expect( "Failed to build caching client" );
let request = GenerateContentRequest
{
contents: vec![ Content
{
role: "user".to_string(),
parts : vec![ Part { text : Some( "Test caching with deterministic response".to_string() ), ..Default::default() } ],
} ],
generation_config: Some( GenerationConfig
{
temperature: Some( 0.0 ), ..Default::default()
} ),
..Default::default()
};
let start = std::time::Instant::now();
let result1 = client.models().by_name( "gemini-flash-latest" ).generate_content( &request ).await;
let result2 = client.models().by_name( "gemini-flash-latest" ).generate_content( &request ).await;
let elapsed = start.elapsed();
assert!( result1.is_ok(), "First cached request failed : {:?}", result1.err() );
assert!( result2.is_ok(), "Second cached request failed : {:?}", result2.err() );
if elapsed > Duration::from_secs( 2 )
{
println!( "⚠️ Request caching might not be fully implemented - both requests took {elapsed:?}" );
}
}
#[ tokio::test ]
async fn integration_test_multimodal_content_real_api()
{
let client = create_integration_client();
let image_data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==";
let request = GenerateContentRequest
{
contents: vec![ Content
{
role: "user".to_string(),
parts: vec!
[
Part { text : Some( "Describe this image briefly:".to_string() ), ..Default::default() },
Part
{
inline_data: Some( Blob
{
mime_type: "image/png".to_string(),
data: image_data.to_string(),
} ),
..Default::default()
}
],
} ],
..Default::default()
};
let result = client
.models()
.by_name( "gemini-flash-latest" )
.generate_content( &request )
.await;
assert!( result.is_ok(), "Failed multimodal request with real API: {:?}", result.err() );
let response = result.unwrap();
assert!( !response.candidates.is_empty(), "No candidates in multimodal response" );
assert!( !response.candidates[0].content.parts.is_empty(), "No content parts in multimodal response" );
}
#[ tokio::test ]
async fn integration_test_function_calling_real_api()
{
let client = create_integration_client();
let request = GenerateContentRequest
{
contents: vec![ Content
{
role: "user".to_string(),
parts : vec![ Part { text : Some( "What's the weather like in Paris? Use the weather function.".to_string() ), ..Default::default() } ],
} ],
tools: Some( vec![ Tool
{
function_declarations: Some( vec![ FunctionDeclaration
{
name: "get_weather".to_string(),
description: "Get current weather for a city".to_string(),
parameters: Some( json!(
{
"type": "object",
"properties":
{
"city": { "type": "string", "description": "City name" },
"units": { "type": "string", "enum": ["celsius", "fahrenheit"] }
},
"required": ["city"]
} ) ),
} ] ),
code_execution: None,
google_search_retrieval: None,
code_execution_tool: None,
} ] ),
..Default::default()
};
let result = client
.models()
.by_name( "gemini-flash-latest" )
.generate_content( &request )
.await;
assert!( result.is_ok(), "Failed function calling with real API: {:?}", result.err() );
let response = result.unwrap();
assert!( !response.candidates.is_empty(), "No candidates in function calling response" );
let has_function_call = response.candidates[0].content.parts
.iter()
.any( | part | part.function_call.is_some() );
let has_text_response = response.candidates[0].content.parts
.iter()
.any( | part | part.text.is_some() );
assert!( has_function_call || has_text_response, "No function call or text response in function calling test" );
}