#![ cfg( feature = "integration" ) ]
#[ path = "common/mod.rs" ] mod common;
use common::create_integration_client;
use api_gemini::{ client::Client, models::*, error::Error };
use serde_json::json;
macro_rules! setup_test_client
{
() =>
{
create_integration_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( "gemini-embedding-001" )
.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 ),
max_output_tokens: Some( 500 ),
stop_sequences: None,
..Default::default()
}),
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 or blank" );
},
_ => panic!( "Expected AuthenticationError" ),
}
}
#[ cfg( feature = "chat" ) ]
#[ tokio::test ]
async fn test_chat_empty_messages_rejected()
{
let client = Client::builder()
.api_key( "dummy-api-key".to_string() )
.build()
.expect( "Client must accept any non-empty key" );
let request = ChatCompletionRequest
{
messages : vec![],
model : "gemini-flash-latest".to_string(),
..Default::default()
};
let result = client.chat().complete( &request ).await;
assert!( result.is_err(), "Empty messages must fail without any API call" );
match result.unwrap_err()
{
Error::InvalidArgument( msg ) =>
{
assert!(
msg.contains( "at least one message" ),
"Error must explain that messages cannot be empty : {msg}"
);
},
other => panic!( "Expected InvalidArgument for empty messages, got : {other:?}" ),
}
}
#[ cfg( feature = "chat" ) ]
#[ tokio::test ]
async fn test_chat_invalid_role_rejected()
{
let client = Client::builder()
.api_key( "dummy-api-key".to_string() )
.build()
.expect( "Client must accept any non-empty key" );
let request = ChatCompletionRequest
{
messages : vec![ ChatMessage
{
role : "unknown_role".to_string(),
content : "Hello".to_string(),
} ],
model : "gemini-flash-latest".to_string(),
..Default::default()
};
let result = client.chat().complete( &request ).await;
assert!( result.is_err(), "Invalid role must fail without any API call" );
match result.unwrap_err()
{
Error::InvalidArgument( msg ) =>
{
assert!(
msg.contains( "unknown_role" ) || msg.contains( "Invalid message role" ),
"Error must identify the bad role value : {msg}"
);
},
other => panic!( "Expected InvalidArgument for invalid role, got : {other:?}" ),
}
}
#[ cfg( feature = "chat" ) ]
#[ tokio::test ]
async fn test_chat_multiple_system_messages_rejected()
{
let client = Client::builder()
.api_key( "dummy-api-key".to_string() )
.build()
.expect( "Client must accept any non-empty key" );
let request = ChatCompletionRequest
{
messages : vec![
ChatMessage { role : "system".to_string(), content : "You are a helpful assistant.".to_string() },
ChatMessage { role : "user".to_string(), content : "Hello.".to_string() },
ChatMessage { role : "system".to_string(), content : "Also be a coding assistant.".to_string() },
],
model : "gemini-flash-latest".to_string(),
..Default::default()
};
let result = client.chat().complete( &request ).await;
assert!( result.is_err(), "Two system messages must fail without any API call" );
match result.unwrap_err()
{
Error::InvalidArgument( msg ) =>
{
assert!(
msg.contains( "Multiple system" ) || msg.contains( "Only one system" ),
"Error must explain the single-system-message constraint : {msg}"
);
},
other => panic!( "Expected InvalidArgument for multiple system messages, got : {other:?}" ),
}
}
#[ cfg( feature = "chat" ) ]
#[ tokio::test ]
async fn test_chat_no_user_message_rejected()
{
let client = Client::builder()
.api_key( "dummy-api-key".to_string() )
.build()
.expect( "Client must accept any non-empty key" );
let request = ChatCompletionRequest
{
messages : vec![ ChatMessage
{
role : "system".to_string(),
content : "You are a helpful assistant.".to_string(),
} ],
model : "gemini-flash-latest".to_string(),
..Default::default()
};
let result = client.chat().complete( &request ).await;
assert!( result.is_err(), "System-only conversation must fail without any API call" );
match result.unwrap_err()
{
Error::InvalidArgument( msg ) =>
{
assert!(
msg.contains( "user message" ),
"Error must state that a user message is required : {msg}"
);
},
other => panic!( "Expected InvalidArgument for no-user conversation, got : {other:?}" ),
}
}
#[ cfg( feature = "chat" ) ]
#[ tokio::test ]
async fn test_chat_empty_message_content_rejected()
{
let client = Client::builder()
.api_key( "dummy-api-key".to_string() )
.build()
.expect( "Client must accept any non-empty key" );
let request = ChatCompletionRequest
{
messages : vec![ ChatMessage
{
role : "user".to_string(),
content : String::new(),
} ],
model : "gemini-flash-latest".to_string(),
..Default::default()
};
let result = client.chat().complete( &request ).await;
assert!( result.is_err(), "Empty message content must fail without any API call" );
match result.unwrap_err()
{
Error::InvalidArgument( msg ) =>
{
assert!(
msg.contains( "empty content" ) || msg.contains( "non-empty content" ),
"Error must explain that message content cannot be empty : {msg}"
);
},
other => panic!( "Expected InvalidArgument for empty message content, got : {other:?}" ),
}
}
#[ test ]
fn test_whitespace_api_key_rejected_by_builder()
{
let result = Client::builder()
.api_key( " ".to_string() )
.build();
assert!( result.is_err(), "Whitespace-only API key must be rejected by the builder" );
match result.unwrap_err()
{
Error::AuthenticationError( msg ) =>
{
assert!(
msg.contains( "empty" ) || msg.contains( "blank" ) || msg.contains( "whitespace" ),
"Error must explain the key is empty or blank : {msg}"
);
},
other => panic!( "Expected AuthenticationError for whitespace API key, got : {other:?}" ),
}
}
#[ tokio::test ]
async fn test_empty_model_name_generate_content_rejected()
{
let client = Client::builder()
.api_key( "dummy-api-key".to_string() )
.build()
.expect( "Client must accept any non-empty key" );
let request = GenerateContentRequest
{
contents : vec![ Content
{
role : "user".to_string(),
parts : vec![ Part { text : Some( "Hello".to_string() ), ..Default::default() } ],
} ],
..Default::default()
};
let result = client
.models()
.by_name( "" )
.generate_content( &request )
.await;
assert!( result.is_err(), "Empty model name must be rejected before HTTP call" );
match result.unwrap_err()
{
Error::InvalidArgument( msg ) =>
{
assert!(
msg.to_lowercase().contains( "model" ),
"Error must identify the model ID as the cause : {msg}"
);
},
other => panic!( "Expected InvalidArgument for empty model name, got : {other:?}" ),
}
}
#[ tokio::test ]
async fn test_whitespace_model_name_generate_content_rejected()
{
let client = Client::builder()
.api_key( "dummy-api-key".to_string() )
.build()
.expect( "Client must accept any non-empty key" );
let request = GenerateContentRequest
{
contents : vec![ Content
{
role : "user".to_string(),
parts : vec![ Part { text : Some( "Hello".to_string() ), ..Default::default() } ],
} ],
..Default::default()
};
let result = client
.models()
.by_name( " " )
.generate_content( &request )
.await;
assert!( result.is_err(), "Whitespace model name must be rejected before HTTP call" );
match result.unwrap_err()
{
Error::InvalidArgument( _ ) => {},
other => panic!( "Expected InvalidArgument for whitespace model name, got : {other:?}" ),
}
}
#[ cfg( feature = "chat" ) ]
#[ tokio::test ]
async fn test_chat_system_instruction_uses_system_field_not_user_message()
{
use api_gemini::models::{ ChatCompletionRequest, ChatMessage };
let client = Client::builder()
.api_key( "dummy-api-key".to_string() )
.build()
.expect( "Client must accept any non-empty key" );
let request = ChatCompletionRequest
{
messages : vec![
ChatMessage { role : "system".to_string(), content : "You are a test assistant.".to_string() },
ChatMessage { role : "user".to_string(), content : "Hello.".to_string() },
ChatMessage { role : "assistant".to_string(), content : "Hi there!".to_string() },
ChatMessage { role : "user".to_string(), content : "Goodbye.".to_string() },
],
model : "gemini-flash-latest".to_string(),
..Default::default()
};
let result = client.chat().complete( &request ).await;
match result
{
Err( api_gemini::error::Error::AuthenticationError( _ ) )
| Err( api_gemini::error::Error::ApiError( _ ) )
| Err( api_gemini::error::Error::NetworkError( _ ) ) =>
{
},
Err( api_gemini::error::Error::InvalidArgument( msg ) ) =>
panic!(
"Got InvalidArgument — system_instruction mapping is broken (request never reached HTTP layer): {msg}"
),
Err( other ) => panic!( "Unexpected error variant: {other:?}" ),
Ok( _ ) =>
{
},
}
}
#[ cfg( feature = "chat" ) ]
#[ tokio::test ]
async fn test_chat_system_only_still_rejected_after_fix()
{
use api_gemini::models::{ ChatCompletionRequest, ChatMessage };
let client = Client::builder()
.api_key( "dummy-api-key".to_string() )
.build()
.expect( "Client must accept any non-empty key" );
let request = ChatCompletionRequest
{
messages : vec![
ChatMessage { role : "system".to_string(), content : "Be a bot.".to_string() },
],
model : "gemini-flash-latest".to_string(),
..Default::default()
};
let result = client.chat().complete( &request ).await;
match result.unwrap_err()
{
api_gemini::error::Error::InvalidArgument( msg ) =>
assert!( msg.contains( "user message" ), "Must say a user message is required : {msg}" ),
other => panic!( "Expected InvalidArgument for system-only chat, got : {other:?}" ),
}
}
#[ cfg( feature = "chat" ) ]
#[ cfg( feature = "integration" ) ]
#[ tokio::test ]
async fn test_chat_adjacent_user_messages_forwarded_to_api()
{
use api_gemini::models::{ ChatCompletionRequest, ChatMessage };
let client = create_integration_client();
let request = ChatCompletionRequest
{
messages : vec![
ChatMessage { role : "user".to_string(), content : "What is 2+2?".to_string() },
ChatMessage { role : "user".to_string(), content : "Please answer with just the number.".to_string() },
],
model : "gemini-flash-latest".to_string(),
temperature : Some( 0.1 ),
..Default::default()
};
let result = client.chat().complete( &request ).await;
match result
{
Ok( response ) =>
{
assert!(
!response.choices.is_empty(),
"If API accepts adjacent user messages, choices must be non-empty"
);
assert!(
!response.choices[ 0 ].message.content.is_empty(),
"Response content must not be empty when API accepts adjacent user messages"
);
},
Err( e ) =>
{
match e
{
Error::ApiError( _ ) | Error::InvalidArgument( _ ) => {},
other => panic!(
"Adjacent user messages must yield ApiError or InvalidArgument, not : {other:?}"
),
}
},
}
}
#[ tokio::test ]
async fn test_candidate_count_greater_than_one_deprecated()
{
let client = setup_test_client!();
let request = GenerateContentRequest
{
contents : vec![ Content
{
role : "user".to_string(),
parts : vec![ Part { text : Some( "Say hello.".to_string() ), ..Default::default() } ],
} ],
generation_config : Some( GenerationConfig
{
candidate_count : Some( 2 ),
..Default::default()
} ),
..Default::default()
};
let result = client
.models()
.by_name( "gemini-flash-latest" )
.generate_content( &request )
.await;
assert!( result.is_err(), "candidateCount > 1 must be rejected by the Gemini API" );
match result.unwrap_err()
{
Error::ApiError( _ ) | Error::InvalidArgument( _ ) => {},
other => panic!( "Expected ApiError or InvalidArgument for candidateCount > 1, got : {other:?}" ),
}
}