Readur/tests/unit_smart_sync_service_tes...

355 lines
14 KiB
Rust

use std::{collections::HashMap, sync::Arc};
use uuid::Uuid;
use readur::{
AppState,
models::{CreateWebDAVDirectory, FileIngestionInfo, User, AuthProvider},
services::webdav::{
SmartSyncService, SmartSyncStrategy, SmartSyncDecision,
WebDAVService, WebDAVConfig
},
test_utils::{TestContext, TestAuthHelper},
};
// Note: Mocking is complex due to WebDAV service dependencies
// These tests focus on the logic we can test without full WebDAV integration
/// Helper function to create test setup with database
async fn create_test_state() -> (TestContext, Arc<AppState>, Uuid) {
// Create a fresh test context for each test, following the pattern used in all other tests
let test_context = TestContext::new().await;
let auth_helper = TestAuthHelper::new(test_context.app().clone());
let test_user = auth_helper.create_test_user().await;
let state = test_context.state().clone();
let user_id = test_user.user_response.id;
(test_context, state, user_id)
}
/// Helper function to create directory info for testing
fn create_directory_info(path: &str, etag: &str) -> FileIngestionInfo {
FileIngestionInfo {
relative_path: path.to_string(),
full_path: path.to_string(),
#[allow(deprecated)]
path: path.to_string(),
name: path.split('/').last().unwrap_or("").to_string(),
size: 0,
mime_type: "".to_string(),
last_modified: Some(chrono::Utc::now()),
etag: etag.to_string(),
is_directory: true,
created_at: Some(chrono::Utc::now()),
permissions: Some(0),
owner: None,
group: None,
metadata: None,
}
}
#[tokio::test]
async fn test_evaluate_sync_need_first_time_no_known_directories() {
// Unit Test: First-time sync evaluation with no existing directory ETags
// Expected: Should return RequiresSync(FullDeepScan)
let (_test_context, state, user_id) = create_test_state().await;
let smart_sync_service = SmartSyncService::new(state.clone());
// Test evaluation - should detect no known directories and require deep scan
let webdav_service = create_real_webdav_service();
let decision = smart_sync_service.evaluate_sync_need(user_id, &webdav_service, "/Documents", None).await;
match decision {
Ok(SmartSyncDecision::RequiresSync(SmartSyncStrategy::FullDeepScan)) => {
println!("✅ First-time sync correctly requires FullDeepScan");
}
Ok(other) => panic!("Expected FullDeepScan for first-time sync, got: {:?}", other),
Err(_) => {
println!("✅ First-time sync evaluation failed as expected in test environment");
}
}
}
#[tokio::test]
async fn test_evaluate_sync_need_no_changes_skip_sync() {
// Unit Test: Smart sync evaluation with no directory changes
// Expected: Should return SkipSync
let (_test_context, state, user_id) = create_test_state().await;
let smart_sync_service = SmartSyncService::new(state.clone());
// Pre-populate database with known directory ETags
let known_directories = vec![
("/Documents", "root-etag-unchanged"),
("/Documents/Projects", "projects-etag-unchanged"),
("/Documents/Archive", "archive-etag-unchanged"),
];
for (path, etag) in &known_directories {
let dir = CreateWebDAVDirectory {
user_id,
directory_path: path.to_string(),
directory_etag: etag.to_string(),
file_count: 5,
total_size_bytes: 1024000,
};
state.db.create_or_update_webdav_directory(&dir).await
.expect("Failed to create directory tracking");
}
// For this test, we need to mock the WebDAV service to return unchanged ETags
// This would require a more sophisticated mock that can be injected into SmartSyncService
// Verify known directories were created
let stored_dirs = state.db.list_webdav_directories(user_id).await
.expect("Failed to list directories");
assert_eq!(stored_dirs.len(), 3, "Should have 3 known directories");
println!("✅ Test setup complete: Known directories in database for no-change scenario");
// TODO: Complete this test with mocked WebDAV service returning same ETags
}
#[tokio::test]
async fn test_strategy_selection_few_changes_targeted_scan() {
// Unit Test: Strategy selection logic for small number of changes
// Expected: Should use TargetedScan for few changed directories
let change_ratio = 2.0 / 10.0; // 2 changed out of 10 total = 20%
let new_dirs_count = 1;
// This logic mirrors what's in SmartSyncService::evaluate_sync_need
let should_use_targeted = change_ratio <= 0.3 && new_dirs_count <= 5;
assert!(should_use_targeted, "Should use targeted scan for small changes");
println!("✅ Strategy selection: Small changes correctly trigger targeted scan");
}
#[tokio::test]
async fn test_strategy_selection_many_changes_full_scan() {
// Unit Test: Strategy selection logic for many changes
// Expected: Should fall back to FullDeepScan for efficiency
let scenarios = vec![
(4.0 / 10.0, 2), // 40% change ratio > 30% threshold
(2.0 / 10.0, 6), // Low ratio but 6 new dirs > 5 threshold
(5.0 / 10.0, 8), // Both thresholds exceeded
];
for (change_ratio, new_dirs_count) in scenarios {
let should_use_full_scan = change_ratio > 0.3 || new_dirs_count > 5;
assert!(should_use_full_scan,
"Ratio {:.1}% with {} new dirs should trigger full scan",
change_ratio * 100.0, new_dirs_count);
}
println!("✅ Strategy selection: Many changes correctly trigger full deep scan");
}
#[tokio::test]
async fn test_directory_etag_comparison_logic() {
// Unit Test: Directory ETag comparison and change detection
// Expected: Should correctly identify changed, new, and unchanged directories
let (_test_context, state, user_id) = create_test_state().await;
// Setup known directories in database
let known_dirs = vec![
("/Documents", "root-etag-old"),
("/Documents/Projects", "projects-etag-stable"),
("/Documents/Archive", "archive-etag-old"),
("/Documents/ToBeDeleted", "deleted-etag"), // This won't appear in "current"
];
for (path, etag) in &known_dirs {
let dir = CreateWebDAVDirectory {
user_id,
directory_path: path.to_string(),
directory_etag: etag.to_string(),
file_count: 3,
total_size_bytes: 512000,
};
state.db.create_or_update_webdav_directory(&dir).await
.expect("Failed to create known directory");
}
// Simulate current directories from WebDAV (what we'd get from discovery)
let current_dirs = vec![
create_directory_info("/Documents", "root-etag-new"), // Changed
create_directory_info("/Documents/Projects", "projects-etag-stable"), // Unchanged
create_directory_info("/Documents/Archive", "archive-etag-new"), // Changed
create_directory_info("/Documents/NewFolder", "new-folder-etag"), // New
];
// Get known directories from database
let known_map: HashMap<String, String> = state.db.list_webdav_directories(user_id).await
.expect("Failed to get known directories")
.into_iter()
.filter(|dir| dir.directory_path.starts_with("/Documents"))
.map(|dir| (dir.directory_path, dir.directory_etag))
.collect();
// Perform comparison logic (mirrors SmartSyncService logic)
let mut changed_directories = Vec::new();
let mut new_directories = Vec::new();
let mut unchanged_directories = Vec::new();
for current_dir in &current_dirs {
match known_map.get(&current_dir.relative_path) {
Some(known_etag) => {
if known_etag != &current_dir.etag {
changed_directories.push(current_dir.relative_path.clone());
} else {
unchanged_directories.push(current_dir.relative_path.clone());
}
}
None => {
new_directories.push(current_dir.relative_path.clone());
}
}
}
// Verify comparison results
assert_eq!(changed_directories.len(), 2, "Should detect 2 changed directories");
assert!(changed_directories.contains(&"/Documents".to_string()));
assert!(changed_directories.contains(&"/Documents/Archive".to_string()));
assert_eq!(new_directories.len(), 1, "Should detect 1 new directory");
assert!(new_directories.contains(&"/Documents/NewFolder".to_string()));
assert_eq!(unchanged_directories.len(), 1, "Should detect 1 unchanged directory");
assert!(unchanged_directories.contains(&"/Documents/Projects".to_string()));
// Note: Deleted directories (/Documents/ToBeDeleted) would need separate logic
// to detect directories that exist in DB but not in current WebDAV response
println!("✅ Directory ETag comparison correctly identifies changes, new, and unchanged directories");
}
#[tokio::test]
async fn test_bulk_directory_fetching_performance() {
// Unit Test: Bulk directory ETag fetching vs individual queries
// Expected: Should fetch all relevant directories in single database query
let (_test_context, state, user_id) = create_test_state().await;
// Create many directories across different folder hierarchies
let directories = (0..50).map(|i| {
let path = if i < 20 {
format!("/Documents/Folder{}", i)
} else if i < 35 {
format!("/Photos/Album{}", i - 20)
} else {
format!("/Documents/Subfolder/Deep{}", i - 35)
};
(path, format!("etag-{}", i))
}).collect::<Vec<_>>();
// Insert all directories
for (path, etag) in &directories {
let dir = CreateWebDAVDirectory {
user_id,
directory_path: path.clone(),
directory_etag: etag.clone(),
file_count: 1,
total_size_bytes: 100000,
};
state.db.create_or_update_webdav_directory(&dir).await
.expect("Failed to create directory");
}
// Test bulk fetch for specific folder path
let start = std::time::Instant::now();
let documents_dirs = state.db.list_webdav_directories(user_id).await
.expect("Failed to fetch directories");
let fetch_duration = start.elapsed();
// Filter to Documents folder (simulates SmartSyncService filtering)
let filtered_dirs: Vec<_> = documents_dirs
.into_iter()
.filter(|dir| dir.directory_path.starts_with("/Documents"))
.collect();
// Verify bulk fetch results
assert!(filtered_dirs.len() >= 35, "Should fetch Documents directories"); // 20 + 15 deep
assert!(fetch_duration.as_millis() < 100, "Bulk fetch should be fast (< 100ms)");
println!("✅ Bulk directory fetching: {} directories in {:?}",
filtered_dirs.len(), fetch_duration);
}
#[tokio::test]
async fn test_smart_sync_error_handling() {
// Unit Test: Error handling and fallback behavior
// Expected: Should handle various error conditions gracefully
let (_test_context, state, user_id) = create_test_state().await;
let smart_sync_service = SmartSyncService::new(state.clone());
// Test database error handling (simulate by using invalid user ID)
let invalid_user_id = Uuid::new_v4();
// This should not panic, but handle the error gracefully
let webdav_service = create_real_webdav_service();
let decision = smart_sync_service.evaluate_sync_need(invalid_user_id, &webdav_service, "/Documents", None).await;
match decision {
Ok(SmartSyncDecision::RequiresSync(SmartSyncStrategy::FullDeepScan)) => {
println!("✅ Database error handled - falls back to full deep scan");
}
Err(e) => {
println!("✅ Database error properly returned: {}", e);
}
other => panic!("Unexpected result for invalid user: {:?}", other),
}
println!("✅ Error handling test completed");
}
/// Helper function to create a real WebDAV service for tests that need it
fn create_real_webdav_service() -> WebDAVService {
let config = WebDAVConfig {
server_url: "https://test.example.com".to_string(),
username: "test".to_string(),
password: "test".to_string(),
watch_folders: vec!["/Documents".to_string()],
file_extensions: vec!["pdf".to_string(), "txt".to_string()],
timeout_seconds: 30,
server_type: Some("generic".to_string()),
};
WebDAVService::new(config).expect("Failed to create WebDAV service")
}
// Additional unit test stubs for specific functionality
#[tokio::test]
async fn test_targeted_scan_directory_selection() {
// Unit Test: Targeted scan should only process changed directories
println!("📝 Unit test stub: Targeted scan directory selection logic");
// TODO: Test that TargetedScan only processes specific changed directories
}
#[tokio::test]
async fn test_directory_etag_update_after_scan() {
// Unit Test: Directory ETags should be updated after successful scan
println!("📝 Unit test stub: Directory ETag update after scan completion");
// TODO: Test that perform_smart_sync updates directory ETags in database
}
#[tokio::test]
async fn test_deep_scan_vs_targeted_scan_coverage() {
// Unit Test: Deep scan should process all directories, targeted scan only specific ones
println!("📝 Unit test stub: Deep vs targeted scan coverage comparison");
// TODO: Test that FullDeepScan processes entire hierarchy, TargetedScan processes subset
}
#[tokio::test]
async fn test_smart_sync_decision_caching() {
// Unit Test: Smart sync decisions should be efficient for repeated calls
println!("📝 Unit test stub: Smart sync decision efficiency");
// TODO: Test performance of repeated smart sync evaluations
}