use bytes::Bytes; use chrono::Utc; use futures::Stream; use std::collections::HashMap; use std::path::PathBuf; use std::pin::Pin; use std::sync::{Arc, Mutex}; use uuid::Uuid; use crate::application::dtos::trash_dto::TrashedItemDto; use crate::application::ports::storage_ports::{FileReadPort, FileWritePort}; use crate::application::ports::trash_ports::TrashUseCase; use crate::common::errors::{DomainError, ErrorKind, Result}; use crate::domain::entities::file::File; use crate::domain::entities::folder::Folder; use crate::domain::entities::trashed_item::{TrashedItem, TrashedItemType}; use crate::domain::repositories::folder_repository::FolderRepository; use crate::domain::repositories::trash_repository::TrashRepository; use crate::domain::services::path_service::StoragePath; /// Test-only service that mirrors `TrashService` logic but accepts generic repos, /// allowing mock repositories to be injected in unit tests. #[allow(dead_code)] struct TrashServiceForTest { trash_repository: Arc, file_read_port: Arc, file_write_port: Arc, folder_storage_port: Arc, retention_days: u32, } impl TrashServiceForTest where TR: TrashRepository, FR: FileReadPort, FW: FileWritePort, FoR: FolderRepository, { #[allow(dead_code)] fn new( trash_repository: Arc, file_read_port: Arc, file_write_port: Arc, folder_storage_port: Arc, retention_days: u32, ) -> Self { Self { trash_repository, file_read_port, file_write_port, folder_storage_port, retention_days, } } } impl TrashUseCase for TrashServiceForTest where TR: TrashRepository, FR: FileReadPort, FW: FileWritePort, FoR: FolderRepository, { async fn get_trash_items(&self, user_id: Uuid) -> Result> { let items = self.trash_repository.get_trash_items(&user_id).await?; Ok(items .into_iter() .map(|item| { let days_until_deletion = item.days_until_deletion(); TrashedItemDto { id: item.id().to_string(), original_id: item.original_id().to_string(), item_type: match item.item_type() { TrashedItemType::File => "file".to_string(), TrashedItemType::Folder => "folder".to_string(), }, name: item.name().to_string(), original_path: item.original_path().to_string(), trashed_at: item.trashed_at(), days_until_deletion, } }) .collect()) } async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: Uuid) -> Result<()> { let item_uuid = Uuid::parse_str(item_id) .map_err(|e| DomainError::validation_error(format!("Invalid item ID: {}", e)))?; match item_type { "file" => { let file = self.file_read_port.get_file(item_id).await.map_err(|e| { DomainError::new( ErrorKind::NotFound, "File", format!("Error retrieving file {}: {}", item_id, e), ) })?; let original_path = file.storage_path().to_string(); let trashed_item = TrashedItem::new( item_uuid, user_id, TrashedItemType::File, file.name().to_string(), original_path, self.retention_days, ); self.trash_repository .add_to_trash(&trashed_item) .await .map_err(|e| { DomainError::internal_error( "TrashRepository", format!("Failed to add file to trash: {}", e), ) })?; self.file_write_port .move_to_trash(item_id) .await .map_err(|e| { DomainError::new( ErrorKind::InternalError, "File", format!("Error moving file {} to trash: {}", item_id, e), ) })?; Ok(()) } "folder" => { let folder = self .folder_storage_port .get_folder(item_id) .await .map_err(|e| { DomainError::new( ErrorKind::NotFound, "Folder", format!("Error retrieving folder {}: {}", item_id, e), ) })?; let original_path = folder.storage_path().to_string(); let trashed_item = TrashedItem::new( item_uuid, user_id, TrashedItemType::Folder, folder.name().to_string(), original_path, self.retention_days, ); self.trash_repository .add_to_trash(&trashed_item) .await .map_err(|e| { DomainError::internal_error( "TrashRepository", format!("Failed to add folder to trash: {}", e), ) })?; self.folder_storage_port .move_to_trash(item_id) .await .map_err(|e| { DomainError::new( ErrorKind::InternalError, "Folder", format!("Error moving folder {} to trash: {}", item_id, e), ) })?; Ok(()) } _ => Err(DomainError::validation_error(format!( "Invalid item type: {}", item_type ))), } } async fn restore_item(&self, trash_id: &str, user_id: Uuid) -> Result<()> { let trash_uuid = Uuid::parse_str(trash_id) .map_err(|e| DomainError::validation_error(format!("Invalid trash ID: {}", e)))?; let item = self .trash_repository .get_trash_item(&trash_uuid, &user_id) .await?; match item { Some(item) => { match item.item_type() { TrashedItemType::File => { let file_id = item.original_id().to_string(); let original_path = item.original_path().to_string(); let result = self .file_write_port .restore_from_trash(&file_id, &original_path) .await; if let Err(e) = result && !format!("{}", e).contains("not found") { return Err(DomainError::new( ErrorKind::InternalError, "File", format!("Error restoring file {} from trash: {}", file_id, e), )); } } TrashedItemType::Folder => { let folder_id = item.original_id().to_string(); let original_path = item.original_path().to_string(); let result = self .folder_storage_port .restore_from_trash(&folder_id, &original_path) .await; if let Err(e) = result && !format!("{}", e).contains("not found") { return Err(DomainError::new( ErrorKind::InternalError, "Folder", format!("Error restoring folder {} from trash: {}", folder_id, e), )); } } } self.trash_repository .restore_from_trash(&trash_uuid, &user_id) .await .map_err(|e| { DomainError::new( ErrorKind::InternalError, "Trash", format!("Error removing trash entry after restoration: {}", e), ) })?; Ok(()) } None => Ok(()), } } async fn delete_permanently(&self, trash_id: &str, user_id: Uuid) -> Result<()> { let trash_uuid = Uuid::parse_str(trash_id) .map_err(|e| DomainError::validation_error(format!("Invalid trash ID: {}", e)))?; let item = self .trash_repository .get_trash_item(&trash_uuid, &user_id) .await?; match item { Some(item) => { match item.item_type() { TrashedItemType::File => { let file_id = item.original_id().to_string(); let result = self.file_write_port.delete_file_permanently(&file_id).await; if let Err(e) = result && !format!("{}", e).contains("not found") { return Err(DomainError::new( ErrorKind::InternalError, "File", format!("Error deleting file {} permanently: {}", file_id, e), )); } } TrashedItemType::Folder => { let folder_id = item.original_id().to_string(); let result = self .folder_storage_port .delete_folder_permanently(&folder_id) .await; if let Err(e) = result && !format!("{}", e).contains("not found") { return Err(DomainError::new( ErrorKind::InternalError, "Folder", format!("Error deleting folder {} permanently: {}", folder_id, e), )); } } } self.trash_repository .delete_permanently(&trash_uuid, &user_id) .await .map_err(|e| { DomainError::new( ErrorKind::InternalError, "Trash", format!("Error removing trash entry: {}", e), ) })?; Ok(()) } None => Ok(()), } } async fn empty_trash(&self, user_id: Uuid) -> Result<()> { self.trash_repository.clear_trash(&user_id).await } } // Mock repositories for testing #[allow(dead_code)] struct MockTrashRepository { trash_items: Mutex>, /// Shared refs to the file/folder trashed maps so `clear_trash` can /// simulate the PG CASCADE + trigger behaviour. trashed_files: Arc>>, trashed_folders: Arc>>, } impl MockTrashRepository { #[allow(dead_code)] fn new( trashed_files: Arc>>, trashed_folders: Arc>>, ) -> Self { Self { trash_items: Mutex::new(HashMap::new()), trashed_files, trashed_folders, } } } impl TrashRepository for MockTrashRepository { async fn add_to_trash(&self, item: &TrashedItem) -> Result<()> { let mut items = self.trash_items.lock().unwrap(); items.insert(item.id(), item.clone()); Ok(()) } async fn get_trash_items(&self, user_id: &Uuid) -> Result> { let items = self.trash_items.lock().unwrap(); let user_items = items .values() .filter(|item| item.user_id() == *user_id) .cloned() .collect(); Ok(user_items) } async fn get_trash_item(&self, id: &Uuid, user_id: &Uuid) -> Result> { let items = self.trash_items.lock().unwrap(); let item = items .get(id) .filter(|item| item.user_id() == *user_id) .cloned(); Ok(item) } async fn restore_from_trash(&self, id: &Uuid, user_id: &Uuid) -> Result<()> { let mut items = self.trash_items.lock().unwrap(); if let Some(item) = items.get(id) && item.user_id() == *user_id { items.remove(id); } Ok(()) } async fn delete_permanently(&self, id: &Uuid, user_id: &Uuid) -> Result<()> { let mut items = self.trash_items.lock().unwrap(); if let Some(item) = items.get(id) && item.user_id() == *user_id { items.remove(id); } Ok(()) } async fn clear_trash(&self, user_id: &Uuid) -> Result<()> { let mut items = self.trash_items.lock().unwrap(); items.retain(|_, item| item.user_id() != *user_id); // Simulate PG CASCADE: clear trashed file/folder storage too self.trashed_files.lock().unwrap().clear(); self.trashed_folders.lock().unwrap().clear(); Ok(()) } async fn delete_expired_bulk(&self) -> Result<(u64, u64)> { let mut items = self.trash_items.lock().unwrap(); let now = Utc::now(); let before = items.len() as u64; items.retain(|_, item| item.deletion_date() > now); let deleted = before - items.len() as u64; Ok((deleted, 0)) } } #[allow(dead_code)] struct MockFileRepository { files: Mutex>, trashed_files: Arc>>, } impl MockFileRepository { #[allow(dead_code)] fn new(trashed_files: Arc>>) -> Self { Self { files: Mutex::new(HashMap::new()), trashed_files, } } #[allow(dead_code)] fn add_test_file(&self, id: &str, name: &str, path: &str) { let file = File::new( id.to_string(), name.to_string(), StoragePath::from_string(path), 100, "text/plain".to_string(), None, ) .unwrap(); let mut files = self.files.lock().unwrap(); files.insert(id.to_string(), file); } } impl FileReadPort for MockFileRepository { async fn get_file(&self, id: &str) -> std::result::Result { let files = self.files.lock().unwrap(); if let Some(file) = files.get(id) { Ok(file.clone()) } else { Err(DomainError::not_found("File", id.to_string())) } } async fn list_files( &self, _folder_id: Option<&str>, ) -> std::result::Result, DomainError> { Ok(vec![]) } async fn get_file_stream( &self, _id: &str, ) -> std::result::Result< Box> + Send>, DomainError, > { unimplemented!() } async fn get_file_range_stream( &self, _id: &str, _start: u64, _end: Option, ) -> std::result::Result< Box> + Send>, DomainError, > { unimplemented!() } async fn get_file_path(&self, _id: &str) -> std::result::Result { unimplemented!() } async fn get_parent_folder_id(&self, _path: &str) -> std::result::Result { unimplemented!() } async fn get_folder_id_by_path( &self, _folder_path: &str, ) -> std::result::Result { unimplemented!() } async fn get_blob_hash(&self, _file_id: &str) -> std::result::Result { Ok(String::new()) } async fn search_files_paginated( &self, _folder_id: Option<&str>, _criteria: &crate::application::dtos::search_dto::SearchCriteriaDto, _user_id: Uuid, ) -> std::result::Result<(Vec, usize), DomainError> { Ok((Vec::new(), 0)) } async fn count_files( &self, _folder_id: Option<&str>, _criteria: &crate::application::dtos::search_dto::SearchCriteriaDto, _user_id: Uuid, ) -> std::result::Result { Ok(0) } async fn stream_files_in_subtree( &self, _folder_id: &str, ) -> std::result::Result< Pin> + Send>>, DomainError, > { Ok(Box::pin(futures::stream::empty())) } async fn get_file_for_owner( &self, id: &str, _owner_id: Uuid, ) -> std::result::Result { // In this mock, ignore ownership — trash tests don't focus on ownership self.get_file(id).await } } impl FileWritePort for MockFileRepository { async fn save_file_from_temp( &self, _name: String, _folder_id: Option, _content_type: String, _temp_path: &std::path::Path, _size: u64, _pre_computed_hash: Option, ) -> std::result::Result { unimplemented!() } async fn move_file( &self, _file_id: &str, _target_folder_id: Option, ) -> std::result::Result { unimplemented!() } async fn rename_file( &self, _file_id: &str, _new_name: &str, ) -> std::result::Result { unimplemented!() } async fn delete_file(&self, _id: &str) -> std::result::Result<(), DomainError> { Ok(()) } async fn update_file_content_from_temp( &self, _file_id: &str, _temp_path: &std::path::Path, _size: u64, _content_type: Option, _pre_computed_hash: Option, _modified_at: Option, ) -> std::result::Result { Ok(String::new()) } async fn register_file_deferred( &self, _name: String, _folder_id: Option, _content_type: String, _size: u64, ) -> std::result::Result<(File, PathBuf), DomainError> { unimplemented!() } async fn copy_file( &self, _file_id: &str, _target_folder_id: Option, ) -> std::result::Result { unimplemented!() } async fn move_to_trash(&self, id: &str) -> std::result::Result<(), DomainError> { let mut files = self.files.lock().unwrap(); let mut trashed = self.trashed_files.lock().unwrap(); if let Some(file) = files.remove(id) { trashed.insert(id.to_string(), file); Ok(()) } else { Err(DomainError::not_found("File", id.to_string())) } } async fn restore_from_trash( &self, id: &str, _original_path: &str, ) -> std::result::Result<(), DomainError> { let mut files = self.files.lock().unwrap(); let mut trashed = self.trashed_files.lock().unwrap(); if let Some(file) = trashed.remove(id) { files.insert(id.to_string(), file); Ok(()) } else { Err(DomainError::not_found( "File", format!("File {} not found in trash", id), )) } } async fn delete_file_permanently(&self, id: &str) -> std::result::Result<(), DomainError> { let mut trashed = self.trashed_files.lock().unwrap(); if trashed.remove(id).is_some() { Ok(()) } else { Err(DomainError::not_found( "File", format!("File {} not found in trash", id), )) } } } #[allow(dead_code)] struct MockFolderRepository { folders: Mutex>, trashed_folders: Arc>>, } impl MockFolderRepository { #[allow(dead_code)] fn new(trashed_folders: Arc>>) -> Self { Self { folders: Mutex::new(HashMap::new()), trashed_folders, } } #[allow(dead_code)] fn add_test_folder(&self, id: &str, name: &str, path: &str) { let folder = Folder::new( id.to_string(), name.to_string(), StoragePath::from_string(path), None, ) .unwrap(); let mut folders = self.folders.lock().unwrap(); folders.insert(id.to_string(), folder); } } impl FolderRepository for MockFolderRepository { async fn create_folder( &self, _name: String, _parent_id: Option, ) -> std::result::Result { unimplemented!() } async fn get_folder(&self, id: &str) -> std::result::Result { let folders = self.folders.lock().unwrap(); if let Some(folder) = folders.get(id) { Ok(folder.clone()) } else { Err(DomainError::not_found("Folder", id.to_string())) } } async fn get_folder_by_path( &self, _storage_path: &StoragePath, ) -> std::result::Result { unimplemented!() } async fn list_folders( &self, _parent_id: Option<&str>, ) -> std::result::Result, DomainError> { Ok(vec![]) } async fn list_folders_by_owner( &self, _parent_id: Option<&str>, _owner_id: Uuid, ) -> std::result::Result, DomainError> { Ok(vec![]) } async fn list_folders_paginated( &self, _parent_id: Option<&str>, _offset: usize, _limit: usize, _include_total: bool, ) -> std::result::Result<(Vec, Option), DomainError> { Ok((vec![], Some(0))) } async fn list_folders_by_owner_paginated( &self, _parent_id: Option<&str>, _owner_id: Uuid, _offset: usize, _limit: usize, _include_total: bool, ) -> std::result::Result<(Vec, Option), DomainError> { Ok((vec![], Some(0))) } async fn rename_folder( &self, _id: &str, _new_name: String, ) -> std::result::Result { unimplemented!() } async fn move_folder( &self, _id: &str, _new_parent_id: Option<&str>, ) -> std::result::Result { unimplemented!() } async fn delete_folder(&self, _id: &str) -> std::result::Result<(), DomainError> { Ok(()) } async fn folder_exists( &self, _storage_path: &StoragePath, ) -> std::result::Result { Ok(false) } async fn get_folder_path(&self, _id: &str) -> std::result::Result { Ok(StoragePath::from_string("/")) } async fn move_to_trash(&self, id: &str) -> std::result::Result<(), DomainError> { let mut folders = self.folders.lock().unwrap(); let mut trashed = self.trashed_folders.lock().unwrap(); if let Some(folder) = folders.remove(id) { trashed.insert(id.to_string(), folder); Ok(()) } else { Err(DomainError::not_found("Folder", id.to_string())) } } async fn restore_from_trash( &self, id: &str, _original_path: &str, ) -> std::result::Result<(), DomainError> { let mut folders = self.folders.lock().unwrap(); let mut trashed = self.trashed_folders.lock().unwrap(); if let Some(folder) = trashed.remove(id) { folders.insert(id.to_string(), folder); Ok(()) } else { Err(DomainError::not_found( "Folder", format!("Folder {} not found in trash", id), )) } } async fn delete_folder_permanently(&self, id: &str) -> std::result::Result<(), DomainError> { let mut trashed = self.trashed_folders.lock().unwrap(); if trashed.remove(id).is_some() { Ok(()) } else { Err(DomainError::not_found( "Folder", format!("Folder {} not found in trash", id), )) } } async fn create_home_folder( &self, _user_id: Uuid, _name: String, ) -> std::result::Result { Ok(Folder::default()) } } #[cfg(integration_tests)] mod tests { #[allow(unused_imports)] use super::*; #[allow(unused_imports)] use crate::application::ports::trash_ports::TrashUseCase; #[allow(unused_imports)] use crate::infrastructure::repositories::pg::file_blob_read_repository::FileBlobReadRepository; #[allow(unused_imports)] use crate::infrastructure::repositories::pg::file_blob_write_repository::FileBlobWriteRepository; #[tokio::test] async fn test_move_file_to_trash() { // Arrange let trashed_files = Arc::new(Mutex::new(HashMap::new())); let trashed_folders = Arc::new(Mutex::new(HashMap::new())); let trash_repo = Arc::new(MockTrashRepository::new( trashed_files.clone(), trashed_folders.clone(), )); let file_repo = Arc::new(MockFileRepository::new(trashed_files)); let folder_repo = Arc::new(MockFolderRepository::new(trashed_folders)); let service = TrashServiceForTest::new( trash_repo.clone(), file_repo.clone(), file_repo.clone(), folder_repo.clone(), 30, // 30 days retention ); let file_id = "550e8400-e29b-41d4-a716-446655440000"; let user_id = "550e8400-e29b-41d4-a716-446655440001"; let user_uuid = Uuid::parse_str(user_id).unwrap(); // Add a test file to the repository file_repo.add_test_file(file_id, "test.txt", "/test/path/test.txt"); // Act let result = service.move_to_trash(file_id, "file", user_uuid).await; // Assert assert!(result.is_ok(), "Moving file to trash failed: {:?}", result); // Verify the file is in trash let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); assert_eq!( trash_items.len(), 1, "Should have exactly one item in trash" ); let trash_item = &trash_items[0]; assert_eq!( trash_item.original_id().to_string(), file_id, "Original ID should match file ID" ); assert_eq!( trash_item.user_id().to_string(), user_id, "User ID should match" ); assert_eq!( *trash_item.item_type(), TrashedItemType::File, "Item type should be File" ); assert_eq!(trash_item.name(), "test.txt", "File name should match"); // Verify file is moved in file repository let files = file_repo.files.lock().unwrap(); let trashed_files = file_repo.trashed_files.lock().unwrap(); assert!( files.get(file_id).is_none(), "File should no longer be in main storage" ); assert!( trashed_files.get(file_id).is_some(), "File should be in trash storage" ); } #[tokio::test] async fn test_move_folder_to_trash() { // Arrange let trashed_files = Arc::new(Mutex::new(HashMap::new())); let trashed_folders = Arc::new(Mutex::new(HashMap::new())); let trash_repo = Arc::new(MockTrashRepository::new( trashed_files.clone(), trashed_folders.clone(), )); let file_repo = Arc::new(MockFileRepository::new(trashed_files)); let folder_repo = Arc::new(MockFolderRepository::new(trashed_folders)); let service = TrashServiceForTest::new( trash_repo.clone(), file_repo.clone(), file_repo.clone(), folder_repo.clone(), 30, // 30 days retention ); let folder_id = "550e8400-e29b-41d4-a716-446655440002"; let user_id = "550e8400-e29b-41d4-a716-446655440001"; let user_uuid = Uuid::parse_str(user_id).unwrap(); // Add a test folder to the repository folder_repo.add_test_folder(folder_id, "test_folder", "/test/path/test_folder"); // Act let result = service.move_to_trash(folder_id, "folder", user_uuid).await; // Assert assert!( result.is_ok(), "Moving folder to trash failed: {:?}", result ); // Verify the folder is in trash let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); assert_eq!( trash_items.len(), 1, "Should have exactly one item in trash" ); let trash_item = &trash_items[0]; assert_eq!( trash_item.original_id().to_string(), folder_id, "Original ID should match folder ID" ); assert_eq!( trash_item.user_id().to_string(), user_id, "User ID should match" ); assert_eq!( *trash_item.item_type(), TrashedItemType::Folder, "Item type should be Folder" ); assert_eq!(trash_item.name(), "test_folder", "Folder name should match"); } #[tokio::test] async fn test_restore_file_from_trash() { // Arrange let trashed_files = Arc::new(Mutex::new(HashMap::new())); let trashed_folders = Arc::new(Mutex::new(HashMap::new())); let trash_repo = Arc::new(MockTrashRepository::new( trashed_files.clone(), trashed_folders.clone(), )); let file_repo = Arc::new(MockFileRepository::new(trashed_files)); let folder_repo = Arc::new(MockFolderRepository::new(trashed_folders)); let service = TrashServiceForTest::new( trash_repo.clone(), file_repo.clone(), file_repo.clone(), folder_repo.clone(), 30, // 30 days retention ); let file_id = "550e8400-e29b-41d4-a716-446655440000"; let user_id = "550e8400-e29b-41d4-a716-446655440001"; let user_uuid = Uuid::parse_str(user_id).unwrap(); let file_path = "/test/path/test.txt"; // Add a test file and move it to trash file_repo.add_test_file(file_id, "test.txt", file_path); service .move_to_trash(file_id, "file", user_uuid) .await .unwrap(); // Get the trash item ID let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); let trash_id = trash_items[0].id().to_string(); // Act let result = service.restore_item(&trash_id, user_uuid).await; // Assert assert!( result.is_ok(), "Restoring file from trash failed: {:?}", result ); // Verify the file is restored in file repository { let files = file_repo.files.lock().unwrap(); let trashed_files = file_repo.trashed_files.lock().unwrap(); assert!( files.get(file_id).is_some(), "File should be back in main storage" ); assert!( trashed_files.get(file_id).is_none(), "File should no longer be in trash storage" ); } // Verify the trash item is removed let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); assert_eq!( trash_items.len(), 0, "Trash should be empty after restoration" ); } #[tokio::test] async fn test_delete_permanently() { // Arrange let trashed_files = Arc::new(Mutex::new(HashMap::new())); let trashed_folders = Arc::new(Mutex::new(HashMap::new())); let trash_repo = Arc::new(MockTrashRepository::new( trashed_files.clone(), trashed_folders.clone(), )); let file_repo = Arc::new(MockFileRepository::new(trashed_files)); let folder_repo = Arc::new(MockFolderRepository::new(trashed_folders)); let service = TrashServiceForTest::new( trash_repo.clone(), file_repo.clone(), file_repo.clone(), folder_repo.clone(), 30, // 30 days retention ); let file_id = "550e8400-e29b-41d4-a716-446655440000"; let user_id = "550e8400-e29b-41d4-a716-446655440001"; let user_uuid = Uuid::parse_str(user_id).unwrap(); // Add a test file and move it to trash file_repo.add_test_file(file_id, "test.txt", "/test/path/test.txt"); service .move_to_trash(file_id, "file", user_uuid) .await .unwrap(); // Get the trash item ID let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); let trash_id = trash_items[0].id().to_string(); // Act let result = service.delete_permanently(&trash_id, user_uuid).await; // Assert assert!( result.is_ok(), "Deleting file permanently failed: {:?}", result ); // Verify the file is permanently deleted { let files = file_repo.files.lock().unwrap(); let trashed_files = file_repo.trashed_files.lock().unwrap(); assert!( files.get(file_id).is_none(), "File should not be in main storage" ); assert!( trashed_files.get(file_id).is_none(), "File should not be in trash storage" ); } // Verify the trash item is removed let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); assert_eq!( trash_items.len(), 0, "Trash should be empty after permanent deletion" ); } #[tokio::test] async fn test_empty_trash() { // Arrange let trashed_files = Arc::new(Mutex::new(HashMap::new())); let trashed_folders = Arc::new(Mutex::new(HashMap::new())); let trash_repo = Arc::new(MockTrashRepository::new( trashed_files.clone(), trashed_folders.clone(), )); let file_repo = Arc::new(MockFileRepository::new(trashed_files)); let folder_repo = Arc::new(MockFolderRepository::new(trashed_folders)); let service = TrashServiceForTest::new( trash_repo.clone(), file_repo.clone(), file_repo.clone(), folder_repo.clone(), 30, // 30 days retention ); let user_id = "550e8400-e29b-41d4-a716-446655440001"; let user_uuid = Uuid::parse_str(user_id).unwrap(); // Add multiple files and folders to trash let file_ids = [ "550e8400-e29b-41d4-a716-446655440010", "550e8400-e29b-41d4-a716-446655440011", ]; let folder_ids = [ "550e8400-e29b-41d4-a716-446655440020", "550e8400-e29b-41d4-a716-446655440021", ]; // Add test files and folders for (i, file_id) in file_ids.iter().enumerate() { file_repo.add_test_file( file_id, &format!("test{}.txt", i), &format!("/test/path/test{}.txt", i), ); service .move_to_trash(file_id, "file", user_uuid) .await .unwrap(); } for (i, folder_id) in folder_ids.iter().enumerate() { folder_repo.add_test_folder( folder_id, &format!("folder{}", i), &format!("/test/path/folder{}", i), ); service .move_to_trash(folder_id, "folder", user_uuid) .await .unwrap(); } // Verify items are in trash let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); assert_eq!(trash_items.len(), 4, "Should have 4 items in trash"); // Act let result = service.empty_trash(user_uuid).await; // Assert assert!(result.is_ok(), "Emptying trash failed: {:?}", result); // Verify all items are permanently deleted for file_id in &file_ids { let files = file_repo.files.lock().unwrap(); let trashed_files = file_repo.trashed_files.lock().unwrap(); assert!( files.get(*file_id).is_none(), "File should not be in main storage" ); assert!( trashed_files.get(*file_id).is_none(), "File should not be in trash storage" ); } for folder_id in &folder_ids { let folders = folder_repo.folders.lock().unwrap(); let trashed_folders = folder_repo.trashed_folders.lock().unwrap(); assert!( folders.get(*folder_id).is_none(), "Folder should not be in main storage" ); assert!( trashed_folders.get(*folder_id).is_none(), "Folder should not be in trash storage" ); } // Verify the trash is empty let trash_items = trash_repo.get_trash_items(&user_uuid).await.unwrap(); assert_eq!(trash_items.len(), 0, "Trash should be empty after emptying"); } }