Initial Commit
This commit is contained in:
@@ -0,0 +1,497 @@
|
||||
use anyhow::Result;
|
||||
use docx_mcp::docx_handler::{DocxHandler, DocxStyle, TableData};
|
||||
use docx_mcp::pure_converter::PureRustConverter;
|
||||
use tempfile::TempDir;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::*;
|
||||
|
||||
fn setup_test_handler_with_content() -> (DocxHandler, String, TempDir) {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let mut handler = DocxHandler::new_with_temp_dir(temp_dir.path()).unwrap();
|
||||
let doc_id = handler.create_document().unwrap();
|
||||
|
||||
// Add comprehensive content for testing
|
||||
handler.add_heading(&doc_id, "Test Document Title", 1).unwrap();
|
||||
handler.add_paragraph(&doc_id, "This is a comprehensive test document with various content types.", None).unwrap();
|
||||
|
||||
handler.add_heading(&doc_id, "Table Example", 2).unwrap();
|
||||
let table_data = TableData {
|
||||
rows: vec![
|
||||
vec!["Product".to_string(), "Price".to_string(), "Quantity".to_string()],
|
||||
vec!["Widget A".to_string(), "$10.00".to_string(), "5".to_string()],
|
||||
vec!["Widget B".to_string(), "$15.00".to_string(), "3".to_string()],
|
||||
],
|
||||
headers: Some(vec!["Product".to_string(), "Price".to_string(), "Quantity".to_string()]),
|
||||
border_style: Some("single".to_string()),
|
||||
};
|
||||
handler.add_table(&doc_id, table_data).unwrap();
|
||||
|
||||
handler.add_heading(&doc_id, "List Example", 2).unwrap();
|
||||
let list_items = vec![
|
||||
"First important point".to_string(),
|
||||
"Second key feature".to_string(),
|
||||
"Third critical aspect".to_string(),
|
||||
];
|
||||
handler.add_list(&doc_id, list_items, false).unwrap();
|
||||
|
||||
handler.add_paragraph(&doc_id, "Conclusion: This document demonstrates various formatting capabilities.", None).unwrap();
|
||||
|
||||
(handler, doc_id, temp_dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pure_converter_creation() {
|
||||
let converter = PureRustConverter::new();
|
||||
// Just verify it can be created without panicking
|
||||
assert!(true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_text_from_docx() -> Result<()> {
|
||||
let (handler, doc_id, _temp_dir) = setup_test_handler_with_content();
|
||||
|
||||
let metadata = handler.get_metadata(&doc_id)?;
|
||||
let converter = PureRustConverter::new();
|
||||
|
||||
let extracted_text = converter.extract_text_from_docx(&metadata.path)?;
|
||||
|
||||
// Should contain all the content we added
|
||||
assert!(extracted_text.contains("Test Document Title"));
|
||||
assert!(extracted_text.contains("comprehensive test document"));
|
||||
assert!(extracted_text.contains("Table Example"));
|
||||
assert!(extracted_text.contains("Widget A"));
|
||||
assert!(extracted_text.contains("First important point"));
|
||||
assert!(extracted_text.contains("Conclusion"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_text_empty_document() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let mut handler = DocxHandler::new_with_temp_dir(temp_dir.path()).unwrap();
|
||||
let doc_id = handler.create_document().unwrap();
|
||||
|
||||
let metadata = handler.get_metadata(&doc_id)?;
|
||||
let converter = PureRustConverter::new();
|
||||
|
||||
let extracted_text = converter.extract_text_from_docx(&metadata.path)?;
|
||||
|
||||
// Empty document should return empty or whitespace-only text
|
||||
assert!(extracted_text.trim().is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_docx_to_pdf_basic() -> Result<()> {
|
||||
let (handler, doc_id, temp_dir) = setup_test_handler_with_content();
|
||||
|
||||
let metadata = handler.get_metadata(&doc_id)?;
|
||||
let converter = PureRustConverter::new();
|
||||
|
||||
let output_path = temp_dir.path().join("test_output.pdf");
|
||||
converter.convert_docx_to_pdf(&metadata.path, &output_path)?;
|
||||
|
||||
// Verify PDF file was created
|
||||
assert!(output_path.exists());
|
||||
|
||||
// Check file size is reasonable (should be larger than empty PDF)
|
||||
let file_size = fs::metadata(&output_path)?.len();
|
||||
assert!(file_size > 1000); // PDF should be at least 1KB
|
||||
|
||||
// Verify it's actually a PDF file (starts with PDF signature)
|
||||
let pdf_content = fs::read(&output_path)?;
|
||||
assert!(pdf_content.starts_with(b"%PDF"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_docx_to_pdf_with_complex_content() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let mut handler = DocxHandler::new_with_temp_dir(temp_dir.path()).unwrap();
|
||||
let doc_id = handler.create_document().unwrap();
|
||||
|
||||
// Add content with special characters and formatting
|
||||
handler.add_paragraph(&doc_id, "Special characters: éñüñ, 中文, русский, العربية", None)?;
|
||||
|
||||
let style = DocxStyle {
|
||||
font_family: Some("Arial".to_string()),
|
||||
font_size: Some(16),
|
||||
bold: Some(true),
|
||||
italic: Some(false),
|
||||
underline: Some(true),
|
||||
color: Some("#FF0000".to_string()),
|
||||
alignment: Some("center".to_string()),
|
||||
line_spacing: Some(1.5),
|
||||
};
|
||||
handler.add_paragraph(&doc_id, "Bold and underlined text", Some(style))?;
|
||||
|
||||
// Add multiple headings
|
||||
for level in 1..=3 {
|
||||
handler.add_heading(&doc_id, &format!("Heading Level {}", level), level)?;
|
||||
}
|
||||
|
||||
let metadata = handler.get_metadata(&doc_id)?;
|
||||
let converter = PureRustConverter::new();
|
||||
|
||||
let output_path = temp_dir.path().join("complex_output.pdf");
|
||||
converter.convert_docx_to_pdf(&metadata.path, &output_path)?;
|
||||
|
||||
assert!(output_path.exists());
|
||||
let file_size = fs::metadata(&output_path)?.len();
|
||||
assert!(file_size > 2000); // Should be larger due to more content
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_docx_to_images() -> Result<()> {
|
||||
let (handler, doc_id, temp_dir) = setup_test_handler_with_content();
|
||||
|
||||
let metadata = handler.get_metadata(&doc_id)?;
|
||||
let converter = PureRustConverter::new();
|
||||
|
||||
let output_dir = temp_dir.path().join("images");
|
||||
fs::create_dir_all(&output_dir)?;
|
||||
|
||||
let image_paths = converter.convert_docx_to_images(&metadata.path, &output_dir)?;
|
||||
|
||||
// Should generate at least one image
|
||||
assert!(!image_paths.is_empty());
|
||||
|
||||
// Verify all generated images exist
|
||||
for image_path in &image_paths {
|
||||
assert!(image_path.exists(), "Generated image should exist: {:?}", image_path);
|
||||
|
||||
let file_size = fs::metadata(image_path)?.len();
|
||||
assert!(file_size > 100, "Image file should have reasonable size");
|
||||
|
||||
// Verify it's a PNG file (our default format)
|
||||
if image_path.extension().and_then(|s| s.to_str()) == Some("png") {
|
||||
let image_content = fs::read(image_path)?;
|
||||
assert!(image_content.starts_with(&[0x89, 0x50, 0x4E, 0x47]), "Should be valid PNG");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_docx_to_images_custom_format() -> Result<()> {
|
||||
let (handler, doc_id, temp_dir) = setup_test_handler_with_content();
|
||||
|
||||
let metadata = handler.get_metadata(&doc_id)?;
|
||||
let converter = PureRustConverter::new();
|
||||
|
||||
let output_dir = temp_dir.path().join("jpeg_images");
|
||||
fs::create_dir_all(&output_dir)?;
|
||||
|
||||
let image_paths = converter.convert_docx_to_images_with_format(&metadata.path, &output_dir, "jpeg", 150)?;
|
||||
|
||||
assert!(!image_paths.is_empty());
|
||||
|
||||
for image_path in &image_paths {
|
||||
assert!(image_path.exists());
|
||||
|
||||
// Verify JPEG format
|
||||
if image_path.extension().and_then(|s| s.to_str()) == Some("jpg") ||
|
||||
image_path.extension().and_then(|s| s.to_str()) == Some("jpeg") {
|
||||
let image_content = fs::read(image_path)?;
|
||||
assert!(image_content.starts_with(&[0xFF, 0xD8, 0xFF]), "Should be valid JPEG");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pdf_generation_with_embedded_fonts() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let mut handler = DocxHandler::new_with_temp_dir(temp_dir.path()).unwrap();
|
||||
let doc_id = handler.create_document().unwrap();
|
||||
|
||||
// Add text that might require different fonts
|
||||
handler.add_paragraph(&doc_id, "Regular ASCII text", None)?;
|
||||
handler.add_paragraph(&doc_id, "Unicode: àáâãäå çèéêë ìíîï ñòóôõö ùúûü ýÿ", None)?;
|
||||
handler.add_paragraph(&doc_id, "Math symbols: ∑ ∏ ∫ √ ≤ ≥ ≠ ± ∞", None)?;
|
||||
|
||||
let metadata = handler.get_metadata(&doc_id)?;
|
||||
let converter = PureRustConverter::new();
|
||||
|
||||
let output_path = temp_dir.path().join("embedded_fonts.pdf");
|
||||
converter.convert_docx_to_pdf(&metadata.path, &output_path)?;
|
||||
|
||||
assert!(output_path.exists());
|
||||
let file_size = fs::metadata(&output_path)?.len();
|
||||
assert!(file_size > 5000); // Should be larger due to embedded fonts
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_batch_conversion() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let mut handler = DocxHandler::new_with_temp_dir(temp_dir.path()).unwrap();
|
||||
|
||||
// Create multiple documents
|
||||
let mut doc_paths = Vec::new();
|
||||
for i in 0..3 {
|
||||
let doc_id = handler.create_document().unwrap();
|
||||
handler.add_paragraph(&doc_id, &format!("Document {} content", i), None)?;
|
||||
|
||||
let metadata = handler.get_metadata(&doc_id)?;
|
||||
doc_paths.push(metadata.path);
|
||||
}
|
||||
|
||||
let converter = PureRustConverter::new();
|
||||
let output_dir = temp_dir.path().join("batch_output");
|
||||
fs::create_dir_all(&output_dir)?;
|
||||
|
||||
// Convert all documents to PDF
|
||||
for (i, doc_path) in doc_paths.iter().enumerate() {
|
||||
let output_path = output_dir.join(format!("document_{}.pdf", i));
|
||||
converter.convert_docx_to_pdf(doc_path, &output_path)?;
|
||||
|
||||
assert!(output_path.exists());
|
||||
}
|
||||
|
||||
// Verify all PDFs were created
|
||||
let pdf_files: Vec<_> = fs::read_dir(&output_dir)?
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| entry.path().extension().and_then(|s| s.to_str()) == Some("pdf"))
|
||||
.collect();
|
||||
|
||||
assert_eq!(pdf_files.len(), 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_handling_invalid_docx() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let converter = PureRustConverter::new();
|
||||
|
||||
// Create a fake DOCX file (actually just text)
|
||||
let fake_docx = temp_dir.path().join("fake.docx");
|
||||
fs::write(&fake_docx, "This is not a DOCX file").unwrap();
|
||||
|
||||
// Should handle the error gracefully
|
||||
let result = converter.extract_text_from_docx(&fake_docx);
|
||||
assert!(result.is_err());
|
||||
|
||||
let output_path = temp_dir.path().join("output.pdf");
|
||||
let result = converter.convert_docx_to_pdf(&fake_docx, &output_path);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_handling_nonexistent_file() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let converter = PureRustConverter::new();
|
||||
|
||||
let nonexistent = temp_dir.path().join("nonexistent.docx");
|
||||
|
||||
let result = converter.extract_text_from_docx(&nonexistent);
|
||||
assert!(result.is_err());
|
||||
|
||||
let output_path = temp_dir.path().join("output.pdf");
|
||||
let result = converter.convert_docx_to_pdf(&nonexistent, &output_path);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_large_document_conversion() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let mut handler = DocxHandler::new_with_temp_dir(temp_dir.path()).unwrap();
|
||||
let doc_id = handler.create_document().unwrap();
|
||||
|
||||
// Create a large document with many pages
|
||||
for i in 0..50 {
|
||||
handler.add_heading(&doc_id, &format!("Section {}", i + 1), 1)?;
|
||||
|
||||
for j in 0..10 {
|
||||
let content = format!("This is paragraph {} in section {}. It contains enough text to make the document substantial and test the conversion capabilities with larger files.", j + 1, i + 1);
|
||||
handler.add_paragraph(&doc_id, &content, None)?;
|
||||
}
|
||||
|
||||
if i % 10 == 9 {
|
||||
handler.add_page_break(&doc_id)?;
|
||||
}
|
||||
}
|
||||
|
||||
let metadata = handler.get_metadata(&doc_id)?;
|
||||
let converter = PureRustConverter::new();
|
||||
|
||||
// Test PDF conversion
|
||||
let pdf_path = temp_dir.path().join("large_document.pdf");
|
||||
converter.convert_docx_to_pdf(&metadata.path, &pdf_path)?;
|
||||
|
||||
assert!(pdf_path.exists());
|
||||
let pdf_size = fs::metadata(&pdf_path)?.len();
|
||||
assert!(pdf_size > 50000); // Should be a substantial PDF
|
||||
|
||||
// Test image conversion (but only first few pages to avoid excessive test time)
|
||||
let images_dir = temp_dir.path().join("large_images");
|
||||
fs::create_dir_all(&images_dir)?;
|
||||
|
||||
let image_paths = converter.convert_docx_to_images(&metadata.path, &images_dir)?;
|
||||
assert!(!image_paths.is_empty());
|
||||
|
||||
// Should generate multiple images for multiple pages
|
||||
assert!(image_paths.len() >= 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_text_extraction_accuracy() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let mut handler = DocxHandler::new_with_temp_dir(temp_dir.path()).unwrap();
|
||||
let doc_id = handler.create_document().unwrap();
|
||||
|
||||
// Add specific test content
|
||||
let test_sentences = vec![
|
||||
"The quick brown fox jumps over the lazy dog.",
|
||||
"Pack my box with five dozen liquor jugs.",
|
||||
"How vexingly quick daft zebras jump!",
|
||||
"Sphinx of black quartz, judge my vow.",
|
||||
];
|
||||
|
||||
for sentence in &test_sentences {
|
||||
handler.add_paragraph(&doc_id, sentence, None)?;
|
||||
}
|
||||
|
||||
let metadata = handler.get_metadata(&doc_id)?;
|
||||
let converter = PureRustConverter::new();
|
||||
|
||||
let extracted_text = converter.extract_text_from_docx(&metadata.path)?;
|
||||
|
||||
// Verify all sentences are present in the extracted text
|
||||
for sentence in &test_sentences {
|
||||
assert!(extracted_text.contains(sentence),
|
||||
"Extracted text should contain: '{}'", sentence);
|
||||
}
|
||||
|
||||
// Check word count accuracy
|
||||
let expected_words: usize = test_sentences.iter()
|
||||
.map(|s| s.split_whitespace().count())
|
||||
.sum();
|
||||
let extracted_words = extracted_text.split_whitespace().count();
|
||||
|
||||
// Should be approximately equal (allowing for minor differences)
|
||||
let word_diff = if extracted_words > expected_words {
|
||||
extracted_words - expected_words
|
||||
} else {
|
||||
expected_words - extracted_words
|
||||
};
|
||||
assert!(word_diff <= 5, "Word count difference too large: expected ~{}, got {}", expected_words, extracted_words);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conversion_with_different_page_sizes() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let mut handler = DocxHandler::new_with_temp_dir(temp_dir.path()).unwrap();
|
||||
let doc_id = handler.create_document().unwrap();
|
||||
|
||||
handler.add_paragraph(&doc_id, "This document tests page size handling during conversion.", None)?;
|
||||
|
||||
let metadata = handler.get_metadata(&doc_id)?;
|
||||
let converter = PureRustConverter::new();
|
||||
|
||||
// Test different output formats and sizes
|
||||
let test_cases = vec![
|
||||
("a4.pdf", "A4"),
|
||||
("letter.pdf", "Letter"),
|
||||
("legal.pdf", "Legal"),
|
||||
];
|
||||
|
||||
for (filename, _page_size) in test_cases {
|
||||
let output_path = temp_dir.path().join(filename);
|
||||
|
||||
// Note: In a full implementation, you'd pass page_size to the converter
|
||||
converter.convert_docx_to_pdf(&metadata.path, &output_path)?;
|
||||
|
||||
assert!(output_path.exists());
|
||||
let file_size = fs::metadata(&output_path)?.len();
|
||||
assert!(file_size > 500); // Reasonable minimum size
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Parametrized test for different image formats
|
||||
#[rstest]
|
||||
#[case("png", &[0x89, 0x50, 0x4E, 0x47])]
|
||||
#[case("jpeg", &[0xFF, 0xD8, 0xFF])]
|
||||
fn test_image_format_conversion(#[case] format: &str, #[case] signature: &[u8]) -> Result<()> {
|
||||
let (handler, doc_id, temp_dir) = setup_test_handler_with_content();
|
||||
|
||||
let metadata = handler.get_metadata(&doc_id)?;
|
||||
let converter = PureRustConverter::new();
|
||||
|
||||
let output_dir = temp_dir.path().join(format!("{}_images", format));
|
||||
fs::create_dir_all(&output_dir)?;
|
||||
|
||||
let image_paths = converter.convert_docx_to_images_with_format(&metadata.path, &output_dir, format, 100)?;
|
||||
|
||||
assert!(!image_paths.is_empty());
|
||||
|
||||
for image_path in &image_paths {
|
||||
assert!(image_path.exists());
|
||||
|
||||
let image_content = fs::read(image_path)?;
|
||||
assert!(image_content.starts_with(signature),
|
||||
"Image should have correct format signature for {}", format);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conversion_thread_safety() -> Result<()> {
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let temp_path = Arc::new(temp_dir.path().to_path_buf());
|
||||
|
||||
let handles: Vec<_> = (0..3).map(|i| {
|
||||
let temp_path = Arc::clone(&temp_path);
|
||||
thread::spawn(move || -> Result<()> {
|
||||
let mut handler = DocxHandler::new_with_temp_dir(&temp_path)?;
|
||||
let doc_id = handler.create_document()?;
|
||||
|
||||
handler.add_paragraph(&doc_id, &format!("Thread {} test content", i), None)?;
|
||||
|
||||
let metadata = handler.get_metadata(&doc_id)?;
|
||||
let converter = PureRustConverter::new();
|
||||
|
||||
let pdf_path = temp_path.join(format!("thread_{}.pdf", i));
|
||||
converter.convert_docx_to_pdf(&metadata.path, &pdf_path)?;
|
||||
|
||||
assert!(pdf_path.exists());
|
||||
Ok(())
|
||||
})
|
||||
}).collect();
|
||||
|
||||
// Wait for all threads to complete
|
||||
for handle in handles {
|
||||
handle.join().unwrap()?;
|
||||
}
|
||||
|
||||
// Verify all PDFs were created
|
||||
let pdf_count = fs::read_dir(&temp_dir)?
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| entry.path().extension().and_then(|s| s.to_str()) == Some("pdf"))
|
||||
.count();
|
||||
|
||||
assert_eq!(pdf_count, 3);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
use anyhow::Result;
|
||||
use docx_mcp::docx_handler::{DocxHandler, DocxStyle, TableData};
|
||||
use tempfile::TempDir;
|
||||
use std::path::PathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::*;
|
||||
use chrono::Utc;
|
||||
|
||||
fn setup_test_handler() -> (DocxHandler, TempDir) {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let handler = DocxHandler::new_with_temp_dir(temp_dir.path()).unwrap();
|
||||
(handler, temp_dir)
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn handler_and_doc() -> (DocxHandler, String, TempDir) {
|
||||
let (mut handler, temp_dir) = setup_test_handler();
|
||||
let doc_id = handler.create_document().unwrap();
|
||||
(handler, doc_id, temp_dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_document() {
|
||||
let (mut handler, _temp_dir) = setup_test_handler();
|
||||
|
||||
let doc_id = handler.create_document().unwrap();
|
||||
assert!(!doc_id.is_empty());
|
||||
|
||||
// Document should be in the handler's registry
|
||||
assert!(handler.documents.contains_key(&doc_id));
|
||||
|
||||
let metadata = handler.get_metadata(&doc_id).unwrap();
|
||||
assert_eq!(metadata.id, doc_id);
|
||||
assert!(metadata.path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_paragraph() {
|
||||
let (mut handler, doc_id, _temp_dir) = handler_and_doc();
|
||||
|
||||
let result = handler.add_paragraph(&doc_id, "Test paragraph", None);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Verify content was added by extracting text
|
||||
let text = handler.extract_text(&doc_id).unwrap();
|
||||
assert!(text.contains("Test paragraph"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_paragraph_with_style() {
|
||||
let (mut handler, doc_id, _temp_dir) = handler_and_doc();
|
||||
|
||||
let style = DocxStyle {
|
||||
font_family: Some("Arial".to_string()),
|
||||
font_size: Some(14),
|
||||
bold: Some(true),
|
||||
italic: Some(false),
|
||||
underline: Some(false),
|
||||
color: Some("#FF0000".to_string()),
|
||||
alignment: Some("center".to_string()),
|
||||
line_spacing: Some(1.5),
|
||||
};
|
||||
|
||||
let result = handler.add_paragraph(&doc_id, "Styled paragraph", Some(style));
|
||||
assert!(result.is_ok());
|
||||
|
||||
let text = handler.extract_text(&doc_id).unwrap();
|
||||
assert!(text.contains("Styled paragraph"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_heading() {
|
||||
let (mut handler, doc_id, _temp_dir) = handler_and_doc();
|
||||
|
||||
for level in 1..=6 {
|
||||
let heading_text = format!("Heading Level {}", level);
|
||||
let result = handler.add_heading(&doc_id, &heading_text, level);
|
||||
assert!(result.is_ok(), "Failed to add heading level {}", level);
|
||||
|
||||
let text = handler.extract_text(&doc_id).unwrap();
|
||||
assert!(text.contains(&heading_text));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_table() {
|
||||
let (mut handler, doc_id, _temp_dir) = handler_and_doc();
|
||||
|
||||
let table_data = TableData {
|
||||
rows: vec![
|
||||
vec!["Name".to_string(), "Age".to_string(), "City".to_string()],
|
||||
vec!["John".to_string(), "30".to_string(), "NYC".to_string()],
|
||||
vec!["Jane".to_string(), "25".to_string(), "LA".to_string()],
|
||||
],
|
||||
headers: Some(vec!["Name".to_string(), "Age".to_string(), "City".to_string()]),
|
||||
border_style: Some("single".to_string()),
|
||||
};
|
||||
|
||||
let result = handler.add_table(&doc_id, table_data);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let text = handler.extract_text(&doc_id).unwrap();
|
||||
assert!(text.contains("John"));
|
||||
assert!(text.contains("Jane"));
|
||||
assert!(text.contains("NYC"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_list() {
|
||||
let (mut handler, doc_id, _temp_dir) = handler_and_doc();
|
||||
|
||||
let items = vec![
|
||||
"First item".to_string(),
|
||||
"Second item".to_string(),
|
||||
"Third item".to_string(),
|
||||
];
|
||||
|
||||
// Test unordered list
|
||||
let result = handler.add_list(&doc_id, items.clone(), false);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Test ordered list
|
||||
let result = handler.add_list(&doc_id, items.clone(), true);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let text = handler.extract_text(&doc_id).unwrap();
|
||||
assert!(text.contains("First item"));
|
||||
assert!(text.contains("Second item"));
|
||||
assert!(text.contains("Third item"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_header_footer() {
|
||||
let (mut handler, doc_id, _temp_dir) = handler_and_doc();
|
||||
|
||||
let header_result = handler.set_header(&doc_id, "Document Header");
|
||||
assert!(header_result.is_ok());
|
||||
|
||||
let footer_result = handler.set_footer(&doc_id, "Document Footer");
|
||||
assert!(footer_result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_page_break() {
|
||||
let (mut handler, doc_id, _temp_dir) = handler_and_doc();
|
||||
|
||||
handler.add_paragraph(&doc_id, "Before page break", None).unwrap();
|
||||
|
||||
let result = handler.add_page_break(&doc_id);
|
||||
assert!(result.is_ok());
|
||||
|
||||
handler.add_paragraph(&doc_id, "After page break", None).unwrap();
|
||||
|
||||
let text = handler.extract_text(&doc_id).unwrap();
|
||||
assert!(text.contains("Before page break"));
|
||||
assert!(text.contains("After page break"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_text_empty_document() {
|
||||
let (handler, doc_id, _temp_dir) = handler_and_doc();
|
||||
|
||||
let text = handler.extract_text(&doc_id).unwrap();
|
||||
// Empty document might have some default content or be truly empty
|
||||
assert!(text.is_empty() || text.trim().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_save_and_close_document() {
|
||||
let (mut handler, doc_id, temp_dir) = handler_and_doc();
|
||||
|
||||
handler.add_paragraph(&doc_id, "Test content", None).unwrap();
|
||||
|
||||
let save_path = temp_dir.path().join("test_output.docx");
|
||||
let save_result = handler.save_document(&doc_id, &save_path);
|
||||
assert!(save_result.is_ok());
|
||||
assert!(save_path.exists());
|
||||
|
||||
let close_result = handler.close_document(&doc_id);
|
||||
assert!(close_result.is_ok());
|
||||
assert!(!handler.documents.contains_key(&doc_id));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_open_existing_document() {
|
||||
let (mut handler, doc_id, temp_dir) = handler_and_doc();
|
||||
|
||||
// Create and save a document
|
||||
handler.add_paragraph(&doc_id, "Original content", None).unwrap();
|
||||
let save_path = temp_dir.path().join("existing.docx");
|
||||
handler.save_document(&doc_id, &save_path).unwrap();
|
||||
handler.close_document(&doc_id).unwrap();
|
||||
|
||||
// Open the saved document
|
||||
let opened_doc_id = handler.open_document(&save_path).unwrap();
|
||||
assert_ne!(opened_doc_id, doc_id); // Should be a new ID
|
||||
|
||||
let text = handler.extract_text(&opened_doc_id).unwrap();
|
||||
assert!(text.contains("Original content"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_documents() {
|
||||
let (mut handler, _temp_dir) = setup_test_handler();
|
||||
|
||||
// Initially should be empty
|
||||
let docs = handler.list_documents();
|
||||
let initial_count = docs.len();
|
||||
|
||||
// Create some documents
|
||||
let _doc1 = handler.create_document().unwrap();
|
||||
let _doc2 = handler.create_document().unwrap();
|
||||
let _doc3 = handler.create_document().unwrap();
|
||||
|
||||
let docs = handler.list_documents();
|
||||
assert_eq!(docs.len(), initial_count + 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_document_not_found_error() {
|
||||
let (handler, _temp_dir) = setup_test_handler();
|
||||
|
||||
let fake_id = "nonexistent-document-id";
|
||||
|
||||
let result = handler.extract_text(fake_id);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("Document not found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_metadata() {
|
||||
let (handler, doc_id, _temp_dir) = handler_and_doc();
|
||||
|
||||
let metadata = handler.get_metadata(&doc_id).unwrap();
|
||||
|
||||
assert_eq!(metadata.id, doc_id);
|
||||
assert!(metadata.path.exists());
|
||||
assert!(metadata.created_at <= Utc::now());
|
||||
assert!(metadata.modified_at <= Utc::now());
|
||||
assert_eq!(metadata.page_count, Some(1));
|
||||
assert_eq!(metadata.word_count, Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_document_operations() {
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
|
||||
let (handler, _temp_dir) = setup_test_handler();
|
||||
let handler = Arc::new(Mutex::new(handler));
|
||||
|
||||
let handles: Vec<_> = (0..5).map(|i| {
|
||||
let handler = Arc::clone(&handler);
|
||||
thread::spawn(move || {
|
||||
let doc_id = {
|
||||
let mut h = handler.lock().unwrap();
|
||||
h.create_document().unwrap()
|
||||
};
|
||||
|
||||
{
|
||||
let mut h = handler.lock().unwrap();
|
||||
h.add_paragraph(&doc_id, &format!("Thread {} content", i), None).unwrap();
|
||||
}
|
||||
|
||||
{
|
||||
let h = handler.lock().unwrap();
|
||||
let text = h.extract_text(&doc_id).unwrap();
|
||||
assert!(text.contains(&format!("Thread {} content", i)));
|
||||
}
|
||||
|
||||
doc_id
|
||||
})
|
||||
}).collect();
|
||||
|
||||
let doc_ids: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
|
||||
|
||||
// All documents should be different
|
||||
let mut unique_ids = doc_ids.clone();
|
||||
unique_ids.sort();
|
||||
unique_ids.dedup();
|
||||
assert_eq!(unique_ids.len(), doc_ids.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_large_document_creation() {
|
||||
let (mut handler, doc_id, _temp_dir) = handler_and_doc();
|
||||
|
||||
// Add many paragraphs to test performance
|
||||
for i in 0..100 {
|
||||
let content = format!("Paragraph number {} with some content to make it realistic", i);
|
||||
handler.add_paragraph(&doc_id, &content, None).unwrap();
|
||||
}
|
||||
|
||||
let text = handler.extract_text(&doc_id).unwrap();
|
||||
assert!(text.contains("Paragraph number 0"));
|
||||
assert!(text.contains("Paragraph number 99"));
|
||||
|
||||
// Verify word count
|
||||
let words: Vec<&str> = text.split_whitespace().collect();
|
||||
assert!(words.len() > 1000); // Should have many words
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_special_characters_in_content() {
|
||||
let (mut handler, doc_id, _temp_dir) = handler_and_doc();
|
||||
|
||||
let special_content = "Special chars: éñüñdéd, 中文, русский, العربية, 🚀📝✨";
|
||||
handler.add_paragraph(&doc_id, special_content, None).unwrap();
|
||||
|
||||
let text = handler.extract_text(&doc_id).unwrap();
|
||||
assert!(text.contains("éñüñdéd"));
|
||||
assert!(text.contains("🚀📝✨"));
|
||||
}
|
||||
@@ -0,0 +1,910 @@
|
||||
use anyhow::Result;
|
||||
use docx_mcp::docx_tools::DocxToolsProvider;
|
||||
use docx_mcp::security::SecurityConfig;
|
||||
use mcp_core::{ToolProvider, ToolResult};
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tokio_test;
|
||||
|
||||
/// Test complete document creation workflow from start to finish
|
||||
#[tokio::test]
|
||||
async fn test_complete_document_workflow() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
std::env::set_var("TMPDIR", temp_dir.path());
|
||||
|
||||
let provider = DocxToolsProvider::new();
|
||||
|
||||
// Step 1: Create a new document
|
||||
let create_result = provider.call_tool("create_document", json!({})).await;
|
||||
let doc_id = match create_result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
value["document_id"].as_str().unwrap().to_string()
|
||||
},
|
||||
ToolResult::Error(e) => panic!("Failed to create document: {}", e),
|
||||
};
|
||||
|
||||
// Step 2: Add document structure
|
||||
let title_result = provider.call_tool("add_heading", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "Annual Report 2024",
|
||||
"level": 1
|
||||
})).await;
|
||||
assert!(matches!(title_result, ToolResult::Success(_)));
|
||||
|
||||
// Step 3: Add introduction
|
||||
let intro_result = provider.call_tool("add_paragraph", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "This annual report provides a comprehensive overview of our company's performance, achievements, and strategic direction for the year 2024.",
|
||||
"style": {
|
||||
"font_size": 12,
|
||||
"alignment": "justify"
|
||||
}
|
||||
})).await;
|
||||
assert!(matches!(intro_result, ToolResult::Success(_)));
|
||||
|
||||
// Step 4: Add executive summary section
|
||||
let exec_heading_result = provider.call_tool("add_heading", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "Executive Summary",
|
||||
"level": 2
|
||||
})).await;
|
||||
assert!(matches!(exec_heading_result, ToolResult::Success(_)));
|
||||
|
||||
let exec_content = provider.call_tool("add_list", json!({
|
||||
"document_id": doc_id,
|
||||
"items": [
|
||||
"Record revenue growth of 15% year-over-year",
|
||||
"Successful expansion into three new markets",
|
||||
"Launch of five innovative products",
|
||||
"Achievement of carbon neutrality goals",
|
||||
"Increased employee satisfaction by 20%"
|
||||
],
|
||||
"ordered": false
|
||||
})).await;
|
||||
assert!(matches!(exec_content, ToolResult::Success(_)));
|
||||
|
||||
// Step 5: Add financial data table
|
||||
let financial_heading = provider.call_tool("add_heading", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "Financial Highlights",
|
||||
"level": 2
|
||||
})).await;
|
||||
assert!(matches!(financial_heading, ToolResult::Success(_)));
|
||||
|
||||
let table_result = provider.call_tool("add_table", json!({
|
||||
"document_id": doc_id,
|
||||
"rows": [
|
||||
["Metric", "2023", "2024", "Change"],
|
||||
["Revenue ($M)", "120.5", "138.6", "+15%"],
|
||||
["Operating Income ($M)", "24.1", "29.3", "+22%"],
|
||||
["Net Income ($M)", "18.2", "22.7", "+25%"],
|
||||
["Employees", "1,250", "1,420", "+14%"]
|
||||
]
|
||||
})).await;
|
||||
assert!(matches!(table_result, ToolResult::Success(_)));
|
||||
|
||||
// Step 6: Add page break and new section
|
||||
let page_break_result = provider.call_tool("add_page_break", json!({
|
||||
"document_id": doc_id
|
||||
})).await;
|
||||
assert!(matches!(page_break_result, ToolResult::Success(_)));
|
||||
|
||||
let strategy_heading = provider.call_tool("add_heading", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "Strategic Initiatives",
|
||||
"level": 2
|
||||
})).await;
|
||||
assert!(matches!(strategy_heading, ToolResult::Success(_)));
|
||||
|
||||
// Step 7: Add multiple paragraphs with different styles
|
||||
let bold_paragraph = provider.call_tool("add_paragraph", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "Digital Transformation: Our commitment to digital innovation remains at the forefront of our strategic priorities.",
|
||||
"style": {
|
||||
"bold": true,
|
||||
"font_size": 13
|
||||
}
|
||||
})).await;
|
||||
assert!(matches!(bold_paragraph, ToolResult::Success(_)));
|
||||
|
||||
let regular_paragraph = provider.call_tool("add_paragraph", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "Throughout 2024, we have invested significantly in technology infrastructure, data analytics capabilities, and employee digital skills development. This comprehensive approach has resulted in improved operational efficiency and enhanced customer experience across all touchpoints."
|
||||
})).await;
|
||||
assert!(matches!(regular_paragraph, ToolResult::Success(_)));
|
||||
|
||||
// Step 8: Set document header and footer
|
||||
let header_result = provider.call_tool("set_header", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "Annual Report 2024 | Confidential"
|
||||
})).await;
|
||||
assert!(matches!(header_result, ToolResult::Success(_)));
|
||||
|
||||
let footer_result = provider.call_tool("set_footer", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "© 2024 Company Name. All rights reserved."
|
||||
})).await;
|
||||
assert!(matches!(footer_result, ToolResult::Success(_)));
|
||||
|
||||
// Step 9: Verify document content
|
||||
let extract_result = provider.call_tool("extract_text", json!({
|
||||
"document_id": doc_id
|
||||
})).await;
|
||||
|
||||
match extract_result {
|
||||
ToolResult::Success(value) => {
|
||||
let text = value["text"].as_str().unwrap();
|
||||
|
||||
// Verify all content is present
|
||||
assert!(text.contains("Annual Report 2024"));
|
||||
assert!(text.contains("Executive Summary"));
|
||||
assert!(text.contains("Record revenue growth"));
|
||||
assert!(text.contains("Financial Highlights"));
|
||||
assert!(text.contains("Revenue ($M)"));
|
||||
assert!(text.contains("138.6"));
|
||||
assert!(text.contains("Strategic Initiatives"));
|
||||
assert!(text.contains("Digital Transformation"));
|
||||
|
||||
println!("Document contains {} characters of text", text.len());
|
||||
assert!(text.len() > 1000, "Document should have substantial content");
|
||||
},
|
||||
ToolResult::Error(e) => panic!("Failed to extract text: {}", e),
|
||||
}
|
||||
|
||||
// Step 10: Get document metadata
|
||||
let metadata_result = provider.call_tool("get_metadata", json!({
|
||||
"document_id": doc_id
|
||||
})).await;
|
||||
|
||||
match metadata_result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
let metadata = &value["metadata"];
|
||||
assert_eq!(metadata["id"], doc_id);
|
||||
assert!(metadata["path"].is_string());
|
||||
},
|
||||
ToolResult::Error(e) => panic!("Failed to get metadata: {}", e),
|
||||
}
|
||||
|
||||
// Step 11: Export to different formats
|
||||
let output_dir = temp_dir.path().join("exports");
|
||||
fs::create_dir_all(&output_dir)?;
|
||||
|
||||
// Export to PDF
|
||||
let pdf_path = output_dir.join("annual_report.pdf");
|
||||
let pdf_result = provider.call_tool("convert_to_pdf", json!({
|
||||
"document_id": doc_id,
|
||||
"output_path": pdf_path.to_str().unwrap()
|
||||
})).await;
|
||||
assert!(matches!(pdf_result, ToolResult::Success(_)));
|
||||
assert!(pdf_path.exists());
|
||||
|
||||
// Export to markdown
|
||||
let md_path = output_dir.join("annual_report.md");
|
||||
let md_result = provider.call_tool("export_to_markdown", json!({
|
||||
"document_id": doc_id,
|
||||
"output_path": md_path.to_str().unwrap()
|
||||
})).await;
|
||||
assert!(matches!(md_result, ToolResult::Success(_)));
|
||||
assert!(md_path.exists());
|
||||
|
||||
// Step 12: Save the original document
|
||||
let save_path = output_dir.join("annual_report.docx");
|
||||
let save_result = provider.call_tool("save_document", json!({
|
||||
"document_id": doc_id,
|
||||
"output_path": save_path.to_str().unwrap()
|
||||
})).await;
|
||||
assert!(matches!(save_result, ToolResult::Success(_)));
|
||||
assert!(save_path.exists());
|
||||
|
||||
println!("Complete workflow test successful! Generated files:");
|
||||
println!("- PDF: {:?}", pdf_path);
|
||||
println!("- Markdown: {:?}", md_path);
|
||||
println!("- DOCX: {:?}", save_path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test document editing and revision workflow
|
||||
#[tokio::test]
|
||||
async fn test_document_editing_workflow() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
std::env::set_var("TMPDIR", temp_dir.path());
|
||||
|
||||
let provider = DocxToolsProvider::new();
|
||||
|
||||
// Create initial document
|
||||
let create_result = provider.call_tool("create_document", json!({})).await;
|
||||
let doc_id = match create_result {
|
||||
ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(),
|
||||
_ => panic!("Failed to create document"),
|
||||
};
|
||||
|
||||
// Add initial content
|
||||
provider.call_tool("add_heading", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "Project Status Report",
|
||||
"level": 1
|
||||
})).await;
|
||||
|
||||
provider.call_tool("add_paragraph", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "Current project status and upcoming milestones."
|
||||
})).await;
|
||||
|
||||
// Add tasks list
|
||||
provider.call_tool("add_heading", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "Current Tasks",
|
||||
"level": 2
|
||||
})).await;
|
||||
|
||||
provider.call_tool("add_list", json!({
|
||||
"document_id": doc_id,
|
||||
"items": [
|
||||
"Complete user interface design",
|
||||
"Implement backend API",
|
||||
"Write unit tests",
|
||||
"Deploy to staging environment"
|
||||
],
|
||||
"ordered": true
|
||||
})).await;
|
||||
|
||||
// Search for specific content
|
||||
let search_result = provider.call_tool("search_text", json!({
|
||||
"document_id": doc_id,
|
||||
"search_term": "backend",
|
||||
"case_sensitive": false
|
||||
})).await;
|
||||
|
||||
match search_result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
let matches = value["matches"].as_array().unwrap();
|
||||
assert!(!matches.is_empty());
|
||||
assert!(value["total_matches"].as_u64().unwrap() > 0);
|
||||
},
|
||||
ToolResult::Error(e) => panic!("Search failed: {}", e),
|
||||
}
|
||||
|
||||
// Get word count before modifications
|
||||
let word_count_before = provider.call_tool("get_word_count", json!({
|
||||
"document_id": doc_id
|
||||
})).await;
|
||||
|
||||
let initial_word_count = match word_count_before {
|
||||
ToolResult::Success(value) => {
|
||||
value["statistics"]["words"].as_u64().unwrap()
|
||||
},
|
||||
_ => panic!("Failed to get word count"),
|
||||
};
|
||||
|
||||
// Add more content (simulating document expansion)
|
||||
provider.call_tool("add_heading", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "Completed Items",
|
||||
"level": 2
|
||||
})).await;
|
||||
|
||||
provider.call_tool("add_table", json!({
|
||||
"document_id": doc_id,
|
||||
"rows": [
|
||||
["Task", "Completed Date", "Notes"],
|
||||
["Requirements gathering", "2024-01-15", "All stakeholders interviewed"],
|
||||
["Architecture design", "2024-01-22", "Approved by tech committee"],
|
||||
["Database schema", "2024-01-28", "Optimized for performance"]
|
||||
]
|
||||
})).await;
|
||||
|
||||
// Add risks section
|
||||
provider.call_tool("add_heading", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "Identified Risks",
|
||||
"level": 2
|
||||
})).await;
|
||||
|
||||
provider.call_tool("add_paragraph", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "The following risks have been identified and mitigation strategies are in place:",
|
||||
"style": {
|
||||
"italic": true
|
||||
}
|
||||
})).await;
|
||||
|
||||
provider.call_tool("add_list", json!({
|
||||
"document_id": doc_id,
|
||||
"items": [
|
||||
"Resource constraints may delay delivery",
|
||||
"Third-party API changes could impact integration",
|
||||
"Security requirements may require additional development time"
|
||||
],
|
||||
"ordered": false
|
||||
})).await;
|
||||
|
||||
// Get word count after modifications
|
||||
let word_count_after = provider.call_tool("get_word_count", json!({
|
||||
"document_id": doc_id
|
||||
})).await;
|
||||
|
||||
let final_word_count = match word_count_after {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
let stats = &value["statistics"];
|
||||
let words = stats["words"].as_u64().unwrap();
|
||||
let chars = stats["characters"].as_u64().unwrap();
|
||||
let sentences = stats["sentences"].as_u64().unwrap();
|
||||
|
||||
println!("Document statistics: {} words, {} characters, {} sentences",
|
||||
words, chars, sentences);
|
||||
|
||||
assert!(words > 0);
|
||||
assert!(chars > 0);
|
||||
assert!(sentences > 0);
|
||||
|
||||
words
|
||||
},
|
||||
ToolResult::Error(e) => panic!("Failed to get final word count: {}", e),
|
||||
};
|
||||
|
||||
// Verify document grew
|
||||
assert!(final_word_count > initial_word_count,
|
||||
"Document should have more words after additions: {} -> {}",
|
||||
initial_word_count, final_word_count);
|
||||
|
||||
// Perform find and replace operation
|
||||
let replace_result = provider.call_tool("find_and_replace", json!({
|
||||
"document_id": doc_id,
|
||||
"find_text": "backend",
|
||||
"replace_text": "server-side",
|
||||
"case_sensitive": false
|
||||
})).await;
|
||||
|
||||
match replace_result {
|
||||
ToolResult::Success(value) => {
|
||||
// Note: The actual implementation might return different result structure
|
||||
println!("Find and replace completed: {:?}", value);
|
||||
},
|
||||
ToolResult::Error(_) => {
|
||||
// This is acceptable as find_and_replace might not be fully implemented
|
||||
println!("Find and replace not fully implemented yet");
|
||||
}
|
||||
}
|
||||
|
||||
// Final verification
|
||||
let final_text = provider.call_tool("extract_text", json!({
|
||||
"document_id": doc_id
|
||||
})).await;
|
||||
|
||||
match final_text {
|
||||
ToolResult::Success(value) => {
|
||||
let text = value["text"].as_str().unwrap();
|
||||
|
||||
// Verify all sections are present
|
||||
assert!(text.contains("Project Status Report"));
|
||||
assert!(text.contains("Current Tasks"));
|
||||
assert!(text.contains("Completed Items"));
|
||||
assert!(text.contains("Identified Risks"));
|
||||
assert!(text.contains("Requirements gathering"));
|
||||
assert!(text.contains("Resource constraints"));
|
||||
|
||||
println!("Final document contains {} characters", text.len());
|
||||
},
|
||||
ToolResult::Error(e) => panic!("Failed to extract final text: {}", e),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test collaborative workflow with multiple document operations
|
||||
#[tokio::test]
|
||||
async fn test_collaborative_workflow() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
std::env::set_var("TMPDIR", temp_dir.path());
|
||||
|
||||
let provider = DocxToolsProvider::new();
|
||||
let mut document_ids = Vec::new();
|
||||
|
||||
// Simulate multiple team members creating documents
|
||||
let team_members = vec!["Alice", "Bob", "Charlie"];
|
||||
|
||||
for member in &team_members {
|
||||
// Each member creates a document
|
||||
let create_result = provider.call_tool("create_document", json!({})).await;
|
||||
let doc_id = match create_result {
|
||||
ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(),
|
||||
_ => panic!("Failed to create document for {}", member),
|
||||
};
|
||||
|
||||
// Add member-specific content
|
||||
provider.call_tool("add_heading", json!({
|
||||
"document_id": doc_id,
|
||||
"text": format!("{}'s Weekly Report", member),
|
||||
"level": 1
|
||||
})).await;
|
||||
|
||||
provider.call_tool("add_paragraph", json!({
|
||||
"document_id": doc_id,
|
||||
"text": format!("This week, {} focused on the following activities and achievements.", member)
|
||||
})).await;
|
||||
|
||||
// Add achievements
|
||||
let achievements = match member {
|
||||
&"Alice" => vec![
|
||||
"Completed user research interviews",
|
||||
"Created wireframes for new features",
|
||||
"Updated design system documentation"
|
||||
],
|
||||
&"Bob" => vec![
|
||||
"Implemented new API endpoints",
|
||||
"Optimized database queries",
|
||||
"Fixed critical security vulnerability"
|
||||
],
|
||||
&"Charlie" => vec![
|
||||
"Deployed version 2.1 to production",
|
||||
"Set up monitoring dashboards",
|
||||
"Conducted security audit"
|
||||
],
|
||||
_ => vec!["General tasks completed"],
|
||||
};
|
||||
|
||||
provider.call_tool("add_list", json!({
|
||||
"document_id": doc_id,
|
||||
"items": achievements,
|
||||
"ordered": false
|
||||
})).await;
|
||||
|
||||
// Add metrics table
|
||||
provider.call_tool("add_heading", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "Key Metrics",
|
||||
"level": 2
|
||||
})).await;
|
||||
|
||||
let metrics = match member {
|
||||
&"Alice" => vec![
|
||||
vec!["Interviews Conducted", "8"],
|
||||
vec!["Designs Created", "12"],
|
||||
vec!["User Stories", "15"]
|
||||
],
|
||||
&"Bob" => vec![
|
||||
vec!["Lines of Code", "2,450"],
|
||||
vec!["Tests Written", "23"],
|
||||
vec!["Bugs Fixed", "7"]
|
||||
],
|
||||
&"Charlie" => vec![
|
||||
vec!["Deployments", "3"],
|
||||
vec!["Issues Resolved", "11"],
|
||||
vec!["System Uptime", "99.9%"]
|
||||
],
|
||||
_ => vec![vec!["Tasks", "5"]],
|
||||
};
|
||||
|
||||
let mut table_rows = vec![vec!["Metric".to_string(), "Value".to_string()]];
|
||||
for metric in metrics {
|
||||
table_rows.push(metric.iter().map(|s| s.to_string()).collect());
|
||||
}
|
||||
|
||||
provider.call_tool("add_table", json!({
|
||||
"document_id": doc_id,
|
||||
"rows": table_rows
|
||||
})).await;
|
||||
|
||||
document_ids.push((member.to_string(), doc_id));
|
||||
}
|
||||
|
||||
// List all documents
|
||||
let list_result = provider.call_tool("list_documents", json!({})).await;
|
||||
match list_result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
let documents = value["documents"].as_array().unwrap();
|
||||
assert!(documents.len() >= 3, "Should have at least 3 documents");
|
||||
|
||||
println!("Found {} documents in the system", documents.len());
|
||||
},
|
||||
ToolResult::Error(e) => panic!("Failed to list documents: {}", e),
|
||||
}
|
||||
|
||||
// Generate a summary document combining all reports
|
||||
let summary_result = provider.call_tool("create_document", json!({})).await;
|
||||
let summary_id = match summary_result {
|
||||
ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(),
|
||||
_ => panic!("Failed to create summary document"),
|
||||
};
|
||||
|
||||
// Add summary header
|
||||
provider.call_tool("add_heading", json!({
|
||||
"document_id": summary_id,
|
||||
"text": "Team Weekly Summary Report",
|
||||
"level": 1
|
||||
})).await;
|
||||
|
||||
provider.call_tool("add_paragraph", json!({
|
||||
"document_id": summary_id,
|
||||
"text": "This document summarizes the key activities and achievements from all team members this week."
|
||||
})).await;
|
||||
|
||||
// Add content from each team member's document
|
||||
for (member, doc_id) in &document_ids {
|
||||
provider.call_tool("add_heading", json!({
|
||||
"document_id": summary_id,
|
||||
"text": format!("{} Highlights", member),
|
||||
"level": 2
|
||||
})).await;
|
||||
|
||||
// Extract text from member's document
|
||||
let extract_result = provider.call_tool("extract_text", json!({
|
||||
"document_id": doc_id
|
||||
})).await;
|
||||
|
||||
match extract_result {
|
||||
ToolResult::Success(value) => {
|
||||
let text = value["text"].as_str().unwrap();
|
||||
|
||||
// Extract key points (simplified - would be more sophisticated in real implementation)
|
||||
let lines: Vec<&str> = text.lines().collect();
|
||||
let summary_text = if lines.len() > 10 {
|
||||
format!("Key activities include multiple achievements in their focus areas. Full details available in {}'s individual report.", member)
|
||||
} else {
|
||||
format!("Summary content from {}'s report.", member)
|
||||
};
|
||||
|
||||
provider.call_tool("add_paragraph", json!({
|
||||
"document_id": summary_id,
|
||||
"text": summary_text
|
||||
})).await;
|
||||
},
|
||||
ToolResult::Error(e) => {
|
||||
println!("Warning: Could not extract text from {}'s document: {}", member, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add team totals table
|
||||
provider.call_tool("add_heading", json!({
|
||||
"document_id": summary_id,
|
||||
"text": "Team Totals",
|
||||
"level": 2
|
||||
})).await;
|
||||
|
||||
provider.call_tool("add_table", json!({
|
||||
"document_id": summary_id,
|
||||
"rows": [
|
||||
["Team Member", "Documents Created", "Key Focus"],
|
||||
["Alice", "1", "Design & Research"],
|
||||
["Bob", "1", "Development & Security"],
|
||||
["Charlie", "1", "Operations & Deployment"],
|
||||
["Total", "3", "Full-stack delivery"]
|
||||
]
|
||||
})).await;
|
||||
|
||||
// Convert all documents to PDF for archival
|
||||
let archive_dir = temp_dir.path().join("weekly_archive");
|
||||
fs::create_dir_all(&archive_dir)?;
|
||||
|
||||
for (member, doc_id) in &document_ids {
|
||||
let pdf_path = archive_dir.join(format!("{}_weekly_report.pdf", member.to_lowercase()));
|
||||
provider.call_tool("convert_to_pdf", json!({
|
||||
"document_id": doc_id,
|
||||
"output_path": pdf_path.to_str().unwrap()
|
||||
})).await;
|
||||
|
||||
if pdf_path.exists() {
|
||||
println!("Archived {}'s report to PDF", member);
|
||||
}
|
||||
}
|
||||
|
||||
// Archive summary document
|
||||
let summary_pdf = archive_dir.join("team_summary.pdf");
|
||||
provider.call_tool("convert_to_pdf", json!({
|
||||
"document_id": summary_id,
|
||||
"output_path": summary_pdf.to_str().unwrap()
|
||||
})).await;
|
||||
|
||||
// Verify all PDFs were created
|
||||
let pdf_count = fs::read_dir(&archive_dir)?
|
||||
.filter_map(|entry| entry.ok())
|
||||
.filter(|entry| entry.path().extension().and_then(|s| s.to_str()) == Some("pdf"))
|
||||
.count();
|
||||
|
||||
assert!(pdf_count >= 3, "Should have created at least 3 PDF files");
|
||||
println!("Successfully archived {} PDF documents", pdf_count);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test security-restricted workflow
|
||||
#[tokio::test]
|
||||
async fn test_security_restricted_workflow() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
std::env::set_var("TMPDIR", temp_dir.path());
|
||||
|
||||
// Create a restrictive security configuration
|
||||
let mut whitelist = HashSet::new();
|
||||
whitelist.insert("open_document".to_string());
|
||||
whitelist.insert("extract_text".to_string());
|
||||
whitelist.insert("get_metadata".to_string());
|
||||
whitelist.insert("search_text".to_string());
|
||||
whitelist.insert("get_word_count".to_string());
|
||||
whitelist.insert("list_documents".to_string());
|
||||
whitelist.insert("get_security_info".to_string());
|
||||
|
||||
let security_config = SecurityConfig {
|
||||
readonly_mode: true,
|
||||
sandbox_mode: true,
|
||||
command_whitelist: Some(whitelist),
|
||||
max_document_size: 1024 * 1024, // 1MB
|
||||
max_open_documents: 5,
|
||||
allow_external_tools: false,
|
||||
allow_network: false,
|
||||
};
|
||||
|
||||
let provider = DocxToolsProvider::new_with_security(security_config);
|
||||
|
||||
// Test security info
|
||||
let security_info = provider.call_tool("get_security_info", json!({})).await;
|
||||
match security_info {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
let security = &value["security"];
|
||||
assert_eq!(security["readonly_mode"], true);
|
||||
assert_eq!(security["sandbox_mode"], true);
|
||||
println!("Security configuration: {}", security["summary"].as_str().unwrap());
|
||||
},
|
||||
ToolResult::Error(e) => panic!("Failed to get security info: {}", e),
|
||||
}
|
||||
|
||||
// Test that write operations are blocked
|
||||
let create_result = provider.call_tool("create_document", json!({})).await;
|
||||
match create_result {
|
||||
ToolResult::Success(value) => {
|
||||
// Should fail security check
|
||||
assert!(!value.get("success").unwrap_or(&json!(true)).as_bool().unwrap());
|
||||
},
|
||||
ToolResult::Error(e) => {
|
||||
assert!(e.contains("Security check failed") || e.contains("Command not allowed"));
|
||||
println!("Create document correctly blocked: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Test that add_paragraph is blocked
|
||||
let paragraph_result = provider.call_tool("add_paragraph", json!({
|
||||
"document_id": "test",
|
||||
"text": "This should be blocked"
|
||||
})).await;
|
||||
|
||||
match paragraph_result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(!value.get("success").unwrap_or(&json!(true)).as_bool().unwrap());
|
||||
},
|
||||
ToolResult::Error(e) => {
|
||||
assert!(e.contains("Security check failed") || e.contains("Command not allowed"));
|
||||
println!("Add paragraph correctly blocked: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Create a test document externally (outside security restrictions)
|
||||
let unrestricted_provider = DocxToolsProvider::new();
|
||||
let create_result = unrestricted_provider.call_tool("create_document", json!({})).await;
|
||||
let doc_id = match create_result {
|
||||
ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(),
|
||||
_ => panic!("Failed to create test document"),
|
||||
};
|
||||
|
||||
// Add content with unrestricted provider
|
||||
unrestricted_provider.call_tool("add_heading", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "Security Test Document",
|
||||
"level": 1
|
||||
})).await;
|
||||
|
||||
unrestricted_provider.call_tool("add_paragraph", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "This document is used to test readonly access capabilities in a security-restricted environment."
|
||||
})).await;
|
||||
|
||||
unrestricted_provider.call_tool("add_list", json!({
|
||||
"document_id": doc_id,
|
||||
"items": [
|
||||
"Test text extraction",
|
||||
"Test search functionality",
|
||||
"Test metadata retrieval",
|
||||
"Test word counting"
|
||||
],
|
||||
"ordered": true
|
||||
})).await;
|
||||
|
||||
// Now test readonly operations with restricted provider
|
||||
// These should work because they're in the whitelist
|
||||
|
||||
// Test text extraction
|
||||
let extract_result = provider.call_tool("extract_text", json!({
|
||||
"document_id": doc_id
|
||||
})).await;
|
||||
|
||||
match extract_result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
let text = value["text"].as_str().unwrap();
|
||||
assert!(text.contains("Security Test Document"));
|
||||
assert!(text.contains("Test text extraction"));
|
||||
println!("Text extraction successful: {} characters", text.len());
|
||||
},
|
||||
ToolResult::Error(e) => panic!("Text extraction should work: {}", e),
|
||||
}
|
||||
|
||||
// Test search functionality
|
||||
let search_result = provider.call_tool("search_text", json!({
|
||||
"document_id": doc_id,
|
||||
"search_term": "security",
|
||||
"case_sensitive": false
|
||||
})).await;
|
||||
|
||||
match search_result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
assert!(value["total_matches"].as_u64().unwrap() > 0);
|
||||
println!("Search successful: found {} matches", value["total_matches"]);
|
||||
},
|
||||
ToolResult::Error(e) => panic!("Search should work: {}", e),
|
||||
}
|
||||
|
||||
// Test metadata retrieval
|
||||
let metadata_result = provider.call_tool("get_metadata", json!({
|
||||
"document_id": doc_id
|
||||
})).await;
|
||||
|
||||
match metadata_result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
let metadata = &value["metadata"];
|
||||
assert_eq!(metadata["id"], doc_id);
|
||||
println!("Metadata retrieval successful");
|
||||
},
|
||||
ToolResult::Error(e) => panic!("Metadata retrieval should work: {}", e),
|
||||
}
|
||||
|
||||
// Test word counting
|
||||
let word_count_result = provider.call_tool("get_word_count", json!({
|
||||
"document_id": doc_id
|
||||
})).await;
|
||||
|
||||
match word_count_result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
let stats = &value["statistics"];
|
||||
assert!(stats["words"].as_u64().unwrap() > 0);
|
||||
println!("Word count successful: {} words", stats["words"]);
|
||||
},
|
||||
ToolResult::Error(e) => panic!("Word count should work: {}", e),
|
||||
}
|
||||
|
||||
// Test document listing
|
||||
let list_result = provider.call_tool("list_documents", json!({})).await;
|
||||
match list_result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
println!("Document listing successful");
|
||||
},
|
||||
ToolResult::Error(e) => panic!("Document listing should work: {}", e),
|
||||
}
|
||||
|
||||
// Test that conversion operations are blocked (not in whitelist)
|
||||
let pdf_result = provider.call_tool("convert_to_pdf", json!({
|
||||
"document_id": doc_id,
|
||||
"output_path": "/tmp/test.pdf"
|
||||
})).await;
|
||||
|
||||
match pdf_result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(!value.get("success").unwrap_or(&json!(true)).as_bool().unwrap());
|
||||
},
|
||||
ToolResult::Error(e) => {
|
||||
assert!(e.contains("Security check failed") || e.contains("Command not allowed"));
|
||||
println!("PDF conversion correctly blocked: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
println!("Security-restricted workflow test completed successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test error recovery workflow
|
||||
#[tokio::test]
|
||||
async fn test_error_recovery_workflow() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
std::env::set_var("TMPDIR", temp_dir.path());
|
||||
|
||||
let provider = DocxToolsProvider::new();
|
||||
|
||||
// Test recovery from invalid document ID
|
||||
let invalid_ops = vec![
|
||||
("extract_text", json!({"document_id": "nonexistent-123"})),
|
||||
("add_paragraph", json!({"document_id": "fake-456", "text": "test"})),
|
||||
("get_metadata", json!({"document_id": "invalid-789"})),
|
||||
("get_word_count", json!({"document_id": "missing-000"})),
|
||||
];
|
||||
|
||||
for (operation, args) in invalid_ops {
|
||||
let result = provider.call_tool(operation, args).await;
|
||||
|
||||
match result {
|
||||
ToolResult::Success(value) => {
|
||||
// Should indicate failure
|
||||
assert!(!value.get("success").unwrap_or(&json!(true)).as_bool().unwrap());
|
||||
assert!(value.get("error").is_some());
|
||||
println!("{} correctly handled invalid document ID", operation);
|
||||
},
|
||||
ToolResult::Error(e) => {
|
||||
assert!(e.contains("Document not found") || e.contains("not found"));
|
||||
println!("{} correctly returned error for invalid document: {}", operation, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test recovery from invalid arguments
|
||||
let invalid_arg_ops = vec![
|
||||
("add_heading", json!({"document_id": "test", "level": 10})), // Invalid level
|
||||
("add_paragraph", json!({"text": "missing document_id"})), // Missing required field
|
||||
("add_table", json!({"document_id": "test", "rows": "not_an_array"})), // Wrong type
|
||||
];
|
||||
|
||||
for (operation, args) in invalid_arg_ops {
|
||||
let result = provider.call_tool(operation, args).await;
|
||||
|
||||
match result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(!value.get("success").unwrap_or(&json!(true)).as_bool().unwrap());
|
||||
println!("{} handled invalid arguments gracefully", operation);
|
||||
},
|
||||
ToolResult::Error(e) => {
|
||||
println!("{} returned error for invalid arguments: {}", operation, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test successful operation after errors
|
||||
let create_result = provider.call_tool("create_document", json!({})).await;
|
||||
let doc_id = match create_result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
value["document_id"].as_str().unwrap().to_string()
|
||||
},
|
||||
ToolResult::Error(e) => panic!("Should be able to create document after errors: {}", e),
|
||||
};
|
||||
|
||||
// Verify normal operations work after handling errors
|
||||
let paragraph_result = provider.call_tool("add_paragraph", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "This should work after error recovery"
|
||||
})).await;
|
||||
|
||||
match paragraph_result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
println!("Normal operations work after error handling");
|
||||
},
|
||||
ToolResult::Error(e) => panic!("Normal operation should work after errors: {}", e),
|
||||
}
|
||||
|
||||
// Test that the document has the expected content
|
||||
let extract_result = provider.call_tool("extract_text", json!({
|
||||
"document_id": doc_id
|
||||
})).await;
|
||||
|
||||
match extract_result {
|
||||
ToolResult::Success(value) => {
|
||||
let text = value["text"].as_str().unwrap();
|
||||
assert!(text.contains("This should work after error recovery"));
|
||||
println!("Error recovery workflow completed successfully");
|
||||
},
|
||||
ToolResult::Error(e) => panic!("Text extraction failed: {}", e),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Vendored
+457
@@ -0,0 +1,457 @@
|
||||
//! Test fixtures and helper data for the docx-mcp test suite
|
||||
|
||||
use anyhow::Result;
|
||||
use docx_mcp::docx_handler::{DocxHandler, DocxStyle, TableData};
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
use tempfile::TempDir;
|
||||
|
||||
pub mod sample_documents;
|
||||
pub mod test_data;
|
||||
|
||||
/// Common test fixture for creating a handler with a temporary directory
|
||||
pub fn create_test_handler() -> (DocxHandler, TempDir) {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let handler = DocxHandler::new_with_temp_dir(temp_dir.path()).unwrap();
|
||||
(handler, temp_dir)
|
||||
}
|
||||
|
||||
/// Create a handler with a document containing basic content
|
||||
pub fn create_handler_with_document() -> (DocxHandler, String, TempDir) {
|
||||
let (mut handler, temp_dir) = create_test_handler();
|
||||
let doc_id = handler.create_document().unwrap();
|
||||
(handler, doc_id, temp_dir)
|
||||
}
|
||||
|
||||
/// Standard document styles for testing
|
||||
pub struct TestStyles;
|
||||
|
||||
impl TestStyles {
|
||||
pub fn basic() -> DocxStyle {
|
||||
DocxStyle {
|
||||
font_family: Some("Calibri".to_string()),
|
||||
font_size: Some(11),
|
||||
bold: Some(false),
|
||||
italic: Some(false),
|
||||
underline: Some(false),
|
||||
color: Some("#000000".to_string()),
|
||||
alignment: Some("left".to_string()),
|
||||
line_spacing: Some(1.15),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn heading() -> DocxStyle {
|
||||
DocxStyle {
|
||||
font_family: Some("Calibri".to_string()),
|
||||
font_size: Some(16),
|
||||
bold: Some(true),
|
||||
italic: Some(false),
|
||||
underline: Some(false),
|
||||
color: Some("#1f4e79".to_string()),
|
||||
alignment: Some("left".to_string()),
|
||||
line_spacing: Some(1.15),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emphasis() -> DocxStyle {
|
||||
DocxStyle {
|
||||
font_family: Some("Calibri".to_string()),
|
||||
font_size: Some(11),
|
||||
bold: Some(true),
|
||||
italic: Some(true),
|
||||
underline: Some(false),
|
||||
color: Some("#c55a11".to_string()),
|
||||
alignment: Some("left".to_string()),
|
||||
line_spacing: Some(1.15),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn centered() -> DocxStyle {
|
||||
DocxStyle {
|
||||
font_family: Some("Calibri".to_string()),
|
||||
font_size: Some(11),
|
||||
bold: Some(false),
|
||||
italic: Some(false),
|
||||
underline: Some(false),
|
||||
color: Some("#000000".to_string()),
|
||||
alignment: Some("center".to_string()),
|
||||
line_spacing: Some(1.15),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Standard table data for testing
|
||||
pub struct TestTables;
|
||||
|
||||
impl TestTables {
|
||||
pub fn simple_2x2() -> TableData {
|
||||
TableData {
|
||||
rows: vec![
|
||||
vec!["Row 1 Col 1".to_string(), "Row 1 Col 2".to_string()],
|
||||
vec!["Row 2 Col 1".to_string(), "Row 2 Col 2".to_string()],
|
||||
],
|
||||
headers: None,
|
||||
border_style: Some("single".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_headers() -> TableData {
|
||||
TableData {
|
||||
rows: vec![
|
||||
vec!["Name".to_string(), "Age".to_string(), "City".to_string()],
|
||||
vec!["John".to_string(), "30".to_string(), "New York".to_string()],
|
||||
vec!["Jane".to_string(), "25".to_string(), "Los Angeles".to_string()],
|
||||
vec!["Bob".to_string(), "35".to_string(), "Chicago".to_string()],
|
||||
],
|
||||
headers: Some(vec!["Name".to_string(), "Age".to_string(), "City".to_string()]),
|
||||
border_style: Some("single".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn financial_data() -> TableData {
|
||||
TableData {
|
||||
rows: vec![
|
||||
vec!["Quarter".to_string(), "Revenue".to_string(), "Profit".to_string(), "Growth".to_string()],
|
||||
vec!["Q1 2024".to_string(), "$1.2M".to_string(), "$240K".to_string(), "15%".to_string()],
|
||||
vec!["Q2 2024".to_string(), "$1.4M".to_string(), "$290K".to_string(), "18%".to_string()],
|
||||
vec!["Q3 2024".to_string(), "$1.6M".to_string(), "$340K".to_string(), "22%".to_string()],
|
||||
vec!["Q4 2024".to_string(), "$1.8M".to_string(), "$380K".to_string(), "25%".to_string()],
|
||||
],
|
||||
headers: Some(vec!["Quarter".to_string(), "Revenue".to_string(), "Profit".to_string(), "Growth".to_string()]),
|
||||
border_style: Some("single".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn large_table(rows: usize, cols: usize) -> TableData {
|
||||
let mut table_rows = Vec::new();
|
||||
|
||||
// Header row
|
||||
let header_row: Vec<String> = (0..cols)
|
||||
.map(|i| format!("Column {}", i + 1))
|
||||
.collect();
|
||||
table_rows.push(header_row.clone());
|
||||
|
||||
// Data rows
|
||||
for row in 0..rows {
|
||||
let data_row: Vec<String> = (0..cols)
|
||||
.map(|col| format!("R{}C{}", row + 1, col + 1))
|
||||
.collect();
|
||||
table_rows.push(data_row);
|
||||
}
|
||||
|
||||
TableData {
|
||||
rows: table_rows,
|
||||
headers: Some(header_row),
|
||||
border_style: Some("single".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Standard list data for testing
|
||||
pub struct TestLists;
|
||||
|
||||
impl TestLists {
|
||||
pub fn simple_bullets() -> Vec<String> {
|
||||
vec![
|
||||
"First bullet point".to_string(),
|
||||
"Second bullet point".to_string(),
|
||||
"Third bullet point".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn numbered_steps() -> Vec<String> {
|
||||
vec![
|
||||
"Open the application".to_string(),
|
||||
"Navigate to the settings menu".to_string(),
|
||||
"Select the desired configuration".to_string(),
|
||||
"Save your changes".to_string(),
|
||||
"Restart the application".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn features_list() -> Vec<String> {
|
||||
vec![
|
||||
"Advanced document editing capabilities".to_string(),
|
||||
"Real-time collaboration tools".to_string(),
|
||||
"Cloud synchronization".to_string(),
|
||||
"Version control and history tracking".to_string(),
|
||||
"Export to multiple formats (PDF, HTML, Markdown)".to_string(),
|
||||
"Template library with professional designs".to_string(),
|
||||
"Advanced formatting and styling options".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn technical_requirements() -> Vec<String> {
|
||||
vec![
|
||||
"Rust 1.70 or higher".to_string(),
|
||||
"Memory: 2GB RAM minimum, 4GB recommended".to_string(),
|
||||
"Storage: 500MB available space".to_string(),
|
||||
"Network: Internet connection for cloud features".to_string(),
|
||||
"OS: Windows 10, macOS 10.15, or Linux (Ubuntu 20.04+)".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn large_list(item_count: usize) -> Vec<String> {
|
||||
(1..=item_count)
|
||||
.map(|i| format!("List item number {} with descriptive content", i))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sample text content for testing
|
||||
pub struct TestContent;
|
||||
|
||||
impl TestContent {
|
||||
pub fn lorem_ipsum() -> &'static str {
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
|
||||
}
|
||||
|
||||
pub fn technical_paragraph() -> &'static str {
|
||||
"This application leverages cutting-edge Rust technology to provide high-performance document processing capabilities. The architecture is built on modern asynchronous programming patterns, ensuring efficient resource utilization and scalability. Key features include memory-safe operations, zero-cost abstractions, and excellent concurrent processing performance."
|
||||
}
|
||||
|
||||
pub fn business_paragraph() -> &'static str {
|
||||
"Our comprehensive business solution addresses the evolving needs of modern enterprises through innovative technology and streamlined workflows. With a focus on productivity enhancement and cost reduction, this platform delivers measurable value across multiple departments and use cases. The solution integrates seamlessly with existing infrastructure while providing robust security and compliance features."
|
||||
}
|
||||
|
||||
pub fn multilingual_content() -> Vec<(&'static str, &'static str)> {
|
||||
vec![
|
||||
("English", "The quick brown fox jumps over the lazy dog."),
|
||||
("Spanish", "El zorro marrón rápido salta sobre el perro perezoso."),
|
||||
("French", "Le renard brun rapide saute par-dessus le chien paresseux."),
|
||||
("German", "Der schnelle braune Fuchs springt über den faulen Hund."),
|
||||
("Italian", "La volpe marrone veloce salta sopra il cane pigro."),
|
||||
("Portuguese", "A raposa marrom rápida pula sobre o cão preguiçoso."),
|
||||
("Japanese", "素早い茶色のキツネは怠惰な犬を飛び越える。"),
|
||||
("Chinese", "敏捷的棕色狐狸跳过懒狗。"),
|
||||
("Korean", "빠른 갈색 여우가 게으른 개를 뛰어넘는다."),
|
||||
("Russian", "Быстрая коричневая лиса прыгает через ленивую собаку."),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn special_characters() -> &'static str {
|
||||
"Special characters test: àáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚě"
|
||||
}
|
||||
|
||||
pub fn symbols_and_math() -> &'static str {
|
||||
"Mathematical symbols: ∑ ∏ ∫ √ ≤ ≥ ≠ ± ∞ ∂ ∇ Ω α β γ δ ε θ λ μ π σ φ ψ ω"
|
||||
}
|
||||
|
||||
pub fn long_paragraph(sentence_count: usize) -> String {
|
||||
let sentences = vec![
|
||||
"This is a comprehensive test of document processing capabilities.",
|
||||
"The system handles various types of content efficiently and accurately.",
|
||||
"Performance optimization ensures smooth operation even with large documents.",
|
||||
"Advanced formatting features provide professional document appearance.",
|
||||
"Error handling mechanisms maintain system stability under all conditions.",
|
||||
"Security features protect sensitive information throughout the process.",
|
||||
"Integration capabilities allow seamless workflow with existing systems.",
|
||||
"User-friendly interfaces make complex operations simple and intuitive.",
|
||||
"Scalable architecture supports growing business requirements.",
|
||||
"Continuous improvements ensure the solution remains cutting-edge.",
|
||||
];
|
||||
|
||||
let mut result = String::new();
|
||||
for i in 0..sentence_count {
|
||||
let sentence = sentences[i % sentences.len()];
|
||||
result.push_str(sentence);
|
||||
if i < sentence_count - 1 {
|
||||
result.push(' ');
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// MCP tool call arguments for testing
|
||||
pub struct TestMcpArgs;
|
||||
|
||||
impl TestMcpArgs {
|
||||
pub fn create_document() -> Value {
|
||||
json!({})
|
||||
}
|
||||
|
||||
pub fn add_paragraph(doc_id: &str, text: &str, style: Option<DocxStyle>) -> Value {
|
||||
let mut args = json!({
|
||||
"document_id": doc_id,
|
||||
"text": text
|
||||
});
|
||||
|
||||
if let Some(s) = style {
|
||||
args["style"] = json!({
|
||||
"font_family": s.font_family,
|
||||
"font_size": s.font_size,
|
||||
"bold": s.bold,
|
||||
"italic": s.italic,
|
||||
"underline": s.underline,
|
||||
"color": s.color,
|
||||
"alignment": s.alignment,
|
||||
"line_spacing": s.line_spacing
|
||||
});
|
||||
}
|
||||
|
||||
args
|
||||
}
|
||||
|
||||
pub fn add_heading(doc_id: &str, text: &str, level: usize) -> Value {
|
||||
json!({
|
||||
"document_id": doc_id,
|
||||
"text": text,
|
||||
"level": level
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_table(doc_id: &str, table_data: &TableData) -> Value {
|
||||
json!({
|
||||
"document_id": doc_id,
|
||||
"rows": table_data.rows
|
||||
})
|
||||
}
|
||||
|
||||
pub fn add_list(doc_id: &str, items: &[String], ordered: bool) -> Value {
|
||||
json!({
|
||||
"document_id": doc_id,
|
||||
"items": items,
|
||||
"ordered": ordered
|
||||
})
|
||||
}
|
||||
|
||||
pub fn extract_text(doc_id: &str) -> Value {
|
||||
json!({
|
||||
"document_id": doc_id
|
||||
})
|
||||
}
|
||||
|
||||
pub fn search_text(doc_id: &str, search_term: &str, case_sensitive: bool) -> Value {
|
||||
json!({
|
||||
"document_id": doc_id,
|
||||
"search_term": search_term,
|
||||
"case_sensitive": case_sensitive
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_metadata(doc_id: &str) -> Value {
|
||||
json!({
|
||||
"document_id": doc_id
|
||||
})
|
||||
}
|
||||
|
||||
pub fn convert_to_pdf(doc_id: &str, output_path: &str) -> Value {
|
||||
json!({
|
||||
"document_id": doc_id,
|
||||
"output_path": output_path
|
||||
})
|
||||
}
|
||||
|
||||
pub fn save_document(doc_id: &str, output_path: &str) -> Value {
|
||||
json!({
|
||||
"document_id": doc_id,
|
||||
"output_path": output_path
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Performance test data generators
|
||||
pub struct PerformanceData;
|
||||
|
||||
impl PerformanceData {
|
||||
pub fn create_large_document(handler: &mut DocxHandler, paragraph_count: usize) -> Result<String> {
|
||||
let doc_id = handler.create_document()?;
|
||||
|
||||
handler.add_heading(&doc_id, "Performance Test Document", 1)?;
|
||||
|
||||
for i in 0..paragraph_count {
|
||||
if i % 50 == 0 && i > 0 {
|
||||
handler.add_heading(&doc_id, &format!("Section {}", i / 50), 2)?;
|
||||
}
|
||||
|
||||
let content = format!(
|
||||
"This is paragraph {} in our performance test document. It contains substantial text content to simulate real-world usage patterns and test system performance under realistic load conditions. The paragraph includes various punctuation marks, numbers like {}, and other elements that affect processing performance.",
|
||||
i + 1, (i + 1) * 7
|
||||
);
|
||||
|
||||
handler.add_paragraph(&doc_id, &content, None)?;
|
||||
|
||||
// Add tables periodically
|
||||
if i % 100 == 99 {
|
||||
let table_data = TestTables::simple_2x2();
|
||||
handler.add_table(&doc_id, table_data)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(doc_id)
|
||||
}
|
||||
|
||||
pub fn create_complex_document(handler: &mut DocxHandler) -> Result<String> {
|
||||
let doc_id = handler.create_document()?;
|
||||
|
||||
// Add comprehensive content with all features
|
||||
handler.add_heading(&doc_id, "Complex Document Test", 1)?;
|
||||
|
||||
handler.set_header(&doc_id, "Complex Document Header")?;
|
||||
handler.set_footer(&doc_id, "Complex Document Footer")?;
|
||||
|
||||
handler.add_paragraph(&doc_id, TestContent::business_paragraph(), Some(TestStyles::basic()))?;
|
||||
|
||||
handler.add_heading(&doc_id, "Technical Details", 2)?;
|
||||
handler.add_paragraph(&doc_id, TestContent::technical_paragraph(), None)?;
|
||||
|
||||
let features_list = TestLists::features_list();
|
||||
handler.add_list(&doc_id, features_list, false)?;
|
||||
|
||||
handler.add_heading(&doc_id, "Financial Overview", 2)?;
|
||||
let financial_table = TestTables::financial_data();
|
||||
handler.add_table(&doc_id, financial_table)?;
|
||||
|
||||
handler.add_page_break(&doc_id)?;
|
||||
|
||||
handler.add_heading(&doc_id, "Multilingual Content", 2)?;
|
||||
for (language, text) in TestContent::multilingual_content() {
|
||||
handler.add_paragraph(&doc_id, &format!("{}: {}", language, text), None)?;
|
||||
}
|
||||
|
||||
handler.add_heading(&doc_id, "Special Characters", 2)?;
|
||||
handler.add_paragraph(&doc_id, TestContent::special_characters(), None)?;
|
||||
handler.add_paragraph(&doc_id, TestContent::symbols_and_math(), None)?;
|
||||
|
||||
Ok(doc_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Error testing utilities
|
||||
pub struct ErrorTestCases;
|
||||
|
||||
impl ErrorTestCases {
|
||||
pub fn invalid_document_ids() -> Vec<&'static str> {
|
||||
vec![
|
||||
"nonexistent-123",
|
||||
"fake-document-id",
|
||||
"invalid-uuid",
|
||||
"",
|
||||
" ",
|
||||
"null",
|
||||
"undefined",
|
||||
]
|
||||
}
|
||||
|
||||
pub fn invalid_mcp_calls() -> Vec<(&'static str, Value)> {
|
||||
vec![
|
||||
("add_paragraph", json!({"text": "missing document_id"})),
|
||||
("add_heading", json!({"document_id": "test", "level": 10})),
|
||||
("add_table", json!({"document_id": "test", "rows": "not_an_array"})),
|
||||
("add_list", json!({"document_id": "test", "items": 123})),
|
||||
("search_text", json!({"document_id": "test"})), // Missing search_term
|
||||
("convert_to_pdf", json!({"document_id": "test"})), // Missing output_path
|
||||
]
|
||||
}
|
||||
|
||||
pub fn security_blocked_operations() -> Vec<(&'static str, Value)> {
|
||||
vec![
|
||||
("create_document", json!({})),
|
||||
("add_paragraph", json!({"document_id": "test", "text": "blocked"})),
|
||||
("save_document", json!({"document_id": "test", "output_path": "/tmp/test.docx"})),
|
||||
("convert_to_pdf", json!({"document_id": "test", "output_path": "/tmp/test.pdf"})),
|
||||
("find_and_replace", json!({"document_id": "test", "find_text": "a", "replace_text": "b"})),
|
||||
]
|
||||
}
|
||||
}
|
||||
Vendored
+509
@@ -0,0 +1,509 @@
|
||||
//! Sample document templates and content for testing
|
||||
|
||||
use anyhow::Result;
|
||||
use docx_mcp::docx_handler::{DocxHandler, DocxStyle, TableData};
|
||||
use super::{TestStyles, TestTables, TestLists, TestContent};
|
||||
|
||||
/// Creates a business letter document for testing
|
||||
pub fn create_business_letter(handler: &mut DocxHandler) -> Result<String> {
|
||||
let doc_id = handler.create_document()?;
|
||||
|
||||
// Header
|
||||
handler.set_header(&doc_id, "ACME Corporation | 123 Business St, City, State 12345")?;
|
||||
|
||||
// Date
|
||||
handler.add_paragraph(&doc_id, "December 15, 2024", Some(TestStyles::basic()))?;
|
||||
handler.add_paragraph(&doc_id, "", None)?; // Empty line
|
||||
|
||||
// Recipient
|
||||
handler.add_paragraph(&doc_id, "Ms. Jane Smith", Some(TestStyles::basic()))?;
|
||||
handler.add_paragraph(&doc_id, "Director of Operations", Some(TestStyles::basic()))?;
|
||||
handler.add_paragraph(&doc_id, "XYZ Company", Some(TestStyles::basic()))?;
|
||||
handler.add_paragraph(&doc_id, "456 Corporate Ave", Some(TestStyles::basic()))?;
|
||||
handler.add_paragraph(&doc_id, "Business City, State 67890", Some(TestStyles::basic()))?;
|
||||
handler.add_paragraph(&doc_id, "", None)?; // Empty line
|
||||
|
||||
// Subject
|
||||
handler.add_paragraph(&doc_id, "RE: Partnership Proposal", Some(TestStyles::emphasis()))?;
|
||||
handler.add_paragraph(&doc_id, "", None)?; // Empty line
|
||||
|
||||
// Salutation
|
||||
handler.add_paragraph(&doc_id, "Dear Ms. Smith,", Some(TestStyles::basic()))?;
|
||||
handler.add_paragraph(&doc_id, "", None)?; // Empty line
|
||||
|
||||
// Body paragraphs
|
||||
handler.add_paragraph(&doc_id,
|
||||
"I am writing to propose a strategic partnership between ACME Corporation and XYZ Company that would benefit both organizations significantly. Our companies share similar values and complementary strengths that could create substantial value for our respective customers.",
|
||||
Some(TestStyles::basic()))?;
|
||||
|
||||
handler.add_paragraph(&doc_id,
|
||||
"ACME Corporation has been a leader in technology solutions for over 15 years, with a strong track record of innovation and customer satisfaction. We believe that combining our technical expertise with your operational excellence would create a powerful synergy in the marketplace.",
|
||||
Some(TestStyles::basic()))?;
|
||||
|
||||
handler.add_paragraph(&doc_id,
|
||||
"The proposed partnership would include joint product development, shared marketing initiatives, and coordinated customer support efforts. We estimate this collaboration could increase revenue for both companies by 25% within the first year.",
|
||||
Some(TestStyles::basic()))?;
|
||||
|
||||
handler.add_paragraph(&doc_id,
|
||||
"I would welcome the opportunity to discuss this proposal in more detail at your convenience. Please let me know when you might be available for a meeting or conference call.",
|
||||
Some(TestStyles::basic()))?;
|
||||
|
||||
handler.add_paragraph(&doc_id, "", None)?; // Empty line
|
||||
|
||||
// Closing
|
||||
handler.add_paragraph(&doc_id, "Sincerely,", Some(TestStyles::basic()))?;
|
||||
handler.add_paragraph(&doc_id, "", None)?; // Space for signature
|
||||
handler.add_paragraph(&doc_id, "", None)?; // Space for signature
|
||||
handler.add_paragraph(&doc_id, "John Doe", Some(TestStyles::basic()))?;
|
||||
handler.add_paragraph(&doc_id, "Chief Executive Officer", Some(TestStyles::basic()))?;
|
||||
handler.add_paragraph(&doc_id, "ACME Corporation", Some(TestStyles::basic()))?;
|
||||
|
||||
// Footer
|
||||
handler.set_footer(&doc_id, "ACME Corporation - Confidential and Proprietary")?;
|
||||
|
||||
Ok(doc_id)
|
||||
}
|
||||
|
||||
/// Creates a technical report document for testing
|
||||
pub fn create_technical_report(handler: &mut DocxHandler) -> Result<String> {
|
||||
let doc_id = handler.create_document()?;
|
||||
|
||||
// Title page
|
||||
handler.add_paragraph(&doc_id, "", None)?; // Empty line for spacing
|
||||
handler.add_paragraph(&doc_id, "", None)?;
|
||||
handler.add_paragraph(&doc_id, "", None)?;
|
||||
|
||||
handler.add_heading(&doc_id, "System Performance Analysis Report", 1)?;
|
||||
handler.add_paragraph(&doc_id, "", None)?;
|
||||
handler.add_paragraph(&doc_id, "Quarterly Assessment - Q4 2024", Some(TestStyles::centered()))?;
|
||||
handler.add_paragraph(&doc_id, "", None)?;
|
||||
handler.add_paragraph(&doc_id, "Prepared by: Technical Team", Some(TestStyles::centered()))?;
|
||||
handler.add_paragraph(&doc_id, "Date: December 15, 2024", Some(TestStyles::centered()))?;
|
||||
|
||||
handler.add_page_break(&doc_id)?;
|
||||
|
||||
// Executive Summary
|
||||
handler.add_heading(&doc_id, "Executive Summary", 1)?;
|
||||
handler.add_paragraph(&doc_id,
|
||||
"This report provides a comprehensive analysis of system performance metrics for Q4 2024. Key findings include significant improvements in response times, enhanced security measures, and successful implementation of new monitoring capabilities.",
|
||||
Some(TestStyles::basic()))?;
|
||||
|
||||
let summary_points = vec![
|
||||
"Average response time improved by 35%".to_string(),
|
||||
"System uptime achieved 99.97%".to_string(),
|
||||
"Security incidents reduced by 60%".to_string(),
|
||||
"User satisfaction increased to 94%".to_string(),
|
||||
];
|
||||
handler.add_list(&doc_id, summary_points, false)?;
|
||||
|
||||
// Performance Metrics
|
||||
handler.add_heading(&doc_id, "Performance Metrics", 1)?;
|
||||
|
||||
handler.add_heading(&doc_id, "Response Time Analysis", 2)?;
|
||||
handler.add_paragraph(&doc_id,
|
||||
"Response time measurements were collected continuously throughout Q4 2024. The data shows consistent improvement across all service endpoints.",
|
||||
Some(TestStyles::basic()))?;
|
||||
|
||||
let response_time_data = TableData {
|
||||
rows: vec![
|
||||
vec!["Service".to_string(), "Q3 2024 (ms)".to_string(), "Q4 2024 (ms)".to_string(), "Improvement".to_string()],
|
||||
vec!["Authentication".to_string(), "245".to_string(), "158".to_string(), "35.5%".to_string()],
|
||||
vec!["Database Query".to_string(), "892".to_string(), "623".to_string(), "30.2%".to_string()],
|
||||
vec!["File Processing".to_string(), "1,240".to_string(), "789".to_string(), "36.4%".to_string()],
|
||||
vec!["Report Generation".to_string(), "3,450".to_string(), "2,180".to_string(), "36.8%".to_string()],
|
||||
],
|
||||
headers: Some(vec!["Service".to_string(), "Q3 2024 (ms)".to_string(), "Q4 2024 (ms)".to_string(), "Improvement".to_string()]),
|
||||
border_style: Some("single".to_string()),
|
||||
};
|
||||
handler.add_table(&doc_id, response_time_data)?;
|
||||
|
||||
handler.add_heading(&doc_id, "System Reliability", 2)?;
|
||||
handler.add_paragraph(&doc_id,
|
||||
"System reliability metrics demonstrate exceptional stability and availability throughout the quarter.",
|
||||
Some(TestStyles::basic()))?;
|
||||
|
||||
let reliability_data = TableData {
|
||||
rows: vec![
|
||||
vec!["Metric".to_string(), "Target".to_string(), "Actual".to_string(), "Status".to_string()],
|
||||
vec!["Uptime".to_string(), "99.9%".to_string(), "99.97%".to_string(), "✓ Exceeded".to_string()],
|
||||
vec!["MTBF (hours)".to_string(), "720".to_string(), "892".to_string(), "✓ Exceeded".to_string()],
|
||||
vec!["Recovery Time (min)".to_string(), "15".to_string(), "8.5".to_string(), "✓ Exceeded".to_string()],
|
||||
],
|
||||
headers: Some(vec!["Metric".to_string(), "Target".to_string(), "Actual".to_string(), "Status".to_string()]),
|
||||
border_style: Some("single".to_string()),
|
||||
};
|
||||
handler.add_table(&doc_id, reliability_data)?;
|
||||
|
||||
// Security Analysis
|
||||
handler.add_heading(&doc_id, "Security Analysis", 1)?;
|
||||
handler.add_paragraph(&doc_id,
|
||||
"Security monitoring and incident response capabilities were significantly enhanced during Q4 2024.",
|
||||
Some(TestStyles::basic()))?;
|
||||
|
||||
let security_improvements = vec![
|
||||
"Implemented advanced threat detection algorithms".to_string(),
|
||||
"Enhanced encryption protocols for data transmission".to_string(),
|
||||
"Deployed automated incident response systems".to_string(),
|
||||
"Conducted comprehensive security audits".to_string(),
|
||||
"Updated access control mechanisms".to_string(),
|
||||
];
|
||||
handler.add_list(&doc_id, security_improvements, true)?;
|
||||
|
||||
// Recommendations
|
||||
handler.add_heading(&doc_id, "Recommendations", 1)?;
|
||||
handler.add_paragraph(&doc_id,
|
||||
"Based on the analysis conducted, the following recommendations are proposed for Q1 2025:",
|
||||
Some(TestStyles::basic()))?;
|
||||
|
||||
let recommendations = vec![
|
||||
"Continue performance optimization initiatives".to_string(),
|
||||
"Expand monitoring coverage to include new services".to_string(),
|
||||
"Implement predictive analytics for proactive maintenance".to_string(),
|
||||
"Enhance disaster recovery procedures".to_string(),
|
||||
"Invest in additional security training for staff".to_string(),
|
||||
];
|
||||
handler.add_list(&doc_id, recommendations, true)?;
|
||||
|
||||
// Footer
|
||||
handler.set_footer(&doc_id, "Technical Report Q4 2024 - Confidential")?;
|
||||
|
||||
Ok(doc_id)
|
||||
}
|
||||
|
||||
/// Creates a meeting minutes document for testing
|
||||
pub fn create_meeting_minutes(handler: &mut DocxHandler) -> Result<String> {
|
||||
let doc_id = handler.create_document()?;
|
||||
|
||||
// Header
|
||||
handler.add_heading(&doc_id, "Project Steering Committee Meeting Minutes", 1)?;
|
||||
handler.add_paragraph(&doc_id, "", None)?;
|
||||
|
||||
// Meeting details
|
||||
let meeting_details = TableData {
|
||||
rows: vec![
|
||||
vec!["Date:".to_string(), "December 15, 2024".to_string()],
|
||||
vec!["Time:".to_string(), "2:00 PM - 3:30 PM PST".to_string()],
|
||||
vec!["Location:".to_string(), "Conference Room A / Virtual".to_string()],
|
||||
vec!["Chair:".to_string(), "Sarah Johnson".to_string()],
|
||||
vec!["Secretary:".to_string(), "Mike Chen".to_string()],
|
||||
],
|
||||
headers: None,
|
||||
border_style: Some("single".to_string()),
|
||||
};
|
||||
handler.add_table(&doc_id, meeting_details)?;
|
||||
|
||||
// Attendees
|
||||
handler.add_heading(&doc_id, "Attendees", 2)?;
|
||||
let attendees = vec![
|
||||
"Sarah Johnson (Chair) - Project Director".to_string(),
|
||||
"Mike Chen (Secretary) - Technical Lead".to_string(),
|
||||
"Lisa Wang - Product Manager".to_string(),
|
||||
"David Rodriguez - Engineering Manager".to_string(),
|
||||
"Jennifer Kim - QA Manager".to_string(),
|
||||
"Alex Thompson - DevOps Lead".to_string(),
|
||||
];
|
||||
handler.add_list(&doc_id, attendees, false)?;
|
||||
|
||||
// Agenda Items
|
||||
handler.add_heading(&doc_id, "Agenda Items Discussed", 2)?;
|
||||
|
||||
handler.add_heading(&doc_id, "1. Project Status Update", 3)?;
|
||||
handler.add_paragraph(&doc_id,
|
||||
"Mike Chen presented the current project status, highlighting that development is 85% complete and on schedule for the January 31st deadline.",
|
||||
Some(TestStyles::basic()))?;
|
||||
|
||||
let status_highlights = vec![
|
||||
"Core functionality implementation: 100% complete".to_string(),
|
||||
"User interface development: 90% complete".to_string(),
|
||||
"Testing and QA: 70% complete".to_string(),
|
||||
"Documentation: 60% complete".to_string(),
|
||||
];
|
||||
handler.add_list(&doc_id, status_highlights, false)?;
|
||||
|
||||
handler.add_heading(&doc_id, "2. Budget Review", 3)?;
|
||||
handler.add_paragraph(&doc_id,
|
||||
"Lisa Wang reported that the project is currently 5% under budget with strong cost controls in place.",
|
||||
Some(TestStyles::basic()))?;
|
||||
|
||||
let budget_data = TableData {
|
||||
rows: vec![
|
||||
vec!["Category".to_string(), "Budgeted".to_string(), "Actual".to_string(), "Remaining".to_string()],
|
||||
vec!["Development".to_string(), "$180,000".to_string(), "$168,000".to_string(), "$12,000".to_string()],
|
||||
vec!["Testing".to_string(), "$45,000".to_string(), "$38,000".to_string(), "$7,000".to_string()],
|
||||
vec!["Infrastructure".to_string(), "$30,000".to_string(), "$28,000".to_string(), "$2,000".to_string()],
|
||||
vec!["Total".to_string(), "$255,000".to_string(), "$234,000".to_string(), "$21,000".to_string()],
|
||||
],
|
||||
headers: Some(vec!["Category".to_string(), "Budgeted".to_string(), "Actual".to_string(), "Remaining".to_string()]),
|
||||
border_style: Some("single".to_string()),
|
||||
};
|
||||
handler.add_table(&doc_id, budget_data)?;
|
||||
|
||||
handler.add_heading(&doc_id, "3. Risk Assessment", 3)?;
|
||||
handler.add_paragraph(&doc_id,
|
||||
"David Rodriguez presented the updated risk register with mitigation strategies for identified risks.",
|
||||
Some(TestStyles::basic()))?;
|
||||
|
||||
let risks = vec![
|
||||
"Third-party API integration delays - Medium risk, mitigation plan in place".to_string(),
|
||||
"Resource availability during holidays - Low risk, backup resources identified".to_string(),
|
||||
"Performance requirements validation - Medium risk, load testing scheduled".to_string(),
|
||||
];
|
||||
handler.add_list(&doc_id, risks, false)?;
|
||||
|
||||
// Action Items
|
||||
handler.add_heading(&doc_id, "Action Items", 2)?;
|
||||
|
||||
let action_items_data = TableData {
|
||||
rows: vec![
|
||||
vec!["Action Item".to_string(), "Owner".to_string(), "Due Date".to_string(), "Status".to_string()],
|
||||
vec!["Complete load testing scenarios".to_string(), "Jennifer Kim".to_string(), "Dec 22, 2024".to_string(), "In Progress".to_string()],
|
||||
vec!["Finalize API integration testing".to_string(), "Mike Chen".to_string(), "Dec 20, 2024".to_string(), "Not Started".to_string()],
|
||||
vec!["Update project documentation".to_string(), "Lisa Wang".to_string(), "Jan 10, 2025".to_string(), "Not Started".to_string()],
|
||||
vec!["Prepare deployment checklist".to_string(), "Alex Thompson".to_string(), "Jan 15, 2025".to_string(), "Not Started".to_string()],
|
||||
],
|
||||
headers: Some(vec!["Action Item".to_string(), "Owner".to_string(), "Due Date".to_string(), "Status".to_string()]),
|
||||
border_style: Some("single".to_string()),
|
||||
};
|
||||
handler.add_table(&doc_id, action_items_data)?;
|
||||
|
||||
// Next Meeting
|
||||
handler.add_heading(&doc_id, "Next Meeting", 2)?;
|
||||
handler.add_paragraph(&doc_id,
|
||||
"The next steering committee meeting is scheduled for January 5, 2025, at 2:00 PM PST in Conference Room A.",
|
||||
Some(TestStyles::basic()))?;
|
||||
|
||||
// Footer
|
||||
handler.set_footer(&doc_id, "Project Steering Committee - Meeting Minutes")?;
|
||||
|
||||
Ok(doc_id)
|
||||
}
|
||||
|
||||
/// Creates a product specification document for testing
|
||||
pub fn create_product_spec(handler: &mut DocxHandler) -> Result<String> {
|
||||
let doc_id = handler.create_document()?;
|
||||
|
||||
// Title page
|
||||
handler.add_paragraph(&doc_id, "", None)?;
|
||||
handler.add_paragraph(&doc_id, "", None)?;
|
||||
handler.add_heading(&doc_id, "Product Requirements Specification", 1)?;
|
||||
handler.add_paragraph(&doc_id, "", None)?;
|
||||
handler.add_paragraph(&doc_id, "Document Management System v2.0", Some(TestStyles::centered()))?;
|
||||
handler.add_paragraph(&doc_id, "", None)?;
|
||||
handler.add_paragraph(&doc_id, "Version 1.0", Some(TestStyles::centered()))?;
|
||||
handler.add_paragraph(&doc_id, "December 15, 2024", Some(TestStyles::centered()))?;
|
||||
|
||||
handler.add_page_break(&doc_id)?;
|
||||
|
||||
// Table of Contents (simplified)
|
||||
handler.add_heading(&doc_id, "Table of Contents", 1)?;
|
||||
let toc_items = vec![
|
||||
"1. Introduction".to_string(),
|
||||
"2. System Overview".to_string(),
|
||||
"3. Functional Requirements".to_string(),
|
||||
"4. Non-Functional Requirements".to_string(),
|
||||
"5. User Interface Requirements".to_string(),
|
||||
"6. System Architecture".to_string(),
|
||||
"7. Security Requirements".to_string(),
|
||||
];
|
||||
handler.add_list(&doc_id, toc_items, true)?;
|
||||
|
||||
// Introduction
|
||||
handler.add_heading(&doc_id, "1. Introduction", 1)?;
|
||||
|
||||
handler.add_heading(&doc_id, "1.1 Purpose", 2)?;
|
||||
handler.add_paragraph(&doc_id,
|
||||
"This document specifies the requirements for the Document Management System version 2.0. The system is designed to provide comprehensive document storage, retrieval, and collaboration capabilities for enterprise users.",
|
||||
Some(TestStyles::basic()))?;
|
||||
|
||||
handler.add_heading(&doc_id, "1.2 Scope", 2)?;
|
||||
handler.add_paragraph(&doc_id,
|
||||
"The Document Management System will support multiple file formats, version control, user collaboration, and advanced search capabilities. The system will be deployed as a web-based application with mobile support.",
|
||||
Some(TestStyles::basic()))?;
|
||||
|
||||
// System Overview
|
||||
handler.add_heading(&doc_id, "2. System Overview", 1)?;
|
||||
handler.add_paragraph(&doc_id,
|
||||
"The Document Management System consists of several integrated components working together to provide a seamless document management experience.",
|
||||
Some(TestStyles::basic()))?;
|
||||
|
||||
let system_components = vec![
|
||||
"Document Storage Engine".to_string(),
|
||||
"Version Control System".to_string(),
|
||||
"Search and Indexing Service".to_string(),
|
||||
"User Authentication and Authorization".to_string(),
|
||||
"Collaboration Tools".to_string(),
|
||||
"Reporting and Analytics".to_string(),
|
||||
];
|
||||
handler.add_list(&doc_id, system_components, false)?;
|
||||
|
||||
// Functional Requirements
|
||||
handler.add_heading(&doc_id, "3. Functional Requirements", 1)?;
|
||||
|
||||
handler.add_heading(&doc_id, "3.1 Document Upload and Storage", 2)?;
|
||||
let upload_requirements = vec![
|
||||
"FR-001: System shall support upload of files up to 100MB in size".to_string(),
|
||||
"FR-002: System shall support common file formats (PDF, DOCX, XLSX, PPTX, TXT)".to_string(),
|
||||
"FR-003: System shall automatically generate file metadata upon upload".to_string(),
|
||||
"FR-004: System shall provide drag-and-drop upload functionality".to_string(),
|
||||
];
|
||||
handler.add_list(&doc_id, upload_requirements, false)?;
|
||||
|
||||
handler.add_heading(&doc_id, "3.2 Search and Retrieval", 2)?;
|
||||
let search_requirements = vec![
|
||||
"FR-005: System shall provide full-text search capabilities".to_string(),
|
||||
"FR-006: System shall support advanced search with multiple criteria".to_string(),
|
||||
"FR-007: System shall provide search result ranking and relevance scoring".to_string(),
|
||||
"FR-008: System shall support search within specific document types".to_string(),
|
||||
];
|
||||
handler.add_list(&doc_id, search_requirements, false)?;
|
||||
|
||||
// Non-Functional Requirements
|
||||
handler.add_heading(&doc_id, "4. Non-Functional Requirements", 1)?;
|
||||
|
||||
let nfr_data = TableData {
|
||||
rows: vec![
|
||||
vec!["Requirement".to_string(), "Specification".to_string(), "Priority".to_string()],
|
||||
vec!["Performance".to_string(), "Page load time < 3 seconds".to_string(), "High".to_string()],
|
||||
vec!["Scalability".to_string(), "Support 1000+ concurrent users".to_string(), "High".to_string()],
|
||||
vec!["Availability".to_string(), "99.9% uptime".to_string(), "High".to_string()],
|
||||
vec!["Security".to_string(), "Role-based access control".to_string(), "Critical".to_string()],
|
||||
vec!["Usability".to_string(), "Intuitive interface, minimal training".to_string(), "Medium".to_string()],
|
||||
],
|
||||
headers: Some(vec!["Requirement".to_string(), "Specification".to_string(), "Priority".to_string()]),
|
||||
border_style: Some("single".to_string()),
|
||||
};
|
||||
handler.add_table(&doc_id, nfr_data)?;
|
||||
|
||||
// Security Requirements
|
||||
handler.add_heading(&doc_id, "7. Security Requirements", 1)?;
|
||||
handler.add_paragraph(&doc_id,
|
||||
"Security is paramount for the Document Management System. The following security measures must be implemented:",
|
||||
Some(TestStyles::basic()))?;
|
||||
|
||||
let security_requirements = vec![
|
||||
"SEC-001: All data transmission must use HTTPS/TLS 1.3".to_string(),
|
||||
"SEC-002: User passwords must meet complexity requirements".to_string(),
|
||||
"SEC-003: System must support multi-factor authentication".to_string(),
|
||||
"SEC-004: All user actions must be logged for audit purposes".to_string(),
|
||||
"SEC-005: Document access must be controlled by user permissions".to_string(),
|
||||
"SEC-006: System must support data encryption at rest".to_string(),
|
||||
];
|
||||
handler.add_list(&doc_id, security_requirements, true)?;
|
||||
|
||||
// Footer
|
||||
handler.set_footer(&doc_id, "Product Requirements Specification v1.0 - Confidential")?;
|
||||
|
||||
Ok(doc_id)
|
||||
}
|
||||
|
||||
/// Creates a test document with international content
|
||||
pub fn create_multilingual_document(handler: &mut DocxHandler) -> Result<String> {
|
||||
let doc_id = handler.create_document()?;
|
||||
|
||||
handler.add_heading(&doc_id, "Multilingual Content Test Document", 1)?;
|
||||
handler.add_paragraph(&doc_id,
|
||||
"This document contains text in multiple languages to test internationalization and Unicode support.",
|
||||
Some(TestStyles::basic()))?;
|
||||
|
||||
for (language, text) in TestContent::multilingual_content() {
|
||||
handler.add_heading(&doc_id, language, 2)?;
|
||||
handler.add_paragraph(&doc_id, text, Some(TestStyles::basic()))?;
|
||||
handler.add_paragraph(&doc_id, "", None)?; // Empty line
|
||||
}
|
||||
|
||||
handler.add_heading(&doc_id, "Special Characters and Symbols", 2)?;
|
||||
handler.add_paragraph(&doc_id, TestContent::special_characters(), Some(TestStyles::basic()))?;
|
||||
handler.add_paragraph(&doc_id, TestContent::symbols_and_math(), Some(TestStyles::basic()))?;
|
||||
|
||||
// Currency symbols
|
||||
handler.add_paragraph(&doc_id, "Currency symbols: $ € £ ¥ ₹ ₽ ₩ ₪ ₫ ₱", Some(TestStyles::basic()))?;
|
||||
|
||||
// Emoji (if supported)
|
||||
handler.add_paragraph(&doc_id, "Emoji test: 📄 📝 💼 🔒 🌍 ✅ ❌ ⚠️", Some(TestStyles::basic()))?;
|
||||
|
||||
Ok(doc_id)
|
||||
}
|
||||
|
||||
/// Creates a document with complex formatting for testing
|
||||
pub fn create_formatted_document(handler: &mut DocxHandler) -> Result<String> {
|
||||
let doc_id = handler.create_document()?;
|
||||
|
||||
handler.add_heading(&doc_id, "Formatting Test Document", 1)?;
|
||||
|
||||
// Different paragraph styles
|
||||
handler.add_paragraph(&doc_id, "This paragraph uses the default style.", Some(TestStyles::basic()))?;
|
||||
handler.add_paragraph(&doc_id, "This paragraph uses bold formatting.", Some(DocxStyle {
|
||||
bold: Some(true),
|
||||
..TestStyles::basic()
|
||||
}))?;
|
||||
handler.add_paragraph(&doc_id, "This paragraph uses italic formatting.", Some(DocxStyle {
|
||||
italic: Some(true),
|
||||
..TestStyles::basic()
|
||||
}))?;
|
||||
handler.add_paragraph(&doc_id, "This paragraph is centered.", Some(TestStyles::centered()))?;
|
||||
handler.add_paragraph(&doc_id, "This paragraph uses emphasis styling.", Some(TestStyles::emphasis()))?;
|
||||
|
||||
// Different font sizes
|
||||
handler.add_heading(&doc_id, "Font Size Tests", 2)?;
|
||||
for size in [8, 10, 12, 14, 16, 18, 24] {
|
||||
let style = DocxStyle {
|
||||
font_size: Some(size),
|
||||
..TestStyles::basic()
|
||||
};
|
||||
handler.add_paragraph(&doc_id, &format!("This text is {} point size.", size), Some(style))?;
|
||||
}
|
||||
|
||||
// Color tests
|
||||
handler.add_heading(&doc_id, "Color Tests", 2)?;
|
||||
let colors = vec![
|
||||
("#000000", "Black"),
|
||||
("#FF0000", "Red"),
|
||||
("#00FF00", "Green"),
|
||||
("#0000FF", "Blue"),
|
||||
("#FF00FF", "Magenta"),
|
||||
("#00FFFF", "Cyan"),
|
||||
("#800080", "Purple"),
|
||||
];
|
||||
|
||||
for (color_code, color_name) in colors {
|
||||
let style = DocxStyle {
|
||||
color: Some(color_code.to_string()),
|
||||
..TestStyles::basic()
|
||||
};
|
||||
handler.add_paragraph(&doc_id, &format!("This text is in {}", color_name), Some(style))?;
|
||||
}
|
||||
|
||||
// Alignment tests
|
||||
handler.add_heading(&doc_id, "Alignment Tests", 2)?;
|
||||
let alignments = vec![
|
||||
("left", "Left aligned text"),
|
||||
("center", "Center aligned text"),
|
||||
("right", "Right aligned text"),
|
||||
("justify", "Justified text that should span the full width of the line when there is enough content to make it meaningful"),
|
||||
];
|
||||
|
||||
for (alignment, text) in alignments {
|
||||
let style = DocxStyle {
|
||||
alignment: Some(alignment.to_string()),
|
||||
..TestStyles::basic()
|
||||
};
|
||||
handler.add_paragraph(&doc_id, text, Some(style))?;
|
||||
}
|
||||
|
||||
// Complex table with formatting
|
||||
handler.add_heading(&doc_id, "Formatted Table", 2)?;
|
||||
let formatted_table = TableData {
|
||||
rows: vec![
|
||||
vec!["Item".to_string(), "Price".to_string(), "Discount".to_string(), "Final Price".to_string()],
|
||||
vec!["Widget A".to_string(), "$100.00".to_string(), "10%".to_string(), "$90.00".to_string()],
|
||||
vec!["Widget B".to_string(), "$150.00".to_string(), "15%".to_string(), "$127.50".to_string()],
|
||||
vec!["Widget C".to_string(), "$200.00".to_string(), "20%".to_string(), "$160.00".to_string()],
|
||||
vec!["Total".to_string(), "$450.00".to_string(), "".to_string(), "$377.50".to_string()],
|
||||
],
|
||||
headers: Some(vec!["Item".to_string(), "Price".to_string(), "Discount".to_string(), "Final Price".to_string()]),
|
||||
border_style: Some("single".to_string()),
|
||||
};
|
||||
handler.add_table(&doc_id, formatted_table)?;
|
||||
|
||||
Ok(doc_id)
|
||||
}
|
||||
Vendored
+392
@@ -0,0 +1,392 @@
|
||||
//! Test data generators and utilities
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Generates test data for various document types and scenarios
|
||||
pub struct TestDataGenerator;
|
||||
|
||||
impl TestDataGenerator {
|
||||
/// Generate test paragraphs with varying complexity
|
||||
pub fn generate_paragraphs(count: usize, complexity: ParagraphComplexity) -> Vec<String> {
|
||||
let base_sentences = match complexity {
|
||||
ParagraphComplexity::Simple => vec![
|
||||
"This is a simple sentence.",
|
||||
"Another basic statement follows.",
|
||||
"The text remains straightforward.",
|
||||
"No complex structures here.",
|
||||
"Plain language is used throughout.",
|
||||
],
|
||||
ParagraphComplexity::Medium => vec![
|
||||
"This sentence demonstrates moderate complexity with additional clauses and descriptive elements.",
|
||||
"Furthermore, the content includes various punctuation marks, numbers like 123, and technical terms.",
|
||||
"The writing style incorporates both simple and compound sentence structures for variety.",
|
||||
"Additionally, references to specific dates (December 15, 2024) and percentages (85%) are included.",
|
||||
"These paragraphs simulate realistic document content found in business communications.",
|
||||
],
|
||||
ParagraphComplexity::Complex => vec![
|
||||
"This comprehensive sentence exemplifies sophisticated linguistic structures, incorporating multiple subordinate clauses, technical terminology, and complex syntactical arrangements that challenge both human readers and automated processing systems.",
|
||||
"Moreover, the content integrates diverse elements including numerical data (such as 42.7% improvement rates), temporal references (spanning Q3 2024 through Q1 2025), geographical locations (Silicon Valley, New York, London), and industry-specific jargon that reflects real-world document complexity.",
|
||||
"The methodology employed in generating these test paragraphs considers various factors: readability indices, sentence length distribution, vocabulary diversity, and the inclusion of special characters (e.g., àáâãäå, €£¥, ∑∏∫) to ensure comprehensive testing coverage.",
|
||||
"Consequently, these multi-faceted paragraphs serve as effective benchmarks for evaluating system performance under realistic conditions, while simultaneously providing sufficient content variation to identify potential edge cases and optimization opportunities.",
|
||||
],
|
||||
};
|
||||
|
||||
(0..count)
|
||||
.map(|i| {
|
||||
let sentence_count = match complexity {
|
||||
ParagraphComplexity::Simple => 2 + (i % 3),
|
||||
ParagraphComplexity::Medium => 3 + (i % 4),
|
||||
ParagraphComplexity::Complex => 2 + (i % 3),
|
||||
};
|
||||
|
||||
let mut paragraph = String::new();
|
||||
for j in 0..sentence_count {
|
||||
let sentence = &base_sentences[j % base_sentences.len()];
|
||||
if j > 0 {
|
||||
paragraph.push(' ');
|
||||
}
|
||||
paragraph.push_str(sentence);
|
||||
}
|
||||
|
||||
paragraph
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Generate table data with specified dimensions and content type
|
||||
pub fn generate_table_data(rows: usize, cols: usize, content_type: TableContentType) -> Vec<Vec<String>> {
|
||||
let mut table_data = Vec::new();
|
||||
|
||||
// Generate header row
|
||||
let headers: Vec<String> = (0..cols)
|
||||
.map(|i| match content_type {
|
||||
TableContentType::Generic => format!("Column {}", i + 1),
|
||||
TableContentType::Financial => match i {
|
||||
0 => "Period".to_string(),
|
||||
1 => "Revenue".to_string(),
|
||||
2 => "Expenses".to_string(),
|
||||
3 => "Profit".to_string(),
|
||||
_ => format!("Metric {}", i + 1),
|
||||
},
|
||||
TableContentType::Personnel => match i {
|
||||
0 => "Name".to_string(),
|
||||
1 => "Department".to_string(),
|
||||
2 => "Role".to_string(),
|
||||
3 => "Start Date".to_string(),
|
||||
_ => format!("Field {}", i + 1),
|
||||
},
|
||||
TableContentType::Technical => match i {
|
||||
0 => "Component".to_string(),
|
||||
1 => "Version".to_string(),
|
||||
2 => "Status".to_string(),
|
||||
3 => "Last Updated".to_string(),
|
||||
_ => format!("Attribute {}", i + 1),
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
table_data.push(headers);
|
||||
|
||||
// Generate data rows
|
||||
for row in 0..rows {
|
||||
let row_data: Vec<String> = (0..cols)
|
||||
.map(|col| match content_type {
|
||||
TableContentType::Generic => format!("R{}C{}", row + 1, col + 1),
|
||||
TableContentType::Financial => match col {
|
||||
0 => format!("Q{} 2024", (row % 4) + 1),
|
||||
1 => format!("${:.1}M", 100.0 + row as f64 * 12.5),
|
||||
2 => format!("${:.1}M", 70.0 + row as f64 * 8.2),
|
||||
3 => format!("${:.1}M", 30.0 + row as f64 * 4.3),
|
||||
_ => format!("{:.1}%", 15.0 + row as f64 * 2.1),
|
||||
},
|
||||
TableContentType::Personnel => match col {
|
||||
0 => format!("Employee {}", row + 1),
|
||||
1 => ["Engineering", "Sales", "Marketing", "Operations"][(row % 4)].to_string(),
|
||||
2 => ["Manager", "Developer", "Analyst", "Specialist"][(row % 4)].to_string(),
|
||||
3 => format!("2024-{:02}-{:02}", ((row % 12) + 1), ((row % 28) + 1)),
|
||||
_ => format!("Data {}", row + 1),
|
||||
},
|
||||
TableContentType::Technical => match col {
|
||||
0 => format!("Component-{}", row + 1),
|
||||
1 => format!("v{}.{}.{}", (row % 3) + 1, (row % 5), (row % 10)),
|
||||
2 => ["Active", "Pending", "Deprecated", "Testing"][(row % 4)].to_string(),
|
||||
3 => format!("2024-12-{:02}", ((row % 28) + 1)),
|
||||
_ => format!("Value {}", row + 1),
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
table_data.push(row_data);
|
||||
}
|
||||
|
||||
table_data
|
||||
}
|
||||
|
||||
/// Generate list items with specified count and category
|
||||
pub fn generate_list_items(count: usize, category: ListCategory) -> Vec<String> {
|
||||
let base_items = match category {
|
||||
ListCategory::Tasks => vec![
|
||||
"Complete project documentation",
|
||||
"Review code changes and pull requests",
|
||||
"Update system configuration files",
|
||||
"Run comprehensive test suite",
|
||||
"Deploy to staging environment",
|
||||
"Conduct security audit",
|
||||
"Optimize database performance",
|
||||
"Update user interface components",
|
||||
"Implement new feature requirements",
|
||||
"Fix reported bugs and issues",
|
||||
],
|
||||
ListCategory::Features => vec![
|
||||
"Advanced search and filtering capabilities",
|
||||
"Real-time collaboration tools",
|
||||
"Automated backup and recovery",
|
||||
"Multi-language support",
|
||||
"Mobile-responsive design",
|
||||
"Integration with third-party services",
|
||||
"Customizable dashboard and reports",
|
||||
"Role-based access control",
|
||||
"API for external integrations",
|
||||
"Advanced analytics and insights",
|
||||
],
|
||||
ListCategory::Requirements => vec![
|
||||
"System must support 1000+ concurrent users",
|
||||
"Response time must be under 200ms for 95% of requests",
|
||||
"Uptime must exceed 99.9% availability",
|
||||
"Data must be encrypted both in transit and at rest",
|
||||
"User interface must be accessible (WCAG 2.1 AA)",
|
||||
"System must support multi-factor authentication",
|
||||
"Backup processes must complete within 2 hours",
|
||||
"Security patches must be applied within 24 hours",
|
||||
"System must scale horizontally to handle peak loads",
|
||||
"Audit logs must be maintained for minimum 7 years",
|
||||
],
|
||||
ListCategory::Benefits => vec![
|
||||
"Increased operational efficiency by 35%",
|
||||
"Reduced manual processing time by 60%",
|
||||
"Improved data accuracy and consistency",
|
||||
"Enhanced security and compliance posture",
|
||||
"Better user experience and satisfaction",
|
||||
"Lower total cost of ownership",
|
||||
"Faster time-to-market for new features",
|
||||
"Improved scalability and performance",
|
||||
"Better decision-making through analytics",
|
||||
"Reduced maintenance and support costs",
|
||||
],
|
||||
};
|
||||
|
||||
(0..count)
|
||||
.map(|i| {
|
||||
let base_item = &base_items[i % base_items.len()];
|
||||
if count > base_items.len() {
|
||||
format!("{} (item {})", base_item, i + 1)
|
||||
} else {
|
||||
base_item.clone()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Generate realistic business data for testing
|
||||
pub fn generate_business_data() -> BusinessDataSet {
|
||||
BusinessDataSet {
|
||||
companies: vec![
|
||||
"Acme Corporation".to_string(),
|
||||
"Global Tech Solutions".to_string(),
|
||||
"Innovation Partners LLC".to_string(),
|
||||
"Digital Dynamics Inc".to_string(),
|
||||
"Future Systems Ltd".to_string(),
|
||||
],
|
||||
departments: vec![
|
||||
"Engineering".to_string(),
|
||||
"Sales & Marketing".to_string(),
|
||||
"Human Resources".to_string(),
|
||||
"Operations".to_string(),
|
||||
"Finance & Accounting".to_string(),
|
||||
"Research & Development".to_string(),
|
||||
],
|
||||
positions: vec![
|
||||
"Software Engineer".to_string(),
|
||||
"Product Manager".to_string(),
|
||||
"Sales Representative".to_string(),
|
||||
"Data Analyst".to_string(),
|
||||
"Project Manager".to_string(),
|
||||
"UX Designer".to_string(),
|
||||
],
|
||||
locations: vec![
|
||||
"San Francisco, CA".to_string(),
|
||||
"New York, NY".to_string(),
|
||||
"Austin, TX".to_string(),
|
||||
"Seattle, WA".to_string(),
|
||||
"Boston, MA".to_string(),
|
||||
"Chicago, IL".to_string(),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate MCP tool call test data
|
||||
pub fn generate_mcp_test_calls() -> Vec<McpTestCall> {
|
||||
vec![
|
||||
McpTestCall {
|
||||
tool_name: "create_document".to_string(),
|
||||
args: json!({}),
|
||||
expected_success: true,
|
||||
expected_result_keys: vec!["success".to_string(), "document_id".to_string()],
|
||||
},
|
||||
McpTestCall {
|
||||
tool_name: "add_paragraph".to_string(),
|
||||
args: json!({
|
||||
"document_id": "test-doc-id",
|
||||
"text": "Test paragraph content"
|
||||
}),
|
||||
expected_success: true,
|
||||
expected_result_keys: vec!["success".to_string()],
|
||||
},
|
||||
McpTestCall {
|
||||
tool_name: "add_heading".to_string(),
|
||||
args: json!({
|
||||
"document_id": "test-doc-id",
|
||||
"text": "Test Heading",
|
||||
"level": 1
|
||||
}),
|
||||
expected_success: true,
|
||||
expected_result_keys: vec!["success".to_string()],
|
||||
},
|
||||
McpTestCall {
|
||||
tool_name: "extract_text".to_string(),
|
||||
args: json!({
|
||||
"document_id": "test-doc-id"
|
||||
}),
|
||||
expected_success: true,
|
||||
expected_result_keys: vec!["success".to_string(), "text".to_string()],
|
||||
},
|
||||
McpTestCall {
|
||||
tool_name: "get_metadata".to_string(),
|
||||
args: json!({
|
||||
"document_id": "test-doc-id"
|
||||
}),
|
||||
expected_success: true,
|
||||
expected_result_keys: vec!["success".to_string(), "metadata".to_string()],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// Generate performance test scenarios
|
||||
pub fn generate_performance_scenarios() -> Vec<PerformanceScenario> {
|
||||
vec![
|
||||
PerformanceScenario {
|
||||
name: "Small Document".to_string(),
|
||||
paragraph_count: 10,
|
||||
table_count: 1,
|
||||
list_count: 2,
|
||||
expected_max_time_ms: 1000,
|
||||
},
|
||||
PerformanceScenario {
|
||||
name: "Medium Document".to_string(),
|
||||
paragraph_count: 100,
|
||||
table_count: 5,
|
||||
list_count: 10,
|
||||
expected_max_time_ms: 5000,
|
||||
},
|
||||
PerformanceScenario {
|
||||
name: "Large Document".to_string(),
|
||||
paragraph_count: 500,
|
||||
table_count: 20,
|
||||
list_count: 30,
|
||||
expected_max_time_ms: 15000,
|
||||
},
|
||||
PerformanceScenario {
|
||||
name: "Extra Large Document".to_string(),
|
||||
paragraph_count: 1000,
|
||||
table_count: 50,
|
||||
list_count: 50,
|
||||
expected_max_time_ms: 30000,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Complexity levels for generated paragraphs
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ParagraphComplexity {
|
||||
Simple,
|
||||
Medium,
|
||||
Complex,
|
||||
}
|
||||
|
||||
/// Content types for generated tables
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TableContentType {
|
||||
Generic,
|
||||
Financial,
|
||||
Personnel,
|
||||
Technical,
|
||||
}
|
||||
|
||||
/// Categories for generated lists
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ListCategory {
|
||||
Tasks,
|
||||
Features,
|
||||
Requirements,
|
||||
Benefits,
|
||||
}
|
||||
|
||||
/// Business data set for realistic testing
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BusinessDataSet {
|
||||
pub companies: Vec<String>,
|
||||
pub departments: Vec<String>,
|
||||
pub positions: Vec<String>,
|
||||
pub locations: Vec<String>,
|
||||
}
|
||||
|
||||
/// MCP tool call test data
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct McpTestCall {
|
||||
pub tool_name: String,
|
||||
pub args: Value,
|
||||
pub expected_success: bool,
|
||||
pub expected_result_keys: Vec<String>,
|
||||
}
|
||||
|
||||
/// Performance test scenario data
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PerformanceScenario {
|
||||
pub name: String,
|
||||
pub paragraph_count: usize,
|
||||
pub table_count: usize,
|
||||
pub list_count: usize,
|
||||
pub expected_max_time_ms: u64,
|
||||
}
|
||||
|
||||
/// Utility functions for test data validation
|
||||
pub struct TestDataValidator;
|
||||
|
||||
impl TestDataValidator {
|
||||
/// Validate that text contains expected content
|
||||
pub fn validate_text_content(text: &str, expected_keywords: &[&str]) -> bool {
|
||||
expected_keywords.iter().all(|keyword| text.contains(keyword))
|
||||
}
|
||||
|
||||
/// Validate table structure
|
||||
pub fn validate_table_structure(rows: &[Vec<String>], expected_cols: usize) -> bool {
|
||||
!rows.is_empty() && rows.iter().all(|row| row.len() == expected_cols)
|
||||
}
|
||||
|
||||
/// Validate MCP response structure
|
||||
pub fn validate_mcp_response(response: &Value, expected_keys: &[String]) -> bool {
|
||||
expected_keys.iter().all(|key| response.get(key).is_some())
|
||||
}
|
||||
|
||||
/// Generate hash for test data consistency checking
|
||||
pub fn generate_content_hash(content: &str) -> u64 {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
let mut hasher = DefaultHasher::new();
|
||||
content.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
use docx_mcp::docx_tools::DocxToolsProvider;
|
||||
use docx_mcp::security::SecurityConfig;
|
||||
use mcp_core::{ToolProvider, ToolResult};
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use tokio_test;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::*;
|
||||
|
||||
async fn create_test_provider() -> (DocxToolsProvider, TempDir) {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
std::env::set_var("TMPDIR", temp_dir.path());
|
||||
|
||||
let provider = DocxToolsProvider::new();
|
||||
(provider, temp_dir)
|
||||
}
|
||||
|
||||
async fn create_test_provider_with_security(config: SecurityConfig) -> (DocxToolsProvider, TempDir) {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
std::env::set_var("TMPDIR", temp_dir.path());
|
||||
|
||||
let provider = DocxToolsProvider::new_with_security(config);
|
||||
(provider, temp_dir)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_tools_default_config() {
|
||||
let (provider, _temp_dir) = create_test_provider().await;
|
||||
|
||||
let tools = provider.list_tools().await;
|
||||
|
||||
// Should have all tools in default configuration
|
||||
assert!(tools.len() > 20);
|
||||
|
||||
let tool_names: Vec<_> = tools.iter().map(|t| &t.name).collect();
|
||||
assert!(tool_names.contains(&&"create_document".to_string()));
|
||||
assert!(tool_names.contains(&&"add_paragraph".to_string()));
|
||||
assert!(tool_names.contains(&&"convert_to_pdf".to_string()));
|
||||
assert!(tool_names.contains(&&"extract_text".to_string()));
|
||||
assert!(tool_names.contains(&&"get_security_info".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_list_tools_readonly_config() {
|
||||
let config = SecurityConfig {
|
||||
readonly_mode: true,
|
||||
..Default::default()
|
||||
};
|
||||
let (provider, _temp_dir) = create_test_provider_with_security(config).await;
|
||||
|
||||
let tools = provider.list_tools().await;
|
||||
let tool_names: Vec<_> = tools.iter().map(|t| &t.name).collect();
|
||||
|
||||
// Should include readonly tools
|
||||
assert!(tool_names.contains(&&"extract_text".to_string()));
|
||||
assert!(tool_names.contains(&&"get_metadata".to_string()));
|
||||
assert!(tool_names.contains(&&"search_text".to_string()));
|
||||
|
||||
// Should not include write tools
|
||||
assert!(!tool_names.contains(&&"create_document".to_string()));
|
||||
assert!(!tool_names.contains(&&"add_paragraph".to_string()));
|
||||
assert!(!tool_names.contains(&&"save_document".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_create_document_tool() {
|
||||
let (provider, _temp_dir) = create_test_provider().await;
|
||||
|
||||
let result = provider.call_tool("create_document", json!({})).await;
|
||||
|
||||
match result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
assert!(value["document_id"].is_string());
|
||||
let doc_id = value["document_id"].as_str().unwrap();
|
||||
assert!(!doc_id.is_empty());
|
||||
}
|
||||
ToolResult::Error(e) => panic!("Expected success, got error: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_paragraph_tool() {
|
||||
let (provider, _temp_dir) = create_test_provider().await;
|
||||
|
||||
// First create a document
|
||||
let create_result = provider.call_tool("create_document", json!({})).await;
|
||||
let doc_id = match create_result {
|
||||
ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(),
|
||||
_ => panic!("Failed to create document"),
|
||||
};
|
||||
|
||||
// Add paragraph
|
||||
let args = json!({
|
||||
"document_id": doc_id,
|
||||
"text": "Test paragraph content"
|
||||
});
|
||||
|
||||
let result = provider.call_tool("add_paragraph", args).await;
|
||||
|
||||
match result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
}
|
||||
ToolResult::Error(e) => panic!("Expected success, got error: {}", e),
|
||||
}
|
||||
|
||||
// Verify content was added
|
||||
let extract_args = json!({"document_id": doc_id});
|
||||
let extract_result = provider.call_tool("extract_text", extract_args).await;
|
||||
|
||||
match extract_result {
|
||||
ToolResult::Success(value) => {
|
||||
let text = value["text"].as_str().unwrap();
|
||||
assert!(text.contains("Test paragraph content"));
|
||||
}
|
||||
ToolResult::Error(e) => panic!("Failed to extract text: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_paragraph_with_style() {
|
||||
let (provider, _temp_dir) = create_test_provider().await;
|
||||
|
||||
let create_result = provider.call_tool("create_document", json!({})).await;
|
||||
let doc_id = match create_result {
|
||||
ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(),
|
||||
_ => panic!("Failed to create document"),
|
||||
};
|
||||
|
||||
let args = json!({
|
||||
"document_id": doc_id,
|
||||
"text": "Styled paragraph",
|
||||
"style": {
|
||||
"font_size": 16,
|
||||
"bold": true,
|
||||
"color": "#FF0000",
|
||||
"alignment": "center"
|
||||
}
|
||||
});
|
||||
|
||||
let result = provider.call_tool("add_paragraph", args).await;
|
||||
|
||||
match result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
}
|
||||
ToolResult::Error(e) => panic!("Expected success, got error: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_table_tool() {
|
||||
let (provider, _temp_dir) = create_test_provider().await;
|
||||
|
||||
let create_result = provider.call_tool("create_document", json!({})).await;
|
||||
let doc_id = match create_result {
|
||||
ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(),
|
||||
_ => panic!("Failed to create document"),
|
||||
};
|
||||
|
||||
let args = json!({
|
||||
"document_id": doc_id,
|
||||
"rows": [
|
||||
["Name", "Age", "City"],
|
||||
["Alice", "30", "New York"],
|
||||
["Bob", "25", "Los Angeles"]
|
||||
]
|
||||
});
|
||||
|
||||
let result = provider.call_tool("add_table", args).await;
|
||||
|
||||
match result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
}
|
||||
ToolResult::Error(e) => panic!("Expected success, got error: {}", e),
|
||||
}
|
||||
|
||||
// Verify table content
|
||||
let extract_args = json!({"document_id": doc_id});
|
||||
let extract_result = provider.call_tool("extract_text", extract_args).await;
|
||||
|
||||
match extract_result {
|
||||
ToolResult::Success(value) => {
|
||||
let text = value["text"].as_str().unwrap();
|
||||
assert!(text.contains("Alice"));
|
||||
assert!(text.contains("New York"));
|
||||
}
|
||||
ToolResult::Error(e) => panic!("Failed to extract text: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_heading_tool() {
|
||||
let (provider, _temp_dir) = create_test_provider().await;
|
||||
|
||||
let create_result = provider.call_tool("create_document", json!({})).await;
|
||||
let doc_id = match create_result {
|
||||
ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(),
|
||||
_ => panic!("Failed to create document"),
|
||||
};
|
||||
|
||||
// Test different heading levels
|
||||
for level in 1..=6 {
|
||||
let args = json!({
|
||||
"document_id": doc_id,
|
||||
"text": format!("Heading Level {}", level),
|
||||
"level": level
|
||||
});
|
||||
|
||||
let result = provider.call_tool("add_heading", args).await;
|
||||
|
||||
match result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
}
|
||||
ToolResult::Error(e) => panic!("Expected success for level {}, got error: {}", level, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_list_tool() {
|
||||
let (provider, _temp_dir) = create_test_provider().await;
|
||||
|
||||
let create_result = provider.call_tool("create_document", json!({})).await;
|
||||
let doc_id = match create_result {
|
||||
ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(),
|
||||
_ => panic!("Failed to create document"),
|
||||
};
|
||||
|
||||
// Test ordered list
|
||||
let ordered_args = json!({
|
||||
"document_id": doc_id,
|
||||
"items": ["First item", "Second item", "Third item"],
|
||||
"ordered": true
|
||||
});
|
||||
|
||||
let result = provider.call_tool("add_list", ordered_args).await;
|
||||
assert!(matches!(result, ToolResult::Success(_)));
|
||||
|
||||
// Test unordered list
|
||||
let unordered_args = json!({
|
||||
"document_id": doc_id,
|
||||
"items": ["Bullet one", "Bullet two", "Bullet three"],
|
||||
"ordered": false
|
||||
});
|
||||
|
||||
let result = provider.call_tool("add_list", unordered_args).await;
|
||||
assert!(matches!(result, ToolResult::Success(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_metadata_tool() {
|
||||
let (provider, _temp_dir) = create_test_provider().await;
|
||||
|
||||
let create_result = provider.call_tool("create_document", json!({})).await;
|
||||
let doc_id = match create_result {
|
||||
ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(),
|
||||
_ => panic!("Failed to create document"),
|
||||
};
|
||||
|
||||
let args = json!({"document_id": doc_id});
|
||||
let result = provider.call_tool("get_metadata", args).await;
|
||||
|
||||
match result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
let metadata = &value["metadata"];
|
||||
assert_eq!(metadata["id"], doc_id);
|
||||
assert!(metadata["path"].is_string());
|
||||
assert!(metadata["created_at"].is_string());
|
||||
}
|
||||
ToolResult::Error(e) => panic!("Expected success, got error: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_search_text_tool() {
|
||||
let (provider, _temp_dir) = create_test_provider().await;
|
||||
|
||||
let create_result = provider.call_tool("create_document", json!({})).await;
|
||||
let doc_id = match create_result {
|
||||
ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(),
|
||||
_ => panic!("Failed to create document"),
|
||||
};
|
||||
|
||||
// Add some content to search
|
||||
let add_args = json!({
|
||||
"document_id": doc_id,
|
||||
"text": "This is a test document with searchable content. The word test appears multiple times."
|
||||
});
|
||||
provider.call_tool("add_paragraph", add_args).await;
|
||||
|
||||
// Search for text
|
||||
let search_args = json!({
|
||||
"document_id": doc_id,
|
||||
"search_term": "test",
|
||||
"case_sensitive": false
|
||||
});
|
||||
|
||||
let result = provider.call_tool("search_text", search_args).await;
|
||||
|
||||
match result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
let matches = value["matches"].as_array().unwrap();
|
||||
assert!(matches.len() > 0);
|
||||
assert!(value["total_matches"].as_u64().unwrap() > 0);
|
||||
}
|
||||
ToolResult::Error(e) => panic!("Expected success, got error: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_word_count_tool() {
|
||||
let (provider, _temp_dir) = create_test_provider().await;
|
||||
|
||||
let create_result = provider.call_tool("create_document", json!({})).await;
|
||||
let doc_id = match create_result {
|
||||
ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(),
|
||||
_ => panic!("Failed to create document"),
|
||||
};
|
||||
|
||||
// Add content with known word count
|
||||
let content = "This sentence has exactly five words. This is another sentence with seven words total.";
|
||||
let add_args = json!({
|
||||
"document_id": doc_id,
|
||||
"text": content
|
||||
});
|
||||
provider.call_tool("add_paragraph", add_args).await;
|
||||
|
||||
let args = json!({"document_id": doc_id});
|
||||
let result = provider.call_tool("get_word_count", args).await;
|
||||
|
||||
match result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
let stats = &value["statistics"];
|
||||
assert!(stats["words"].as_u64().unwrap() > 10);
|
||||
assert!(stats["characters"].as_u64().unwrap() > 0);
|
||||
assert!(stats["sentences"].as_u64().unwrap() >= 2);
|
||||
}
|
||||
ToolResult::Error(e) => panic!("Expected success, got error: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_security_info_tool() {
|
||||
let config = SecurityConfig {
|
||||
readonly_mode: true,
|
||||
sandbox_mode: true,
|
||||
..Default::default()
|
||||
};
|
||||
let (provider, _temp_dir) = create_test_provider_with_security(config).await;
|
||||
|
||||
let result = provider.call_tool("get_security_info", json!({})).await;
|
||||
|
||||
match result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
let security = &value["security"];
|
||||
assert_eq!(security["readonly_mode"], true);
|
||||
assert_eq!(security["sandbox_mode"], true);
|
||||
assert!(security["summary"].is_string());
|
||||
}
|
||||
ToolResult::Error(e) => panic!("Expected success, got error: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_readonly_mode_blocks_write_operations() {
|
||||
let config = SecurityConfig {
|
||||
readonly_mode: true,
|
||||
..Default::default()
|
||||
};
|
||||
let (provider, _temp_dir) = create_test_provider_with_security(config).await;
|
||||
|
||||
// Should fail to create document in readonly mode
|
||||
let result = provider.call_tool("create_document", json!({})).await;
|
||||
|
||||
match result {
|
||||
ToolResult::Error(e) => {
|
||||
assert!(e.contains("Security check failed"));
|
||||
assert!(e.contains("Command not allowed"));
|
||||
}
|
||||
ToolResult::Success(_) => panic!("Expected security error, got success"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_document_not_found_error() {
|
||||
let (provider, _temp_dir) = create_test_provider().await;
|
||||
|
||||
let args = json!({"document_id": "nonexistent-doc-id"});
|
||||
let result = provider.call_tool("extract_text", args).await;
|
||||
|
||||
match result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(!value["success"].as_bool().unwrap());
|
||||
assert!(value["error"].as_str().unwrap().contains("Document not found"));
|
||||
}
|
||||
ToolResult::Error(_) => {
|
||||
// This is also acceptable - depends on implementation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_invalid_tool_name() {
|
||||
let (provider, _temp_dir) = create_test_provider().await;
|
||||
|
||||
let result = provider.call_tool("nonexistent_tool", json!({})).await;
|
||||
|
||||
match result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(!value["success"].as_bool().unwrap());
|
||||
assert!(value["error"].as_str().unwrap().contains("Unknown tool"));
|
||||
}
|
||||
ToolResult::Error(e) => {
|
||||
assert!(e.contains("Unknown tool"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_documents() {
|
||||
let (provider, _temp_dir) = create_test_provider().await;
|
||||
|
||||
let mut doc_ids = Vec::new();
|
||||
|
||||
// Create multiple documents
|
||||
for i in 0..3 {
|
||||
let result = provider.call_tool("create_document", json!({})).await;
|
||||
let doc_id = match result {
|
||||
ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(),
|
||||
_ => panic!("Failed to create document {}", i),
|
||||
};
|
||||
|
||||
// Add unique content to each
|
||||
let args = json!({
|
||||
"document_id": doc_id,
|
||||
"text": format!("Document {} content", i)
|
||||
});
|
||||
provider.call_tool("add_paragraph", args).await;
|
||||
|
||||
doc_ids.push(doc_id);
|
||||
}
|
||||
|
||||
// List documents
|
||||
let list_result = provider.call_tool("list_documents", json!({})).await;
|
||||
|
||||
match list_result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
let documents = value["documents"].as_array().unwrap();
|
||||
assert!(documents.len() >= 3);
|
||||
}
|
||||
ToolResult::Error(e) => panic!("Failed to list documents: {}", e),
|
||||
}
|
||||
|
||||
// Verify each document has its unique content
|
||||
for (i, doc_id) in doc_ids.iter().enumerate() {
|
||||
let args = json!({"document_id": doc_id});
|
||||
let result = provider.call_tool("extract_text", args).await;
|
||||
|
||||
match result {
|
||||
ToolResult::Success(value) => {
|
||||
let text = value["text"].as_str().unwrap();
|
||||
assert!(text.contains(&format!("Document {} content", i)));
|
||||
}
|
||||
ToolResult::Error(e) => panic!("Failed to extract text from document {}: {}", i, e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_export_to_markdown() {
|
||||
let (provider, temp_dir) = create_test_provider().await;
|
||||
|
||||
let create_result = provider.call_tool("create_document", json!({})).await;
|
||||
let doc_id = match create_result {
|
||||
ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(),
|
||||
_ => panic!("Failed to create document"),
|
||||
};
|
||||
|
||||
// Add content
|
||||
provider.call_tool("add_heading", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "Test Document",
|
||||
"level": 1
|
||||
})).await;
|
||||
|
||||
provider.call_tool("add_paragraph", json!({
|
||||
"document_id": doc_id,
|
||||
"text": "This is a test paragraph."
|
||||
})).await;
|
||||
|
||||
// Export to markdown
|
||||
let output_path = temp_dir.path().join("test_export.md");
|
||||
let args = json!({
|
||||
"document_id": doc_id,
|
||||
"output_path": output_path.to_str().unwrap()
|
||||
});
|
||||
|
||||
let result = provider.call_tool("export_to_markdown", args).await;
|
||||
|
||||
match result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap());
|
||||
assert!(output_path.exists());
|
||||
|
||||
let content = std::fs::read_to_string(&output_path).unwrap();
|
||||
assert!(content.contains("# Test Document"));
|
||||
assert!(content.contains("test paragraph"));
|
||||
}
|
||||
ToolResult::Error(e) => panic!("Expected success, got error: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// Parametrized test using rstest
|
||||
#[rstest]
|
||||
#[case("create_document", json!({}))]
|
||||
#[case("list_documents", json!({}))]
|
||||
#[case("get_security_info", json!({}))]
|
||||
#[tokio::test]
|
||||
async fn test_tools_without_document_id(#[case] tool_name: &str, #[case] args: serde_json::Value) {
|
||||
let (provider, _temp_dir) = create_test_provider().await;
|
||||
|
||||
let result = provider.call_tool(tool_name, args).await;
|
||||
|
||||
// These tools should work without requiring a document_id
|
||||
match result {
|
||||
ToolResult::Success(value) => {
|
||||
assert!(value["success"].as_bool().unwrap_or(false));
|
||||
}
|
||||
ToolResult::Error(e) => panic!("Tool {} failed: {}", tool_name, e),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tool_input_validation() {
|
||||
let (provider, _temp_dir) = create_test_provider().await;
|
||||
|
||||
// Missing required arguments should fail gracefully
|
||||
let result = provider.call_tool("add_paragraph", json!({})).await;
|
||||
|
||||
match result {
|
||||
ToolResult::Success(value) => {
|
||||
// Should fail due to missing document_id
|
||||
assert!(!value["success"].as_bool().unwrap_or(true));
|
||||
}
|
||||
ToolResult::Error(_) => {
|
||||
// This is also acceptable
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,573 @@
|
||||
use anyhow::Result;
|
||||
use docx_mcp::docx_handler::{DocxHandler, DocxStyle, TableData};
|
||||
use docx_mcp::pure_converter::PureRustConverter;
|
||||
use docx_mcp::docx_tools::DocxToolsProvider;
|
||||
use docx_mcp::security::SecurityConfig;
|
||||
use mcp_core::{ToolProvider, ToolResult};
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
const PERFORMANCE_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
const STRESS_TEST_ITERATIONS: usize = 100;
|
||||
|
||||
#[test]
|
||||
fn test_large_document_performance() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let mut handler = DocxHandler::new_with_temp_dir(temp_dir.path()).unwrap();
|
||||
|
||||
let start = Instant::now();
|
||||
let doc_id = handler.create_document().unwrap();
|
||||
let creation_time = start.elapsed();
|
||||
|
||||
println!("Document creation took: {:?}", creation_time);
|
||||
assert!(creation_time < Duration::from_millis(500), "Document creation should be fast");
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
// Add substantial content
|
||||
for i in 0..1000 {
|
||||
if i % 50 == 0 {
|
||||
handler.add_heading(&doc_id, &format!("Section {}", i / 50 + 1), 2)?;
|
||||
}
|
||||
|
||||
let content = format!(
|
||||
"This is paragraph number {} in our performance test. It contains enough text to make the test meaningful and simulate real-world usage patterns. The paragraph includes various punctuation marks, numbers like {}, and other elements that might affect processing performance.",
|
||||
i, i * 7
|
||||
);
|
||||
handler.add_paragraph(&doc_id, &content, None)?;
|
||||
|
||||
// Add a table every 100 paragraphs
|
||||
if i % 100 == 99 {
|
||||
let table_data = TableData {
|
||||
rows: vec![
|
||||
vec!["Item".to_string(), "Value".to_string(), "Status".to_string()],
|
||||
vec![format!("Item {}", i), format!("${}.00", i * 10), "Active".to_string()],
|
||||
],
|
||||
headers: Some(vec!["Item".to_string(), "Value".to_string(), "Status".to_string()]),
|
||||
border_style: Some("single".to_string()),
|
||||
};
|
||||
handler.add_table(&doc_id, table_data)?;
|
||||
}
|
||||
}
|
||||
|
||||
let content_addition_time = start.elapsed();
|
||||
println!("Adding 1000 paragraphs took: {:?}", content_addition_time);
|
||||
assert!(content_addition_time < PERFORMANCE_TIMEOUT, "Content addition took too long");
|
||||
|
||||
// Test text extraction performance
|
||||
let start = Instant::now();
|
||||
let text = handler.extract_text(&doc_id)?;
|
||||
let extraction_time = start.elapsed();
|
||||
|
||||
println!("Text extraction took: {:?}", extraction_time);
|
||||
println!("Extracted text length: {} characters", text.len());
|
||||
assert!(extraction_time < Duration::from_secs(10), "Text extraction should be reasonably fast");
|
||||
assert!(text.len() > 100000, "Should extract substantial amount of text");
|
||||
|
||||
// Test PDF conversion performance
|
||||
let metadata = handler.get_metadata(&doc_id)?;
|
||||
let converter = PureRustConverter::new();
|
||||
let pdf_path = temp_dir.path().join("large_performance_test.pdf");
|
||||
|
||||
let start = Instant::now();
|
||||
converter.convert_docx_to_pdf(&metadata.path, &pdf_path)?;
|
||||
let conversion_time = start.elapsed();
|
||||
|
||||
println!("PDF conversion took: {:?}", conversion_time);
|
||||
assert!(conversion_time < PERFORMANCE_TIMEOUT, "PDF conversion took too long");
|
||||
assert!(pdf_path.exists(), "PDF should be created");
|
||||
|
||||
let pdf_size = std::fs::metadata(&pdf_path)?.len();
|
||||
println!("Generated PDF size: {} bytes", pdf_size);
|
||||
assert!(pdf_size > 50000, "PDF should have substantial size");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_document_stress() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let temp_path = Arc::new(temp_dir.path().to_path_buf());
|
||||
let results = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
let thread_count = 8;
|
||||
let operations_per_thread = 20;
|
||||
|
||||
let start = Instant::now();
|
||||
|
||||
let handles: Vec<_> = (0..thread_count).map(|thread_id| {
|
||||
let temp_path = Arc::clone(&temp_path);
|
||||
let results = Arc::clone(&results);
|
||||
|
||||
thread::spawn(move || -> Result<()> {
|
||||
let mut handler = DocxHandler::new_with_temp_dir(&temp_path)?;
|
||||
let mut local_results = Vec::new();
|
||||
|
||||
for op_id in 0..operations_per_thread {
|
||||
let doc_start = Instant::now();
|
||||
|
||||
// Create document
|
||||
let doc_id = handler.create_document()?;
|
||||
|
||||
// Add varied content
|
||||
handler.add_heading(&doc_id, &format!("Thread {} Document {}", thread_id, op_id), 1)?;
|
||||
|
||||
for i in 0..10 {
|
||||
let content = format!("Thread {} operation {} paragraph {}", thread_id, op_id, i);
|
||||
handler.add_paragraph(&doc_id, &content, None)?;
|
||||
}
|
||||
|
||||
// Add a small table
|
||||
let table_data = TableData {
|
||||
rows: vec![
|
||||
vec!["Col1".to_string(), "Col2".to_string()],
|
||||
vec![format!("T{}", thread_id), format!("O{}", op_id)],
|
||||
],
|
||||
headers: None,
|
||||
border_style: Some("single".to_string()),
|
||||
};
|
||||
handler.add_table(&doc_id, table_data)?;
|
||||
|
||||
// Extract text
|
||||
let text = handler.extract_text(&doc_id)?;
|
||||
assert!(text.contains(&format!("Thread {} Document {}", thread_id, op_id)));
|
||||
|
||||
let doc_duration = doc_start.elapsed();
|
||||
local_results.push((thread_id, op_id, doc_duration));
|
||||
|
||||
// Cleanup
|
||||
handler.close_document(&doc_id)?;
|
||||
}
|
||||
|
||||
// Store results
|
||||
{
|
||||
let mut results_guard = results.lock().unwrap();
|
||||
results_guard.extend(local_results);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}).collect();
|
||||
|
||||
// Wait for all threads
|
||||
for handle in handles {
|
||||
handle.join().unwrap()?;
|
||||
}
|
||||
|
||||
let total_duration = start.elapsed();
|
||||
let results_guard = results.lock().unwrap();
|
||||
|
||||
println!("Concurrent stress test completed in: {:?}", total_duration);
|
||||
println!("Total operations: {}", results_guard.len());
|
||||
|
||||
let avg_duration = results_guard.iter()
|
||||
.map(|(_, _, duration)| duration.as_millis())
|
||||
.sum::<u128>() as f64 / results_guard.len() as f64;
|
||||
|
||||
println!("Average operation duration: {:.2}ms", avg_duration);
|
||||
|
||||
// Verify all operations completed
|
||||
assert_eq!(results_guard.len(), thread_count * operations_per_thread);
|
||||
assert!(total_duration < Duration::from_secs(60), "Stress test took too long");
|
||||
assert!(avg_duration < 1000.0, "Average operation should be under 1 second");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_intensive_operations() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let mut handler = DocxHandler::new_with_temp_dir(temp_dir.path()).unwrap();
|
||||
|
||||
let mut doc_ids = Vec::new();
|
||||
|
||||
// Create many documents simultaneously
|
||||
for i in 0..50 {
|
||||
let doc_id = handler.create_document()?;
|
||||
|
||||
// Add substantial content to each
|
||||
handler.add_heading(&doc_id, &format!("Memory Test Document {}", i), 1)?;
|
||||
|
||||
for j in 0..100 {
|
||||
let content = format!(
|
||||
"Document {} paragraph {}. This paragraph contains substantial text content to test memory usage patterns. It includes various data that might accumulate in memory during processing and needs to be handled efficiently by the system.",
|
||||
i, j
|
||||
);
|
||||
handler.add_paragraph(&doc_id, &content, None)?;
|
||||
}
|
||||
|
||||
// Add a large table
|
||||
let mut table_rows = vec![vec!["ID".to_string(), "Name".to_string(), "Description".to_string()]];
|
||||
for k in 0..20 {
|
||||
table_rows.push(vec![
|
||||
format!("ID-{}", k),
|
||||
format!("Item-{}", k),
|
||||
format!("Description for item {} in document {}", k, i),
|
||||
]);
|
||||
}
|
||||
|
||||
let table_data = TableData {
|
||||
rows: table_rows,
|
||||
headers: Some(vec!["ID".to_string(), "Name".to_string(), "Description".to_string()]),
|
||||
border_style: Some("single".to_string()),
|
||||
};
|
||||
handler.add_table(&doc_id, table_data)?;
|
||||
|
||||
doc_ids.push(doc_id);
|
||||
}
|
||||
|
||||
println!("Created {} documents with substantial content", doc_ids.len());
|
||||
|
||||
// Test that all documents are accessible
|
||||
for (i, doc_id) in doc_ids.iter().enumerate() {
|
||||
let text = handler.extract_text(doc_id)?;
|
||||
assert!(text.contains(&format!("Memory Test Document {}", i)));
|
||||
assert!(text.len() > 10000, "Document should have substantial text");
|
||||
}
|
||||
|
||||
// Test batch operations
|
||||
let start = Instant::now();
|
||||
let mut total_text_length = 0;
|
||||
|
||||
for doc_id in &doc_ids {
|
||||
let text = handler.extract_text(doc_id)?;
|
||||
total_text_length += text.len();
|
||||
}
|
||||
|
||||
let batch_extraction_time = start.elapsed();
|
||||
println!("Batch text extraction took: {:?}", batch_extraction_time);
|
||||
println!("Total extracted text: {} characters", total_text_length);
|
||||
|
||||
assert!(batch_extraction_time < Duration::from_secs(30), "Batch extraction should be reasonable");
|
||||
assert!(total_text_length > 500000, "Should extract substantial total text");
|
||||
|
||||
// Cleanup all documents
|
||||
for doc_id in doc_ids {
|
||||
handler.close_document(&doc_id)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mcp_tool_performance() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
std::env::set_var("TMPDIR", temp_dir.path());
|
||||
|
||||
let provider = DocxToolsProvider::new();
|
||||
let mut operation_times = Vec::new();
|
||||
|
||||
// Test document creation performance
|
||||
let start = Instant::now();
|
||||
let create_result = tokio_test::block_on(async {
|
||||
provider.call_tool("create_document", json!({})).await
|
||||
});
|
||||
let creation_time = start.elapsed();
|
||||
operation_times.push(("create_document", creation_time));
|
||||
|
||||
let doc_id = match create_result {
|
||||
ToolResult::Success(value) => value["document_id"].as_str().unwrap().to_string(),
|
||||
_ => panic!("Failed to create document"),
|
||||
};
|
||||
|
||||
// Test paragraph addition performance
|
||||
let start = Instant::now();
|
||||
for i in 0..100 {
|
||||
let args = json!({
|
||||
"document_id": doc_id,
|
||||
"text": format!("Performance test paragraph {} with substantial content for timing measurements", i)
|
||||
});
|
||||
|
||||
let result = tokio_test::block_on(async {
|
||||
provider.call_tool("add_paragraph", args).await
|
||||
});
|
||||
|
||||
match result {
|
||||
ToolResult::Success(_) => {},
|
||||
ToolResult::Error(e) => panic!("Failed to add paragraph {}: {}", i, e),
|
||||
}
|
||||
}
|
||||
let paragraph_addition_time = start.elapsed();
|
||||
operation_times.push(("add_100_paragraphs", paragraph_addition_time));
|
||||
|
||||
// Test heading performance
|
||||
let start = Instant::now();
|
||||
for level in 1..=6 {
|
||||
let args = json!({
|
||||
"document_id": doc_id,
|
||||
"text": format!("Heading Level {}", level),
|
||||
"level": level
|
||||
});
|
||||
|
||||
tokio_test::block_on(async {
|
||||
provider.call_tool("add_heading", args).await
|
||||
});
|
||||
}
|
||||
let heading_time = start.elapsed();
|
||||
operation_times.push(("add_headings", heading_time));
|
||||
|
||||
// Test table performance
|
||||
let start = Instant::now();
|
||||
let table_args = json!({
|
||||
"document_id": doc_id,
|
||||
"rows": [
|
||||
["Product", "Price", "Quantity", "Total"],
|
||||
["Item 1", "$10.00", "5", "$50.00"],
|
||||
["Item 2", "$15.00", "3", "$45.00"],
|
||||
["Item 3", "$12.00", "7", "$84.00"],
|
||||
["Item 4", "$8.00", "10", "$80.00"]
|
||||
]
|
||||
});
|
||||
|
||||
tokio_test::block_on(async {
|
||||
provider.call_tool("add_table", table_args).await
|
||||
});
|
||||
let table_time = start.elapsed();
|
||||
operation_times.push(("add_table", table_time));
|
||||
|
||||
// Test text extraction performance
|
||||
let start = Instant::now();
|
||||
let extract_args = json!({"document_id": doc_id});
|
||||
let extract_result = tokio_test::block_on(async {
|
||||
provider.call_tool("extract_text", extract_args).await
|
||||
});
|
||||
let extraction_time = start.elapsed();
|
||||
operation_times.push(("extract_text", extraction_time));
|
||||
|
||||
match extract_result {
|
||||
ToolResult::Success(value) => {
|
||||
let text = value["text"].as_str().unwrap();
|
||||
println!("Extracted text length: {} characters", text.len());
|
||||
assert!(text.len() > 5000, "Should extract substantial text");
|
||||
},
|
||||
ToolResult::Error(e) => panic!("Text extraction failed: {}", e),
|
||||
}
|
||||
|
||||
// Test metadata retrieval performance
|
||||
let start = Instant::now();
|
||||
let metadata_args = json!({"document_id": doc_id});
|
||||
tokio_test::block_on(async {
|
||||
provider.call_tool("get_metadata", metadata_args).await
|
||||
});
|
||||
let metadata_time = start.elapsed();
|
||||
operation_times.push(("get_metadata", metadata_time));
|
||||
|
||||
// Print performance results
|
||||
println!("\nMCP Tool Performance Results:");
|
||||
for (operation, duration) in &operation_times {
|
||||
println!("{}: {:?}", operation, duration);
|
||||
}
|
||||
|
||||
// Verify reasonable performance
|
||||
for (operation, duration) in &operation_times {
|
||||
match operation.as_ref() {
|
||||
"create_document" => assert!(duration < &Duration::from_millis(500), "Document creation too slow"),
|
||||
"add_100_paragraphs" => assert!(duration < &Duration::from_secs(10), "Paragraph addition too slow"),
|
||||
"extract_text" => assert!(duration < &Duration::from_secs(5), "Text extraction too slow"),
|
||||
_ => assert!(duration < &Duration::from_secs(2), "Operation {} too slow", operation),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_security_overhead_performance() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
std::env::set_var("TMPDIR", temp_dir.path());
|
||||
|
||||
// Test with default (permissive) security
|
||||
let default_provider = DocxToolsProvider::new();
|
||||
|
||||
// Test with restrictive security
|
||||
let restrictive_config = SecurityConfig {
|
||||
readonly_mode: true,
|
||||
sandbox_mode: true,
|
||||
max_document_size: 1024 * 1024, // 1MB
|
||||
max_open_documents: 10,
|
||||
allow_external_tools: false,
|
||||
allow_network: false,
|
||||
..Default::default()
|
||||
};
|
||||
let restrictive_provider = DocxToolsProvider::new_with_security(restrictive_config);
|
||||
|
||||
let operations = vec![
|
||||
("list_documents", json!({})),
|
||||
("get_security_info", json!({})),
|
||||
];
|
||||
|
||||
for (operation, args) in operations {
|
||||
// Test default provider
|
||||
let start = Instant::now();
|
||||
let _result = tokio_test::block_on(async {
|
||||
default_provider.call_tool(operation, args.clone()).await
|
||||
});
|
||||
let default_time = start.elapsed();
|
||||
|
||||
// Test restrictive provider
|
||||
let start = Instant::now();
|
||||
let _result = tokio_test::block_on(async {
|
||||
restrictive_provider.call_tool(operation, args.clone()).await
|
||||
});
|
||||
let restrictive_time = start.elapsed();
|
||||
|
||||
println!("Operation {}: Default={:?}, Restrictive={:?}",
|
||||
operation, default_time, restrictive_time);
|
||||
|
||||
// Security overhead should be minimal
|
||||
let overhead_ratio = restrictive_time.as_nanos() as f64 / default_time.as_nanos() as f64;
|
||||
assert!(overhead_ratio < 3.0, "Security overhead too high for {}: {}x", operation, overhead_ratio);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conversion_performance_scaling() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let converter = PureRustConverter::new();
|
||||
|
||||
let document_sizes = vec![10, 50, 100, 250];
|
||||
let mut performance_data = Vec::new();
|
||||
|
||||
for &size in &document_sizes {
|
||||
let mut handler = DocxHandler::new_with_temp_dir(temp_dir.path())?;
|
||||
let doc_id = handler.create_document()?;
|
||||
|
||||
// Create document with specified number of paragraphs
|
||||
handler.add_heading(&doc_id, &format!("Test Document - {} paragraphs", size), 1)?;
|
||||
|
||||
for i in 0..size {
|
||||
let content = format!("Paragraph {} content for performance scaling test. This paragraph contains enough text to make the performance test meaningful and realistic.", i);
|
||||
handler.add_paragraph(&doc_id, &content, None)?;
|
||||
|
||||
if i % 20 == 19 {
|
||||
handler.add_heading(&doc_id, &format!("Section {}", i / 20 + 1), 2)?;
|
||||
}
|
||||
}
|
||||
|
||||
let metadata = handler.get_metadata(&doc_id)?;
|
||||
|
||||
// Test text extraction scaling
|
||||
let start = Instant::now();
|
||||
let text = handler.extract_text(&doc_id)?;
|
||||
let extraction_time = start.elapsed();
|
||||
|
||||
// Test PDF conversion scaling
|
||||
let pdf_path = temp_dir.path().join(format!("scale_test_{}.pdf", size));
|
||||
let start = Instant::now();
|
||||
converter.convert_docx_to_pdf(&metadata.path, &pdf_path)?;
|
||||
let conversion_time = start.elapsed();
|
||||
|
||||
performance_data.push((size, text.len(), extraction_time, conversion_time));
|
||||
|
||||
println!("Size: {} paragraphs, Text: {} chars, Extract: {:?}, Convert: {:?}",
|
||||
size, text.len(), extraction_time, conversion_time);
|
||||
|
||||
handler.close_document(&doc_id)?;
|
||||
}
|
||||
|
||||
// Analyze scaling behavior
|
||||
for i in 1..performance_data.len() {
|
||||
let (prev_size, _, prev_extract, prev_convert) = performance_data[i-1];
|
||||
let (curr_size, _, curr_extract, curr_convert) = performance_data[i];
|
||||
|
||||
let size_ratio = curr_size as f64 / prev_size as f64;
|
||||
let extract_ratio = curr_extract.as_nanos() as f64 / prev_extract.as_nanos() as f64;
|
||||
let convert_ratio = curr_convert.as_nanos() as f64 / prev_convert.as_nanos() as f64;
|
||||
|
||||
println!("Size {}→{}: Extract scaling {:.2}, Convert scaling {:.2}",
|
||||
prev_size, curr_size, extract_ratio / size_ratio, convert_ratio / size_ratio);
|
||||
|
||||
// Performance should scale reasonably (not exponentially)
|
||||
assert!(extract_ratio / size_ratio < 3.0, "Text extraction scaling too poor");
|
||||
assert!(convert_ratio / size_ratio < 5.0, "PDF conversion scaling too poor");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_handling_performance() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
std::env::set_var("TMPDIR", temp_dir.path());
|
||||
|
||||
let provider = DocxToolsProvider::new();
|
||||
let error_operations = vec![
|
||||
("extract_text", json!({"document_id": "nonexistent"})),
|
||||
("add_paragraph", json!({"document_id": "fake", "text": "test"})),
|
||||
("get_metadata", json!({"document_id": "invalid"})),
|
||||
("unknown_tool", json!({})),
|
||||
];
|
||||
|
||||
for (operation, args) in error_operations {
|
||||
let start = Instant::now();
|
||||
|
||||
let result = tokio_test::block_on(async {
|
||||
provider.call_tool(operation, args).await
|
||||
});
|
||||
|
||||
let error_time = start.elapsed();
|
||||
println!("Error handling for {}: {:?}", operation, error_time);
|
||||
|
||||
// Error handling should be fast
|
||||
assert!(error_time < Duration::from_millis(100),
|
||||
"Error handling for {} too slow: {:?}", operation, error_time);
|
||||
|
||||
// Should return appropriate error
|
||||
match result {
|
||||
ToolResult::Error(_) | ToolResult::Success(_) => {}, // Both are acceptable for error cases
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resource_cleanup_performance() -> Result<()> {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let mut handler = DocxHandler::new_with_temp_dir(temp_dir.path())?;
|
||||
|
||||
let document_count = 50;
|
||||
let mut doc_ids = Vec::new();
|
||||
|
||||
// Create many documents
|
||||
let creation_start = Instant::now();
|
||||
for i in 0..document_count {
|
||||
let doc_id = handler.create_document()?;
|
||||
handler.add_paragraph(&doc_id, &format!("Document {} content", i), None)?;
|
||||
doc_ids.push(doc_id);
|
||||
}
|
||||
let creation_time = creation_start.elapsed();
|
||||
|
||||
println!("Created {} documents in {:?}", document_count, creation_time);
|
||||
|
||||
// Verify all documents exist
|
||||
let initial_count = handler.list_documents().len();
|
||||
assert_eq!(initial_count, document_count);
|
||||
|
||||
// Test cleanup performance
|
||||
let cleanup_start = Instant::now();
|
||||
for doc_id in doc_ids {
|
||||
handler.close_document(&doc_id)?;
|
||||
}
|
||||
let cleanup_time = cleanup_start.elapsed();
|
||||
|
||||
println!("Cleaned up {} documents in {:?}", document_count, cleanup_time);
|
||||
|
||||
// Verify cleanup worked
|
||||
let final_count = handler.list_documents().len();
|
||||
assert_eq!(final_count, 0);
|
||||
|
||||
// Cleanup should be reasonably fast
|
||||
assert!(cleanup_time < Duration::from_secs(5), "Cleanup took too long");
|
||||
|
||||
let avg_cleanup_time = cleanup_time.as_nanos() / document_count as u128;
|
||||
println!("Average cleanup time per document: {}ns", avg_cleanup_time);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
use docx_mcp::security::{SecurityConfig, SecurityMiddleware, SecurityError};
|
||||
use serde_json::json;
|
||||
use std::collections::HashSet;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_security_config() {
|
||||
let config = SecurityConfig::default();
|
||||
|
||||
assert!(!config.readonly_mode);
|
||||
assert!(config.command_whitelist.is_none());
|
||||
assert!(config.command_blacklist.is_none());
|
||||
assert_eq!(config.max_document_size, 100 * 1024 * 1024);
|
||||
assert_eq!(config.max_open_documents, 50);
|
||||
assert!(config.allow_external_tools);
|
||||
assert!(config.allow_network);
|
||||
assert!(!config.sandbox_mode);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readonly_mode_allows_only_safe_commands() {
|
||||
let config = SecurityConfig {
|
||||
readonly_mode: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Should allow readonly commands
|
||||
assert!(config.is_command_allowed("open_document"));
|
||||
assert!(config.is_command_allowed("extract_text"));
|
||||
assert!(config.is_command_allowed("get_metadata"));
|
||||
assert!(config.is_command_allowed("search_text"));
|
||||
assert!(config.is_command_allowed("export_to_markdown"));
|
||||
|
||||
// Should block write commands
|
||||
assert!(!config.is_command_allowed("create_document"));
|
||||
assert!(!config.is_command_allowed("add_paragraph"));
|
||||
assert!(!config.is_command_allowed("save_document"));
|
||||
assert!(!config.is_command_allowed("find_and_replace"));
|
||||
assert!(!config.is_command_allowed("convert_to_pdf"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_whitelist() {
|
||||
let mut whitelist = HashSet::new();
|
||||
whitelist.insert("open_document".to_string());
|
||||
whitelist.insert("extract_text".to_string());
|
||||
|
||||
let config = SecurityConfig {
|
||||
command_whitelist: Some(whitelist),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Should allow whitelisted commands
|
||||
assert!(config.is_command_allowed("open_document"));
|
||||
assert!(config.is_command_allowed("extract_text"));
|
||||
|
||||
// Should block non-whitelisted commands
|
||||
assert!(!config.is_command_allowed("create_document"));
|
||||
assert!(!config.is_command_allowed("add_paragraph"));
|
||||
assert!(!config.is_command_allowed("get_metadata"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_command_blacklist() {
|
||||
let mut blacklist = HashSet::new();
|
||||
blacklist.insert("save_document".to_string());
|
||||
blacklist.insert("convert_to_pdf".to_string());
|
||||
|
||||
let config = SecurityConfig {
|
||||
command_blacklist: Some(blacklist),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Should allow non-blacklisted commands
|
||||
assert!(config.is_command_allowed("open_document"));
|
||||
assert!(config.is_command_allowed("extract_text"));
|
||||
assert!(config.is_command_allowed("add_paragraph"));
|
||||
|
||||
// Should block blacklisted commands
|
||||
assert!(!config.is_command_allowed("save_document"));
|
||||
assert!(!config.is_command_allowed("convert_to_pdf"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_whitelist_overrides_blacklist() {
|
||||
let mut whitelist = HashSet::new();
|
||||
whitelist.insert("save_document".to_string());
|
||||
|
||||
let mut blacklist = HashSet::new();
|
||||
blacklist.insert("save_document".to_string());
|
||||
|
||||
let config = SecurityConfig {
|
||||
command_whitelist: Some(whitelist),
|
||||
command_blacklist: Some(blacklist),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Whitelist should take precedence
|
||||
assert!(config.is_command_allowed("save_document"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_external_tools_restriction() {
|
||||
let config = SecurityConfig {
|
||||
allow_external_tools: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Should block conversion commands that might use external tools
|
||||
assert!(!config.is_command_allowed("convert_to_pdf"));
|
||||
assert!(!config.is_command_allowed("convert_to_images"));
|
||||
|
||||
// Should allow other commands
|
||||
assert!(config.is_command_allowed("open_document"));
|
||||
assert!(config.is_command_allowed("add_paragraph"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_security_middleware_command_check() {
|
||||
let config = SecurityConfig {
|
||||
readonly_mode: true,
|
||||
..Default::default()
|
||||
};
|
||||
let middleware = SecurityMiddleware::new(config);
|
||||
|
||||
let safe_args = json!({"document_id": "test"});
|
||||
|
||||
// Should pass readonly commands
|
||||
let result = middleware.check_command("extract_text", &safe_args);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Should fail write commands
|
||||
let result = middleware.check_command("add_paragraph", &safe_args);
|
||||
assert!(matches!(result, Err(SecurityError::CommandNotAllowed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sandbox_mode_path_restrictions() {
|
||||
let config = SecurityConfig {
|
||||
sandbox_mode: true,
|
||||
..Default::default()
|
||||
};
|
||||
let middleware = SecurityMiddleware::new(config);
|
||||
|
||||
// Should allow temp directory paths
|
||||
let temp_args = json!({"path": "/tmp/docx-mcp/test.docx"});
|
||||
let result = middleware.check_command("open_document", &temp_args);
|
||||
assert!(result.is_ok());
|
||||
|
||||
// Should block paths outside temp directory
|
||||
let home_args = json!({"path": "/home/user/documents/test.docx"});
|
||||
let result = middleware.check_command("open_document", &home_args);
|
||||
assert!(matches!(result, Err(SecurityError::PathNotAllowed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_size_limits() {
|
||||
use tempfile::NamedTempFile;
|
||||
use std::io::Write;
|
||||
|
||||
let config = SecurityConfig {
|
||||
max_document_size: 100, // 100 bytes limit
|
||||
..Default::default()
|
||||
};
|
||||
let middleware = SecurityMiddleware::new(config);
|
||||
|
||||
// Create a test file larger than limit
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
let large_content = vec![0u8; 200]; // 200 bytes
|
||||
temp_file.write_all(&large_content).unwrap();
|
||||
temp_file.flush().unwrap();
|
||||
|
||||
let args = json!({"path": temp_file.path().to_str().unwrap()});
|
||||
let result = middleware.check_command("open_document", &args);
|
||||
|
||||
assert!(matches!(result, Err(SecurityError::FileTooLarge { .. })));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_readonly_commands_list() {
|
||||
let readonly_commands = SecurityConfig::get_readonly_commands();
|
||||
|
||||
// Should include expected readonly commands
|
||||
assert!(readonly_commands.contains("open_document"));
|
||||
assert!(readonly_commands.contains("extract_text"));
|
||||
assert!(readonly_commands.contains("get_metadata"));
|
||||
assert!(readonly_commands.contains("search_text"));
|
||||
assert!(readonly_commands.contains("analyze_formatting"));
|
||||
|
||||
// Should not include write commands
|
||||
assert!(!readonly_commands.contains("create_document"));
|
||||
assert!(!readonly_commands.contains("add_paragraph"));
|
||||
assert!(!readonly_commands.contains("save_document"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_commands_list() {
|
||||
let write_commands = SecurityConfig::get_write_commands();
|
||||
|
||||
// Should include expected write commands
|
||||
assert!(write_commands.contains("create_document"));
|
||||
assert!(write_commands.contains("add_paragraph"));
|
||||
assert!(write_commands.contains("save_document"));
|
||||
assert!(write_commands.contains("find_and_replace"));
|
||||
|
||||
// Should not include readonly commands
|
||||
assert!(!write_commands.contains("open_document"));
|
||||
assert!(!write_commands.contains("extract_text"));
|
||||
assert!(!write_commands.contains("get_metadata"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_security_summary() {
|
||||
let config = SecurityConfig {
|
||||
readonly_mode: true,
|
||||
sandbox_mode: true,
|
||||
allow_external_tools: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let summary = config.get_summary();
|
||||
assert!(summary.contains("READONLY MODE"));
|
||||
assert!(summary.contains("SANDBOX MODE"));
|
||||
assert!(summary.contains("No external tools"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_combined_security_modes() {
|
||||
let mut whitelist = HashSet::new();
|
||||
whitelist.insert("open_document".to_string());
|
||||
whitelist.insert("extract_text".to_string());
|
||||
|
||||
let config = SecurityConfig {
|
||||
readonly_mode: true,
|
||||
sandbox_mode: true,
|
||||
command_whitelist: Some(whitelist),
|
||||
allow_external_tools: false,
|
||||
allow_network: false,
|
||||
max_document_size: 1024,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Should only allow whitelisted readonly commands
|
||||
assert!(config.is_command_allowed("open_document"));
|
||||
assert!(config.is_command_allowed("extract_text"));
|
||||
|
||||
// Should block everything else
|
||||
assert!(!config.is_command_allowed("get_metadata")); // Not in whitelist
|
||||
assert!(!config.is_command_allowed("add_paragraph")); // Not readonly
|
||||
assert!(!config.is_command_allowed("convert_to_pdf")); // External tools disabled
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recursive_path_argument_checking() {
|
||||
let config = SecurityConfig {
|
||||
sandbox_mode: true,
|
||||
..Default::default()
|
||||
};
|
||||
let middleware = SecurityMiddleware::new(config);
|
||||
|
||||
// Complex nested arguments with paths
|
||||
let nested_args = json!({
|
||||
"document_id": "test",
|
||||
"options": {
|
||||
"output_path": "/home/user/bad/path.docx",
|
||||
"settings": {
|
||||
"temp_file": "/tmp/safe/path.tmp"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"/home/user/another/bad/path.docx",
|
||||
"/tmp/docx-mcp/safe/path.docx"
|
||||
]
|
||||
});
|
||||
|
||||
let result = middleware.check_command("some_command", &nested_args);
|
||||
assert!(matches!(result, Err(SecurityError::PathNotAllowed(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_security_error_messages() {
|
||||
let error = SecurityError::CommandNotAllowed("dangerous_command".to_string());
|
||||
assert!(error.to_string().contains("dangerous_command"));
|
||||
|
||||
let error = SecurityError::PathNotAllowed("/bad/path".to_string());
|
||||
assert!(error.to_string().contains("/bad/path"));
|
||||
|
||||
let error = SecurityError::FileTooLarge { size: 2000, max_size: 1000 };
|
||||
assert!(error.to_string().contains("2000"));
|
||||
assert!(error.to_string().contains("1000"));
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn readonly_config() -> SecurityConfig {
|
||||
SecurityConfig {
|
||||
readonly_mode: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn sandbox_config() -> SecurityConfig {
|
||||
SecurityConfig {
|
||||
sandbox_mode: true,
|
||||
allow_external_tools: false,
|
||||
allow_network: false,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn restrictive_config() -> SecurityConfig {
|
||||
let mut whitelist = HashSet::new();
|
||||
whitelist.insert("open_document".to_string());
|
||||
whitelist.insert("extract_text".to_string());
|
||||
|
||||
SecurityConfig {
|
||||
readonly_mode: true,
|
||||
sandbox_mode: true,
|
||||
command_whitelist: Some(whitelist),
|
||||
max_document_size: 1024 * 1024, // 1MB
|
||||
max_open_documents: 5,
|
||||
allow_external_tools: false,
|
||||
allow_network: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("open_document", true)]
|
||||
#[case("extract_text", true)]
|
||||
#[case("get_metadata", true)]
|
||||
#[case("create_document", false)]
|
||||
#[case("add_paragraph", false)]
|
||||
#[case("save_document", false)]
|
||||
fn test_readonly_mode_commands(readonly_config: SecurityConfig, #[case] command: &str, #[case] expected: bool) {
|
||||
assert_eq!(readonly_config.is_command_allowed(command), expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("open_document", true)]
|
||||
#[case("extract_text", true)]
|
||||
#[case("add_paragraph", false)] // Not in whitelist
|
||||
#[case("get_metadata", false)] // Not in whitelist
|
||||
fn test_restrictive_mode_commands(restrictive_config: SecurityConfig, #[case] command: &str, #[case] expected: bool) {
|
||||
assert_eq!(restrictive_config.is_command_allowed(command), expected);
|
||||
}
|
||||
Reference in New Issue
Block a user