use api_gemini::{ client::Client, models::*, error::Error };
use serde_json::json;
fn create_test_client() -> Client
{
match Client::new()
{
Ok(client) => client,
Err(error) => 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 : {error:?}\n\
\n💡 To run only unit tests: cargo test --no-default-features\n"
),
}
}
macro_rules! setup_test_client
{
() =>
{
create_test_client()
};
}
#[ tokio::test ]
async fn test_list_models_integration()
{
let client = setup_test_client!();
let result = client.models().list().await;
assert!( result.is_ok(), "Failed to list models : {:?}", result.err() );
let models = result.unwrap();
assert!( !models.models.is_empty(), "No models returned" );
let model_names: Vec< _ > = models.models
.iter()
.map( |m| m.name.as_str() )
.collect();
assert!(
model_names.iter().any( |&name| name.contains( "gemini" ) ),
"No Gemini models found in list"
);
}
#[ tokio::test ]
async fn test_get_model_integration()
{
let client = setup_test_client!();
let result = client.models().get( "models/gemini-2.5-pro" ).await;
assert!( result.is_ok(), "Failed to get model : {:?}", result.err() );
let model = result.unwrap();
assert_eq!( model.name, "models/gemini-2.5-pro" );
assert!( model.supported_generation_methods.is_some() );
assert!( model.input_token_limit.is_some() );
}
#[ tokio::test ]
async fn test_generate_content_simple()
{
let client = setup_test_client!();
let request = GenerateContentRequest
{
contents: vec!
[
Content
{
role: "user".to_string(),
parts: vec!
[
Part
{
text: Some( "Say 'Hello, World!' and nothing else.".to_string() ),
inline_data: None,
function_call: None,
function_response: None,
..Default::default()
}
],
}
],
generation_config: None,
safety_settings: None,
tools: None,
tool_config: None,
system_instruction: None,
cached_content: None,
};
let result = client
.models()
.by_name( "gemini-flash-latest" )
.generate_content( &request )
.await;
assert!( result.is_ok(), "Failed to generate content : {:?}", result.err() );
let response = result.unwrap();
assert!( !response.candidates.is_empty() );
let text = response.candidates[ 0 ].content.parts[ 0 ].text.as_ref().unwrap();
assert!( text.contains( "Hello, World" ) || text.contains( "Hello World" ) );
}
#[ tokio::test ]
async fn test_generate_content_with_parameters()
{
let client = setup_test_client!();
let request = GenerateContentRequest
{
contents: vec!
[
Content
{
role: "user".to_string(),
parts: vec!
[
Part
{
text: Some( "Generate exactly 5 random words.".to_string() ),
inline_data: None,
function_call: None,
function_response: None,
..Default::default()
}
],
}
],
generation_config: Some( GenerationConfig
{
temperature: Some( 0.1 ), top_k: Some( 10 ),
top_p: Some( 0.8 ),
max_output_tokens: Some( 500 ),
stop_sequences: None,
candidate_count: None,
}),
safety_settings: None,
tools: None,
tool_config: None,
system_instruction: None,
cached_content: None,
};
let result = client
.models()
.by_name( "gemini-flash-latest" )
.generate_content( &request )
.await;
assert!( result.is_ok() );
let response = result.unwrap();
assert!( !response.candidates.is_empty() );
}
#[ tokio::test ]
async fn test_generate_content_multimodal()
{
let client = setup_test_client!();
let test_image_base64 = "iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mP8z8BQz0AEYBxVSF+FABJADveWkH6oAAAAAElFTkSuQmCC";
let request = GenerateContentRequest
{
contents: vec!
[
Content
{
role: "user".to_string(),
parts: vec!
[
Part
{
text: Some( "Describe this image in one word.".to_string() ),
inline_data: None,
function_call: None,
function_response: None,
..Default::default()
},
Part
{
text: None,
inline_data: Some( Blob
{
mime_type: "image/png".to_string(),
data: test_image_base64.to_string(),
}),
function_call: None,
function_response: None,
..Default::default()
},
],
}
],
generation_config: None,
safety_settings: None,
tools: None,
tool_config: None,
system_instruction: None,
cached_content: None,
};
let result = client
.models()
.by_name( "gemini-flash-latest" )
.generate_content( &request )
.await;
assert!( result.is_ok(), "Failed to generate multimodal content : {:?}", result.err() );
let response = result.unwrap();
assert!( !response.candidates.is_empty() );
let text = response.candidates[ 0 ].content.parts[ 0 ].text.as_ref().unwrap();
assert!( !text.is_empty(), "Response should not be empty" );
}
#[ tokio::test ]
async fn test_generate_content_function_calling()
{
let client = setup_test_client!();
let tools = vec!
[
Tool
{
function_declarations: Some( vec!
[
FunctionDeclaration
{
name: "get_current_time".to_string(),
description: "Get the current time".to_string(),
parameters: Some( json!
({
"type": "object",
"properties": {
"timezone": {
"type": "string",
"description": "The timezone to get time for"
}
}
})),
}
]),
code_execution: None,
google_search_retrieval: None,
code_execution_tool: None,
}
];
let request = GenerateContentRequest
{
contents: vec!
[
Content
{
role: "user".to_string(),
parts: vec!
[
Part
{
text: Some( "What time is it in Tokyo?".to_string() ),
inline_data: None,
function_call: None,
function_response: None,
..Default::default()
}
],
}
],
generation_config: None,
safety_settings: None,
tools: Some( tools ),
tool_config: None,
system_instruction: None,
cached_content: None,
};
let result = client
.models()
.by_name( "gemini-flash-latest" )
.generate_content( &request )
.await;
assert!( result.is_ok() );
let response = result.unwrap();
assert!( !response.candidates.is_empty() );
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
.first()
.and_then( |part| part.text.as_ref() )
.is_some();
assert!( has_function_call || has_text_response );
}
#[ tokio::test ]
async fn test_embed_content()
{
let client = setup_test_client!();
let request = EmbedContentRequest
{
content: Content
{
role: "user".to_string(),
parts: vec!
[
Part
{
text: Some( "The quick brown fox jumps over the lazy dog".to_string() ),
inline_data: None,
function_call: None,
function_response: None,
..Default::default()
}
],
},
task_type: Some( "RETRIEVAL_DOCUMENT".to_string() ),
title: None,
output_dimensionality: None,
};
let result = client
.models()
.by_name( "text-embedding-004" )
.embed_content( &request )
.await;
assert!( result.is_ok(), "Failed to embed content : {:?}", result.err() );
let response = result.unwrap();
assert!( !response.embedding.values.is_empty() );
assert!( response.embedding.values.len() > 100 ); }
#[ tokio::test ]
async fn test_invalid_model_error()
{
let client = setup_test_client!();
let request = GenerateContentRequest
{
contents: vec!
[
Content
{
role: "user".to_string(),
parts: vec!
[
Part
{
text: Some( "Hello".to_string() ),
inline_data: None,
function_call: None,
function_response: None,
..Default::default()
}
],
}
],
generation_config: None,
safety_settings: None,
tools: None,
tool_config: None,
system_instruction: None,
cached_content: None,
};
let result = client
.models()
.by_name( "invalid-model-name" )
.generate_content( &request )
.await;
assert!( result.is_err() );
match result.err().unwrap()
{
Error::InvalidArgument( _ ) | Error::ApiError( _ ) => {},
other => panic!( "Expected InvalidArgument or ApiError, got : {other:?}" ),
}
}
#[ tokio::test ]
async fn test_empty_content_error()
{
let client = setup_test_client!();
let request = GenerateContentRequest
{
contents: vec![],
generation_config: None,
safety_settings: None,
tools: None,
tool_config: None,
system_instruction: None,
cached_content: None,
};
let result = client
.models()
.by_name( "gemini-flash-latest" )
.generate_content( &request )
.await;
assert!( result.is_err() );
}
#[ tokio::test ]
async fn test_safety_settings()
{
let client = setup_test_client!();
let safety_settings = vec!
[
SafetySetting
{
category: "HARM_CATEGORY_HARASSMENT".to_string(),
threshold: "BLOCK_NONE".to_string(),
}
];
let request = GenerateContentRequest
{
contents: vec!
[
Content
{
role: "user".to_string(),
parts: vec!
[
Part
{
text: Some( "Write a story about a brave knight.".to_string() ),
inline_data: None,
function_call: None,
function_response: None,
..Default::default()
}
],
}
],
generation_config: None,
safety_settings: Some( safety_settings ),
tools: None,
tool_config: None,
system_instruction: None,
cached_content: None,
};
let result = tokio::time::timeout(
core ::time::Duration::from_secs(25),
client
.models()
.by_name( "gemini-flash-latest" )
.generate_content( &request )
).await;
match result
{
Ok(api_result) => assert!( api_result.is_ok(), "Safety settings API call failed : {:?}", api_result.err() ),
Err(timeout_err) => panic!("Safety settings API call timed out after 25 seconds - API may be overloaded : {timeout_err:?}"),
}
}
#[ tokio::test ]
async fn test_multi_turn_conversation()
{
let client = setup_test_client!();
let request = GenerateContentRequest
{
contents: vec!
[
Content
{
role: "user".to_string(),
parts: vec!
[
Part
{
text: Some( "What is 2+2?".to_string() ),
inline_data: None,
function_call: None,
function_response: None,
..Default::default()
}
],
},
Content
{
role: "model".to_string(),
parts: vec!
[
Part
{
text: Some( "2+2 equals 4.".to_string() ),
inline_data: None,
function_call: None,
function_response: None,
..Default::default()
}
],
},
Content
{
role: "user".to_string(),
parts: vec!
[
Part
{
text: Some( "What about 3+3?".to_string() ),
inline_data: None,
function_call: None,
function_response: None,
..Default::default()
}
],
},
],
generation_config: None,
safety_settings: None,
tools: None,
tool_config: None,
system_instruction: None,
cached_content: None,
};
let result = client
.models()
.by_name( "gemini-flash-latest" )
.generate_content( &request )
.await;
assert!( result.is_ok() );
let response = result.unwrap();
assert!( !response.candidates.is_empty() );
}
#[ tokio::test ]
async fn test_multiple_candidates_generation()
{
let client = setup_test_client!();
let request = GenerateContentRequest
{
contents: vec!
[
Content
{
role: "user".to_string(),
parts: vec!
[
Part
{
text: Some( "Write a very short poem".to_string() ),
inline_data: None,
function_call: None,
function_response: None,
..Default::default()
}
],
}
],
generation_config: Some( GenerationConfig
{
temperature: Some( 0.9 ),
top_k: Some( 40 ),
top_p: Some( 0.95 ),
candidate_count: Some( 2 ), max_output_tokens: Some( 500 ),
stop_sequences: None,
}),
safety_settings: None,
tools: None,
tool_config: None,
system_instruction: None,
cached_content: None,
};
let result = client
.models()
.by_name( "gemini-flash-latest" )
.generate_content( &request )
.await;
assert!( result.is_ok(), "Failed to generate multiple candidates : {:?}", result.err() );
let response = result.unwrap();
assert!( !response.candidates.is_empty() );
for candidate in &response.candidates
{
assert!( !candidate.content.parts.is_empty() );
assert!( candidate.content.parts[ 0 ].text.is_some() );
}
}
#[ test ]
fn test_client_builder_validation()
{
let client = Client::builder()
.api_key( "test-api-key".to_string() )
.build();
assert!( client.is_ok() );
let client = Client::builder()
.api_key( String::new() )
.build();
assert!( client.is_err() );
match client.unwrap_err()
{
Error::AuthenticationError( msg ) =>
{
assert_eq!( msg, "API key cannot be empty" );
},
_ => panic!( "Expected AuthenticationError" ),
}
}