diff --git a/tests/webdav_comprehensive_tests.rs b/tests/webdav_comprehensive_tests.rs new file mode 100644 index 0000000..11ac1ad --- /dev/null +++ b/tests/webdav_comprehensive_tests.rs @@ -0,0 +1,352 @@ +use std::sync::Arc; +use std::time::Duration; +use tokio::time::timeout; +use uuid::Uuid; + +use readur::{ + webdav_service::{WebDAVService, WebDAVConfig, RetryConfig}, + webdav_scheduler::WebDAVScheduler, + models::*, + db::Database, + config::Config, + AppState, +}; + +#[tokio::test] +async fn test_retry_config_default() { + let retry_config = RetryConfig::default(); + + assert_eq!(retry_config.max_retries, 3); + assert_eq!(retry_config.initial_delay_ms, 1000); + assert_eq!(retry_config.max_delay_ms, 30000); + assert_eq!(retry_config.backoff_multiplier, 2.0); + assert_eq!(retry_config.timeout_seconds, 120); +} + +#[tokio::test] +async fn test_webdav_service_with_custom_retry() { + let config = WebDAVConfig { + server_url: "https://cloud.example.com".to_string(), + username: "testuser".to_string(), + password: "testpass".to_string(), + watch_folders: vec!["/Documents".to_string()], + file_extensions: vec!["pdf".to_string()], + timeout_seconds: 30, + server_type: Some("nextcloud".to_string()), + }; + + let retry_config = RetryConfig { + max_retries: 5, + initial_delay_ms: 500, + max_delay_ms: 10000, + backoff_multiplier: 1.5, + timeout_seconds: 60, + }; + + let result = WebDAVService::new_with_retry(config, retry_config); + assert!(result.is_ok()); +} + +#[test] +fn test_webdav_config_builder() { + let config = WebDAVConfig { + server_url: "https://nextcloud.example.com".to_string(), + username: "admin".to_string(), + password: "secret123".to_string(), + watch_folders: vec!["/Documents".to_string(), "/Photos".to_string()], + file_extensions: vec!["pdf".to_string(), "png".to_string(), "jpg".to_string()], + timeout_seconds: 60, + server_type: Some("nextcloud".to_string()), + }; + + // Test Nextcloud URL construction + let service = WebDAVService::new(config.clone()).unwrap(); + // Note: We can't directly test the private base_webdav_url field, + // but we can test that the service was created successfully + + assert_eq!(config.watch_folders.len(), 2); + assert_eq!(config.file_extensions.len(), 3); +} + +#[test] +fn test_notification_models() { + let notification_id = Uuid::new_v4(); + let user_id = Uuid::new_v4(); + + let notification = Notification { + id: notification_id, + user_id, + notification_type: "success".to_string(), + title: "WebDAV Sync Complete".to_string(), + message: "Successfully processed 5 files".to_string(), + read: false, + action_url: Some("/documents".to_string()), + metadata: Some(serde_json::json!({ + "sync_type": "webdav", + "files_processed": 5 + })), + created_at: chrono::Utc::now(), + }; + + assert_eq!(notification.id, notification_id); + assert_eq!(notification.user_id, user_id); + assert_eq!(notification.notification_type, "success"); + assert_eq!(notification.title, "WebDAV Sync Complete"); + assert!(!notification.read); + assert_eq!(notification.action_url, Some("/documents".to_string())); + + // Test metadata extraction + let metadata = notification.metadata.unwrap(); + assert_eq!(metadata["sync_type"], "webdav"); + assert_eq!(metadata["files_processed"], 5); +} + +#[test] +fn test_create_notification_model() { + let create_notification = CreateNotification { + notification_type: "warning".to_string(), + title: "WebDAV Connection Issue".to_string(), + message: "Unable to connect to WebDAV server, retrying...".to_string(), + action_url: Some("/settings".to_string()), + metadata: Some(serde_json::json!({ + "error_type": "connection_timeout", + "retry_count": 2 + })), + }; + + assert_eq!(create_notification.notification_type, "warning"); + assert_eq!(create_notification.title, "WebDAV Connection Issue"); + assert_eq!(create_notification.action_url, Some("/settings".to_string())); + + let metadata = create_notification.metadata.unwrap(); + assert_eq!(metadata["error_type"], "connection_timeout"); + assert_eq!(metadata["retry_count"], 2); +} + +#[test] +fn test_notification_summary() { + let user_id = Uuid::new_v4(); + + let notification1 = Notification { + id: Uuid::new_v4(), + user_id, + notification_type: "success".to_string(), + title: "Sync Complete".to_string(), + message: "10 files processed".to_string(), + read: false, + action_url: None, + metadata: None, + created_at: chrono::Utc::now(), + }; + + let notification2 = Notification { + id: Uuid::new_v4(), + user_id, + notification_type: "error".to_string(), + title: "Sync Failed".to_string(), + message: "Connection timeout".to_string(), + read: true, + action_url: Some("/settings".to_string()), + metadata: None, + created_at: chrono::Utc::now(), + }; + + let summary = NotificationSummary { + unread_count: 1, + recent_notifications: vec![notification1, notification2], + }; + + assert_eq!(summary.unread_count, 1); + assert_eq!(summary.recent_notifications.len(), 2); + assert!(!summary.recent_notifications[0].read); + assert!(summary.recent_notifications[1].read); +} + +#[test] +fn test_webdav_error_handling() { + // Test error classification for retry logic + let timeout_error = anyhow::anyhow!("Connection timeout occurred"); + let network_error = anyhow::anyhow!("Network connection failed"); + let auth_error = anyhow::anyhow!("401 Unauthorized"); + + // These would be tested by WebDAVService::is_retryable_error if it were public + // For now, we test that errors can be created and formatted + assert!(timeout_error.to_string().contains("timeout")); + assert!(network_error.to_string().contains("connection")); + assert!(auth_error.to_string().contains("401")); +} + +#[test] +fn test_webdav_file_filtering() { + let supported_extensions = vec!["pdf", "png", "jpg", "jpeg", "tiff", "bmp", "txt"]; + + // Test file extension extraction and filtering + let test_files = vec![ + "document.pdf", + "image.PNG", // Test case insensitivity + "photo.jpg", + "spreadsheet.xlsx", // Should be filtered out + "text.txt", + "archive.zip", // Should be filtered out + "picture.jpeg", + ]; + + let mut supported_count = 0; + for filename in test_files { + if let Some(extension) = std::path::Path::new(filename) + .extension() + .and_then(|ext| ext.to_str()) + { + let ext_lower = extension.to_lowercase(); + if supported_extensions.contains(&ext_lower.as_str()) { + supported_count += 1; + } + } + } + + assert_eq!(supported_count, 5); // pdf, PNG, jpg, txt, jpeg +} + +#[test] +fn test_priority_calculation() { + // Test OCR priority calculation based on file size + let test_cases = vec![ + (500_000, 10), // 500KB -> highest priority + (2_000_000, 8), // 2MB -> high priority + (7_000_000, 6), // 7MB -> medium priority + (25_000_000, 4), // 25MB -> low priority + (100_000_000, 2), // 100MB -> lowest priority + ]; + + for (file_size, expected_priority) in test_cases { + let priority = match file_size { + 0..=1048576 => 10, // <= 1MB + ..=5242880 => 8, // 1-5MB + ..=10485760 => 6, // 5-10MB + ..=52428800 => 4, // 10-50MB + _ => 2, // > 50MB + }; + + assert_eq!(priority, expected_priority, + "File size {} bytes should have priority {}", file_size, expected_priority); + } +} + +#[test] +fn test_webdav_url_construction() { + // Test different server types and URL construction + let test_cases = vec![ + ("nextcloud", "https://cloud.example.com", "testuser", "https://cloud.example.com/remote.php/dav/files/testuser"), + ("owncloud", "https://cloud.example.com/", "admin", "https://cloud.example.com/remote.php/dav/files/admin"), + ("generic", "https://webdav.example.com", "user", "https://webdav.example.com/webdav"), + ]; + + for (server_type, server_url, username, expected_base) in test_cases { + let config = WebDAVConfig { + server_url: server_url.to_string(), + username: username.to_string(), + password: "password".to_string(), + watch_folders: vec!["/Documents".to_string()], + file_extensions: vec!["pdf".to_string()], + timeout_seconds: 30, + server_type: Some(server_type.to_string()), + }; + + let service = WebDAVService::new(config); + assert!(service.is_ok(), "Failed to create WebDAV service for {} server type", server_type); + + // Note: We can't directly test the URL construction since base_webdav_url is private, + // but we can verify the service was created successfully with the config + } +} + +#[test] +fn test_settings_webdav_integration() { + let mut settings = Settings::default(); + + // Test enabling WebDAV + settings.webdav_enabled = true; + settings.webdav_server_url = Some("https://nextcloud.example.com".to_string()); + settings.webdav_username = Some("testuser".to_string()); + settings.webdav_password = Some("testpass".to_string()); + settings.webdav_auto_sync = true; + settings.webdav_sync_interval_minutes = 30; + settings.webdav_watch_folders = vec!["/Documents".to_string(), "/Photos".to_string()]; + + assert!(settings.webdav_enabled); + assert_eq!(settings.webdav_server_url, Some("https://nextcloud.example.com".to_string())); + assert_eq!(settings.webdav_username, Some("testuser".to_string())); + assert!(settings.webdav_auto_sync); + assert_eq!(settings.webdav_sync_interval_minutes, 30); + assert_eq!(settings.webdav_watch_folders.len(), 2); + + // Test that we can build a WebDAVConfig from settings + if let (Some(server_url), Some(username)) = (&settings.webdav_server_url, &settings.webdav_username) { + let webdav_config = WebDAVConfig { + server_url: server_url.clone(), + username: username.clone(), + password: settings.webdav_password.clone().unwrap_or_default(), + watch_folders: settings.webdav_watch_folders.clone(), + file_extensions: settings.webdav_file_extensions.clone(), + timeout_seconds: 30, + server_type: Some("nextcloud".to_string()), + }; + + assert_eq!(webdav_config.server_url, "https://nextcloud.example.com"); + assert_eq!(webdav_config.username, "testuser"); + assert_eq!(webdav_config.watch_folders.len(), 2); + } +} + +#[test] +fn test_backoff_calculation() { + let retry_config = RetryConfig::default(); + let mut delay = retry_config.initial_delay_ms; + + // Test exponential backoff calculation + let expected_delays = vec![1000, 2000, 4000]; // 1s, 2s, 4s with 2.0 multiplier + + for expected in expected_delays { + assert_eq!(delay, expected); + delay = ((delay as f64 * retry_config.backoff_multiplier) as u64) + .min(retry_config.max_delay_ms); + } + + // Test that delay doesn't exceed max + for _ in 0..10 { + delay = ((delay as f64 * retry_config.backoff_multiplier) as u64) + .min(retry_config.max_delay_ms); + assert!(delay <= retry_config.max_delay_ms); + } +} + +// Mock test for WebDAV scheduler (without actual database) +#[test] +fn test_webdav_scheduler_creation() { + // Create mock state - in a real test environment you'd use test database + let config = Config { + database_url: "postgres://test".to_string(), + server_address: "127.0.0.1:3000".to_string(), + upload_path: "/tmp/test_uploads".to_string(), + watch_folder: "/tmp/test_watch".to_string(), + jwt_secret: "test_secret".to_string(), + allowed_file_types: vec!["pdf".to_string(), "png".to_string()], + watch_interval_seconds: Some(10), + file_stability_check_ms: Some(1000), + max_file_age_hours: Some(24), + cpu_priority: "normal".to_string(), + memory_limit_mb: 512, + concurrent_ocr_jobs: 4, + max_file_size_mb: 50, + ocr_language: "eng".to_string(), + ocr_timeout_seconds: 300, + }; + + // Note: This is a minimal test since we can't easily mock the database + // In a full integration test, you'd set up a test database + + assert_eq!(config.server_address, "127.0.0.1:3000"); + assert_eq!(config.upload_path, "/tmp/test_uploads"); + + // The scheduler would be tested with a real database in integration tests +} \ No newline at end of file diff --git a/tests/webdav_integration_tests.rs b/tests/webdav_integration_tests.rs new file mode 100644 index 0000000..7fe458b --- /dev/null +++ b/tests/webdav_integration_tests.rs @@ -0,0 +1,541 @@ +use std::sync::Arc; +use axum::{ + body::{Body, to_bytes}, + http::{Request, StatusCode}, + Router, +}; +use tower::ServiceExt; +use serde_json::{json, Value}; +use uuid::Uuid; + +use readur::{ + db::Database, + config::Config, + models::*, + routes, + AppState, +}; + +// Test database URL - in real tests you'd use a separate test database +const TEST_DATABASE_URL: &str = "postgres://postgres:password@localhost:5432/readur_test"; + +fn create_empty_update_settings() -> UpdateSettings { + UpdateSettings { + ocr_language: None, + concurrent_ocr_jobs: None, + ocr_timeout_seconds: None, + max_file_size_mb: None, + allowed_file_types: None, + auto_rotate_images: None, + enable_image_preprocessing: None, + search_results_per_page: None, + search_snippet_length: None, + fuzzy_search_threshold: None, + retention_days: None, + enable_auto_cleanup: None, + enable_compression: None, + memory_limit_mb: None, + cpu_priority: None, + enable_background_ocr: None, + ocr_page_segmentation_mode: None, + ocr_engine_mode: None, + ocr_min_confidence: None, + ocr_dpi: None, + ocr_enhance_contrast: None, + ocr_remove_noise: None, + ocr_detect_orientation: None, + ocr_whitelist_chars: None, + ocr_blacklist_chars: None, + webdav_enabled: None, + webdav_server_url: None, + webdav_username: None, + webdav_password: None, + webdav_watch_folders: None, + webdav_file_extensions: None, + webdav_auto_sync: None, + webdav_sync_interval_minutes: None, + } +} + +async fn setup_test_app() -> (Router, Arc) { + let config = Config { + database_url: TEST_DATABASE_URL.to_string(), + server_address: "127.0.0.1:0".to_string(), + upload_path: "/tmp/test_uploads".to_string(), + watch_folder: "/tmp/test_watch".to_string(), + jwt_secret: "test_jwt_secret_for_integration_tests".to_string(), + allowed_file_types: vec!["pdf".to_string(), "png".to_string()], + watch_interval_seconds: Some(10), + file_stability_check_ms: Some(1000), + max_file_age_hours: Some(24), + cpu_priority: "normal".to_string(), + memory_limit_mb: 512, + concurrent_ocr_jobs: 4, + max_file_size_mb: 50, + ocr_language: "eng".to_string(), + ocr_timeout_seconds: 300, + }; + + // Try to connect to test database, fall back to regular database if not available + let db_url = if Database::new(TEST_DATABASE_URL).await.is_ok() { + TEST_DATABASE_URL.to_string() + } else { + std::env::var("DATABASE_URL").unwrap_or_else(|_| + "postgres://postgres:password@localhost:5432/readur".to_string()) + }; + + let db = Database::new(&db_url).await.expect("Failed to connect to test database"); + let state = Arc::new(AppState { db, config }); + + let app = Router::new() + .nest("/api/auth", routes::auth::router()) + .nest("/api/webdav", routes::webdav::router()) + .nest("/api/notifications", routes::notifications::router()) + .nest("/api/settings", routes::settings::router()) + .with_state(state.clone()); + + (app, state) +} + +async fn create_test_user(state: &AppState) -> (User, String) { + let create_user = CreateUser { + username: format!("testuser_{}", Uuid::new_v4()), + email: format!("test_{}@example.com", Uuid::new_v4()), + password: "testpassword123".to_string(), + role: Some(UserRole::User), + }; + + let user = state.db.create_user(create_user).await + .expect("Failed to create test user"); + + // Create a simple JWT token for testing (in real tests you'd use proper JWT) + let token = format!("Bearer test_token_for_user_{}", user.id); + + (user, token) +} + +async fn setup_webdav_settings(state: &AppState, user_id: Uuid) { + let update_settings = UpdateSettings { + webdav_enabled: Some(true), + webdav_server_url: Some(Some("https://demo.nextcloud.com".to_string())), + webdav_username: Some(Some("demo_user".to_string())), + webdav_password: Some(Some("demo_password".to_string())), + webdav_watch_folders: Some(vec!["/Documents".to_string()]), + webdav_file_extensions: Some(vec!["pdf".to_string(), "png".to_string()]), + webdav_auto_sync: Some(true), + webdav_sync_interval_minutes: Some(60), + ocr_language: None, + concurrent_ocr_jobs: None, + ocr_timeout_seconds: None, + max_file_size_mb: None, + allowed_file_types: None, + auto_rotate_images: None, + enable_image_preprocessing: None, + search_results_per_page: None, + search_snippet_length: None, + fuzzy_search_threshold: None, + retention_days: None, + enable_auto_cleanup: None, + enable_compression: None, + memory_limit_mb: None, + cpu_priority: None, + enable_background_ocr: None, + ocr_page_segmentation_mode: None, + ocr_engine_mode: None, + ocr_min_confidence: None, + ocr_dpi: None, + ocr_enhance_contrast: None, + ocr_remove_noise: None, + ocr_detect_orientation: None, + ocr_whitelist_chars: None, + ocr_blacklist_chars: None, + }; + + state.db.create_or_update_settings(user_id, &update_settings).await + .expect("Failed to setup WebDAV settings"); +} + +#[tokio::test] +async fn test_webdav_test_connection_endpoint() { + let (app, state) = setup_test_app().await; + let (_user, token) = create_test_user(&state).await; + + let test_connection_request = json!({ + "server_url": "https://demo.nextcloud.com", + "username": "demo_user", + "password": "demo_password", + "server_type": "nextcloud" + }); + + let request = Request::builder() + .method("POST") + .uri("/api/webdav/test-connection") + .header("Authorization", token) + .header("Content-Type", "application/json") + .body(Body::from(test_connection_request.to_string())) + .unwrap(); + + let response = app.clone().oneshot(request).await.unwrap(); + + // Note: This will likely fail with connection error since demo.nextcloud.com + // may not accept these credentials, but we're testing the endpoint structure + assert!( + response.status() == StatusCode::OK || + response.status() == StatusCode::INTERNAL_SERVER_ERROR + ); + + let body = to_bytes(response.into_body(), usize::MAX).await.unwrap(); + let result: Value = serde_json::from_slice(&body).unwrap(); + + // Should have a result structure even if connection fails + assert!(result.get("success").is_some()); + assert!(result.get("message").is_some()); +} + +#[tokio::test] +async fn test_webdav_estimate_crawl_endpoint() { + let (app, state) = setup_test_app().await; + let (user, token) = create_test_user(&state).await; + + // Setup WebDAV settings first + setup_webdav_settings(&state, user.id).await; + + let crawl_request = json!({ + "folders": ["/Documents", "/Photos"] + }); + + let request = Request::builder() + .method("POST") + .uri("/api/webdav/estimate-crawl") + .header("Authorization", token) + .header("Content-Type", "application/json") + .body(Body::from(crawl_request.to_string())) + .unwrap(); + + let response = app.clone().oneshot(request).await.unwrap(); + + // Even if WebDAV connection fails, should return estimate structure + assert!( + response.status() == StatusCode::OK || + response.status() == StatusCode::INTERNAL_SERVER_ERROR + ); + + let body = to_bytes(response.into_body(), usize::MAX).await.unwrap(); + let result: Value = serde_json::from_slice(&body).unwrap(); + + // Should have crawl estimate structure + assert!(result.get("folders").is_some()); + assert!(result.get("total_files").is_some()); + assert!(result.get("total_supported_files").is_some()); +} + +#[tokio::test] +async fn test_webdav_sync_status_endpoint() { + let (app, state) = setup_test_app().await; + let (user, token) = create_test_user(&state).await; + + setup_webdav_settings(&state, user.id).await; + + let request = Request::builder() + .method("GET") + .uri("/api/webdav/sync-status") + .header("Authorization", token) + .body(Body::empty()) + .unwrap(); + + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let body = to_bytes(response.into_body(), usize::MAX).await.unwrap(); + let result: Value = serde_json::from_slice(&body).unwrap(); + + // Should return sync status structure + assert!(result.get("is_running").is_some()); + assert!(result.get("files_processed").is_some()); + assert!(result.get("files_remaining").is_some()); + assert!(result.get("errors").is_some()); +} + +#[tokio::test] +async fn test_webdav_start_sync_endpoint() { + let (app, state) = setup_test_app().await; + let (user, token) = create_test_user(&state).await; + + setup_webdav_settings(&state, user.id).await; + + let request = Request::builder() + .method("POST") + .uri("/api/webdav/start-sync") + .header("Authorization", token) + .body(Body::empty()) + .unwrap(); + + let response = app.clone().oneshot(request).await.unwrap(); + + // Should accept the sync request (even if it fails later due to invalid credentials) + let status = response.status(); + assert!( + status == StatusCode::OK || + status == StatusCode::BAD_REQUEST // If WebDAV not properly configured + ); + + let body = to_bytes(response.into_body(), usize::MAX).await.unwrap(); + let result: Value = serde_json::from_slice(&body).unwrap(); + + if status == StatusCode::OK { + assert_eq!(result.get("success").unwrap(), &json!(true)); + assert!(result.get("message").is_some()); + } +} + +#[tokio::test] +async fn test_notifications_endpoints() { + let (app, state) = setup_test_app().await; + let (user, token) = create_test_user(&state).await; + + // Create a test notification directly in the database + let create_notification = CreateNotification { + notification_type: "success".to_string(), + title: "Test WebDAV Sync".to_string(), + message: "Successfully processed 3 test files".to_string(), + action_url: Some("/documents".to_string()), + metadata: Some(json!({ + "sync_type": "webdav_test", + "files_processed": 3 + })), + }; + + let notification = state.db.create_notification(user.id, &create_notification).await + .expect("Failed to create test notification"); + + // Test GET /api/notifications + let request = Request::builder() + .method("GET") + .uri("/api/notifications") + .header("Authorization", token.clone()) + .body(Body::empty()) + .unwrap(); + + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let body = to_bytes(response.into_body(), usize::MAX).await.unwrap(); + let notifications: Vec = serde_json::from_slice(&body).unwrap(); + assert!(notifications.len() >= 1); + + // Test GET /api/notifications/summary + let request = Request::builder() + .method("GET") + .uri("/api/notifications/summary") + .header("Authorization", token.clone()) + .body(Body::empty()) + .unwrap(); + + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let body = to_bytes(response.into_body(), usize::MAX).await.unwrap(); + let summary: Value = serde_json::from_slice(&body).unwrap(); + assert!(summary.get("unread_count").is_some()); + assert!(summary.get("recent_notifications").is_some()); + + // Test POST /api/notifications/{id}/read + let request = Request::builder() + .method("POST") + .uri(&format!("/api/notifications/{}/read", notification.id)) + .header("Authorization", token.clone()) + .body(Body::empty()) + .unwrap(); + + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + // Test POST /api/notifications/read-all + let request = Request::builder() + .method("POST") + .uri("/api/notifications/read-all") + .header("Authorization", token.clone()) + .body(Body::empty()) + .unwrap(); + + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + // Test DELETE /api/notifications/{id} + let request = Request::builder() + .method("DELETE") + .uri(&format!("/api/notifications/{}", notification.id)) + .header("Authorization", token) + .body(Body::empty()) + .unwrap(); + + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); +} + +#[tokio::test] +async fn test_settings_webdav_integration() { + let (app, state) = setup_test_app().await; + let (_user, token) = create_test_user(&state).await; + + // Test updating WebDAV settings + let settings_update = json!({ + "webdav_enabled": true, + "webdav_server_url": "https://test.nextcloud.com", + "webdav_username": "testuser", + "webdav_password": "testpass", + "webdav_watch_folders": ["/Documents", "/Photos"], + "webdav_file_extensions": ["pdf", "png", "jpg"], + "webdav_auto_sync": true, + "webdav_sync_interval_minutes": 30 + }); + + let request = Request::builder() + .method("PUT") + .uri("/api/settings") + .header("Authorization", token.clone()) + .header("Content-Type", "application/json") + .body(Body::from(settings_update.to_string())) + .unwrap(); + + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + // Test retrieving settings + let request = Request::builder() + .method("GET") + .uri("/api/settings") + .header("Authorization", token) + .body(Body::empty()) + .unwrap(); + + let response = app.clone().oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + let body = to_bytes(response.into_body(), usize::MAX).await.unwrap(); + let settings: Value = serde_json::from_slice(&body).unwrap(); + + assert_eq!(settings.get("webdav_enabled").unwrap(), &json!(true)); + assert_eq!(settings.get("webdav_server_url").unwrap(), &json!("https://test.nextcloud.com")); + assert_eq!(settings.get("webdav_username").unwrap(), &json!("testuser")); + assert_eq!(settings.get("webdav_auto_sync").unwrap(), &json!(true)); + assert_eq!(settings.get("webdav_sync_interval_minutes").unwrap(), &json!(30)); + + let folders = settings.get("webdav_watch_folders").unwrap().as_array().unwrap(); + assert_eq!(folders.len(), 2); + assert!(folders.contains(&json!("/Documents"))); + assert!(folders.contains(&json!("/Photos"))); +} + +#[tokio::test] +async fn test_notification_database_operations() { + let (_, state) = setup_test_app().await; + let (user, _token) = create_test_user(&state).await; + + // Test creating notification + let create_notification = CreateNotification { + notification_type: "info".to_string(), + title: "WebDAV Sync Started".to_string(), + message: "Synchronization with WebDAV server has begun".to_string(), + action_url: Some("/sync-status".to_string()), + metadata: Some(json!({ + "sync_id": "sync_123", + "folders": ["/Documents", "/Photos"] + })), + }; + + let notification = state.db.create_notification(user.id, &create_notification).await + .expect("Failed to create notification"); + + assert_eq!(notification.user_id, user.id); + assert_eq!(notification.notification_type, "info"); + assert_eq!(notification.title, "WebDAV Sync Started"); + assert!(!notification.read); + + // Test getting user notifications + let notifications = state.db.get_user_notifications(user.id, 10, 0).await + .expect("Failed to get notifications"); + + assert!(notifications.len() >= 1); + let found_notification = notifications.iter() + .find(|n| n.id == notification.id) + .expect("Created notification not found"); + + assert_eq!(found_notification.title, "WebDAV Sync Started"); + + // Test getting unread count + let unread_count = state.db.get_unread_notification_count(user.id).await + .expect("Failed to get unread count"); + + assert!(unread_count >= 1); + + // Test marking as read + state.db.mark_notification_read(user.id, notification.id).await + .expect("Failed to mark notification as read"); + + let updated_notifications = state.db.get_user_notifications(user.id, 10, 0).await + .expect("Failed to get updated notifications"); + + let updated_notification = updated_notifications.iter() + .find(|n| n.id == notification.id) + .expect("Notification not found after update"); + + assert!(updated_notification.read); + + // Test getting notification summary + let summary = state.db.get_notification_summary(user.id).await + .expect("Failed to get notification summary"); + + assert!(summary.recent_notifications.len() >= 1); + assert!(summary.unread_count >= 0); + + // Test deleting notification + state.db.delete_notification(user.id, notification.id).await + .expect("Failed to delete notification"); + + let final_notifications = state.db.get_user_notifications(user.id, 10, 0).await + .expect("Failed to get final notifications"); + + assert!(!final_notifications.iter().any(|n| n.id == notification.id)); +} + +#[tokio::test] +async fn test_webdav_settings_validation() { + let (_, state) = setup_test_app().await; + let (user, _token) = create_test_user(&state).await; + + // Test invalid WebDAV settings (missing required fields) + let mut invalid_settings = create_empty_update_settings(); + invalid_settings.webdav_enabled = Some(true); + invalid_settings.webdav_server_url = Some(Some("".to_string())); // Empty URL should cause issues + invalid_settings.webdav_username = Some(None); // Missing username + invalid_settings.webdav_password = Some(Some("password".to_string())); + + // This should succeed in database but fail when trying to create WebDAV config + let settings = state.db.create_or_update_settings(user.id, &invalid_settings).await + .expect("Failed to save settings"); + + assert!(settings.webdav_enabled); + assert_eq!(settings.webdav_server_url, Some("".to_string())); + assert_eq!(settings.webdav_username, None); + + // Test valid WebDAV settings + let mut valid_settings = create_empty_update_settings(); + valid_settings.webdav_enabled = Some(true); + valid_settings.webdav_server_url = Some(Some("https://valid.nextcloud.com".to_string())); + valid_settings.webdav_username = Some(Some("validuser".to_string())); + valid_settings.webdav_password = Some(Some("validpass".to_string())); + valid_settings.webdav_watch_folders = Some(vec!["/Documents".to_string()]); + valid_settings.webdav_file_extensions = Some(vec!["pdf".to_string(), "png".to_string()]); + valid_settings.webdav_auto_sync = Some(true); + valid_settings.webdav_sync_interval_minutes = Some(60); + + let valid_result = state.db.create_or_update_settings(user.id, &valid_settings).await + .expect("Failed to save valid settings"); + + assert!(valid_result.webdav_enabled); + assert_eq!(valid_result.webdav_server_url, Some("https://valid.nextcloud.com".to_string())); + assert_eq!(valid_result.webdav_username, Some("validuser".to_string())); + assert_eq!(valid_result.webdav_watch_folders, vec!["/Documents".to_string()]); + assert_eq!(valid_result.webdav_sync_interval_minutes, 60); +} \ No newline at end of file