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, 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 = 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 ¤t_dirs { match known_map.get(¤t_dir.relative_path) { Some(known_etag) => { if known_etag != ¤t_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::>(); // 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 }