(&t.text)
+ .unwrap_or_else(|_| json!({"success": false, "error": t.text.clone()})),
+ _ => json!({"success": false, "error": "non-text response"}),
+ };
+ if val.get("success").and_then(|v| v.as_bool()).unwrap_or(false) {
+ ToolResult::Success(val)
+ } else {
+ ToolResult::Error(val.get("error").and_then(|v| v.as_str()).unwrap_or("Unknown error").to_string())
+ }
+}
+
+async fn create_test_provider() -> (DocxToolsProvider, TempDir) {
+ let temp_dir = TempDir::new().unwrap();
+ let provider = DocxToolsProvider::with_base_dir(temp_dir.path());
+ (provider, temp_dir)
+}
+
+async fn create_test_provider_with_security(config: SecurityConfig) -> (DocxToolsProvider, TempDir) {
+ let temp_dir = TempDir::new().unwrap();
+ let provider = DocxToolsProvider::with_base_dir_and_security(temp_dir.path(), 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 = tool_result(&provider, "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 = tool_result(&provider, "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 = tool_result(&provider, "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 = tool_result(&provider, "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 = tool_result(&provider, "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 = tool_result(&provider, "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 = tool_result(&provider, "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 = tool_result(&provider, "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 = tool_result(&provider, "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 = tool_result(&provider, "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 = tool_result(&provider, "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 = tool_result(&provider, "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 = tool_result(&provider, "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 = tool_result(&provider, "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 = tool_result(&provider, "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 = tool_result(&provider, "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 = tool_result(&provider, "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."
+ });
+ tool_result(&provider, "add_paragraph", add_args).await;
+
+ // Search for text
+ let search_args = json!({
+ "document_id": doc_id,
+ "search_term": "test",
+ "case_sensitive": false
+ });
+
+ let result = tool_result(&provider, "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 = tool_result(&provider, "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
+ });
+ tool_result(&provider, "add_paragraph", add_args).await;
+
+ let args = json!({"document_id": doc_id});
+ let result = tool_result(&provider, "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 = tool_result(&provider, "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 = tool_result(&provider, "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 = tool_result(&provider, "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 = tool_result(&provider, "nonexistent_tool", json!({})).await;
+
+ match result {
+ ToolResult::Success(value) => {
+ assert!(!value["success"].as_bool().unwrap());
+ let err = value["error"].as_str().unwrap();
+ assert!(err.contains("Unknown or unsupported tool") || err.contains("Unknown tool"));
+ }
+ ToolResult::Error(e) => {
+ assert!(e.contains("Unknown or unsupported tool") || 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 = tool_result(&provider, "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)
+ });
+ tool_result(&provider, "add_paragraph", args).await;
+
+ doc_ids.push(doc_id);
+ }
+
+ // List documents
+ let list_result = tool_result(&provider, "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 = tool_result(&provider, "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 = tool_result(&provider, "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
+ tool_result(&provider, "add_heading", json!({
+ "document_id": doc_id,
+ "text": "Test Document",
+ "level": 1
+ })).await;
+
+ tool_result(&provider, "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 = tool_result(&provider, "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),
+ }
+}
+
+#[tokio::test]
+async fn test_export_to_html() {
+ let (provider, temp_dir) = create_test_provider().await;
+
+ let create_result = tool_result(&provider, "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
+ tool_result(&provider, "add_heading", json!({
+ "document_id": doc_id,
+ "text": "Test Document",
+ "level": 1
+ })).await;
+ tool_result(&provider, "add_paragraph", json!({
+ "document_id": doc_id,
+ "text": "This is a test paragraph."
+ })).await;
+
+ // Export to HTML
+ let output_path = temp_dir.path().join("test_export.html");
+ let args = json!({
+ "document_id": doc_id,
+ "output_path": output_path.to_str().unwrap()
+ });
+ let result = tool_result(&provider, "export_to_html", args).await;
+ match result {
+ ToolResult::Success(value) => {
+ assert!(value["success"].as_bool().unwrap());
+ assert!(output_path.exists());
+ let html = std::fs::read_to_string(&output_path).unwrap();
+ assert!(html.contains("") || html.contains("") || html.contains("
"));
+ }
+ ToolResult::Error(e) => panic!("Expected success, got error: {}", e),
+ }
+}
+
+#[tokio::test]
+async fn test_get_storage_info_tool() {
+ let (provider, _temp_dir) = create_test_provider().await;
+ // Create a couple of docs to ensure some files exist
+ for _ in 0..2 {
+ let _ = tool_result(&provider, "create_document", json!({})).await;
+ }
+ let result = tool_result(&provider, "get_storage_info", json!({})).await;
+ match result {
+ ToolResult::Success(value) => {
+ assert!(value["success"].as_bool().unwrap());
+ let storage = &value["storage"];
+ assert!(storage["file_count"].is_number());
+ assert!(storage["total_bytes"].is_number());
+ }
+ ToolResult::Error(e) => panic!("get_storage_info failed: {}", e),
+ }
+}
+
+#[tokio::test]
+async fn test_list_tools_includes_new_exports() {
+ let (provider, _temp_dir) = create_test_provider().await;
+ let tools = provider.list_tools().await;
+ let names: Vec<_> = tools.iter().map(|t| t.name.clone()).collect();
+ assert!(names.contains(&"export_to_markdown".to_string()));
+ assert!(names.contains(&"export_to_html".to_string()));
+}
+
+// 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 = tool_result(&provider, 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 = tool_result(&provider, "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
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/performance_tests.rs b/tests/performance_tests.rs
new file mode 100644
index 0000000..8f16aa6
--- /dev/null
+++ b/tests/performance_tests.rs
@@ -0,0 +1,582 @@
+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::types::{CallToolResponse, ToolResponseContent};
+use serde_json::{json, Value};
+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_base_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()),
+ col_widths: None,
+ merges: None,
+ cell_shading: None,
+ };
+ 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_base_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()),
+ col_widths: None,
+ merges: None,
+ cell_shading: None,
+ };
+ 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::() 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_base_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()),
+ col_widths: None,
+ merges: None,
+ cell_shading: None,
+ };
+ 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();
+ let provider = DocxToolsProvider::with_base_dir(temp_dir.path());
+ let mut operation_times = Vec::new();
+
+ // Test document creation performance
+ let start = Instant::now();
+ let create_resp: CallToolResponse = tokio_test::block_on(async {
+ provider.call_tool("create_document", json!({})).await
+ });
+ let create_result = match create_resp.content.get(0) {
+ Some(ToolResponseContent::Text(t)) => serde_json::from_str::(&t.text)
+ .map_err(|e| e.to_string()),
+ _ => Err("non-text response".to_string())
+ };
+ let creation_time = start.elapsed();
+ operation_times.push(("create_document", creation_time));
+
+ let doc_id = match create_result {
+ Ok(value) if value.get("success").and_then(|v| v.as_bool()).unwrap_or(false) => 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: CallToolResponse = tokio_test::block_on(async {
+ provider.call_tool("add_paragraph", args).await
+ });
+ if let Some(ToolResponseContent::Text(t)) = result.content.get(0) {
+ let v: Value = serde_json::from_str(&t.text).unwrap_or(json!({"success": false}));
+ assert!(v.get("success").and_then(|b| b.as_bool()).unwrap_or(false), "Failed to add paragraph {}: {}", i, t.text);
+ } else {
+ panic!("Non-text response for add_paragraph");
+ }
+ }
+ 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_resp: CallToolResponse = 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_resp.content.get(0) {
+ Some(ToolResponseContent::Text(t)) => {
+ let value: Value = serde_json::from_str(&t.text).unwrap();
+ let text = value["text"].as_str().unwrap();
+ println!("Extracted text length: {} characters", text.len());
+ assert!(text.len() > 5000, "Should extract substantial text");
+ },
+ _ => panic!("Text extraction failed"),
+ }
+
+ // 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();
+
+ // Test with default (permissive) security
+ let default_provider = DocxToolsProvider::with_base_dir(temp_dir.path());
+
+ // 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::with_base_dir_and_security(temp_dir.path(), 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 reasonable but may vary on CI; allow up to 15x for very fast baselines
+ let overhead_ratio = restrictive_time.as_nanos() as f64 / default_time.as_nanos() as f64;
+ assert!(overhead_ratio < 15.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_base_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();
+ let provider = DocxToolsProvider::with_base_dir(temp_dir.path());
+ 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
+ // Ensure we got a response shape; don't match legacy types here
+ }
+
+ Ok(())
+}
+
+#[test]
+fn test_resource_cleanup_performance() -> Result<()> {
+ let temp_dir = TempDir::new().unwrap();
+ let mut handler = DocxHandler::new_with_base_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(())
+}
\ No newline at end of file
diff --git a/tests/security_tests.rs b/tests/security_tests.rs
new file mode 100644
index 0000000..660f8ff
--- /dev/null
+++ b/tests/security_tests.rs
@@ -0,0 +1,353 @@
+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),
+ command_blacklist: None,
+ ..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_whitelist: None,
+ 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),
+ command_blacklist: None,
+ 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,
+ command_blacklist: None,
+ ..Default::default()
+ }
+}
+
+#[fixture]
+fn sandbox_config() -> SecurityConfig {
+ SecurityConfig {
+ sandbox_mode: true,
+ allow_external_tools: false,
+ allow_network: false,
+ command_blacklist: None,
+ ..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),
+ command_blacklist: None,
+ 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);
+}
\ No newline at end of file