Files
readur/tests/integration_error_handling_edge_cases_tests.rs

853 lines
29 KiB
Rust

/*!
* Error Handling and Edge Cases Integration Tests
*
* Tests comprehensive error scenarios and edge cases including:
* - Network failure recovery
* - Invalid input handling
* - Resource exhaustion scenarios
* - Authentication edge cases
* - File upload edge cases
* - Database constraint violations
* - Malformed request handling
* - Rate limiting and throttling
* - Concurrent operation conflicts
*/
use reqwest::Client;
use serde_json::{json, Value};
use std::time::{Duration, Instant};
use tokio::time::sleep;
use uuid::Uuid;
use readur::models::{CreateUser, LoginRequest, LoginResponse, UserRole};
fn get_base_url() -> String {
std::env::var("API_URL").unwrap_or_else(|_| "http://localhost:8000".to_string())
}
/// Test client for error handling scenarios
struct ErrorHandlingTestClient {
client: Client,
token: Option<String>,
}
impl ErrorHandlingTestClient {
fn new() -> Self {
Self {
client: Client::new(),
token: None,
}
}
fn new_with_timeout(timeout: Duration) -> Self {
let client = Client::builder()
.timeout(timeout)
.build()
.expect("Failed to create client with timeout");
Self {
client,
token: None,
}
}
/// Register and login with potential error handling
async fn safe_register_and_login(&mut self, role: UserRole) -> Result<String, Box<dyn std::error::Error>> {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let random_suffix = uuid::Uuid::new_v4().to_string().replace("-", "")[..8].to_string();
let username = format!("error_test_{}_{}_{}", role.to_string(), timestamp, random_suffix);
let email = format!("error_test_{}@example.com", timestamp);
let password = "testpassword123";
// Register user with retry logic
let mut attempts = 0;
let max_attempts = 3;
while attempts < max_attempts {
let user_data = CreateUser {
username: username.clone(),
email: email.clone(),
password: password.to_string(),
role: Some(role.clone()),
};
match self.client
.post(&format!("{}/api/auth/register", get_base_url()))
.json(&user_data)
.send()
.await
{
Ok(response) => {
if response.status().is_success() {
break;
} else {
attempts += 1;
if attempts >= max_attempts {
return Err(format!("Registration failed after {} attempts: {}", max_attempts, response.text().await?).into());
}
sleep(Duration::from_millis(100 * attempts as u64)).await;
}
}
Err(e) => {
attempts += 1;
if attempts >= max_attempts {
return Err(format!("Registration network error after {} attempts: {}", max_attempts, e).into());
}
sleep(Duration::from_millis(100 * attempts as u64)).await;
}
}
}
// Login with retry logic
let login_data = LoginRequest {
username: username.clone(),
password: password.to_string(),
};
attempts = 0;
while attempts < max_attempts {
match self.client
.post(&format!("{}/api/auth/login", get_base_url()))
.json(&login_data)
.send()
.await
{
Ok(response) => {
if response.status().is_success() {
let login_result: LoginResponse = response.json().await?;
self.token = Some(login_result.token.clone());
return Ok(login_result.token);
} else {
attempts += 1;
if attempts >= max_attempts {
return Err(format!("Login failed after {} attempts: {}", max_attempts, response.text().await?).into());
}
sleep(Duration::from_millis(100 * attempts as u64)).await;
}
}
Err(e) => {
attempts += 1;
if attempts >= max_attempts {
return Err(format!("Login network error after {} attempts: {}", max_attempts, e).into());
}
sleep(Duration::from_millis(100 * attempts as u64)).await;
}
}
}
Err("Failed to login after retries".into())
}
}
#[tokio::test]
async fn test_invalid_authentication_scenarios() {
let client = Client::new();
println!("🔐 Testing invalid authentication scenarios...");
// Test 1: Empty credentials
let empty_login = json!({
"username": "",
"password": ""
});
let response = client
.post(&format!("{}/api/auth/login", get_base_url()))
.json(&empty_login)
.send()
.await
.expect("Request should complete");
assert_eq!(response.status(), 401);
println!("✅ Empty credentials properly rejected");
// Test 2: Invalid username format
let invalid_username = json!({
"username": "user@with@multiple@ats",
"password": "validpassword123"
});
let response = client
.post(&format!("{}/api/auth/login", get_base_url()))
.json(&invalid_username)
.send()
.await
.expect("Request should complete");
assert!(!response.status().is_success());
println!("✅ Invalid username format properly rejected");
// Test 3: SQL injection attempt in login
let sql_injection = json!({
"username": "admin'; DROP TABLE users; --",
"password": "password"
});
let response = client
.post(&format!("{}/api/auth/login", get_base_url()))
.json(&sql_injection)
.send()
.await
.expect("Request should complete");
assert!(!response.status().is_success());
println!("✅ SQL injection attempt in login properly rejected");
// Test 4: Extremely long credentials
let long_username = "a".repeat(10000);
let long_password = "b".repeat(10000);
let long_creds = json!({
"username": long_username,
"password": long_password
});
let response = client
.post(&format!("{}/api/auth/login", get_base_url()))
.json(&long_creds)
.send()
.await
.expect("Request should complete");
assert!(!response.status().is_success());
println!("✅ Extremely long credentials properly rejected");
// Test 5: Invalid JWT token format
let invalid_token_response = client
.get(&format!("{}/api/auth/me", get_base_url()))
.header("Authorization", "Bearer invalid-jwt-token-format")
.send()
.await
.expect("Request should complete");
assert_eq!(invalid_token_response.status(), 401);
println!("✅ Invalid JWT token properly rejected");
// Test 6: Malformed Authorization header
let malformed_auth_response = client
.get(&format!("{}/api/auth/me", get_base_url()))
.header("Authorization", "InvalidFormat token")
.send()
.await
.expect("Request should complete");
assert_eq!(malformed_auth_response.status(), 401);
println!("✅ Malformed Authorization header properly rejected");
println!("🎉 Invalid authentication scenarios test passed!");
}
#[tokio::test]
async fn test_malformed_request_handling() {
let mut client = ErrorHandlingTestClient::new();
// Setup a valid user for testing authenticated endpoints
client.safe_register_and_login(UserRole::User).await
.expect("Failed to setup test user");
let token = client.token.as_ref().unwrap();
println!("🔧 Testing malformed request handling...");
// Test 1: Invalid JSON in request body
let invalid_json_response = client.client
.post(&format!("{}/api/sources", get_base_url()))
.header("Authorization", format!("Bearer {}", token))
.header("Content-Type", "application/json")
.body("{invalid json syntax")
.send()
.await
.expect("Request should complete");
assert_eq!(invalid_json_response.status(), 400);
println!("✅ Invalid JSON properly rejected");
// Test 2: Missing required fields
let missing_fields = json!({
"name": "Test Source"
// Missing source_type and config
});
let response = client.client
.post(&format!("{}/api/sources", get_base_url()))
.header("Authorization", format!("Bearer {}", token))
.json(&missing_fields)
.send()
.await
.expect("Request should complete");
assert!(!response.status().is_success());
println!("✅ Missing required fields properly rejected");
// Test 3: Invalid enum values
let invalid_enum = json!({
"name": "Test Source",
"source_type": "invalid_source_type",
"config": {}
});
let response = client.client
.post(&format!("{}/api/sources", get_base_url()))
.header("Authorization", format!("Bearer {}", token))
.json(&invalid_enum)
.send()
.await
.expect("Request should complete");
assert!(!response.status().is_success());
println!("✅ Invalid enum values properly rejected");
// Test 4: Nested object validation
let invalid_nested = json!({
"name": "Test Source",
"source_type": "webdav",
"config": {
"server_url": "not-a-valid-url",
"username": "",
"password": "",
"sync_interval_minutes": -1 // Invalid negative value
}
});
let response = client.client
.post(&format!("{}/api/sources", get_base_url()))
.header("Authorization", format!("Bearer {}", token))
.json(&invalid_nested)
.send()
.await
.expect("Request should complete");
assert!(!response.status().is_success());
println!("✅ Invalid nested object validation working");
// Test 5: Extra unexpected fields (should be ignored gracefully)
let extra_fields = json!({
"name": "Test Source",
"source_type": "webdav",
"config": {
"server_url": "https://valid-url.com",
"username": "testuser",
"password": "testpass",
"auto_sync": true,
"sync_interval_minutes": 60,
"watch_folders": ["/Documents"],
"file_extensions": [".pdf"]
},
"unexpected_field": "should be ignored",
"another_extra": 12345
});
let response = client.client
.post(&format!("{}/api/sources", get_base_url()))
.header("Authorization", format!("Bearer {}", token))
.json(&extra_fields)
.send()
.await
.expect("Request should complete");
// This might succeed if the API gracefully ignores extra fields
println!("✅ Extra fields handling: status {}", response.status());
println!("🎉 Malformed request handling test passed!");
}
#[tokio::test]
async fn test_file_upload_edge_cases() {
let mut client = ErrorHandlingTestClient::new();
client.safe_register_and_login(UserRole::User).await
.expect("Failed to setup test user");
let token = client.token.as_ref().unwrap();
println!("📁 Testing file upload edge cases...");
// Test 1: Empty file upload
let empty_part = reqwest::multipart::Part::text("")
.file_name("empty.txt")
.mime_str("text/plain")
.expect("Failed to create empty part");
let empty_form = reqwest::multipart::Form::new()
.part("file", empty_part);
let response = client.client
.post(&format!("{}/api/documents", get_base_url()))
.header("Authorization", format!("Bearer {}", token))
.multipart(empty_form)
.send()
.await
.expect("Request should complete");
// Empty files might be rejected or accepted depending on implementation
println!("✅ Empty file upload: status {}", response.status());
// Test 2: Extremely large filename
let long_filename = format!("{}.txt", "a".repeat(1000));
let long_filename_part = reqwest::multipart::Part::text("content")
.file_name(long_filename)
.mime_str("text/plain")
.expect("Failed to create long filename part");
let long_filename_form = reqwest::multipart::Form::new()
.part("file", long_filename_part);
let response = client.client
.post(&format!("{}/api/documents", get_base_url()))
.header("Authorization", format!("Bearer {}", token))
.multipart(long_filename_form)
.send()
.await
.expect("Request should complete");
println!("✅ Long filename upload: status {}", response.status());
// Test 3: Filename with special characters
let special_filename = "test<>:\"|?*.txt";
let special_filename_part = reqwest::multipart::Part::text("content")
.file_name(special_filename.to_string())
.mime_str("text/plain")
.expect("Failed to create special filename part");
let special_filename_form = reqwest::multipart::Form::new()
.part("file", special_filename_part);
let response = client.client
.post(&format!("{}/api/documents", get_base_url()))
.header("Authorization", format!("Bearer {}", token))
.multipart(special_filename_form)
.send()
.await
.expect("Request should complete");
println!("✅ Special characters filename: status {}", response.status());
// Test 4: Missing file part
let no_file_form = reqwest::multipart::Form::new()
.text("not_file", "some text");
let response = client.client
.post(&format!("{}/api/documents", get_base_url()))
.header("Authorization", format!("Bearer {}", token))
.multipart(no_file_form)
.send()
.await
.expect("Request should complete");
assert!(!response.status().is_success());
println!("✅ Missing file part properly rejected");
// Test 5: Multiple files (if not supported)
let file1 = reqwest::multipart::Part::text("content1")
.file_name("file1.txt")
.mime_str("text/plain")
.expect("Failed to create file1 part");
let file2 = reqwest::multipart::Part::text("content2")
.file_name("file2.txt")
.mime_str("text/plain")
.expect("Failed to create file2 part");
let multi_file_form = reqwest::multipart::Form::new()
.part("file", file1)
.part("file2", file2);
let response = client.client
.post(&format!("{}/api/documents", get_base_url()))
.header("Authorization", format!("Bearer {}", token))
.multipart(multi_file_form)
.send()
.await
.expect("Request should complete");
println!("✅ Multiple files upload: status {}", response.status());
// Test 6: Invalid MIME type
let invalid_mime_part = reqwest::multipart::Part::text("content")
.file_name("test.txt")
.mime_str("invalid/mime-type");
if let Ok(part) = invalid_mime_part {
let invalid_mime_form = reqwest::multipart::Form::new()
.part("file", part);
let response = client.client
.post(&format!("{}/api/documents", get_base_url()))
.header("Authorization", format!("Bearer {}", token))
.multipart(invalid_mime_form)
.send()
.await
.expect("Request should complete");
println!("✅ Invalid MIME type: status {}", response.status());
} else {
println!("✅ Invalid MIME type rejected at client level");
}
println!("🎉 File upload edge cases test passed!");
}
#[tokio::test]
async fn test_concurrent_operation_conflicts() {
println!("🔄 Testing concurrent operation conflicts...");
// Create multiple clients for concurrent operations
let mut clients = Vec::new();
for i in 0..3 {
let mut client = ErrorHandlingTestClient::new();
client.safe_register_and_login(UserRole::User).await
.expect(&format!("Failed to setup client {}", i));
clients.push(client);
}
println!("✅ Setup {} concurrent clients", clients.len());
// Test 1: Concurrent source creation with same name
let mut handles = Vec::new();
for (i, client) in clients.iter().enumerate() {
let token = client.token.clone().unwrap();
let client_ref = &client.client;
let client_clone = client_ref.clone();
let handle = tokio::spawn(async move {
let source_data = json!({
"name": "Concurrent Test Source", // Same name for all
"source_type": "webdav",
"config": {
"server_url": format!("https://server{}.example.com", i),
"username": "testuser",
"password": "testpass",
"auto_sync": false,
"sync_interval_minutes": 60,
"watch_folders": ["/Documents"],
"file_extensions": [".pdf"]
}
});
let response = client_clone
.post(&format!("{}/api/sources", get_base_url()))
.header("Authorization", format!("Bearer {}", token))
.json(&source_data)
.send()
.await
.expect("Request should complete");
(i, response.status(), response.text().await.unwrap_or_default())
});
handles.push(handle);
}
// Wait for all concurrent operations
let mut results = Vec::new();
for handle in handles {
let result = handle.await.expect("Task should complete");
results.push(result);
}
// Analyze results
let successful_count = results.iter()
.filter(|(_, status, _)| status.is_success())
.count();
println!("✅ Concurrent source creation: {}/{} succeeded", successful_count, results.len());
for (i, status, response) in results {
println!(" Client {}: {} - {}", i, status, response.chars().take(100).collect::<String>());
}
// Test 2: Concurrent document uploads
let upload_content = "Concurrent upload test content";
let mut upload_handles = Vec::new();
for (i, client) in clients.iter().enumerate() {
let token = client.token.clone().unwrap();
let client_ref = &client.client;
let client_clone = client_ref.clone();
let content = format!("{} - Client {}", upload_content, i);
let handle = tokio::spawn(async move {
let part = reqwest::multipart::Part::text(content)
.file_name(format!("concurrent_test_{}.txt", i))
.mime_str("text/plain")
.expect("Failed to create part");
let form = reqwest::multipart::Form::new()
.part("file", part);
let response = client_clone
.post(&format!("{}/api/documents", get_base_url()))
.header("Authorization", format!("Bearer {}", token))
.multipart(form)
.send()
.await
.expect("Request should complete");
(i, response.status())
});
upload_handles.push(handle);
}
let mut upload_results = Vec::new();
for handle in upload_handles {
let result = handle.await.expect("Upload task should complete");
upload_results.push(result);
}
let successful_uploads = upload_results.iter()
.filter(|(_, status)| status.is_success())
.count();
println!("✅ Concurrent document uploads: {}/{} succeeded", successful_uploads, upload_results.len());
println!("🎉 Concurrent operation conflicts test passed!");
}
#[tokio::test]
async fn test_network_timeout_scenarios() {
println!("⏱️ Testing network timeout scenarios...");
// Create client with very short timeout
let short_timeout_client = ErrorHandlingTestClient::new_with_timeout(Duration::from_millis(1));
// Test 1: Registration with timeout
let timeout_result = short_timeout_client.client
.post(&format!("{}/api/auth/register", get_base_url()))
.json(&json!({
"username": "timeout_test",
"email": "timeout@example.com",
"password": "password123",
"role": "user"
}))
.send()
.await;
// Should timeout or succeed very quickly
match timeout_result {
Ok(response) => println!("✅ Short timeout request completed: {}", response.status()),
Err(e) => {
if e.is_timeout() {
println!("✅ Short timeout properly triggered");
} else {
println!("✅ Request failed with error: {}", e);
}
}
}
// Test 2: Normal timeout client
let normal_client = ErrorHandlingTestClient::new_with_timeout(Duration::from_secs(30));
// Test long-running operation (document upload with processing)
let start_time = Instant::now();
let large_content = "Large document content. ".repeat(1000);
let part = reqwest::multipart::Part::text(large_content)
.file_name("large_timeout_test.txt")
.mime_str("text/plain")
.expect("Failed to create large part");
let form = reqwest::multipart::Form::new()
.part("file", part);
// This should complete within normal timeout
let upload_result = normal_client.client
.post(&format!("{}/api/documents", get_base_url()))
.multipart(form)
.send()
.await;
let elapsed = start_time.elapsed();
match upload_result {
Ok(response) => {
println!("✅ Large upload completed in {:?}: {}", elapsed, response.status());
}
Err(e) => {
if e.is_timeout() {
println!("✅ Large upload timed out after {:?}", elapsed);
} else {
println!("✅ Large upload failed: {} after {:?}", e, elapsed);
}
}
}
println!("🎉 Network timeout scenarios test passed!");
}
#[tokio::test]
async fn test_resource_exhaustion_simulation() {
println!("💾 Testing resource exhaustion simulation...");
let mut client = ErrorHandlingTestClient::new();
client.safe_register_and_login(UserRole::User).await
.expect("Failed to setup test user");
let token = client.token.as_ref().unwrap();
// Test 1: Rapid successive requests (stress test)
let rapid_request_count = 20;
let mut rapid_handles = Vec::new();
println!("🚀 Sending {} rapid requests...", rapid_request_count);
for i in 0..rapid_request_count {
let token_clone = token.clone();
let client_clone = client.client.clone();
let handle = tokio::spawn(async move {
let start = Instant::now();
let response = client_clone
.get(&format!("{}/api/documents", get_base_url()))
.header("Authorization", format!("Bearer {}", token_clone))
.send()
.await;
let elapsed = start.elapsed();
match response {
Ok(resp) => (i, resp.status(), elapsed, None),
Err(e) => (i, reqwest::StatusCode::from_u16(500).unwrap(), elapsed, Some(e.to_string())),
}
});
rapid_handles.push(handle);
}
let mut rapid_results = Vec::new();
for handle in rapid_handles {
let result = handle.await.expect("Rapid request task should complete");
rapid_results.push(result);
}
// Analyze rapid request results
let successful_rapid = rapid_results.iter()
.filter(|(_, status, _, _)| status.is_success())
.count();
let avg_response_time = rapid_results.iter()
.map(|(_, _, elapsed, _)| *elapsed)
.sum::<Duration>() / rapid_results.len() as u32;
println!("✅ Rapid requests: {}/{} succeeded, avg response time: {:?}",
successful_rapid, rapid_request_count, avg_response_time);
// Test 2: Large payload stress test
println!("📦 Testing large payload handling...");
let very_large_content = "Very large document content for stress testing. ".repeat(10000);
let large_part = reqwest::multipart::Part::text(very_large_content)
.file_name("stress_test_large.txt")
.mime_str("text/plain")
.expect("Failed to create large stress part");
let large_form = reqwest::multipart::Form::new()
.part("file", large_part);
let large_upload_start = Instant::now();
let large_upload_result = client.client
.post(&format!("{}/api/documents", get_base_url()))
.header("Authorization", format!("Bearer {}", token))
.multipart(large_form)
.send()
.await;
let large_upload_elapsed = large_upload_start.elapsed();
match large_upload_result {
Ok(response) => {
println!("✅ Large payload upload: {} in {:?}", response.status(), large_upload_elapsed);
}
Err(e) => {
println!("✅ Large payload upload failed: {} in {:?}", e, large_upload_elapsed);
}
}
println!("🎉 Resource exhaustion simulation test passed!");
}
#[tokio::test]
async fn test_database_constraint_violations() {
println!("🗄️ Testing database constraint violations...");
let mut client = ErrorHandlingTestClient::new();
client.safe_register_and_login(UserRole::User).await
.expect("Failed to setup test user");
let token = client.token.as_ref().unwrap();
// Test 1: Duplicate email registration attempt
let original_user = json!({
"username": "original_user",
"email": "unique@example.com",
"password": "password123",
"role": "user"
});
let register_response = client.client
.post(&format!("{}/api/auth/register", get_base_url()))
.json(&original_user)
.send()
.await
.expect("First registration should complete");
println!("✅ First user registration: {}", register_response.status());
// Try to register another user with the same email
let duplicate_email_user = json!({
"username": "different_username",
"email": "unique@example.com", // Same email
"password": "different_password",
"role": "user"
});
let duplicate_response = client.client
.post(&format!("{}/api/auth/register", get_base_url()))
.json(&duplicate_email_user)
.send()
.await
.expect("Duplicate email registration should complete");
// Should be rejected due to unique constraint
assert!(!duplicate_response.status().is_success());
println!("✅ Duplicate email registration properly rejected: {}", duplicate_response.status());
// Test 2: Creating source with extremely long name
let long_name = "a".repeat(500);
let long_name_source = json!({
"name": long_name,
"source_type": "webdav",
"config": {
"server_url": "https://example.com",
"username": "user",
"password": "pass",
"auto_sync": false,
"sync_interval_minutes": 60,
"watch_folders": ["/Documents"],
"file_extensions": [".pdf"]
}
});
let long_name_response = client.client
.post(&format!("{}/api/sources", get_base_url()))
.header("Authorization", format!("Bearer {}", token))
.json(&long_name_source)
.send()
.await
.expect("Long name source creation should complete");
println!("✅ Long source name: {}", long_name_response.status());
// Test 3: Invalid foreign key reference (if applicable)
let fake_user_id = Uuid::new_v4().to_string();
// This test depends on the API structure, but we can test accessing resources
// that don't exist or belong to other users
let fake_source_id = Uuid::new_v4().to_string();
let fake_source_response = client.client
.get(&format!("{}/api/sources/{}", get_base_url(), fake_source_id))
.header("Authorization", format!("Bearer {}", token))
.send()
.await
.expect("Fake source access should complete");
assert_eq!(fake_source_response.status(), 404);
println!("✅ Non-existent resource access properly rejected: {}", fake_source_response.status());
println!("🎉 Database constraint violations test passed!");
}