fix auth errors and add primigenial paper trash

This commit is contained in:
DioCrafts
2025-03-24 16:47:42 +01:00
parent 838ae6e0d4
commit 38b0e9594b
58 changed files with 3701 additions and 135 deletions
+11 -11
View File
@@ -94,7 +94,7 @@ impl FolderUseCase for FolderService {
// Crear la carpeta
let folder = self.folder_storage.create_folder(dto.name, dto.parent_id)
.await
.with_context(|| "Failed to create folder")?;
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to create folder: {}", e)))?;
// Convertir a DTO
Ok(FolderDto::from(folder))
@@ -104,7 +104,7 @@ impl FolderUseCase for FolderService {
async fn get_folder(&self, id: &str) -> Result<FolderDto, DomainError> {
let folder = self.folder_storage.get_folder(id)
.await
.with_context(|| format!("Failed to get folder with ID: {}", id))?;
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {}: {}", id, e)))?;
Ok(FolderDto::from(folder))
}
@@ -116,7 +116,7 @@ impl FolderUseCase for FolderService {
let folder = self.folder_storage.get_folder_by_path(&storage_path)
.await
.with_context(|| format!("Failed to get folder at path: {}", path))?;
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder at path: {}: {}", path, e)))?;
Ok(FolderDto::from(folder))
}
@@ -125,7 +125,7 @@ impl FolderUseCase for FolderService {
async fn list_folders(&self, parent_id: Option<&str>) -> Result<Vec<FolderDto>, DomainError> {
let folders = self.folder_storage.list_folders(parent_id)
.await
.with_context(|| format!("Failed to list folders in parent: {:?}", parent_id))?;
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to list folders in parent: {:?}: {}", parent_id, e)))?;
// Convertir a DTOs
Ok(folders.into_iter().map(FolderDto::from).collect())
@@ -148,7 +148,7 @@ impl FolderUseCase for FolderService {
true // Siempre incluir total para mejor UX
)
.await
.with_context(|| format!("Failed to list folders with pagination in parent: {:?}", parent_id))?;
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to list folders with pagination in parent: {:?}: {}", parent_id, e)))?;
// El total es necesario para calcular la paginación
let total = total_items.unwrap_or(folders.len());
@@ -178,7 +178,7 @@ impl FolderUseCase for FolderService {
// Verificar que la carpeta existe
let existing_folder = self.folder_storage.get_folder(id)
.await
.with_context(|| format!("Failed to get folder with ID: {} for renaming", id))?;
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for renaming: {}", id, e)))?;
// Crear transacción para renombrar
let mut transaction = StorageTransaction::new("rename_folder");
@@ -220,7 +220,7 @@ impl FolderUseCase for FolderService {
// Obtener la carpeta renombrada
let folder = self.folder_storage.get_folder(id)
.await
.with_context(|| format!("Failed to get renamed folder with ID: {}", id))?;
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get renamed folder with ID: {}: {}", id, e)))?;
Ok(FolderDto::from(folder))
}
@@ -230,7 +230,7 @@ impl FolderUseCase for FolderService {
// Verificar que la carpeta origen existe
let source_folder = self.folder_storage.get_folder(id)
.await
.with_context(|| format!("Failed to get folder with ID: {} for moving", id))?;
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for moving: {}", id, e)))?;
// Si se especifica un parent_id, verificar que existe
if let Some(parent_id) = &dto.parent_id {
@@ -295,7 +295,7 @@ impl FolderUseCase for FolderService {
// Obtener la carpeta movida
let folder = self.folder_storage.get_folder(id)
.await
.with_context(|| format!("Failed to get moved folder with ID: {}", id))?;
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get moved folder with ID: {}: {}", id, e)))?;
Ok(FolderDto::from(folder))
}
@@ -305,13 +305,13 @@ impl FolderUseCase for FolderService {
// Verificar que la carpeta existe
let _folder = self.folder_storage.get_folder(id)
.await
.with_context(|| format!("Failed to get folder with ID: {} for deletion", id))?;
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to get folder with ID: {} for deletion: {}", id, e)))?;
// En una implementación real, podríamos verificar permisos, dependencias, etc.
// Eliminar la carpeta
self.folder_storage.delete_folder(id)
.await
.with_context(|| format!("Failed to delete folder with ID: {}", id))
.map_err(|e| DomainError::internal_error("FolderStorage", format!("Failed to delete folder with ID: {}: {}", id, e)))
}
}
+5
View File
@@ -10,9 +10,14 @@ pub mod file_retrieval_service;
pub mod file_management_service;
pub mod file_use_case_factory;
pub mod auth_application_service;
pub mod trash_service;
#[cfg(test)]
mod trash_service_test;
// Re-exportar para facilitar acceso
pub use file_upload_service::FileUploadService;
pub use file_retrieval_service::FileRetrievalService;
pub use file_management_service::FileManagementService;
pub use file_use_case_factory::AppFileUseCaseFactory;
pub use trash_service::TrashService;
@@ -192,6 +192,19 @@ impl FolderRepository for FolderRepositoryStub {
async fn get_folder_by_path(&self, _path: &std::path::PathBuf) -> Result<Folder, FolderRepositoryError> {
Err(FolderRepositoryError::Other("Stub repository".to_string()))
}
// Trash functionality stubs
async fn move_to_trash(&self, _folder_id: &str) -> Result<(), FolderRepositoryError> {
Err(FolderRepositoryError::OperationNotSupported("Trash feature temporarily disabled".to_string()))
}
async fn restore_from_trash(&self, _folder_id: &str, _original_path: &str) -> Result<(), FolderRepositoryError> {
Err(FolderRepositoryError::OperationNotSupported("Trash feature temporarily disabled".to_string()))
}
async fn delete_folder_permanently(&self, _folder_id: &str) -> Result<(), FolderRepositoryError> {
Err(FolderRepositoryError::OperationNotSupported("Trash feature temporarily disabled".to_string()))
}
}
/// Stub implementation for initialization dependency issues
+299
View File
@@ -0,0 +1,299 @@
use std::sync::Arc;
use async_trait::async_trait;
use uuid::Uuid;
use tracing::{debug, error, info, instrument};
use crate::application::dtos::trash_dto::TrashedItemDto;
use crate::application::ports::trash_ports::TrashUseCase;
use crate::common::errors::{Result, DomainError, ErrorKind};
use crate::domain::entities::trashed_item::{TrashedItem, TrashedItemType};
use crate::domain::repositories::file_repository::{FileRepository, FileRepositoryResult};
use crate::domain::repositories::folder_repository::{FolderRepository, FolderRepositoryResult};
use crate::domain::repositories::trash_repository::TrashRepository;
/// Servicio de aplicación para operaciones de papelera
pub struct TrashService {
trash_repository: Arc<dyn TrashRepository>,
file_repository: Arc<dyn FileRepository>,
folder_repository: Arc<dyn FolderRepository>,
retention_days: u32,
}
impl TrashService {
pub fn new(
trash_repository: Arc<dyn TrashRepository>,
file_repository: Arc<dyn FileRepository>,
folder_repository: Arc<dyn FolderRepository>,
retention_days: u32,
) -> Self {
Self {
trash_repository,
file_repository,
folder_repository,
retention_days,
}
}
/// Convierte una entidad TrashedItem a un DTO
fn to_dto(&self, item: TrashedItem) -> TrashedItemDto {
// Calcular days_until_deletion antes de mover item.original_path
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,
original_path: item.original_path,
trashed_at: item.trashed_at,
days_until_deletion,
}
}
/// Valida los permisos del usuario sobre un elemento
#[instrument(skip(self))]
async fn validate_user_ownership(&self, _item_id: &str, _user_id: &str) -> Result<()> {
// Aquí implementaríamos la validación de permisos
// Por ahora, simplemente devolvemos Ok ya que no tenemos una implementación completa
// de permisos por usuario
Ok(())
}
}
#[async_trait]
impl TrashUseCase for TrashService {
#[instrument(skip(self))]
async fn get_trash_items(&self, user_id: &str) -> Result<Vec<TrashedItemDto>> {
debug!("Obteniendo elementos en papelera para usuario: {}", user_id);
let user_uuid = Uuid::parse_str(user_id)
.map_err(|e| DomainError::validation_error("User", format!("Invalid user ID: {}", e)))?;
let items = self.trash_repository.get_trash_items(&user_uuid).await?;
let dtos = items.into_iter()
.map(|item| self.to_dto(item))
.collect();
Ok(dtos)
}
#[instrument(skip(self))]
async fn move_to_trash(&self, item_id: &str, item_type: &str, user_id: &str) -> Result<()> {
info!("Moviendo a papelera: tipo={}, id={}, usuario={}", item_type, item_id, user_id);
self.validate_user_ownership(item_id, user_id).await?;
let item_uuid = Uuid::parse_str(item_id)
.map_err(|e| DomainError::validation_error("Item", format!("Invalid item ID: {}", e)))?;
let user_uuid = Uuid::parse_str(user_id)
.map_err(|e| DomainError::validation_error("User", format!("Invalid user ID: {}", e)))?;
match item_type {
"file" => {
// Obtener el archivo para verificar que existe y capturar sus datos
let file = self.file_repository.get_file_by_id(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();
// Crear el elemento de papelera
let trashed_item = TrashedItem::new(
item_uuid,
user_uuid,
TrashedItemType::File,
file.name().to_string(),
original_path,
self.retention_days,
);
// Primero añadimos a la papelera para registrar el elemento
self.trash_repository.add_to_trash(&trashed_item).await?;
// Luego movemos el archivo físicamente a la papelera
self.file_repository.move_to_trash(item_id).await
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"File",
format!("Error moving file {} to trash: {}", item_id, e)
))?;
debug!("Archivo movido a papelera: {}", item_id);
Ok(())
},
"folder" => {
// Obtener la carpeta para verificar que existe y capturar sus datos
let folder = self.folder_repository.get_folder_by_id(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();
// Crear el elemento de papelera
let trashed_item = TrashedItem::new(
item_uuid,
user_uuid,
TrashedItemType::Folder,
folder.name().to_string(),
original_path,
self.retention_days,
);
// Primero añadimos a la papelera para registrar el elemento
self.trash_repository.add_to_trash(&trashed_item).await?;
// Luego movemos la carpeta físicamente a la papelera
self.folder_repository.move_to_trash(item_id).await
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"Folder",
format!("Error moving folder {} to trash: {}", item_id, e)
))?;
debug!("Carpeta movida a papelera: {}", item_id);
Ok(())
},
_ => Err(DomainError::validation_error("Item", format!("Invalid item type: {}", item_type))),
}
}
#[instrument(skip(self))]
async fn restore_item(&self, trash_id: &str, user_id: &str) -> Result<()> {
info!("Restaurando elemento {} para usuario {}", trash_id, user_id);
let trash_uuid = Uuid::parse_str(trash_id)
.map_err(|e| DomainError::validation_error("Trash", format!("Invalid trash ID: {}", e)))?;
let user_uuid = Uuid::parse_str(user_id)
.map_err(|e| DomainError::validation_error("User", format!("Invalid user ID: {}", e)))?;
// Obtener el elemento de la papelera
let item = self.trash_repository.get_trash_item(&trash_uuid, &user_uuid).await?
.ok_or_else(|| DomainError::not_found("TrashedItem", trash_id.to_string()))?;
// Restaurar según tipo
match item.item_type {
TrashedItemType::File => {
// Restaurar el archivo a su ubicación original
let file_id = item.original_id.to_string();
self.file_repository.restore_from_trash(&file_id, &item.original_path).await
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"File",
format!("Error restoring file {} from trash: {}", file_id, e)
))?;
debug!("Archivo restaurado desde papelera: {}", file_id);
},
TrashedItemType::Folder => {
// Restaurar la carpeta a su ubicación original
let folder_id = item.original_id.to_string();
self.folder_repository.restore_from_trash(&folder_id, &item.original_path).await
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"Folder",
format!("Error restoring folder {} from trash: {}", folder_id, e)
))?;
debug!("Carpeta restaurada desde papelera: {}", folder_id);
}
}
// Eliminar el item de la papelera
self.trash_repository.restore_from_trash(&trash_uuid, &user_uuid).await?;
Ok(())
}
#[instrument(skip(self))]
async fn delete_permanently(&self, trash_id: &str, user_id: &str) -> Result<()> {
info!("Eliminando permanentemente elemento {} para usuario {}", trash_id, user_id);
let trash_uuid = Uuid::parse_str(trash_id)
.map_err(|e| DomainError::validation_error("Trash", format!("Invalid trash ID: {}", e)))?;
let user_uuid = Uuid::parse_str(user_id)
.map_err(|e| DomainError::validation_error("User", format!("Invalid user ID: {}", e)))?;
// Obtener el elemento de la papelera
let item = self.trash_repository.get_trash_item(&trash_uuid, &user_uuid).await?
.ok_or_else(|| DomainError::not_found("TrashedItem", trash_id.to_string()))?;
// Eliminar permanentemente según tipo
match item.item_type {
TrashedItemType::File => {
// Eliminar el archivo permanentemente
let file_id = item.original_id.to_string();
self.file_repository.delete_file_permanently(&file_id).await
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"File",
format!("Error deleting file {} permanently: {}", file_id, e)
))?;
debug!("Archivo eliminado permanentemente: {}", file_id);
},
TrashedItemType::Folder => {
// Eliminar la carpeta permanentemente
let folder_id = item.original_id.to_string();
self.folder_repository.delete_folder_permanently(&folder_id).await
.map_err(|e| DomainError::new(
ErrorKind::InternalError,
"Folder",
format!("Error deleting folder {} permanently: {}", folder_id, e)
))?;
debug!("Carpeta eliminada permanentemente: {}", folder_id);
}
}
// Eliminar el item de la papelera
self.trash_repository.delete_permanently(&trash_uuid, &user_uuid).await?;
Ok(())
}
#[instrument(skip(self))]
async fn empty_trash(&self, user_id: &str) -> Result<()> {
info!("Vaciando papelera para usuario {}", user_id);
let user_uuid = Uuid::parse_str(user_id)
.map_err(|e| DomainError::validation_error("User", format!("Invalid user ID: {}", e)))?;
// Obtener todos los elementos en la papelera del usuario
let items = self.trash_repository.get_trash_items(&user_uuid).await?;
// Eliminar permanentemente cada elemento
for item in items {
match item.item_type {
TrashedItemType::File => {
// Eliminar el archivo permanentemente
let file_id = item.original_id.to_string();
if let Err(e) = self.file_repository.delete_file_permanently(&file_id).await {
error!("Error al eliminar archivo {} permanentemente: {}", file_id, e);
}
},
TrashedItemType::Folder => {
// Eliminar la carpeta permanentemente
let folder_id = item.original_id.to_string();
if let Err(e) = self.folder_repository.delete_folder_permanently(&folder_id).await {
error!("Error al eliminar carpeta {} permanentemente: {}", folder_id, e);
}
}
}
}
// Limpiar todos los registros de la papelera para este usuario
self.trash_repository.clear_trash(&user_uuid).await?;
info!("Papelera vaciada completamente para usuario {}", user_id);
Ok(())
}
}
@@ -0,0 +1,496 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use chrono::Utc;
use async_trait::async_trait;
use uuid::Uuid;
use crate::common::errors::{Result, DomainError};
use crate::domain::entities::file::File;
use crate::domain::entities::folder::Folder;
use crate::domain::entities::trashed_item::{TrashedItem, TrashedItemType};
use crate::domain::repositories::file_repository::{FileRepository, FileRepositoryResult};
use crate::domain::repositories::folder_repository::{FolderRepository, FolderRepositoryResult};
use crate::domain::repositories::trash_repository::TrashRepository;
use crate::application::services::trash_service::TrashService;
// Mock repositories for testing
struct MockTrashRepository {
trash_items: Mutex<HashMap<Uuid, TrashedItem>>,
}
impl MockTrashRepository {
fn new() -> Self {
Self {
trash_items: Mutex::new(HashMap::new()),
}
}
}
#[async_trait]
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<Vec<TrashedItem>> {
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<Option<TrashedItem>> {
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) {
if 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) {
if 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);
Ok(())
}
async fn get_expired_items(&self) -> Result<Vec<TrashedItem>> {
let items = self.trash_items.lock().unwrap();
let now = Utc::now();
let expired = items.values()
.filter(|item| item.deletion_date <= now)
.cloned()
.collect();
Ok(expired)
}
}
struct MockFileRepository {
files: Mutex<HashMap<String, File>>,
trashed_files: Mutex<HashMap<String, File>>,
}
impl MockFileRepository {
fn new() -> Self {
Self {
files: Mutex::new(HashMap::new()),
trashed_files: Mutex::new(HashMap::new()),
}
}
fn add_test_file(&self, id: &str, name: &str, path: &str) {
let file = File::new(
Uuid::parse_str(id).unwrap(),
name.to_string(),
path.to_string(),
"text/plain".to_string(),
100,
Uuid::new_v4(),
None,
).unwrap();
let mut files = self.files.lock().unwrap();
files.insert(id.to_string(), file);
}
}
#[async_trait]
impl FileRepository for MockFileRepository {
async fn get_file_by_id(&self, id: &str) -> FileRepositoryResult<File> {
let files = self.files.lock().unwrap();
if let Some(file) = files.get(id) {
Ok(file.clone())
} else {
Err("File not found".into())
}
}
async fn move_to_trash(&self, id: &str) -> FileRepositoryResult<()> {
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("File not found".into())
}
}
async fn restore_from_trash(&self, id: &str, original_path: &str) -> FileRepositoryResult<()> {
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("File not found in trash".into())
}
}
async fn delete_file_permanently(&self, id: &str) -> FileRepositoryResult<()> {
let mut trashed = self.trashed_files.lock().unwrap();
if trashed.remove(id).is_some() {
Ok(())
} else {
Err("File not found in trash".into())
}
}
// Other methods required by the trait (not used in tests)
async fn save_file(&self, _file: &File) -> FileRepositoryResult<()> { Ok(()) }
async fn delete_file(&self, _id: &str) -> FileRepositoryResult<()> { Ok(()) }
async fn get_files_in_folder(&self, _folder_id: Option<&str>) -> FileRepositoryResult<Vec<File>> { Ok(vec![]) }
async fn move_file(&self, _id: &str, _new_folder_id: Option<&str>) -> FileRepositoryResult<()> { Ok(()) }
async fn update_file_data(&self, _id: &str, _new_data: &[u8]) -> FileRepositoryResult<()> { Ok(()) }
async fn get_file_data(&self, _id: &str) -> FileRepositoryResult<Vec<u8>> { Ok(vec![]) }
}
struct MockFolderRepository {
folders: Mutex<HashMap<String, Folder>>,
trashed_folders: Mutex<HashMap<String, Folder>>,
}
impl MockFolderRepository {
fn new() -> Self {
Self {
folders: Mutex::new(HashMap::new()),
trashed_folders: Mutex::new(HashMap::new()),
}
}
fn add_test_folder(&self, id: &str, name: &str, path: &str) {
let folder = Folder::new(
Uuid::parse_str(id).unwrap(),
name.to_string(),
path.to_string(),
None,
).unwrap();
let mut folders = self.folders.lock().unwrap();
folders.insert(id.to_string(), folder);
}
}
#[async_trait]
impl FolderRepository for MockFolderRepository {
async fn get_folder_by_id(&self, id: &str) -> FolderRepositoryResult<Folder> {
let folders = self.folders.lock().unwrap();
if let Some(folder) = folders.get(id) {
Ok(folder.clone())
} else {
Err("Folder not found".into())
}
}
async fn move_to_trash(&self, id: &str) -> FolderRepositoryResult<()> {
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("Folder not found".into())
}
}
async fn restore_from_trash(&self, id: &str, original_path: &str) -> FolderRepositoryResult<()> {
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("Folder not found in trash".into())
}
}
async fn delete_folder_permanently(&self, id: &str) -> FolderRepositoryResult<()> {
let mut trashed = self.trashed_folders.lock().unwrap();
if trashed.remove(id).is_some() {
Ok(())
} else {
Err("Folder not found in trash".into())
}
}
// Other methods required by the trait (not used in tests)
async fn save_folder(&self, _folder: &Folder) -> FolderRepositoryResult<()> { Ok(()) }
async fn delete_folder(&self, _id: &str) -> FolderRepositoryResult<()> { Ok(()) }
async fn get_folders_in_folder(&self, _parent_id: Option<&str>) -> FolderRepositoryResult<Vec<Folder>> { Ok(vec![]) }
async fn move_folder(&self, _id: &str, _new_parent_id: Option<&str>) -> FolderRepositoryResult<()> { Ok(()) }
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_move_file_to_trash() {
// Arrange
let trash_repo = Arc::new(MockTrashRepository::new());
let file_repo = Arc::new(MockFileRepository::new());
let folder_repo = Arc::new(MockFolderRepository::new());
let service = TrashService::new(
trash_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";
// 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_id).await;
// Assert
assert!(result.is_ok(), "Moving file to trash failed: {:?}", result);
// Verify the file is in trash
let user_uuid = Uuid::parse_str(user_id).unwrap();
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 trash_repo = Arc::new(MockTrashRepository::new());
let file_repo = Arc::new(MockFileRepository::new());
let folder_repo = Arc::new(MockFolderRepository::new());
let service = TrashService::new(
trash_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";
// 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_id).await;
// Assert
assert!(result.is_ok(), "Moving folder to trash failed: {:?}", result);
// Verify the folder is in trash
let user_uuid = Uuid::parse_str(user_id).unwrap();
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 trash_repo = Arc::new(MockTrashRepository::new());
let file_repo = Arc::new(MockFileRepository::new());
let folder_repo = Arc::new(MockFolderRepository::new());
let service = TrashService::new(
trash_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 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_id).await.unwrap();
// Get the trash item ID
let user_uuid = Uuid::parse_str(user_id).unwrap();
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_id).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 trash_repo = Arc::new(MockTrashRepository::new());
let file_repo = Arc::new(MockFileRepository::new());
let folder_repo = Arc::new(MockFolderRepository::new());
let service = TrashService::new(
trash_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";
// 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_id).await.unwrap();
// Get the trash item ID
let user_uuid = Uuid::parse_str(user_id).unwrap();
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_id).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 trash_repo = Arc::new(MockTrashRepository::new());
let file_repo = Arc::new(MockFileRepository::new());
let folder_repo = Arc::new(MockFolderRepository::new());
let service = TrashService::new(
trash_repo.clone(),
file_repo.clone(),
folder_repo.clone(),
30, // 30 days retention
);
let user_id = "550e8400-e29b-41d4-a716-446655440001";
// 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_id).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_id).await.unwrap();
}
// Verify items are in trash
let user_uuid = Uuid::parse_str(user_id).unwrap();
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_id).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");
}
}