mirror of
https://github.com/readur/readur.git
synced 2025-12-17 20:35:17 -06:00
391 lines
14 KiB
Rust
391 lines
14 KiB
Rust
/*!
|
|
* 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<String>,
|
|
}
|
|
|
|
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<dyn std::error::Error>> {
|
|
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<String, Box<dyn std::error::Error>> {
|
|
// 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<DocumentUploadResponse, Box<dyn std::error::Error>> {
|
|
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<bool, Box<dyn std::error::Error>> {
|
|
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<Value, Box<dyn std::error::Error>> {
|
|
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<DocumentResponse> = documents_array.iter()
|
|
.map(|doc_value| serde_json::from_value(doc_value.clone()))
|
|
.collect::<Result<Vec<_>, _>>()
|
|
.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
|
|
}
|
|
}
|
|
} |