mirror of
https://github.com/readur/readur.git
synced 2026-01-06 06:20:17 -06:00
853 lines
29 KiB
Rust
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!");
|
|
} |