#[cfg(test)] use readur::services::file_service::FileService; #[cfg(test)] use readur::models::Document; #[cfg(test)] use readur::storage::factory::create_storage_backend; #[cfg(test)] use readur::storage::StorageConfig; #[cfg(test)] use std::fs; #[cfg(test)] use tempfile::TempDir; #[cfg(test)] use uuid::Uuid; #[cfg(test)] async fn create_test_file_service() -> (FileService, TempDir) { let temp_dir = TempDir::new().unwrap(); let upload_path = temp_dir.path().to_string_lossy().to_string(); let storage_config = StorageConfig::Local { upload_path: upload_path.clone() }; let storage_backend = create_storage_backend(storage_config).await.unwrap(); let service = FileService::with_storage(upload_path, storage_backend); (service, temp_dir) } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_save_file() { let (service, _temp_dir) = create_test_file_service().await; let filename = "test.txt"; let data = b"Hello, World!"; let result = service.save_file(filename, data).await; assert!(result.is_ok()); let file_path = result.unwrap(); assert!(fs::metadata(&file_path).is_ok()); let saved_content = fs::read(&file_path).unwrap(); assert_eq!(saved_content, data); } #[tokio::test] async fn test_save_file_with_extension() { let (service, _temp_dir) = create_test_file_service().await; let filename = "document.pdf"; let data = b"PDF content"; let result = service.save_file(filename, data).await; assert!(result.is_ok()); let file_path = result.unwrap(); assert!(file_path.ends_with(".pdf")); } #[tokio::test] async fn test_save_file_without_extension() { let (service, _temp_dir) = create_test_file_service().await; let filename = "document"; let data = b"Some content"; let result = service.save_file(filename, data).await; assert!(result.is_ok()); let file_path = result.unwrap(); // Should not have an extension (check just the filename part) let filename_part = std::path::Path::new(&file_path) .file_name() .unwrap() .to_str() .unwrap(); assert!(!filename_part.contains('.')); } #[tokio::test] async fn test_create_document() { let (service, _temp_dir) = create_test_file_service().await; let user_id = Uuid::new_v4(); let document = service.create_document( "saved_file.pdf", "original_file.pdf", "/path/to/saved_file.pdf", 1024, "application/pdf", user_id, Some("abcd1234hash".to_string()), None, // original_created_at None, // original_modified_at None, // source_path None, // source_type None, // source_id None, // file_permissions None, // file_owner None, // file_group None, // source_metadata ); assert_eq!(document.filename, "saved_file.pdf"); assert_eq!(document.original_filename, "original_file.pdf"); assert_eq!(document.file_path, "/path/to/saved_file.pdf"); assert_eq!(document.file_size, 1024); assert_eq!(document.mime_type, "application/pdf"); assert_eq!(document.user_id, user_id); assert_eq!(document.file_hash, Some("abcd1234hash".to_string())); assert!(document.content.is_none()); assert!(document.ocr_text.is_none()); assert!(document.tags.is_empty()); } #[tokio::test] async fn test_is_allowed_file_type() { let (service, _temp_dir) = create_test_file_service().await; let allowed_types = vec![ "pdf".to_string(), "txt".to_string(), "png".to_string(), "jpg".to_string(), ]; assert!(service.is_allowed_file_type("document.pdf", &allowed_types)); assert!(service.is_allowed_file_type("text.txt", &allowed_types)); assert!(service.is_allowed_file_type("image.PNG", &allowed_types)); // Case insensitive assert!(service.is_allowed_file_type("photo.JPG", &allowed_types)); // Case insensitive assert!(!service.is_allowed_file_type("document.doc", &allowed_types)); assert!(!service.is_allowed_file_type("archive.zip", &allowed_types)); assert!(!service.is_allowed_file_type("noextension", &allowed_types)); } #[tokio::test] async fn test_read_file() { let (service, _temp_dir) = create_test_file_service().await; let filename = "test.txt"; let original_data = b"Hello, World!"; let file_path = service.save_file(filename, original_data).await.unwrap(); let result = service.read_file(&file_path).await; assert!(result.is_ok()); let read_data = result.unwrap(); assert_eq!(read_data, original_data); } #[tokio::test] async fn test_read_nonexistent_file() { let (service, _temp_dir) = create_test_file_service().await; let nonexistent_path = "/path/to/nonexistent/file.txt"; let result = service.read_file(nonexistent_path).await; assert!(result.is_err()); } } #[cfg(test)] mod file_deletion_tests { use super::*; use chrono::Utc; use std::path::Path; use std::fs; fn create_test_document_with_files(_service: &FileService, temp_dir: &TempDir, user_id: uuid::Uuid) -> (Document, String, String, String) { let document_id = uuid::Uuid::new_v4(); // Create main document file let base_path = temp_dir.path().join("documents"); fs::create_dir_all(&base_path).unwrap(); let main_file_path = base_path.join("test_document.pdf"); fs::write(&main_file_path, b"PDF content").unwrap(); // Create thumbnails directory and thumbnail file with correct naming let thumbnails_path = temp_dir.path().join("thumbnails"); fs::create_dir_all(&thumbnails_path).unwrap(); let thumbnail_path = thumbnails_path.join(format!("{}_thumb.jpg", document_id)); fs::write(&thumbnail_path, b"Thumbnail content").unwrap(); // Create processed_images directory and processed image file with correct naming let processed_dir = temp_dir.path().join("processed_images"); fs::create_dir_all(&processed_dir).unwrap(); let processed_path = processed_dir.join(format!("{}_processed.png", document_id)); fs::write(&processed_path, b"Processed content").unwrap(); let document = Document { id: document_id, filename: "test_document.pdf".to_string(), original_filename: "test_document.pdf".to_string(), file_path: main_file_path.to_string_lossy().to_string(), file_size: 1024, mime_type: "application/pdf".to_string(), content: Some("Test document content".to_string()), ocr_text: Some("This is extracted OCR text".to_string()), ocr_confidence: Some(95.5), ocr_word_count: Some(150), ocr_processing_time_ms: Some(1200), ocr_status: Some("completed".to_string()), ocr_error: None, ocr_completed_at: Some(Utc::now()), tags: vec!["test".to_string()], created_at: Utc::now(), updated_at: Utc::now(), user_id, file_hash: Some("hash123".to_string()), original_created_at: None, original_modified_at: None, source_path: None, source_type: None, source_id: None, file_permissions: None, file_owner: None, file_group: None, source_metadata: None, ocr_retry_count: None, ocr_failure_reason: None, }; ( document, main_file_path.to_string_lossy().to_string(), thumbnail_path.to_string_lossy().to_string(), processed_path.to_string_lossy().to_string(), ) } #[tokio::test] async fn test_delete_document_files_success() { let (service, temp_dir) = create_test_file_service().await; let user_id = uuid::Uuid::new_v4(); let (document, main_path, thumb_path, processed_path) = create_test_document_with_files(&service, &temp_dir, user_id); // Verify files exist before deletion assert!(Path::new(&main_path).exists()); assert!(Path::new(&thumb_path).exists()); assert!(Path::new(&processed_path).exists()); // Delete document files let result = service.delete_document_files(&document).await; assert!(result.is_ok()); // Verify main file is deleted assert!(!Path::new(&main_path).exists()); // Verify thumbnail and processed files are deleted assert!(!Path::new(&thumb_path).exists()); assert!(!Path::new(&processed_path).exists()); } #[tokio::test] async fn test_delete_document_files_main_file_missing() { let (service, temp_dir) = create_test_file_service().await; let user_id = uuid::Uuid::new_v4(); let (document, main_path, thumb_path, processed_path) = create_test_document_with_files(&service, &temp_dir, user_id); // Delete main file manually before test fs::remove_file(&main_path).unwrap(); assert!(!Path::new(&main_path).exists()); // Verify thumbnail and processed files still exist assert!(Path::new(&thumb_path).exists()); assert!(Path::new(&processed_path).exists()); // Try to delete document files (should still clean up other files) let result = service.delete_document_files(&document).await; assert!(result.is_ok()); // Verify thumbnail and processed files are deleted despite main file missing assert!(!Path::new(&thumb_path).exists()); assert!(!Path::new(&processed_path).exists()); } #[tokio::test] async fn test_delete_document_files_thumbnail_missing() { let (service, temp_dir) = create_test_file_service().await; let user_id = uuid::Uuid::new_v4(); let (document, main_path, thumb_path, processed_path) = create_test_document_with_files(&service, &temp_dir, user_id); // Delete thumbnail file manually before test fs::remove_file(&thumb_path).unwrap(); assert!(!Path::new(&thumb_path).exists()); // Verify other files still exist assert!(Path::new(&main_path).exists()); assert!(Path::new(&processed_path).exists()); // Delete document files let result = service.delete_document_files(&document).await; assert!(result.is_ok()); // Verify main and processed files are deleted assert!(!Path::new(&main_path).exists()); assert!(!Path::new(&processed_path).exists()); } #[tokio::test] async fn test_delete_document_files_processed_missing() { let (service, temp_dir) = create_test_file_service().await; let user_id = uuid::Uuid::new_v4(); let (document, main_path, thumb_path, processed_path) = create_test_document_with_files(&service, &temp_dir, user_id); // Delete processed file manually before test fs::remove_file(&processed_path).unwrap(); assert!(!Path::new(&processed_path).exists()); // Verify other files still exist assert!(Path::new(&main_path).exists()); assert!(Path::new(&thumb_path).exists()); // Delete document files let result = service.delete_document_files(&document).await; assert!(result.is_ok()); // Verify main and thumbnail files are deleted assert!(!Path::new(&main_path).exists()); assert!(!Path::new(&thumb_path).exists()); } #[tokio::test] async fn test_delete_document_files_all_missing() { let (service, _temp_dir) = create_test_file_service().await; let user_id = uuid::Uuid::new_v4(); let document = Document { id: uuid::Uuid::new_v4(), filename: "nonexistent.pdf".to_string(), original_filename: "nonexistent.pdf".to_string(), file_path: "/nonexistent/path/nonexistent.pdf".to_string(), file_size: 1024, mime_type: "application/pdf".to_string(), content: None, ocr_text: None, ocr_confidence: None, ocr_word_count: None, ocr_processing_time_ms: None, ocr_status: Some("pending".to_string()), ocr_error: None, ocr_completed_at: None, tags: vec![], created_at: Utc::now(), updated_at: Utc::now(), user_id, file_hash: None, original_created_at: None, original_modified_at: None, source_path: None, source_type: None, source_id: None, file_permissions: None, file_owner: None, file_group: None, source_metadata: None, ocr_retry_count: None, ocr_failure_reason: None, }; // Try to delete nonexistent files (should not fail) let result = service.delete_document_files(&document).await; assert!(result.is_ok()); } #[tokio::test] async fn test_delete_document_files_with_different_extensions() { let (service, temp_dir) = create_test_file_service().await; let user_id = uuid::Uuid::new_v4(); let document_id = uuid::Uuid::new_v4(); // Create main document file in documents directory let documents_path = temp_dir.path().join("documents"); fs::create_dir_all(&documents_path).unwrap(); let main_file_path = documents_path.join("test_image.png"); fs::write(&main_file_path, b"PNG content").unwrap(); // Create thumbnail in thumbnails directory with correct naming let thumbnails_path = temp_dir.path().join("thumbnails"); fs::create_dir_all(&thumbnails_path).unwrap(); let thumbnail_path = thumbnails_path.join(format!("{}_thumb.jpg", document_id)); fs::write(&thumbnail_path, b"Thumbnail content").unwrap(); // Create processed image in processed_images directory with correct naming let processed_dir = temp_dir.path().join("processed_images"); fs::create_dir_all(&processed_dir).unwrap(); let processed_path = processed_dir.join(format!("{}_processed.png", document_id)); fs::write(&processed_path, b"Processed content").unwrap(); let document = Document { id: document_id, filename: "test_image.png".to_string(), original_filename: "test_image.png".to_string(), file_path: main_file_path.to_string_lossy().to_string(), file_size: 2048, mime_type: "image/png".to_string(), content: None, ocr_text: Some("Image OCR text".to_string()), ocr_confidence: Some(88.2), ocr_word_count: Some(25), ocr_processing_time_ms: Some(800), ocr_status: Some("completed".to_string()), ocr_error: None, ocr_completed_at: Some(Utc::now()), tags: vec!["image".to_string()], created_at: Utc::now(), updated_at: Utc::now(), user_id, file_hash: Some("imagehash456".to_string()), original_created_at: None, original_modified_at: None, source_path: None, source_type: None, source_id: None, file_permissions: None, file_owner: None, file_group: None, source_metadata: None, ocr_retry_count: None, ocr_failure_reason: None, }; // Verify files exist assert!(Path::new(&main_file_path).exists()); assert!(Path::new(&thumbnail_path).exists()); assert!(Path::new(&processed_path).exists()); // Delete document files let result = service.delete_document_files(&document).await; assert!(result.is_ok()); // Verify all files are deleted assert!(!Path::new(&main_file_path).exists()); assert!(!Path::new(&thumbnail_path).exists()); assert!(!Path::new(&processed_path).exists()); } #[tokio::test] async fn test_delete_document_files_partial_failure_continues() { let (service, temp_dir) = create_test_file_service().await; let user_id = uuid::Uuid::new_v4(); let document_id = uuid::Uuid::new_v4(); // Create main file in documents directory let documents_path = temp_dir.path().join("documents"); fs::create_dir_all(&documents_path).unwrap(); let main_file_path = documents_path.join("readonly_document.pdf"); fs::write(&main_file_path, b"PDF content").unwrap(); // Create thumbnail file in thumbnails directory with correct naming let thumbnails_path = temp_dir.path().join("thumbnails"); fs::create_dir_all(&thumbnails_path).unwrap(); let thumbnail_path = thumbnails_path.join(format!("{}_thumb.jpg", document_id)); fs::write(&thumbnail_path, b"Thumbnail content").unwrap(); let document = Document { id: document_id, filename: "readonly_document.pdf".to_string(), original_filename: "readonly_document.pdf".to_string(), file_path: main_file_path.to_string_lossy().to_string(), file_size: 1024, mime_type: "application/pdf".to_string(), content: Some("Test content".to_string()), ocr_text: None, ocr_confidence: None, ocr_word_count: None, ocr_processing_time_ms: None, ocr_status: Some("pending".to_string()), ocr_error: None, ocr_completed_at: None, tags: vec![], created_at: Utc::now(), updated_at: Utc::now(), user_id, file_hash: Some("hash789".to_string()), original_created_at: None, original_modified_at: None, source_path: None, source_type: None, source_id: None, file_permissions: None, file_owner: None, file_group: None, source_metadata: None, ocr_retry_count: None, ocr_failure_reason: None, }; // Verify files exist assert!(Path::new(&main_file_path).exists()); assert!(Path::new(&thumbnail_path).exists()); // Delete document files (should succeed even if some files can't be deleted) let result = service.delete_document_files(&document).await; assert!(result.is_ok()); // At minimum, the function should attempt to delete all files // and not fail completely if one file can't be deleted } #[tokio::test] async fn test_delete_document_files_with_no_extension() { let (service, temp_dir) = create_test_file_service().await; let user_id = uuid::Uuid::new_v4(); let base_path = temp_dir.path().join("documents"); fs::create_dir_all(&base_path).unwrap(); // Create document with no extension let main_file_path = base_path.join("document_no_ext"); fs::write(&main_file_path, b"Content without extension").unwrap(); let document = Document { id: uuid::Uuid::new_v4(), filename: "document_no_ext".to_string(), original_filename: "document_no_ext".to_string(), file_path: main_file_path.to_string_lossy().to_string(), file_size: 512, mime_type: "text/plain".to_string(), content: Some("Plain text content".to_string()), ocr_text: None, ocr_confidence: None, ocr_word_count: None, ocr_processing_time_ms: None, ocr_status: Some("not_applicable".to_string()), ocr_error: None, ocr_completed_at: None, tags: vec!["text".to_string()], created_at: Utc::now(), updated_at: Utc::now(), user_id, file_hash: Some("texthash".to_string()), original_created_at: None, original_modified_at: None, source_path: None, source_type: None, source_id: None, file_permissions: None, file_owner: None, file_group: None, source_metadata: None, ocr_retry_count: None, ocr_failure_reason: None, }; // Verify file exists assert!(Path::new(&main_file_path).exists()); // Delete document files let result = service.delete_document_files(&document).await; assert!(result.is_ok()); // Verify file is deleted assert!(!Path::new(&main_file_path).exists()); } #[tokio::test] async fn test_delete_document_files_concurrent_calls() { let (service, temp_dir) = create_test_file_service().await; let user_id = uuid::Uuid::new_v4(); let (document, main_path, thumb_path, processed_path) = create_test_document_with_files(&service, &temp_dir, user_id); // Verify files exist assert!(Path::new(&main_path).exists()); assert!(Path::new(&thumb_path).exists()); assert!(Path::new(&processed_path).exists()); // Call delete_document_files concurrently let service_clone = service.clone(); let document_clone = document.clone(); let task1 = tokio::spawn(async move { service.delete_document_files(&document).await }); let task2 = tokio::spawn(async move { service_clone.delete_document_files(&document_clone).await }); // Both calls should complete successfully now that FileService handles concurrent deletions let result1 = task1.await.expect("Task 1 should complete"); let result2 = task2.await.expect("Task 2 should complete"); // Both deletion attempts should succeed - the improved FileService handles // "file not found" errors gracefully as they indicate successful deletion by another task assert!(result1.is_ok(), "First deletion task should succeed: {:?}", result1); assert!(result2.is_ok(), "Second deletion task should succeed: {:?}", result2); // Verify files are deleted assert!(!Path::new(&main_path).exists()); assert!(!Path::new(&thumb_path).exists()); assert!(!Path::new(&processed_path).exists()); } }