#[test]
fn test_image_block_from_url() {
let block = ImageBlock::from_url("https://example.com/image.jpg").unwrap();
assert_eq!(block.url(), "https://example.com/image.jpg");
assert!(matches!(block.detail(), ImageDetail::Auto));
}
#[test]
fn test_image_block_from_base64() {
let block = ImageBlock::from_base64("iVBORw0KGgoAAAA=", "image/jpeg").unwrap();
assert!(block.url().starts_with("data:image/jpeg;base64,"));
assert!(matches!(block.detail(), ImageDetail::Auto));
}
#[test]
fn test_image_block_from_file_path() {
use base64::{Engine as _, engine::general_purpose};
use std::io::Write;
let temp_dir = std::env::temp_dir();
let test_file = temp_dir.join("test_image.png");
let png_bytes = general_purpose::STANDARD
.decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==")
.unwrap();
std::fs::File::create(&test_file)
.unwrap()
.write_all(&png_bytes)
.unwrap();
let block = ImageBlock::from_file_path(&test_file).unwrap();
assert!(block.url().starts_with("data:image/png;base64,"));
assert!(matches!(block.detail(), ImageDetail::Auto));
let no_ext_file = temp_dir.join("test_image_no_ext");
std::fs::File::create(&no_ext_file)
.unwrap()
.write_all(&png_bytes)
.unwrap();
let result = ImageBlock::from_file_path(&no_ext_file);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("extension"));
let bad_ext_file = temp_dir.join("test_image.txt");
std::fs::File::create(&bad_ext_file)
.unwrap()
.write_all(&png_bytes)
.unwrap();
let result = ImageBlock::from_file_path(&bad_ext_file);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Unsupported"));
let _ = std::fs::remove_file(&test_file);
let _ = std::fs::remove_file(&no_ext_file);
let _ = std::fs::remove_file(&bad_ext_file);
}
#[test]
fn test_image_block_with_detail() {
let block = ImageBlock::from_url("https://example.com/image.jpg")
.unwrap()
.with_detail(ImageDetail::High);
assert!(matches!(block.detail(), ImageDetail::High));
}
#[test]
fn test_image_detail_serialization() {
let json = serde_json::to_string(&ImageDetail::Low).unwrap();
assert_eq!(json, "\"low\"");
let json = serde_json::to_string(&ImageDetail::High).unwrap();
assert_eq!(json, "\"high\"");
let json = serde_json::to_string(&ImageDetail::Auto).unwrap();
assert_eq!(json, "\"auto\"");
}
#[test]
fn test_content_block_image_variant() {
let image = ImageBlock::from_url("https://example.com/image.jpg").unwrap();
let block = ContentBlock::Image(image);
match block {
ContentBlock::Image(img) => {
assert_eq!(img.url(), "https://example.com/image.jpg");
}
_ => panic!("Expected Image variant"),
}
}
#[test]
fn test_openai_content_text_format() {
let content = OpenAIContent::Text("Hello".to_string());
let json = serde_json::to_value(&content).unwrap();
assert_eq!(json, serde_json::json!("Hello"));
}
#[test]
#[allow(deprecated)]
fn test_openai_content_parts_format() {
let parts = vec![
OpenAIContentPart::text("What's in this image?"),
OpenAIContentPart::image_url("https://example.com/img.jpg", ImageDetail::High),
];
let content = OpenAIContent::Parts(parts);
let json = serde_json::to_value(&content).unwrap();
assert!(json.is_array());
assert_eq!(json[0]["type"], "text");
assert_eq!(json[0]["text"], "What's in this image?");
assert_eq!(json[1]["type"], "image_url");
assert_eq!(json[1]["image_url"]["url"], "https://example.com/img.jpg");
assert_eq!(json[1]["image_url"]["detail"], "high");
}
#[test]
fn test_openai_content_part_text_serialization() {
let part = OpenAIContentPart::text("Hello world");
let json = serde_json::to_value(&part).unwrap();
assert_eq!(json["type"], "text");
assert_eq!(json["text"], "Hello world");
assert!(json.get("image_url").is_none());
}
#[test]
#[allow(deprecated)]
fn test_openai_content_part_image_serialization() {
let part = OpenAIContentPart::image_url("https://example.com/img.jpg", ImageDetail::Low);
let json = serde_json::to_value(&part).unwrap();
assert_eq!(json["type"], "image_url");
assert_eq!(json["image_url"]["url"], "https://example.com/img.jpg");
assert_eq!(json["image_url"]["detail"], "low");
assert!(json.get("text").is_none());
}
#[test]
#[allow(deprecated)]
fn test_openai_content_part_enum_exhaustiveness() {
let text_part = OpenAIContentPart::text("test");
let image_part = OpenAIContentPart::image_url("url", ImageDetail::Auto);
match text_part {
OpenAIContentPart::Text { .. } => {
}
OpenAIContentPart::ImageUrl { .. } => {
panic!("Text part should not match ImageUrl variant");
}
}
match image_part {
OpenAIContentPart::Text { .. } => {
panic!("Image part should not match Text variant");
}
OpenAIContentPart::ImageUrl { .. } => {
}
}
}
#[test]
fn test_image_detail_display() {
assert_eq!(ImageDetail::Low.to_string(), "low");
assert_eq!(ImageDetail::High.to_string(), "high");
assert_eq!(ImageDetail::Auto.to_string(), "auto");
}
#[test]
fn test_image_block_from_url_rejects_empty() {
let result = ImageBlock::from_url("");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("empty"));
}
#[test]
fn test_image_block_from_url_rejects_invalid_scheme() {
let result = ImageBlock::from_url("ftp://example.com/image.jpg");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("scheme") || err.to_string().contains("http"));
}
#[test]
fn test_image_block_from_url_rejects_relative_path() {
let result = ImageBlock::from_url("/images/photo.jpg");
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), crate::Error::InvalidInput(_)));
}
#[test]
fn test_image_block_from_url_accepts_http() {
let result = ImageBlock::from_url("http://example.com/image.jpg");
assert!(result.is_ok());
assert_eq!(result.unwrap().url(), "http://example.com/image.jpg");
}
#[test]
fn test_image_block_from_url_accepts_https() {
let result = ImageBlock::from_url("https://example.com/image.jpg");
assert!(result.is_ok());
assert_eq!(result.unwrap().url(), "https://example.com/image.jpg");
}
#[test]
fn test_image_block_from_url_accepts_data_uri() {
let data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
let result = ImageBlock::from_url(data_uri);
assert!(result.is_ok());
assert_eq!(result.unwrap().url(), data_uri);
}
#[test]
fn test_image_block_from_url_rejects_malformed_data_uri() {
let result = ImageBlock::from_url("data:notanimage");
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), crate::Error::InvalidInput(_)));
}
#[test]
fn test_from_url_rejects_control_characters() {
let invalid_urls = [
"https://example.com\n/image.jpg", "https://example.com\t/image.jpg", "https://example.com\0/image.jpg", "https://example.com\r/image.jpg", ];
for url in &invalid_urls {
let result = ImageBlock::from_url(*url);
assert!(
result.is_err(),
"Should reject URL with control characters: {:?}",
url
);
let err = result.unwrap_err();
assert!(
err.to_string().contains("control") || err.to_string().contains("character"),
"Error should mention control characters, got: {}",
err
);
}
}
#[test]
fn test_from_url_warns_very_long_url() {
let long_url = format!("https://example.com/{}", "a".repeat(2980));
let result = ImageBlock::from_url(&long_url);
assert!(result.is_ok(), "Should accept long URL (with warning)");
let block = result.unwrap();
assert_eq!(block.url().len(), 3000);
}
#[test]
fn test_from_url_validates_data_uri_base64() {
let invalid_data_uris = [
"data:image/png;base64,", "data:image/png;base64,hello world", "data:image/png;base64,@@@", "data:image/png;base64,ABC", "data:image/png;base64,==abc", "data:image/png;base64,ab==cd", ];
for uri in &invalid_data_uris {
let result = ImageBlock::from_url(*uri);
assert!(
result.is_err(),
"Should reject data URI with invalid base64: {}",
uri
);
}
}
#[test]
fn test_from_url_rejects_javascript_scheme() {
let result = ImageBlock::from_url("javascript:alert(1)");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("http") || err.to_string().contains("scheme"),
"Error should mention scheme requirements, got: {}",
err
);
}
#[test]
fn test_from_url_rejects_file_scheme() {
let result = ImageBlock::from_url("file:///etc/passwd");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("http") || err.to_string().contains("scheme"),
"Error should mention scheme requirements, got: {}",
err
);
}
#[test]
fn test_image_block_from_base64_rejects_empty() {
let result = ImageBlock::from_base64("", "image/png");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("empty"));
}
#[test]
fn test_image_block_from_base64_rejects_invalid_mime() {
let result = ImageBlock::from_base64("somedata", "text/plain");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("MIME") || err.to_string().contains("image"));
}
#[test]
fn test_image_block_from_base64_accepts_valid_input() {
let base64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
let result = ImageBlock::from_base64(base64, "image/png");
assert!(result.is_ok());
let block = result.unwrap();
assert!(block.url().starts_with("data:image/png;base64,"));
}
#[test]
fn test_image_block_from_base64_accepts_all_image_types() {
let base64 = "iVBORw0KGgo="; let mime_types = ["image/jpeg", "image/png", "image/gif", "image/webp"];
for mime in &mime_types {
let result = ImageBlock::from_base64(base64, *mime);
assert!(result.is_ok(), "Should accept {}", mime);
let block = result.unwrap();
assert!(block.url().starts_with(&format!("data:{};base64,", mime)));
}
}
#[test]
fn test_from_base64_rejects_invalid_characters() {
let invalid_inputs = [
"hello world", "test@data", "test#data", "test$data", "test%data", "abc\ndef", ];
for invalid in &invalid_inputs {
let result = ImageBlock::from_base64(invalid, "image/png");
assert!(
result.is_err(),
"Should reject base64 with invalid characters: {}",
invalid
);
let err = result.unwrap_err();
assert!(
err.to_string().contains("base64") || err.to_string().contains("character"),
"Error should mention base64 or character issue, got: {}",
err
);
}
}
#[test]
fn test_from_base64_rejects_malformed_padding() {
let invalid_padding = [
"A", "AB", "ABC", "ABCD===", ];
for invalid in &invalid_padding {
let result = ImageBlock::from_base64(invalid, "image/png");
assert!(
result.is_err(),
"Should reject malformed padding: {}",
invalid
);
}
}
#[test]
fn test_from_base64_rejects_mime_with_semicolon() {
let result = ImageBlock::from_base64("AAAA", "image/png;charset=utf-8");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("MIME") || err.to_string().contains("character"),
"Error should mention MIME or character issue, got: {}",
err
);
}
#[test]
fn test_from_base64_rejects_mime_with_newline() {
let invalid_mimes = [
"image/png\n",
"image/png\r",
"image/png\r\n",
"image/png,extra",
];
for mime in &invalid_mimes {
let result = ImageBlock::from_base64("AAAA", mime);
assert!(
result.is_err(),
"Should reject MIME with control/injection chars: {:?}",
mime
);
}
}
#[test]
fn test_from_base64_warns_large_data() {
let large_base64 = "A".repeat(15_000_000);
let result = ImageBlock::from_base64(&large_base64, "image/png");
assert!(result.is_ok(), "Should accept large base64 (with warning)");
let block = result.unwrap();
assert!(block.url().len() > 15_000_000);
}
#[test]
fn test_from_base64_accepts_all_image_mime_types() {
let valid_data = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
let mime_types = [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/avif",
"image/bmp",
"image/tiff",
];
for mime in &mime_types {
let result = ImageBlock::from_base64(valid_data, *mime);
assert!(result.is_ok(), "Should accept valid MIME type: {}", mime);
}
}
#[test]
fn test_image_block_from_base64_rejects_empty_mime() {
let result = ImageBlock::from_base64("somedata", "");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("MIME") || err.to_string().contains("empty"));
}