Readur/tests/integration_webdav_critical...

478 lines
18 KiB
Rust

use anyhow::Result;
use std::sync::Arc;
use std::time::Instant;
use uuid::Uuid;
use tokio;
use futures::future::join_all;
use readur::{
models::{CreateWebDAVDirectory, CreateUser, UserRole},
db::Database,
test_utils::TestContext,
AppState,
};
/// Integration test that validates the race condition fix
/// Tests that concurrent directory updates are atomic and consistent
#[tokio::test]
async fn test_race_condition_fix_atomic_updates() {
let ctx = TestContext::new().await;
// Ensure cleanup happens even if test fails
let result: Result<()> = async {
let db = Arc::new(ctx.state.db.clone());
// Create a test user first
let create_user = CreateUser {
username: "race_testuser".to_string(),
email: "race@example.com".to_string(),
password: "password123".to_string(),
role: Some(UserRole::User),
};
let user = db.create_user(create_user).await
.expect("Failed to create test user");
let user_id = user.id;
// Create initial directories
let initial_directories = vec![
CreateWebDAVDirectory {
user_id,
directory_path: "/test/dir1".to_string(),
directory_etag: "initial_etag1".to_string(),
file_count: 5,
total_size_bytes: 1024,
},
CreateWebDAVDirectory {
user_id,
directory_path: "/test/dir2".to_string(),
directory_etag: "initial_etag2".to_string(),
file_count: 10,
total_size_bytes: 2048,
},
];
let _ = db.bulk_create_or_update_webdav_directories(&initial_directories).await.unwrap();
// Simulate race condition: multiple tasks trying to update directories simultaneously
let mut handles = vec![];
for i in 0..5 {
let db_clone = Arc::clone(&db);
let handle = tokio::spawn(async move {
let updated_directories = vec![
CreateWebDAVDirectory {
user_id,
directory_path: "/test/dir1".to_string(),
directory_etag: format!("race_etag1_{}", i),
file_count: 5 + i as i64,
total_size_bytes: 1024 + (i * 100) as i64,
},
CreateWebDAVDirectory {
user_id,
directory_path: "/test/dir2".to_string(),
directory_etag: format!("race_etag2_{}", i),
file_count: 10 + i as i64,
total_size_bytes: 2048 + (i * 200) as i64,
},
CreateWebDAVDirectory {
user_id,
directory_path: format!("/test/new_dir_{}", i),
directory_etag: format!("new_etag_{}", i),
file_count: i as i64,
total_size_bytes: (i * 512) as i64,
},
];
// Use the atomic sync operation
db_clone.sync_webdav_directories(user_id, &updated_directories).await
});
handles.push(handle);
}
// Wait for all operations to complete
let results: Vec<_> = join_all(handles).await;
// All operations should succeed (transactions ensure atomicity)
for result in results {
assert!(result.is_ok());
let sync_result = result.unwrap();
assert!(sync_result.is_ok());
}
// Final state should be consistent
let final_directories = db.list_webdav_directories(user_id).await.unwrap();
// Should have 3 directories (dir1, dir2, and one of the new_dir_X)
assert_eq!(final_directories.len(), 3);
// All ETags should be from one consistent transaction
let dir1 = final_directories.iter().find(|d| d.directory_path == "/test/dir1").unwrap();
let dir2 = final_directories.iter().find(|d| d.directory_path == "/test/dir2").unwrap();
// ETags should be from the same transaction (both should end with same number)
let etag1_suffix = dir1.directory_etag.chars().last().unwrap();
let etag2_suffix = dir2.directory_etag.chars().last().unwrap();
assert_eq!(etag1_suffix, etag2_suffix, "ETags should be from same atomic transaction");
Ok(())
}.await;
// Always cleanup database connections and test data
if let Err(e) = ctx.cleanup_and_close().await {
eprintln!("Warning: Test cleanup failed: {}", e);
}
result.unwrap();
}
/// Test that validates directory deletion detection works correctly
#[tokio::test]
async fn test_deletion_detection_fix() {
let ctx = TestContext::new().await;
// Ensure cleanup happens even if test fails
let result: Result<()> = async {
let db = &ctx.state.db;
// Create a test user first
let create_user = CreateUser {
username: "deletion_testuser".to_string(),
email: "deletion@example.com".to_string(),
password: "password123".to_string(),
role: Some(UserRole::User),
};
let user = db.create_user(create_user).await
.expect("Failed to create test user");
let user_id = user.id;
// Create initial directories
let initial_directories = vec![
CreateWebDAVDirectory {
user_id,
directory_path: "/documents/folder1".to_string(),
directory_etag: "etag1".to_string(),
file_count: 5,
total_size_bytes: 1024,
},
CreateWebDAVDirectory {
user_id,
directory_path: "/documents/folder2".to_string(),
directory_etag: "etag2".to_string(),
file_count: 3,
total_size_bytes: 512,
},
CreateWebDAVDirectory {
user_id,
directory_path: "/documents/folder3".to_string(),
directory_etag: "etag3".to_string(),
file_count: 8,
total_size_bytes: 2048,
},
];
let _ = db.bulk_create_or_update_webdav_directories(&initial_directories).await.unwrap();
// Verify all 3 directories exist
let directories_before = db.list_webdav_directories(user_id).await.unwrap();
assert_eq!(directories_before.len(), 3);
// Simulate sync where folder2 and folder3 are deleted from WebDAV server
let current_directories = vec![
CreateWebDAVDirectory {
user_id,
directory_path: "/documents/folder1".to_string(),
directory_etag: "etag1_updated".to_string(), // Updated
file_count: 6,
total_size_bytes: 1200,
},
// folder2 and folder3 are missing (deleted from server)
];
// Use atomic sync which should detect and remove deleted directories
let (updated_directories, deleted_count) = db.sync_webdav_directories(user_id, &current_directories).await.unwrap();
// Should have 1 updated directory and 2 deletions
assert_eq!(updated_directories.len(), 1);
assert_eq!(deleted_count, 2);
// Verify only folder1 remains with updated ETag
let final_directories = db.list_webdav_directories(user_id).await.unwrap();
assert_eq!(final_directories.len(), 1);
assert_eq!(final_directories[0].directory_path, "/documents/folder1");
assert_eq!(final_directories[0].directory_etag, "etag1_updated");
assert_eq!(final_directories[0].file_count, 6);
Ok(())
}.await;
// Always cleanup database connections and test data
if let Err(e) = ctx.cleanup_and_close().await {
eprintln!("Warning: Test cleanup failed: {}", e);
}
result.unwrap();
}
/// Test that validates proper ETag comparison handling
#[tokio::test]
async fn test_etag_comparison_fix() {
use readur::webdav_xml_parser::{compare_etags, weak_compare_etags, strong_compare_etags};
// Test weak vs strong ETag comparison
let strong_etag = "\"abc123\"";
let weak_etag = "W/\"abc123\"";
let different_etag = "\"def456\"";
// Smart comparison should handle weak/strong equivalence
assert!(compare_etags(strong_etag, weak_etag), "Smart comparison should match weak and strong with same content");
assert!(!compare_etags(strong_etag, different_etag), "Smart comparison should reject different content");
// Weak comparison should match regardless of weak/strong
assert!(weak_compare_etags(strong_etag, weak_etag), "Weak comparison should match");
assert!(weak_compare_etags(weak_etag, strong_etag), "Weak comparison should be symmetrical");
// Strong comparison should reject weak ETags
assert!(!strong_compare_etags(strong_etag, weak_etag), "Strong comparison should reject weak ETags");
assert!(!strong_compare_etags(weak_etag, strong_etag), "Strong comparison should reject weak ETags");
assert!(strong_compare_etags(strong_etag, "\"abc123\""), "Strong comparison should match strong ETags");
// Test case sensitivity (ETags should be case-sensitive per RFC)
assert!(!compare_etags("\"ABC123\"", "\"abc123\""), "ETags should be case-sensitive");
// Test various real-world formats
let nextcloud_etag = "\"5f3e7e8a9b2c1d4\"";
let apache_etag = "\"1234-567-890abcdef\"";
let nginx_weak = "W/\"5f3e7e8a\"";
assert!(!compare_etags(nextcloud_etag, apache_etag), "Different ETag values should not match");
assert!(weak_compare_etags(nginx_weak, "\"5f3e7e8a\""), "Weak and strong with same content should match in weak comparison");
}
/// Test performance of bulk operations vs individual operations
#[tokio::test]
async fn test_bulk_operations_performance() {
let ctx = TestContext::new().await;
// Ensure cleanup happens even if test fails
let result: Result<()> = async {
let db = &ctx.state.db;
// Create a test user first
let create_user = CreateUser {
username: "perf_testuser".to_string(),
email: "perf@example.com".to_string(),
password: "password123".to_string(),
role: Some(UserRole::User),
};
let user = db.create_user(create_user).await
.expect("Failed to create test user");
let user_id = user.id;
// Create test data
let test_directories: Vec<_> = (0..100).map(|i| CreateWebDAVDirectory {
user_id,
directory_path: format!("/test/perf/dir{}", i),
directory_etag: format!("etag{}", i),
file_count: i as i64,
total_size_bytes: (i * 1024) as i64,
}).collect();
// Test individual operations (old way)
let start_individual = Instant::now();
for directory in &test_directories {
let _ = db.create_or_update_webdav_directory(directory).await;
}
let individual_duration = start_individual.elapsed();
// Clear data
let _ = db.clear_webdav_directories(user_id).await;
// Test bulk operation (new way)
let start_bulk = Instant::now();
let _ = db.bulk_create_or_update_webdav_directories(&test_directories).await;
let bulk_duration = start_bulk.elapsed();
// Bulk should be faster
assert!(bulk_duration < individual_duration,
"Bulk operations should be faster than individual operations. Bulk: {:?}, Individual: {:?}",
bulk_duration, individual_duration);
// Verify all data was saved correctly
let saved_directories = db.list_webdav_directories(user_id).await.unwrap();
assert_eq!(saved_directories.len(), 100);
Ok(())
}.await;
// Always cleanup database connections and test data
if let Err(e) = ctx.cleanup_and_close().await {
eprintln!("Warning: Test cleanup failed: {}", e);
}
result.unwrap();
}
/// Test transaction rollback behavior
#[tokio::test]
async fn test_transaction_rollback_consistency() {
let ctx = TestContext::new().await;
// Ensure cleanup happens even if test fails
let result: Result<()> = async {
let db = &ctx.state.db;
// Create a test user first
let create_user = CreateUser {
username: "rollback_testuser".to_string(),
email: "rollback@example.com".to_string(),
password: "password123".to_string(),
role: Some(UserRole::User),
};
let user = db.create_user(create_user).await
.expect("Failed to create test user");
let user_id = user.id;
// Create some initial data
let initial_directory = CreateWebDAVDirectory {
user_id,
directory_path: "/test/initial".to_string(),
directory_etag: "initial_etag".to_string(),
file_count: 1,
total_size_bytes: 100,
};
let _ = db.create_or_update_webdav_directory(&initial_directory).await.unwrap();
// Try to create directories where one has invalid data that should cause rollback
let directories_with_failure = vec![
CreateWebDAVDirectory {
user_id,
directory_path: "/test/valid1".to_string(),
directory_etag: "valid_etag1".to_string(),
file_count: 2,
total_size_bytes: 200,
},
CreateWebDAVDirectory {
user_id: Uuid::nil(), // This should cause a constraint violation
directory_path: "/test/invalid".to_string(),
directory_etag: "invalid_etag".to_string(),
file_count: 3,
total_size_bytes: 300,
},
CreateWebDAVDirectory {
user_id,
directory_path: "/test/valid2".to_string(),
directory_etag: "valid_etag2".to_string(),
file_count: 4,
total_size_bytes: 400,
},
];
// This should fail and rollback
let result = db.bulk_create_or_update_webdav_directories(&directories_with_failure).await;
assert!(result.is_err(), "Transaction should fail due to invalid user_id");
// Verify that no partial changes were made - only initial directory should exist
let final_directories = db.list_webdav_directories(user_id).await.unwrap();
assert_eq!(final_directories.len(), 1);
assert_eq!(final_directories[0].directory_path, "/test/initial");
assert_eq!(final_directories[0].directory_etag, "initial_etag");
Ok(())
}.await;
// Always cleanup database connections and test data
if let Err(e) = ctx.cleanup_and_close().await {
eprintln!("Warning: Test cleanup failed: {}", e);
}
result.unwrap();
}
/// Integration test simulating real WebDAV sync scenario
#[tokio::test]
async fn test_full_sync_integration() {
let ctx = TestContext::new().await;
// Ensure cleanup happens even if test fails
let result: Result<()> = async {
let app_state = &ctx.state;
// Create a test user first
let create_user = CreateUser {
username: "sync_testuser".to_string(),
email: "sync@example.com".to_string(),
password: "password123".to_string(),
role: Some(UserRole::User),
};
let user = app_state.db.create_user(create_user).await
.expect("Failed to create test user");
let user_id = user.id;
// Simulate initial sync with some directories
let initial_directories = vec![
CreateWebDAVDirectory {
user_id,
directory_path: "/documents".to_string(),
directory_etag: "docs_etag_v1".to_string(),
file_count: 10,
total_size_bytes: 10240,
},
CreateWebDAVDirectory {
user_id,
directory_path: "/pictures".to_string(),
directory_etag: "pics_etag_v1".to_string(),
file_count: 5,
total_size_bytes: 51200,
},
];
let (saved_dirs, _) = app_state.db.sync_webdav_directories(user_id, &initial_directories).await.unwrap();
assert_eq!(saved_dirs.len(), 2);
// Simulate second sync with changes
let updated_directories = vec![
CreateWebDAVDirectory {
user_id,
directory_path: "/documents".to_string(),
directory_etag: "docs_etag_v2".to_string(), // Changed
file_count: 12,
total_size_bytes: 12288,
},
CreateWebDAVDirectory {
user_id,
directory_path: "/videos".to_string(), // New directory
directory_etag: "videos_etag_v1".to_string(),
file_count: 3,
total_size_bytes: 102400,
},
// /pictures directory was deleted from server
];
let (updated_dirs, deleted_count) = app_state.db.sync_webdav_directories(user_id, &updated_directories).await.unwrap();
// Should have 2 directories (updated documents + new videos) and 1 deletion (pictures)
assert_eq!(updated_dirs.len(), 2);
assert_eq!(deleted_count, 1);
// Verify final state
let final_dirs = app_state.db.list_webdav_directories(user_id).await.unwrap();
assert_eq!(final_dirs.len(), 2);
let docs_dir = final_dirs.iter().find(|d| d.directory_path == "/documents").unwrap();
assert_eq!(docs_dir.directory_etag, "docs_etag_v2");
assert_eq!(docs_dir.file_count, 12);
let videos_dir = final_dirs.iter().find(|d| d.directory_path == "/videos").unwrap();
assert_eq!(videos_dir.directory_etag, "videos_etag_v1");
assert_eq!(videos_dir.file_count, 3);
Ok(())
}.await;
// Always cleanup database connections and test data
if let Err(e) = ctx.cleanup_and_close().await {
eprintln!("Warning: Test cleanup failed: {}", e);
}
result.unwrap();
}