/*! * Integration Tests for Readur OCR System * * Tests complete user workflows against a running server using Rust's reqwest client. * These tests import and use the same models/types as the main application. */ use reqwest::Client; use serde_json::Value; use std::time::{Duration, Instant}; use tokio::time::sleep; use readur::models::{CreateUser, LoginRequest, LoginResponse, DocumentResponse}; use readur::routes::documents::types::DocumentUploadResponse; fn get_base_url() -> String { std::env::var("API_URL").unwrap_or_else(|_| "http://localhost:8000".to_string()) } const TIMEOUT: Duration = Duration::from_secs(30); /// Integration test client that handles authentication and common operations struct TestClient { client: Client, token: Option, } impl TestClient { fn new() -> Self { Self { client: Client::new(), token: None, } } /// Check if server is running and healthy async fn check_server_health(&self) -> Result<(), Box> { let response = self.client .get(&format!("{}/api/health", get_base_url())) .timeout(Duration::from_secs(5)) .send() .await?; if !response.status().is_success() { return Err("Server health check failed".into()); } let health: Value = response.json().await?; if health["status"] != "ok" { return Err("Server is not healthy".into()); } Ok(()) } /// Register a new user and login to get auth token async fn register_and_login(&mut self, username: &str, email: &str, password: &str) -> Result> { // Register user let user_data = CreateUser { username: username.to_string(), email: email.to_string(), password: password.to_string(), role: Some(readur::models::UserRole::User), }; let register_response = self.client .post(&format!("{}/api/auth/register", get_base_url())) .json(&user_data) .send() .await?; if !register_response.status().is_success() { return Err(format!("Registration failed: {}", register_response.text().await?).into()); } // Login to get token let login_data = LoginRequest { username: username.to_string(), password: password.to_string(), }; let login_response = self.client .post(&format!("{}/api/auth/login", get_base_url())) .json(&login_data) .send() .await?; if !login_response.status().is_success() { return Err(format!("Login failed: {}", login_response.text().await?).into()); } let login_result: LoginResponse = login_response.json().await?; self.token = Some(login_result.token.clone()); Ok(login_result.token) } /// Upload a test document async fn upload_document(&self, content: &str, filename: &str) -> Result> { let token = self.token.as_ref().ok_or("Not authenticated")?; let part = reqwest::multipart::Part::text(content.to_string()) .file_name(filename.to_string()) .mime_str("text/plain")?; let form = reqwest::multipart::Form::new() .part("file", part); let response = self.client .post(&format!("{}/api/documents", get_base_url())) .header("Authorization", format!("Bearer {}", token)) .multipart(form) .send() .await?; if !response.status().is_success() { return Err(format!("Upload failed: {}", response.text().await?).into()); } let document: DocumentUploadResponse = response.json().await?; Ok(document) } /// Wait for OCR processing to complete async fn wait_for_ocr_completion(&self, document_id: &str) -> Result> { let token = self.token.as_ref().ok_or("Not authenticated")?; let start = Instant::now(); while start.elapsed() < TIMEOUT { let response = self.client .get(&format!("{}/api/documents", get_base_url())) .header("Authorization", format!("Bearer {}", token)) .send() .await?; if response.status().is_success() { let response_json: serde_json::Value = response.json().await?; let documents = if let Some(docs_array) = response_json.get("documents").and_then(|d| d.as_array()) { // Documents are in a "documents" key docs_array } else if let Some(docs_array) = response_json.as_array() { // Response is directly an array of documents docs_array } else { return Err("Invalid response format: missing documents array".into()); }; for doc_value in documents { let doc: DocumentResponse = serde_json::from_value(doc_value.clone())?; if doc.id.to_string() == document_id { match doc.ocr_status.as_deref() { Some("completed") => return Ok(true), Some("failed") => return Err("OCR processing failed".into()), _ => { sleep(Duration::from_millis(500)).await; continue; } } } } } sleep(Duration::from_millis(500)).await; } Ok(false) } /// Get OCR text for a document async fn get_ocr_text(&self, document_id: &str) -> Result> { let token = self.token.as_ref().ok_or("Not authenticated")?; let response = self.client .get(&format!("{}/api/documents/{}/ocr", get_base_url(), document_id)) .header("Authorization", format!("Bearer {}", token)) .send() .await?; if !response.status().is_success() { return Err(format!("OCR retrieval failed: {}", response.text().await?).into()); } let ocr_data: Value = response.json().await?; Ok(ocr_data) } } #[tokio::test] async fn test_complete_ocr_workflow() { let mut client = TestClient::new(); // Check server health if let Err(e) = client.check_server_health().await { panic!("Server not running at {}: {}", get_base_url(), e); } // Create test user with unique timestamp let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis(); let username = format!("rust_integration_test_{}", timestamp); let email = format!("rust_test_{}@example.com", timestamp); let password = "testpassword123"; let token = client.register_and_login(&username, &email, password).await .expect("Failed to register and login"); println!("✅ User registered and logged in, token: {}", &token[..20]); // Upload test document let test_content = r#"This is a test document for OCR processing. It contains multiple lines of text. The OCR service should extract this text accurately. Document ID: RUST-INTEGRATION-TEST-001 Date: 2024-01-01 Technology: Rust + Axum + SQLx"#; let document = client.upload_document(test_content, "rust_test.txt").await .expect("Failed to upload document"); println!("✅ Document uploaded: {}", document.id); // Validate document response structure using our types assert!(!document.filename.is_empty()); assert!(document.file_size > 0); assert_eq!(document.mime_type, "text/plain"); // Wait for OCR processing let ocr_completed = client.wait_for_ocr_completion(&document.id.to_string()).await .expect("Failed to wait for OCR completion"); assert!(ocr_completed, "OCR processing did not complete within timeout"); println!("✅ OCR processing completed"); // Retrieve OCR text let ocr_data = client.get_ocr_text(&document.id.to_string()).await .expect("Failed to retrieve OCR text"); // Validate OCR response structure assert_eq!(ocr_data["id"], document.id.to_string()); assert_eq!(ocr_data["filename"], document.filename); assert!(ocr_data["has_ocr_text"].as_bool().unwrap_or(false)); // Validate OCR content if available if let Some(ocr_text) = ocr_data["ocr_text"].as_str() { assert!(!ocr_text.is_empty(), "OCR text should not be empty"); assert!(ocr_text.to_lowercase().contains("test document"), "OCR text should contain expected content"); println!("✅ OCR text extracted: {} characters", ocr_text.len()); // Validate optional fields using Rust type checking if let Some(confidence) = ocr_data["ocr_confidence"].as_f64() { assert!((0.0..=100.0).contains(&confidence), "OCR confidence should be 0-100"); println!("✅ OCR confidence: {:.1}%", confidence); } if let Some(word_count) = ocr_data["ocr_word_count"].as_i64() { assert!(word_count > 0, "Word count should be positive"); println!("✅ OCR word count: {}", word_count); } if let Some(processing_time) = ocr_data["ocr_processing_time_ms"].as_i64() { assert!(processing_time >= 0, "Processing time should be non-negative"); println!("✅ OCR processing time: {}ms", processing_time); } } println!("🎉 Complete OCR workflow test passed!"); } #[tokio::test] async fn test_ocr_error_handling() { let mut client = TestClient::new(); // Test unauthorized access let response = client.client .get(&format!("{}/api/documents/test-id/ocr", get_base_url())) .send() .await .expect("Failed to make request"); assert_eq!(response.status(), 401, "Should return 401 for unauthorized access"); // Test with valid auth but invalid document let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis(); let token = client.register_and_login( &format!("rust_error_test_{}", timestamp), &format!("rust_error_{}@test.com", timestamp), "testpass123" ).await.expect("Failed to register and login"); let response = client.client .get(&format!("{}/api/documents/00000000-0000-0000-0000-000000000000/ocr", get_base_url())) .header("Authorization", format!("Bearer {}", token)) .send() .await .expect("Failed to make request"); assert_eq!(response.status(), 404, "Should return 404 for non-existent document"); println!("✅ Error handling tests passed!"); } #[tokio::test] async fn test_health_endpoint() { let client = TestClient::new(); client.check_server_health().await .expect("Health check should pass"); println!("✅ Health endpoint test passed!"); } #[tokio::test] async fn test_document_list_structure() { let mut client = TestClient::new(); // Register and login let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_millis(); let _token = client.register_and_login( &format!("rust_list_test_{}", timestamp), &format!("rust_list_{}@test.com", timestamp), "testpass123" ).await.expect("Failed to register and login"); // Upload a document let document = client.upload_document("Test content for list", "list_test.txt").await .expect("Failed to upload document"); // Get document list let response = client.client .get(&format!("{}/api/documents", get_base_url())) .header("Authorization", format!("Bearer {}", client.token.as_ref().unwrap())) .send() .await .expect("Failed to get documents"); assert!(response.status().is_success()); // Parse as our DocumentResponse type to ensure structure compatibility let response_json: serde_json::Value = response.json().await .expect("Failed to parse response JSON"); let documents_array = if let Some(docs_array) = response_json.get("documents").and_then(|d| d.as_array()) { // Documents are in a "documents" key docs_array } else if let Some(docs_array) = response_json.as_array() { // Response is directly an array of documents docs_array } else { panic!("Failed to find documents array in response"); }; let documents: Vec = documents_array.iter() .map(|doc_value| serde_json::from_value(doc_value.clone())) .collect::, _>>() .expect("Failed to parse documents as DocumentResponse"); // Find our uploaded document let found_doc = documents.iter().find(|d| d.id.to_string() == document.id.to_string()) .expect("Uploaded document should be in list"); // Validate structure matches our types assert_eq!(found_doc.filename, document.filename); assert_eq!(found_doc.file_size, document.file_size); assert_eq!(found_doc.mime_type, document.mime_type); assert!(found_doc.ocr_status.is_some()); println!("✅ Document list structure test passed!"); } /// Helper function to run all integration tests when server is not available #[tokio::test] async fn test_server_availability() { let client = TestClient::new(); match client.check_server_health().await { Ok(_) => println!("✅ Server is running and healthy"), Err(e) => { println!("⚠️ Server not available: {}", e); println!("To run integration tests, start the server with: cargo run"); // Don't fail the test, just skip } } }