From e3c276226a4e01d92c99a044ec752a83adaf6cc4 Mon Sep 17 00:00:00 2001 From: aaldebs99 Date: Fri, 20 Jun 2025 16:09:27 +0000 Subject: [PATCH] feat(tests): add deletion unit tests --- src/tests/document_routes_tests.rs | 373 ++++++ src/tests/documents_tests.rs | 1242 ++++++++++++++++++ src/tests/file_service_tests.rs | 375 ++++++ src/tests/mod.rs | 1 + tests/document_deletion_integration_tests.rs | 597 +++++++++ 5 files changed, 2588 insertions(+) create mode 100644 src/tests/document_routes_tests.rs create mode 100644 tests/document_deletion_integration_tests.rs diff --git a/src/tests/document_routes_tests.rs b/src/tests/document_routes_tests.rs new file mode 100644 index 0000000..4934a5d --- /dev/null +++ b/src/tests/document_routes_tests.rs @@ -0,0 +1,373 @@ +#[cfg(test)] +mod document_routes_deletion_tests { + use crate::models::{UserRole, User, Document, BulkDeleteRequest}; + use crate::routes::documents::{delete_document, bulk_delete_documents}; + use crate::auth::AuthUser; + use crate::AppState; + use axum::{ + extract::{Path, State}, + http::StatusCode, + Json, + }; + use chrono::Utc; + use serde_json::json; + use std::sync::Arc; + use uuid::Uuid; + + // Mock implementations for testing + struct MockAppState { + // Add fields that AppState would have for testing + pub delete_results: std::collections::HashMap, + pub bulk_delete_results: std::collections::HashMap, Vec>, + } + + impl MockAppState { + fn new() -> Self { + Self { + delete_results: std::collections::HashMap::new(), + bulk_delete_results: std::collections::HashMap::new(), + } + } + } + + fn create_test_user(role: UserRole) -> User { + User { + id: Uuid::new_v4(), + username: "testuser".to_string(), + email: "test@example.com".to_string(), + password_hash: "hashed_password".to_string(), + role, + created_at: Utc::now(), + updated_at: Utc::now(), + } + } + + fn create_test_document(user_id: Uuid) -> Document { + Document { + id: Uuid::new_v4(), + filename: "test_document.pdf".to_string(), + original_filename: "test_document.pdf".to_string(), + file_path: "/uploads/test_document.pdf".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()), + } + } + + #[test] + fn test_bulk_delete_request_serialization() { + let request = BulkDeleteRequest { + document_ids: vec![Uuid::new_v4(), Uuid::new_v4()], + }; + + // Test serialization + let json = serde_json::to_string(&request).unwrap(); + assert!(json.contains("document_ids")); + + // Test deserialization + let deserialized: BulkDeleteRequest = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.document_ids.len(), 2); + assert_eq!(deserialized.document_ids, request.document_ids); + } + + #[test] + fn test_bulk_delete_request_empty_list() { + let request = BulkDeleteRequest { + document_ids: vec![], + }; + + let json = serde_json::to_string(&request).unwrap(); + let deserialized: BulkDeleteRequest = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.document_ids.len(), 0); + } + + #[test] + fn test_bulk_delete_request_validation() { + // Test with valid UUIDs + let valid_request = json!({ + "document_ids": [ + "550e8400-e29b-41d4-a716-446655440000", + "550e8400-e29b-41d4-a716-446655440001" + ] + }); + + let result: Result = serde_json::from_value(valid_request); + assert!(result.is_ok()); + assert_eq!(result.unwrap().document_ids.len(), 2); + + // Test with invalid UUIDs should fail + let invalid_request = json!({ + "document_ids": ["not-a-uuid", "also-not-a-uuid"] + }); + + let result: Result = serde_json::from_value(invalid_request); + assert!(result.is_err()); + } + + #[test] + fn test_auth_user_role_permissions() { + let user = create_test_user(UserRole::User); + let admin = create_test_user(UserRole::Admin); + + // Test user role + assert_eq!(user.role, UserRole::User); + assert_ne!(user.role, UserRole::Admin); + + // Test admin role + assert_eq!(admin.role, UserRole::Admin); + assert_ne!(admin.role, UserRole::User); + } + + #[test] + fn test_document_deletion_authorization_logic() { + let user1 = create_test_user(UserRole::User); + let user2 = create_test_user(UserRole::User); + let admin = create_test_user(UserRole::Admin); + + let document = create_test_document(user1.id); + + // User1 should be able to delete their own document + let can_delete_own = document.user_id == user1.id || user1.role == UserRole::Admin; + assert!(can_delete_own); + + // User2 should not be able to delete user1's document + let can_delete_other = document.user_id == user2.id || user2.role == UserRole::Admin; + assert!(!can_delete_other); + + // Admin should be able to delete any document + let admin_can_delete = document.user_id == admin.id || admin.role == UserRole::Admin; + assert!(admin_can_delete); + } + + #[test] + fn test_bulk_delete_authorization_logic() { + let user1 = create_test_user(UserRole::User); + let user2 = create_test_user(UserRole::User); + let admin = create_test_user(UserRole::Admin); + + let doc1_user1 = create_test_document(user1.id); + let doc2_user1 = create_test_document(user1.id); + let doc1_user2 = create_test_document(user2.id); + + let all_documents = vec![&doc1_user1, &doc2_user1, &doc1_user2]; + + // Test what user1 can delete + let user1_can_delete: Vec<&Document> = all_documents + .iter() + .filter(|doc| doc.user_id == user1.id || user1.role == UserRole::Admin) + .cloned() + .collect(); + assert_eq!(user1_can_delete.len(), 2); // Only their own documents + + // Test what admin can delete + let admin_can_delete: Vec<&Document> = all_documents + .iter() + .filter(|doc| doc.user_id == admin.id || admin.role == UserRole::Admin) + .cloned() + .collect(); + assert_eq!(admin_can_delete.len(), 3); // All documents + } + + #[test] + fn test_document_response_format() { + let user = create_test_user(UserRole::User); + let document = create_test_document(user.id); + + // Test successful deletion response format + let success_response = json!({ + "success": true, + "message": "Document deleted successfully", + "document_id": document.id + }); + + assert_eq!(success_response["success"], true); + assert!(success_response["message"].is_string()); + assert_eq!(success_response["document_id"], document.id.to_string()); + + // Test error response format + let error_response = json!({ + "success": false, + "error": "Document not found or not authorized to delete" + }); + + assert_eq!(error_response["success"], false); + assert!(error_response["error"].is_string()); + } + + #[test] + fn test_bulk_delete_response_format() { + let user = create_test_user(UserRole::User); + let doc1 = create_test_document(user.id); + let doc2 = create_test_document(user.id); + + // Test successful bulk deletion response format + let success_response = json!({ + "success": true, + "message": "2 documents deleted successfully", + "deleted_count": 2, + "deleted_documents": [ + { + "id": doc1.id, + "filename": doc1.filename + }, + { + "id": doc2.id, + "filename": doc2.filename + } + ] + }); + + assert_eq!(success_response["success"], true); + assert_eq!(success_response["deleted_count"], 2); + assert!(success_response["deleted_documents"].is_array()); + assert_eq!(success_response["deleted_documents"].as_array().unwrap().len(), 2); + + // Test partial success response format + let partial_response = json!({ + "success": true, + "message": "1 of 2 documents deleted successfully", + "deleted_count": 1, + "requested_count": 2, + "deleted_documents": [ + { + "id": doc1.id, + "filename": doc1.filename + } + ] + }); + + assert_eq!(partial_response["success"], true); + assert_eq!(partial_response["deleted_count"], 1); + assert_eq!(partial_response["requested_count"], 2); + } + + #[test] + fn test_http_status_codes() { + // Test successful deletion status codes + assert_eq!(StatusCode::OK.as_u16(), 200); + + // Test error status codes + assert_eq!(StatusCode::NOT_FOUND.as_u16(), 404); + assert_eq!(StatusCode::UNAUTHORIZED.as_u16(), 401); + assert_eq!(StatusCode::FORBIDDEN.as_u16(), 403); + assert_eq!(StatusCode::BAD_REQUEST.as_u16(), 400); + assert_eq!(StatusCode::INTERNAL_SERVER_ERROR.as_u16(), 500); + } + + #[test] + fn test_path_parameter_parsing() { + let document_id = Uuid::new_v4(); + let path_str = format!("/documents/{}", document_id); + + // Test that UUID can be parsed from path + let parsed_id = document_id.to_string(); + let reparsed_id = Uuid::parse_str(&parsed_id).unwrap(); + assert_eq!(reparsed_id, document_id); + } + + #[test] + fn test_json_request_validation() { + // Test valid JSON request + let valid_json = json!({ + "document_ids": [ + "550e8400-e29b-41d4-a716-446655440000", + "550e8400-e29b-41d4-a716-446655440001" + ] + }); + + let result: Result = serde_json::from_value(valid_json); + assert!(result.is_ok()); + + // Test invalid JSON structure + let invalid_json = json!({ + "wrong_field": ["not-document-ids"] + }); + + let result: Result = serde_json::from_value(invalid_json); + assert!(result.is_err()); + + // Test empty request + let empty_json = json!({ + "document_ids": [] + }); + + let result: Result = serde_json::from_value(empty_json); + assert!(result.is_ok()); + assert_eq!(result.unwrap().document_ids.len(), 0); + } + + #[test] + fn test_concurrent_deletion_safety() { + let user = create_test_user(UserRole::User); + let document = create_test_document(user.id); + + // Test that multiple deletion attempts for the same document + // should be handled gracefully (first succeeds, subsequent ones are no-op) + let document_id = document.id; + + // Simulate concurrent deletions by checking if the same document ID + // would be processed multiple times + let mut processed_ids = std::collections::HashSet::new(); + + // First deletion attempt + let first_attempt = processed_ids.insert(document_id); + assert!(first_attempt); // Should be true (new entry) + + // Second deletion attempt + let second_attempt = processed_ids.insert(document_id); + assert!(!second_attempt); // Should be false (already exists) + } + + #[test] + fn test_bulk_delete_request_size_limits() { + // Test reasonable request size + let reasonable_request = BulkDeleteRequest { + document_ids: (0..10).map(|_| Uuid::new_v4()).collect(), + }; + assert_eq!(reasonable_request.document_ids.len(), 10); + + // Test large request size (should still be valid but might be rate-limited in real app) + let large_request = BulkDeleteRequest { + document_ids: (0..100).map(|_| Uuid::new_v4()).collect(), + }; + assert_eq!(large_request.document_ids.len(), 100); + + // Test very large request size (might need limits in production) + let very_large_request = BulkDeleteRequest { + document_ids: (0..1000).map(|_| Uuid::new_v4()).collect(), + }; + assert_eq!(very_large_request.document_ids.len(), 1000); + } + + #[test] + fn test_error_message_formats() { + // Test error messages for different scenarios + let not_found_error = "Document not found"; + let unauthorized_error = "Not authorized to delete this document"; + let validation_error = "Invalid request format"; + let server_error = "Internal server error occurred during deletion"; + + assert!(!not_found_error.is_empty()); + assert!(!unauthorized_error.is_empty()); + assert!(!validation_error.is_empty()); + assert!(!server_error.is_empty()); + + // Test that error messages are user-friendly + assert!(!not_found_error.contains("SQL")); + assert!(!not_found_error.contains("database")); + assert!(!unauthorized_error.contains("403")); + assert!(!validation_error.contains("serde")); + } +} \ No newline at end of file diff --git a/src/tests/documents_tests.rs b/src/tests/documents_tests.rs index 095d159..273d12f 100644 --- a/src/tests/documents_tests.rs +++ b/src/tests/documents_tests.rs @@ -304,4 +304,1246 @@ mod tests { assert!(ocr_text.len() < 100000); // Reasonable upper limit } } +} + +#[cfg(test)] +mod document_deletion_tests { + use super::*; + use crate::db::documents::DocumentsDB; + use crate::models::{UserRole, User}; + use sqlx::PgPool; + use std::env; + + async fn create_test_db_pool() -> PgPool { + let database_url = env::var("TEST_DATABASE_URL") + .expect("TEST_DATABASE_URL must be set for database tests"); + PgPool::connect(&database_url) + .await + .expect("Failed to connect to test database") + } + + async fn create_test_user(pool: &PgPool, role: UserRole) -> User { + let user_id = Uuid::new_v4(); + let user = User { + id: user_id, + username: format!("testuser_{}", user_id), + email: format!("test_{}@example.com", user_id), + password_hash: "hashed_password".to_string(), + role, + created_at: Utc::now(), + updated_at: Utc::now(), + }; + + // Insert user into database + sqlx::query!( + "INSERT INTO users (id, username, email, password_hash, role, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7)", + user.id, + user.username, + user.email, + user.password_hash, + user.role as UserRole, + user.created_at, + user.updated_at + ) + .execute(pool) + .await + .expect("Failed to insert test user"); + + user + } + + async fn create_and_insert_test_document(pool: &PgPool, user_id: Uuid) -> Document { + let document = super::create_test_document(user_id); + + // Insert document into database + sqlx::query!( + "INSERT INTO documents (id, filename, original_filename, file_path, file_size, mime_type, + content, ocr_text, ocr_confidence, ocr_word_count, ocr_processing_time_ms, ocr_status, + ocr_error, ocr_completed_at, tags, created_at, updated_at, user_id, file_hash) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)", + document.id, + document.filename, + document.original_filename, + document.file_path, + document.file_size as i64, + document.mime_type, + document.content, + document.ocr_text, + document.ocr_confidence, + document.ocr_word_count.map(|x| x as i32), + document.ocr_processing_time_ms.map(|x| x as i32), + document.ocr_status, + document.ocr_error, + document.ocr_completed_at, + &document.tags, + document.created_at, + document.updated_at, + document.user_id, + document.file_hash + ) + .execute(pool) + .await + .expect("Failed to insert test document"); + + document + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_delete_document_as_owner() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + // Create test user and document + let user = create_test_user(&pool, UserRole::User).await; + let document = create_and_insert_test_document(&pool, user.id).await; + + // Delete document as owner + let result = documents_db + .delete_document(document.id, user.id, user.role) + .await + .expect("Failed to delete document"); + + // Verify document was deleted + assert!(result.is_some()); + let deleted_doc = result.unwrap(); + assert_eq!(deleted_doc.id, document.id); + assert_eq!(deleted_doc.user_id, user.id); + + // Verify document no longer exists in database + let found_doc = documents_db + .get_document_by_id(document.id, user.id, user.role) + .await + .expect("Database query failed"); + assert!(found_doc.is_none()); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_delete_document_as_admin() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + // Create regular user and their document + let user = create_test_user(&pool, UserRole::User).await; + let document = create_and_insert_test_document(&pool, user.id).await; + + // Create admin user + let admin = create_test_user(&pool, UserRole::Admin).await; + + // Delete document as admin + let result = documents_db + .delete_document(document.id, admin.id, admin.role) + .await + .expect("Failed to delete document as admin"); + + // Verify document was deleted + assert!(result.is_some()); + let deleted_doc = result.unwrap(); + assert_eq!(deleted_doc.id, document.id); + assert_eq!(deleted_doc.user_id, user.id); // Original owner + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_delete_document_unauthorized() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + // Create two regular users + let user1 = create_test_user(&pool, UserRole::User).await; + let user2 = create_test_user(&pool, UserRole::User).await; + + // Create document owned by user1 + let document = create_and_insert_test_document(&pool, user1.id).await; + + // Try to delete document as user2 (should fail) + let result = documents_db + .delete_document(document.id, user2.id, user2.role) + .await + .expect("Database query failed"); + + // Verify document was not deleted + assert!(result.is_none()); + + // Verify document still exists + let found_doc = documents_db + .get_document_by_id(document.id, user1.id, user1.role) + .await + .expect("Database query failed"); + assert!(found_doc.is_some()); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_delete_nonexistent_document() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user = create_test_user(&pool, UserRole::User).await; + let nonexistent_id = Uuid::new_v4(); + + // Try to delete nonexistent document + let result = documents_db + .delete_document(nonexistent_id, user.id, user.role) + .await + .expect("Database query failed"); + + // Verify nothing was deleted + assert!(result.is_none()); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_bulk_delete_documents_as_owner() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user = create_test_user(&pool, UserRole::User).await; + + // Create multiple documents + let doc1 = create_and_insert_test_document(&pool, user.id).await; + let doc2 = create_and_insert_test_document(&pool, user.id).await; + let doc3 = create_and_insert_test_document(&pool, user.id).await; + + let document_ids = vec![doc1.id, doc2.id, doc3.id]; + + // Delete documents as owner + let result = documents_db + .bulk_delete_documents(&document_ids, user.id, user.role) + .await + .expect("Failed to bulk delete documents"); + + // Verify all documents were deleted + assert_eq!(result.len(), 3); + let deleted_ids: Vec = result.iter().map(|d| d.id).collect(); + assert!(deleted_ids.contains(&doc1.id)); + assert!(deleted_ids.contains(&doc2.id)); + assert!(deleted_ids.contains(&doc3.id)); + + // Verify documents no longer exist + for doc_id in document_ids { + let found_doc = documents_db + .get_document_by_id(doc_id, user.id, user.role) + .await + .expect("Database query failed"); + assert!(found_doc.is_none()); + } + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_bulk_delete_documents_as_admin() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + // Create regular user and their documents + let user = create_test_user(&pool, UserRole::User).await; + let doc1 = create_and_insert_test_document(&pool, user.id).await; + let doc2 = create_and_insert_test_document(&pool, user.id).await; + + // Create admin user + let admin = create_test_user(&pool, UserRole::Admin).await; + + let document_ids = vec![doc1.id, doc2.id]; + + // Delete documents as admin + let result = documents_db + .bulk_delete_documents(&document_ids, admin.id, admin.role) + .await + .expect("Failed to bulk delete documents as admin"); + + // Verify all documents were deleted + assert_eq!(result.len(), 2); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_bulk_delete_documents_mixed_ownership() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + // Create two regular users + let user1 = create_test_user(&pool, UserRole::User).await; + let user2 = create_test_user(&pool, UserRole::User).await; + + // Create documents for both users + let doc1_user1 = create_and_insert_test_document(&pool, user1.id).await; + let doc2_user1 = create_and_insert_test_document(&pool, user1.id).await; + let doc1_user2 = create_and_insert_test_document(&pool, user2.id).await; + + let document_ids = vec![doc1_user1.id, doc2_user1.id, doc1_user2.id]; + + // Try to delete all documents as user1 (should only delete their own) + let result = documents_db + .bulk_delete_documents(&document_ids, user1.id, user1.role) + .await + .expect("Failed to bulk delete documents"); + + // Verify only user1's documents were deleted + assert_eq!(result.len(), 2); + let deleted_ids: Vec = result.iter().map(|d| d.id).collect(); + assert!(deleted_ids.contains(&doc1_user1.id)); + assert!(deleted_ids.contains(&doc2_user1.id)); + assert!(!deleted_ids.contains(&doc1_user2.id)); + + // Verify user2's document still exists + let found_doc = documents_db + .get_document_by_id(doc1_user2.id, user2.id, user2.role) + .await + .expect("Database query failed"); + assert!(found_doc.is_some()); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_bulk_delete_documents_empty_list() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user = create_test_user(&pool, UserRole::User).await; + let empty_ids: Vec = vec![]; + + // Delete empty list of documents + let result = documents_db + .bulk_delete_documents(&empty_ids, user.id, user.role) + .await + .expect("Failed to bulk delete empty list"); + + // Verify empty result + assert_eq!(result.len(), 0); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_bulk_delete_documents_nonexistent_ids() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user = create_test_user(&pool, UserRole::User).await; + + // Create one real document + let real_doc = create_and_insert_test_document(&pool, user.id).await; + + // Mix of real and nonexistent IDs + let document_ids = vec![real_doc.id, Uuid::new_v4(), Uuid::new_v4()]; + + // Delete documents (should only delete the real one) + let result = documents_db + .bulk_delete_documents(&document_ids, user.id, user.role) + .await + .expect("Failed to bulk delete documents"); + + // Verify only the real document was deleted + assert_eq!(result.len(), 1); + assert_eq!(result[0].id, real_doc.id); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_bulk_delete_documents_partial_authorization() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + // Create regular user and admin + let user = create_test_user(&pool, UserRole::User).await; + let admin = create_test_user(&pool, UserRole::Admin).await; + + // Create documents for both users + let user_doc = create_and_insert_test_document(&pool, user.id).await; + let admin_doc = create_and_insert_test_document(&pool, admin.id).await; + + let document_ids = vec![user_doc.id, admin_doc.id]; + + // Admin should be able to delete both + let result = documents_db + .bulk_delete_documents(&document_ids, admin.id, admin.role) + .await + .expect("Failed to bulk delete documents as admin"); + + assert_eq!(result.len(), 2); + + // Recreate documents for user test + let user_doc2 = create_and_insert_test_document(&pool, user.id).await; + let admin_doc2 = create_and_insert_test_document(&pool, admin.id).await; + + let document_ids2 = vec![user_doc2.id, admin_doc2.id]; + + // Regular user should only delete their own + let result2 = documents_db + .bulk_delete_documents(&document_ids2, user.id, user.role) + .await + .expect("Failed to bulk delete documents as user"); + + assert_eq!(result2.len(), 1); + assert_eq!(result2[0].id, user_doc2.id); + } +} + +#[cfg(test)] +mod rbac_deletion_tests { + use super::*; + use crate::db::documents::DocumentsDB; + use crate::models::{UserRole, User}; + use sqlx::PgPool; + use std::env; + + async fn create_test_db_pool() -> PgPool { + let database_url = env::var("TEST_DATABASE_URL") + .expect("TEST_DATABASE_URL must be set for database tests"); + PgPool::connect(&database_url) + .await + .expect("Failed to connect to test database") + } + + async fn create_test_user(pool: &PgPool, role: UserRole) -> User { + let user_id = Uuid::new_v4(); + let user = User { + id: user_id, + username: format!("testuser_{}", user_id), + email: format!("test_{}@example.com", user_id), + password_hash: "hashed_password".to_string(), + role, + created_at: Utc::now(), + updated_at: Utc::now(), + }; + + sqlx::query!( + "INSERT INTO users (id, username, email, password_hash, role, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7)", + user.id, + user.username, + user.email, + user.password_hash, + user.role as UserRole, + user.created_at, + user.updated_at + ) + .execute(pool) + .await + .expect("Failed to insert test user"); + + user + } + + async fn create_and_insert_test_document(pool: &PgPool, user_id: Uuid) -> Document { + let document = super::create_test_document(user_id); + + sqlx::query!( + "INSERT INTO documents (id, filename, original_filename, file_path, file_size, mime_type, + content, ocr_text, ocr_confidence, ocr_word_count, ocr_processing_time_ms, ocr_status, + ocr_error, ocr_completed_at, tags, created_at, updated_at, user_id, file_hash) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)", + document.id, + document.filename, + document.original_filename, + document.file_path, + document.file_size as i64, + document.mime_type, + document.content, + document.ocr_text, + document.ocr_confidence, + document.ocr_word_count.map(|x| x as i32), + document.ocr_processing_time_ms.map(|x| x as i32), + document.ocr_status, + document.ocr_error, + document.ocr_completed_at, + &document.tags, + document.created_at, + document.updated_at, + document.user_id, + document.file_hash + ) + .execute(pool) + .await + .expect("Failed to insert test document"); + + document + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_user_can_delete_own_document() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user = create_test_user(&pool, UserRole::User).await; + let document = create_and_insert_test_document(&pool, user.id).await; + + // User should be able to delete their own document + let result = documents_db + .delete_document(document.id, user.id, user.role) + .await + .expect("Failed to delete document"); + + assert!(result.is_some()); + let deleted_doc = result.unwrap(); + assert_eq!(deleted_doc.id, document.id); + assert_eq!(deleted_doc.user_id, user.id); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_user_cannot_delete_other_user_document() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user1 = create_test_user(&pool, UserRole::User).await; + let user2 = create_test_user(&pool, UserRole::User).await; + + let document = create_and_insert_test_document(&pool, user1.id).await; + + // User2 should NOT be able to delete user1's document + let result = documents_db + .delete_document(document.id, user2.id, user2.role) + .await + .expect("Database query failed"); + + assert!(result.is_none()); + + // Verify document still exists + let found_doc = documents_db + .get_document_by_id(document.id, user1.id, user1.role) + .await + .expect("Database query failed"); + assert!(found_doc.is_some()); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_admin_can_delete_any_document() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user = create_test_user(&pool, UserRole::User).await; + let admin = create_test_user(&pool, UserRole::Admin).await; + + let user_document = create_and_insert_test_document(&pool, user.id).await; + let admin_document = create_and_insert_test_document(&pool, admin.id).await; + + // Admin should be able to delete user's document + let result1 = documents_db + .delete_document(user_document.id, admin.id, admin.role) + .await + .expect("Failed to delete user document as admin"); + + assert!(result1.is_some()); + assert_eq!(result1.unwrap().user_id, user.id); // Original owner + + // Admin should be able to delete their own document + let result2 = documents_db + .delete_document(admin_document.id, admin.id, admin.role) + .await + .expect("Failed to delete admin document as admin"); + + assert!(result2.is_some()); + assert_eq!(result2.unwrap().user_id, admin.id); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_bulk_delete_respects_ownership() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user1 = create_test_user(&pool, UserRole::User).await; + let user2 = create_test_user(&pool, UserRole::User).await; + + // Create documents for both users + let user1_doc1 = create_and_insert_test_document(&pool, user1.id).await; + let user1_doc2 = create_and_insert_test_document(&pool, user1.id).await; + let user2_doc1 = create_and_insert_test_document(&pool, user2.id).await; + let user2_doc2 = create_and_insert_test_document(&pool, user2.id).await; + + let all_document_ids = vec![ + user1_doc1.id, + user1_doc2.id, + user2_doc1.id, + user2_doc2.id + ]; + + // User1 tries to delete all documents (should only delete their own) + let result = documents_db + .bulk_delete_documents(&all_document_ids, user1.id, user1.role) + .await + .expect("Failed to bulk delete documents"); + + // Should only delete user1's documents + assert_eq!(result.len(), 2); + let deleted_ids: Vec = result.iter().map(|d| d.id).collect(); + assert!(deleted_ids.contains(&user1_doc1.id)); + assert!(deleted_ids.contains(&user1_doc2.id)); + assert!(!deleted_ids.contains(&user2_doc1.id)); + assert!(!deleted_ids.contains(&user2_doc2.id)); + + // Verify user2's documents still exist + let user2_doc1_exists = documents_db + .get_document_by_id(user2_doc1.id, user2.id, user2.role) + .await + .expect("Database query failed"); + assert!(user2_doc1_exists.is_some()); + + let user2_doc2_exists = documents_db + .get_document_by_id(user2_doc2.id, user2.id, user2.role) + .await + .expect("Database query failed"); + assert!(user2_doc2_exists.is_some()); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_admin_bulk_delete_all_documents() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user1 = create_test_user(&pool, UserRole::User).await; + let user2 = create_test_user(&pool, UserRole::User).await; + let admin = create_test_user(&pool, UserRole::Admin).await; + + // Create documents for all users + let user1_doc = create_and_insert_test_document(&pool, user1.id).await; + let user2_doc = create_and_insert_test_document(&pool, user2.id).await; + let admin_doc = create_and_insert_test_document(&pool, admin.id).await; + + let all_document_ids = vec![user1_doc.id, user2_doc.id, admin_doc.id]; + + // Admin should be able to delete all documents + let result = documents_db + .bulk_delete_documents(&all_document_ids, admin.id, admin.role) + .await + .expect("Failed to bulk delete documents as admin"); + + // Should delete all documents + assert_eq!(result.len(), 3); + let deleted_ids: Vec = result.iter().map(|d| d.id).collect(); + assert!(deleted_ids.contains(&user1_doc.id)); + assert!(deleted_ids.contains(&user2_doc.id)); + assert!(deleted_ids.contains(&admin_doc.id)); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_role_escalation_prevention() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user = create_test_user(&pool, UserRole::User).await; + let admin = create_test_user(&pool, UserRole::Admin).await; + + let admin_document = create_and_insert_test_document(&pool, admin.id).await; + + // Regular user should NOT be able to delete admin's document + // even if they somehow know the document ID + let result = documents_db + .delete_document(admin_document.id, user.id, user.role) + .await + .expect("Database query failed"); + + assert!(result.is_none()); + + // Verify admin's document still exists + let found_doc = documents_db + .get_document_by_id(admin_document.id, admin.id, admin.role) + .await + .expect("Database query failed"); + assert!(found_doc.is_some()); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_cross_tenant_isolation() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + // Create users that could represent different tenants/organizations + let tenant1_user1 = create_test_user(&pool, UserRole::User).await; + let tenant1_user2 = create_test_user(&pool, UserRole::User).await; + let tenant2_user1 = create_test_user(&pool, UserRole::User).await; + let tenant2_user2 = create_test_user(&pool, UserRole::User).await; + + // Create documents for each tenant + let tenant1_doc1 = create_and_insert_test_document(&pool, tenant1_user1.id).await; + let tenant1_doc2 = create_and_insert_test_document(&pool, tenant1_user2.id).await; + let tenant2_doc1 = create_and_insert_test_document(&pool, tenant2_user1.id).await; + let tenant2_doc2 = create_and_insert_test_document(&pool, tenant2_user2.id).await; + + // Tenant1 user should not be able to delete tenant2 documents + let result1 = documents_db + .delete_document(tenant2_doc1.id, tenant1_user1.id, tenant1_user1.role) + .await + .expect("Database query failed"); + assert!(result1.is_none()); + + let result2 = documents_db + .delete_document(tenant2_doc2.id, tenant1_user2.id, tenant1_user2.role) + .await + .expect("Database query failed"); + assert!(result2.is_none()); + + // Tenant2 user should not be able to delete tenant1 documents + let result3 = documents_db + .delete_document(tenant1_doc1.id, tenant2_user1.id, tenant2_user1.role) + .await + .expect("Database query failed"); + assert!(result3.is_none()); + + let result4 = documents_db + .delete_document(tenant1_doc2.id, tenant2_user2.id, tenant2_user2.role) + .await + .expect("Database query failed"); + assert!(result4.is_none()); + + // Verify all documents still exist + for (doc_id, owner_id, owner_role) in [ + (tenant1_doc1.id, tenant1_user1.id, tenant1_user1.role), + (tenant1_doc2.id, tenant1_user2.id, tenant1_user2.role), + (tenant2_doc1.id, tenant2_user1.id, tenant2_user1.role), + (tenant2_doc2.id, tenant2_user2.id, tenant2_user2.role), + ] { + let found_doc = documents_db + .get_document_by_id(doc_id, owner_id, owner_role) + .await + .expect("Database query failed"); + assert!(found_doc.is_some()); + } + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_permission_consistency_single_vs_bulk() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user1 = create_test_user(&pool, UserRole::User).await; + let user2 = create_test_user(&pool, UserRole::User).await; + + let user1_doc = create_and_insert_test_document(&pool, user1.id).await; + let user2_doc = create_and_insert_test_document(&pool, user2.id).await; + + // Test single deletion permissions + let single_delete_result = documents_db + .delete_document(user2_doc.id, user1.id, user1.role) + .await + .expect("Database query failed"); + assert!(single_delete_result.is_none()); // Should fail + + // Test bulk deletion permissions with same document + let user2_doc2 = create_and_insert_test_document(&pool, user2.id).await; + let bulk_delete_result = documents_db + .bulk_delete_documents(&vec![user2_doc2.id], user1.id, user1.role) + .await + .expect("Database query failed"); + assert_eq!(bulk_delete_result.len(), 0); // Should delete nothing + + // Verify both documents still exist + let doc1_exists = documents_db + .get_document_by_id(user2_doc.id, user2.id, user2.role) + .await + .expect("Database query failed"); + assert!(doc1_exists.is_some()); + + let doc2_exists = documents_db + .get_document_by_id(user2_doc2.id, user2.id, user2.role) + .await + .expect("Database query failed"); + assert!(doc2_exists.is_some()); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_admin_permission_inheritance() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user = create_test_user(&pool, UserRole::User).await; + let admin = create_test_user(&pool, UserRole::Admin).await; + + let user_doc = create_and_insert_test_document(&pool, user.id).await; + + // Admin should have all permissions that a regular user has, plus more + // Test that admin can delete user's document (admin-specific permission) + let admin_delete_result = documents_db + .delete_document(user_doc.id, admin.id, admin.role) + .await + .expect("Failed to delete as admin"); + assert!(admin_delete_result.is_some()); + + // Create another document to test admin's own document deletion + let admin_doc = create_and_insert_test_document(&pool, admin.id).await; + let admin_own_delete_result = documents_db + .delete_document(admin_doc.id, admin.id, admin.role) + .await + .expect("Failed to delete admin's own document"); + assert!(admin_own_delete_result.is_some()); + } + + #[test] + fn test_role_based_logic_unit_tests() { + let user_role = UserRole::User; + let admin_role = UserRole::Admin; + + let user_id = Uuid::new_v4(); + let other_user_id = Uuid::new_v4(); + let admin_id = Uuid::new_v4(); + + // Test user permissions logic + assert!(user_id == user_id || user_role == UserRole::Admin); // Can delete own + assert!(!(other_user_id == user_id || user_role == UserRole::Admin)); // Cannot delete other's + + // Test admin permissions logic + assert!(user_id == admin_id || admin_role == UserRole::Admin); // Can delete user's (admin privilege) + assert!(other_user_id == admin_id || admin_role == UserRole::Admin); // Can delete any (admin privilege) + assert!(admin_id == admin_id || admin_role == UserRole::Admin); // Can delete own + } + + #[test] + fn test_role_comparison() { + assert_eq!(UserRole::User, UserRole::User); + assert_eq!(UserRole::Admin, UserRole::Admin); + assert_ne!(UserRole::User, UserRole::Admin); + assert_ne!(UserRole::Admin, UserRole::User); + } +} + +#[cfg(test)] +mod deletion_error_handling_tests { + use super::*; + use crate::db::documents::DocumentsDB; + use crate::models::{UserRole, User}; + use sqlx::PgPool; + use std::env; + use uuid::Uuid; + + async fn create_test_db_pool() -> PgPool { + let database_url = env::var("TEST_DATABASE_URL") + .expect("TEST_DATABASE_URL must be set for database tests"); + PgPool::connect(&database_url) + .await + .expect("Failed to connect to test database") + } + + async fn create_test_user(pool: &PgPool, role: UserRole) -> User { + let user_id = Uuid::new_v4(); + let user = User { + id: user_id, + username: format!("testuser_{}", user_id), + email: format!("test_{}@example.com", user_id), + password_hash: "hashed_password".to_string(), + role, + created_at: Utc::now(), + updated_at: Utc::now(), + }; + + sqlx::query!( + "INSERT INTO users (id, username, email, password_hash, role, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7)", + user.id, + user.username, + user.email, + user.password_hash, + user.role as UserRole, + user.created_at, + user.updated_at + ) + .execute(pool) + .await + .expect("Failed to insert test user"); + + user + } + + async fn create_and_insert_test_document(pool: &PgPool, user_id: Uuid) -> Document { + let document = super::create_test_document(user_id); + + sqlx::query!( + "INSERT INTO documents (id, filename, original_filename, file_path, file_size, mime_type, + content, ocr_text, ocr_confidence, ocr_word_count, ocr_processing_time_ms, ocr_status, + ocr_error, ocr_completed_at, tags, created_at, updated_at, user_id, file_hash) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)", + document.id, + document.filename, + document.original_filename, + document.file_path, + document.file_size as i64, + document.mime_type, + document.content, + document.ocr_text, + document.ocr_confidence, + document.ocr_word_count.map(|x| x as i32), + document.ocr_processing_time_ms.map(|x| x as i32), + document.ocr_status, + document.ocr_error, + document.ocr_completed_at, + &document.tags, + document.created_at, + document.updated_at, + document.user_id, + document.file_hash + ) + .execute(pool) + .await + .expect("Failed to insert test document"); + + document + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_delete_with_invalid_uuid() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user = create_test_user(&pool, UserRole::User).await; + + // Use malformed UUID (this test assumes the function handles UUID parsing) + let invalid_uuid = Uuid::nil(); // Use nil UUID as "invalid" + + let result = documents_db + .delete_document(invalid_uuid, user.id, user.role) + .await + .expect("Database query should not fail for invalid UUID"); + + // Should return None for non-existent document + assert!(result.is_none()); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_delete_with_sql_injection_attempt() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user = create_test_user(&pool, UserRole::User).await; + let document = create_and_insert_test_document(&pool, user.id).await; + + // Test with legitimate document ID - SQLx should prevent injection + let result = documents_db + .delete_document(document.id, user.id, user.role) + .await + .expect("Query should execute safely"); + + assert!(result.is_some()); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_bulk_delete_with_duplicate_ids() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user = create_test_user(&pool, UserRole::User).await; + let document = create_and_insert_test_document(&pool, user.id).await; + + // Include the same document ID multiple times + let duplicate_ids = vec![document.id, document.id, document.id]; + + let result = documents_db + .bulk_delete_documents(&duplicate_ids, user.id, user.role) + .await + .expect("Bulk delete should handle duplicates"); + + // Should only delete the document once + assert_eq!(result.len(), 1); + assert_eq!(result[0].id, document.id); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_bulk_delete_with_extremely_large_request() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user = create_test_user(&pool, UserRole::User).await; + + // Create a large number of document IDs (mostly non-existent) + let mut large_id_list = Vec::new(); + + // Add one real document + let real_document = create_and_insert_test_document(&pool, user.id).await; + large_id_list.push(real_document.id); + + // Add many fake UUIDs + for _ in 0..500 { + large_id_list.push(Uuid::new_v4()); + } + + let result = documents_db + .bulk_delete_documents(&large_id_list, user.id, user.role) + .await + .expect("Should handle large requests"); + + // Should only delete the one real document + assert_eq!(result.len(), 1); + assert_eq!(result[0].id, real_document.id); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_concurrent_deletion_same_document() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user = create_test_user(&pool, UserRole::User).await; + let document = create_and_insert_test_document(&pool, user.id).await; + + // Create multiple handles to the same database connection pool + let db1 = documents_db.clone(); + let db2 = documents_db.clone(); + + // Attempt concurrent deletions + let doc_id = document.id; + let user_id = user.id; + let user_role = user.role; + + let task1 = tokio::spawn(async move { + db1.delete_document(doc_id, user_id, user_role).await + }); + + let task2 = tokio::spawn(async move { + db2.delete_document(doc_id, user_id, user_role).await + }); + + let result1 = task1.await.unwrap().expect("First deletion should succeed"); + let result2 = task2.await.unwrap().expect("Second deletion should not error"); + + // One should succeed, one should return None + let success_count = [result1.is_some(), result2.is_some()] + .iter() + .filter(|&&x| x) + .count(); + + assert_eq!(success_count, 1, "Exactly one deletion should succeed"); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_delete_document_with_foreign_key_constraints() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user = create_test_user(&pool, UserRole::User).await; + let document = create_and_insert_test_document(&pool, user.id).await; + + // If there are foreign key relationships (like document_labels), + // test that CASCADE deletion works properly + + // Delete the document + let result = documents_db + .delete_document(document.id, user.id, user.role) + .await + .expect("Deletion should handle foreign key constraints"); + + assert!(result.is_some()); + + // Verify related records are also deleted (if any exist) + // This would depend on the actual schema relationships + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_bulk_delete_with_mixed_permissions_and_errors() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user1 = create_test_user(&pool, UserRole::User).await; + let user2 = create_test_user(&pool, UserRole::User).await; + + // Create mix of documents + let user1_doc = create_and_insert_test_document(&pool, user1.id).await; + let user2_doc = create_and_insert_test_document(&pool, user2.id).await; + let nonexistent_id = Uuid::new_v4(); + + let mixed_ids = vec![user1_doc.id, user2_doc.id, nonexistent_id]; + + // User1 attempts to delete all (should only delete their own) + let result = documents_db + .bulk_delete_documents(&mixed_ids, user1.id, user1.role) + .await + .expect("Should handle mixed permissions gracefully"); + + // Should only delete user1's document + assert_eq!(result.len(), 1); + assert_eq!(result[0].id, user1_doc.id); + + // Verify user2's document still exists + let user2_doc_exists = documents_db + .get_document_by_id(user2_doc.id, user2.id, user2.role) + .await + .expect("Query should succeed"); + assert!(user2_doc_exists.is_some()); + } + + #[test] + fn test_error_message_consistency() { + // Test that error conditions produce consistent, user-friendly messages + + // Test various error scenarios that might occur + let not_found_msg = "Document not found"; + let unauthorized_msg = "Not authorized to delete this document"; + let invalid_request_msg = "Invalid request parameters"; + let server_error_msg = "Internal server error"; + + // Verify messages are not empty and don't contain sensitive information + assert!(!not_found_msg.is_empty()); + assert!(!unauthorized_msg.is_empty()); + assert!(!invalid_request_msg.is_empty()); + assert!(!server_error_msg.is_empty()); + + // Verify messages don't leak technical details + assert!(!not_found_msg.to_lowercase().contains("sql")); + assert!(!not_found_msg.to_lowercase().contains("database")); + assert!(!unauthorized_msg.to_lowercase().contains("user_id")); + assert!(!server_error_msg.to_lowercase().contains("panic")); + } + + #[test] + fn test_uuid_edge_cases() { + // Test various UUID edge cases + + let nil_uuid = Uuid::nil(); + let max_uuid = Uuid::max(); + let random_uuid = Uuid::new_v4(); + + // Verify UUIDs are valid + assert_eq!(nil_uuid.to_string(), "00000000-0000-0000-0000-000000000000"); + assert_eq!(max_uuid.to_string(), "ffffffff-ffff-ffff-ffff-ffffffffffff"); + assert!(random_uuid.to_string().len() == 36); // Standard UUID string length + + // Test UUID parsing edge cases + assert!(Uuid::parse_str("invalid-uuid").is_err()); + assert!(Uuid::parse_str("").is_err()); + assert!(Uuid::parse_str("00000000-0000-0000-0000-000000000000").is_ok()); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_delete_after_user_deletion() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user = create_test_user(&pool, UserRole::User).await; + let document = create_and_insert_test_document(&pool, user.id).await; + + // Delete the user first (simulating cascade deletion scenarios) + sqlx::query!("DELETE FROM users WHERE id = $1", user.id) + .execute(&pool) + .await + .expect("User deletion should succeed"); + + // Attempt to delete document after user is gone + // This depends on how foreign key constraints are set up + let result = documents_db + .delete_document(document.id, user.id, user.role) + .await; + + // The behavior here depends on FK constraints: + // - If CASCADE: document might already be deleted + // - If RESTRICT: document still exists but operation might fail + // Test should verify consistent behavior + match result { + Ok(Some(_)) => { + // Document was deleted successfully + }, + Ok(None) => { + // Document not found (possibly already cascade deleted) + }, + Err(_) => { + // Error occurred (foreign key constraint issue) + // This might be expected behavior + } + } + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_bulk_delete_empty_and_null_scenarios() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user = create_test_user(&pool, UserRole::User).await; + + // Test empty list + let empty_result = documents_db + .bulk_delete_documents(&vec![], user.id, user.role) + .await + .expect("Empty list should be handled gracefully"); + assert_eq!(empty_result.len(), 0); + + // Test with only nil UUIDs + let nil_uuids = vec![Uuid::nil(), Uuid::nil()]; + let nil_result = documents_db + .bulk_delete_documents(&nil_uuids, user.id, user.role) + .await + .expect("Nil UUIDs should be handled gracefully"); + assert_eq!(nil_result.len(), 0); + } + + #[test] + fn test_bulk_delete_request_validation_edge_cases() { + use crate::models::BulkDeleteRequest; + use serde_json::json; + + // Test empty request + let empty_request = json!({ "document_ids": [] }); + let parsed: BulkDeleteRequest = serde_json::from_value(empty_request).unwrap(); + assert_eq!(parsed.document_ids.len(), 0); + + // Test single item request + let single_request = json!({ + "document_ids": ["550e8400-e29b-41d4-a716-446655440000"] + }); + let parsed: BulkDeleteRequest = serde_json::from_value(single_request).unwrap(); + assert_eq!(parsed.document_ids.len(), 1); + + // Test malformed JSON should fail + let malformed_request = json!({ "wrong_field": ["not-uuids"] }); + let result: Result = serde_json::from_value(malformed_request); + assert!(result.is_err()); + + // Test mixed valid/invalid UUIDs should fail + let mixed_request = json!({ + "document_ids": [ + "550e8400-e29b-41d4-a716-446655440000", + "not-a-uuid", + "550e8400-e29b-41d4-a716-446655440001" + ] + }); + let result: Result = serde_json::from_value(mixed_request); + assert!(result.is_err()); + } + + #[tokio::test] + #[ignore = "Requires PostgreSQL database"] + async fn test_transaction_rollback_simulation() { + let pool = create_test_db_pool().await; + let documents_db = DocumentsDB::new(pool.clone()); + + let user = create_test_user(&pool, UserRole::User).await; + let document = create_and_insert_test_document(&pool, user.id).await; + + // Verify document exists before deletion + let exists_before = documents_db + .get_document_by_id(document.id, user.id, user.role) + .await + .expect("Query should succeed"); + assert!(exists_before.is_some()); + + // Perform deletion + let deletion_result = documents_db + .delete_document(document.id, user.id, user.role) + .await + .expect("Deletion should succeed"); + assert!(deletion_result.is_some()); + + // Verify document no longer exists + let exists_after = documents_db + .get_document_by_id(document.id, user.id, user.role) + .await + .expect("Query should succeed"); + assert!(exists_after.is_none()); + + // If transaction were to be rolled back, document would exist again + // This test verifies the transaction was committed properly + } } \ No newline at end of file diff --git a/src/tests/file_service_tests.rs b/src/tests/file_service_tests.rs index cd292df..a1d9903 100644 --- a/src/tests/file_service_tests.rs +++ b/src/tests/file_service_tests.rs @@ -130,4 +130,379 @@ mod tests { let result = service.read_file(nonexistent_path).await; assert!(result.is_err()); } +} + +#[cfg(test)] +mod file_deletion_tests { + use super::*; + use crate::models::Document; + use chrono::Utc; + use std::fs; + use std::path::Path; + + fn create_test_document_with_files(service: &FileService, temp_dir: &TempDir, user_id: uuid::Uuid) -> (Document, String, String, String) { + let base_path = temp_dir.path().join("documents"); + fs::create_dir_all(&base_path).unwrap(); + + // Create main document file + let main_file_path = base_path.join("test_document.pdf"); + fs::write(&main_file_path, b"PDF content").unwrap(); + + // Create thumbnail file + let thumbnail_path = base_path.join("test_document_thumb.jpg"); + fs::write(&thumbnail_path, b"Thumbnail content").unwrap(); + + // Create processed image file + let processed_path = base_path.join("test_document_processed.jpg"); + fs::write(&processed_path, b"Processed content").unwrap(); + + let document = Document { + id: uuid::Uuid::new_v4(), + 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()), + }; + + ( + 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(); + 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(); + let user_id = uuid::Uuid::new_v4(); + + let (mut 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(); + 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(); + 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(); + 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, + }; + + // 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(); + let user_id = uuid::Uuid::new_v4(); + + let base_path = temp_dir.path().join("documents"); + fs::create_dir_all(&base_path).unwrap(); + + // Test with PNG document + let main_file_path = base_path.join("test_image.png"); + fs::write(&main_file_path, b"PNG content").unwrap(); + + let thumbnail_path = base_path.join("test_image_thumb.jpg"); + fs::write(&thumbnail_path, b"Thumbnail content").unwrap(); + + let processed_path = base_path.join("test_image_processed.jpg"); + fs::write(&processed_path, b"Processed content").unwrap(); + + let document = Document { + id: uuid::Uuid::new_v4(), + 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()), + }; + + // 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(); + let user_id = uuid::Uuid::new_v4(); + + let base_path = temp_dir.path().join("documents"); + fs::create_dir_all(&base_path).unwrap(); + + // Create main file with readonly permissions to simulate failure + let main_file_path = base_path.join("readonly_document.pdf"); + fs::write(&main_file_path, b"PDF content").unwrap(); + + // Create thumbnail file normally + let thumbnail_path = base_path.join("readonly_document_thumb.jpg"); + fs::write(&thumbnail_path, b"Thumbnail content").unwrap(); + + let document = Document { + id: uuid::Uuid::new_v4(), + 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()), + }; + + // 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(); + 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()), + }; + + // 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(); + 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 (first one deletes, second one is no-op) + let result1 = task1.await.unwrap(); + let result2 = task2.await.unwrap(); + + assert!(result1.is_ok()); + assert!(result2.is_ok()); + + // Verify files are deleted + assert!(!Path::new(&main_path).exists()); + assert!(!Path::new(&thumb_path).exists()); + assert!(!Path::new(&processed_path).exists()); + } } \ No newline at end of file diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 31ef50e..354508d 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -2,6 +2,7 @@ mod helpers; mod auth_tests; mod db_tests; mod documents_tests; +mod document_routes_tests; mod file_service_tests; mod labels_tests; mod ocr_tests; diff --git a/tests/document_deletion_integration_tests.rs b/tests/document_deletion_integration_tests.rs new file mode 100644 index 0000000..102075b --- /dev/null +++ b/tests/document_deletion_integration_tests.rs @@ -0,0 +1,597 @@ +/*! + * Document Deletion Integration Tests + * + * Comprehensive tests for single and bulk document deletion functionality. + * Tests HTTP endpoints, file cleanup, role-based access, and edge cases. + */ + +use reqwest::{Client, multipart}; +use serde_json::{json, Value}; +use std::time::Duration; +use uuid::Uuid; + +use readur::models::{DocumentResponse, CreateUser, LoginRequest, LoginResponse, UserRole}; + +fn get_base_url() -> String { + std::env::var("API_URL").unwrap_or_else(|_| "http://localhost:8000".to_string()) +} + +const TIMEOUT: Duration = Duration::from_secs(30); + +/// Test client for document deletion integration tests +struct DocumentDeletionTestClient { + client: Client, + token: Option, + user_id: Option, +} + +impl DocumentDeletionTestClient { + fn new() -> Self { + Self { + client: Client::new(), + token: None, + user_id: None, + } + } + + /// Check if server is running and healthy + async fn check_server_health(&self) -> Result<(), Box> { + let response = self.client + .get(&format!("{}/api/health", get_base_url())) + .timeout(Duration::from_secs(5)) + .send() + .await?; + + if !response.status().is_success() { + return Err("Server health check failed".into()); + } + + let health: Value = response.json().await?; + if health["status"] != "ok" { + return Err("Server is not healthy".into()); + } + + Ok(()) + } + + /// Register a new user and login to get auth token + async fn register_and_login(&mut self, username: &str, email: &str, password: &str, role: Option) -> Result> { + // Register user + let user_data = CreateUser { + username: username.to_string(), + email: email.to_string(), + password: password.to_string(), + role: Some(role.unwrap_or(UserRole::User)), + }; + + let register_response = self.client + .post(&format!("{}/api/auth/register", get_base_url())) + .json(&user_data) + .timeout(TIMEOUT) + .send() + .await?; + + if !register_response.status().is_success() { + return Err(format!("Registration failed: {}", register_response.text().await?).into()); + } + + // Login to get token + let login_data = LoginRequest { + username: username.to_string(), + password: password.to_string(), + }; + + let login_response = self.client + .post(&format!("{}/api/auth/login", get_base_url())) + .json(&login_data) + .timeout(TIMEOUT) + .send() + .await?; + + if !login_response.status().is_success() { + return Err(format!("Login failed: {}", login_response.text().await?).into()); + } + + let login_result: LoginResponse = login_response.json().await?; + self.token = Some(login_result.token.clone()); + self.user_id = Some(login_result.user.id.to_string()); + + Ok(login_result.token) + } + + /// Upload a test document + async fn upload_document(&self, content: &[u8], filename: &str) -> Result> { + let token = self.token.as_ref().ok_or("Not authenticated")?; + + let form = multipart::Form::new() + .part("file", multipart::Part::bytes(content.to_vec()) + .file_name(filename.to_string()) + .mime_str("text/plain")?); + + let response = self.client + .post(&format!("{}/api/documents", get_base_url())) + .header("Authorization", format!("Bearer {}", token)) + .multipart(form) + .timeout(TIMEOUT) + .send() + .await?; + + if !response.status().is_success() { + return Err(format!("Document upload failed: {}", response.text().await?).into()); + } + + let document: DocumentResponse = response.json().await?; + Ok(document) + } + + /// Delete a single document + async fn delete_document(&self, document_id: &str) -> Result> { + let token = self.token.as_ref().ok_or("Not authenticated")?; + + let response = self.client + .delete(&format!("{}/api/documents/{}", get_base_url(), document_id)) + .header("Authorization", format!("Bearer {}", token)) + .timeout(TIMEOUT) + .send() + .await?; + + let status = response.status(); + let text = response.text().await?; + + if !status.is_success() { + return Err(format!("Document deletion failed ({}): {}", status, text).into()); + } + + let result: Value = serde_json::from_str(&text)?; + Ok(result) + } + + /// Bulk delete documents + async fn bulk_delete_documents(&self, document_ids: &[String]) -> Result> { + let token = self.token.as_ref().ok_or("Not authenticated")?; + + let request_data = json!({ + "document_ids": document_ids + }); + + let response = self.client + .delete(&format!("{}/api/documents", get_base_url())) + .header("Authorization", format!("Bearer {}", token)) + .json(&request_data) + .timeout(TIMEOUT) + .send() + .await?; + + let status = response.status(); + let text = response.text().await?; + + if !status.is_success() { + return Err(format!("Bulk deletion failed ({}): {}", status, text).into()); + } + + let result: Value = serde_json::from_str(&text)?; + Ok(result) + } + + /// Get document by ID + async fn get_document(&self, document_id: &str) -> Result, Box> { + let token = self.token.as_ref().ok_or("Not authenticated")?; + + let response = self.client + .get(&format!("{}/api/documents/{}", get_base_url(), document_id)) + .header("Authorization", format!("Bearer {}", token)) + .timeout(TIMEOUT) + .send() + .await?; + + if response.status() == 404 { + return Ok(None); + } + + if !response.status().is_success() { + return Err(format!("Get document failed: {}", response.text().await?).into()); + } + + let document: DocumentResponse = response.json().await?; + Ok(Some(document)) + } + + /// List documents + async fn list_documents(&self) -> Result> { + let token = self.token.as_ref().ok_or("Not authenticated")?; + + let response = self.client + .get(&format!("{}/api/documents", get_base_url())) + .header("Authorization", format!("Bearer {}", token)) + .timeout(TIMEOUT) + .send() + .await?; + + if !response.status().is_success() { + return Err(format!("List documents failed: {}", response.text().await?).into()); + } + + let result: Value = response.json().await?; + Ok(result) + } +} + +/// Skip test if server is not running +macro_rules! skip_if_server_down { + ($client:expr) => { + if let Err(_) = $client.check_server_health().await { + println!("Skipping test: Server is not running at {}", get_base_url()); + return; + } + }; +} + +#[tokio::test] +async fn test_single_document_deletion_success() { + let mut client = DocumentDeletionTestClient::new(); + skip_if_server_down!(client); + + // Register and login + client.register_and_login( + &format!("testuser_delete_{}", Uuid::new_v4()), + &format!("testuser_delete_{}@example.com", Uuid::new_v4()), + "password123", + None + ).await.expect("Failed to register and login"); + + // Upload a test document + let test_content = b"This is a test document for deletion."; + let document = client.upload_document(test_content, "test_deletion.txt") + .await.expect("Failed to upload document"); + + println!("Uploaded document: {}", document.id); + + // Verify document exists + let retrieved_doc = client.get_document(&document.id.to_string()) + .await.expect("Failed to get document"); + assert!(retrieved_doc.is_some(), "Document should exist before deletion"); + + // Delete the document + let delete_result = client.delete_document(&document.id.to_string()) + .await.expect("Failed to delete document"); + + // Verify deletion response + assert_eq!(delete_result["success"], true); + assert_eq!(delete_result["document_id"], document.id.to_string()); + assert_eq!(delete_result["filename"], document.filename); + + // Verify document no longer exists + let retrieved_doc_after = client.get_document(&document.id.to_string()) + .await.expect("Failed to check document existence"); + assert!(retrieved_doc_after.is_none(), "Document should not exist after deletion"); + + println!("✅ Single document deletion test passed"); +} + +#[tokio::test] +async fn test_bulk_document_deletion_success() { + let mut client = DocumentDeletionTestClient::new(); + skip_if_server_down!(client); + + // Register and login + client.register_and_login( + &format!("testuser_bulk_{}", Uuid::new_v4()), + &format!("testuser_bulk_{}@example.com", Uuid::new_v4()), + "password123", + None + ).await.expect("Failed to register and login"); + + // Upload multiple test documents + let mut document_ids = Vec::new(); + for i in 1..=5 { + let test_content = format!("This is test document number {}", i); + let document = client.upload_document(test_content.as_bytes(), &format!("test_bulk_{}.txt", i)) + .await.expect("Failed to upload document"); + document_ids.push(document.id.to_string()); + } + + println!("Uploaded {} documents for bulk deletion", document_ids.len()); + + // Verify all documents exist + for doc_id in &document_ids { + let retrieved_doc = client.get_document(doc_id) + .await.expect("Failed to get document"); + assert!(retrieved_doc.is_some(), "Document should exist before bulk deletion"); + } + + // Perform bulk deletion + let delete_result = client.bulk_delete_documents(&document_ids) + .await.expect("Failed to bulk delete documents"); + + // Verify bulk deletion response + assert_eq!(delete_result["success"], true); + assert_eq!(delete_result["deleted_count"], 5); + assert_eq!(delete_result["requested_count"], 5); + + let deleted_ids: Vec = delete_result["deleted_document_ids"] + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap().to_string()) + .collect(); + + assert_eq!(deleted_ids.len(), 5); + for doc_id in &document_ids { + assert!(deleted_ids.contains(doc_id), "Document ID should be in deleted list"); + } + + // Verify all documents no longer exist + for doc_id in &document_ids { + let retrieved_doc = client.get_document(doc_id) + .await.expect("Failed to check document existence"); + assert!(retrieved_doc.is_none(), "Document should not exist after bulk deletion"); + } + + println!("✅ Bulk document deletion test passed"); +} + +#[tokio::test] +async fn test_delete_nonexistent_document() { + let mut client = DocumentDeletionTestClient::new(); + skip_if_server_down!(client); + + // Register and login + client.register_and_login( + &format!("testuser_nonexist_{}", Uuid::new_v4()), + &format!("testuser_nonexist_{}@example.com", Uuid::new_v4()), + "password123", + None + ).await.expect("Failed to register and login"); + + // Try to delete a non-existent document + let fake_id = Uuid::new_v4().to_string(); + let delete_result = client.delete_document(&fake_id).await; + + // Should return 404 error + assert!(delete_result.is_err(), "Deleting non-existent document should fail"); + let error_msg = delete_result.unwrap_err().to_string(); + assert!(error_msg.contains("404"), "Should return 404 error for non-existent document"); + + println!("✅ Delete non-existent document test passed"); +} + +#[tokio::test] +async fn test_bulk_delete_empty_request() { + let mut client = DocumentDeletionTestClient::new(); + skip_if_server_down!(client); + + // Register and login + client.register_and_login( + &format!("testuser_empty_{}", Uuid::new_v4()), + &format!("testuser_empty_{}@example.com", Uuid::new_v4()), + "password123", + None + ).await.expect("Failed to register and login"); + + // Try bulk delete with empty array + let delete_result = client.bulk_delete_documents(&[]) + .await.expect("Bulk delete with empty array should succeed"); + + // Should handle empty request gracefully + assert_eq!(delete_result["success"], false); + assert_eq!(delete_result["deleted_count"], 0); + assert!(delete_result["message"].as_str().unwrap().contains("No document IDs provided")); + + println!("✅ Bulk delete empty request test passed"); +} + +#[tokio::test] +async fn test_bulk_delete_mixed_existing_nonexistent() { + let mut client = DocumentDeletionTestClient::new(); + skip_if_server_down!(client); + + // Register and login + client.register_and_login( + &format!("testuser_mixed_{}", Uuid::new_v4()), + &format!("testuser_mixed_{}@example.com", Uuid::new_v4()), + "password123", + None + ).await.expect("Failed to register and login"); + + // Upload one real document + let test_content = b"This is a real document."; + let real_document = client.upload_document(test_content, "real_doc.txt") + .await.expect("Failed to upload document"); + + // Create a list with real and fake IDs + let fake_id = Uuid::new_v4().to_string(); + let mixed_ids = vec![real_document.id.to_string(), fake_id]; + + // Perform bulk deletion + let delete_result = client.bulk_delete_documents(&mixed_ids) + .await.expect("Failed to bulk delete mixed documents"); + + // Should delete only the existing document + assert_eq!(delete_result["success"], true); + assert_eq!(delete_result["deleted_count"], 1); + assert_eq!(delete_result["requested_count"], 2); + + let deleted_ids: Vec = delete_result["deleted_document_ids"] + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap().to_string()) + .collect(); + + assert_eq!(deleted_ids.len(), 1); + assert_eq!(deleted_ids[0], real_document.id.to_string()); + + // Verify real document was deleted + let retrieved_doc = client.get_document(&real_document.id.to_string()) + .await.expect("Failed to check document existence"); + assert!(retrieved_doc.is_none(), "Real document should be deleted"); + + println!("✅ Bulk delete mixed existing/non-existent test passed"); +} + +#[tokio::test] +async fn test_unauthorized_deletion() { + let client = DocumentDeletionTestClient::new(); + skip_if_server_down!(client); + + // Try to delete without authentication + let fake_id = Uuid::new_v4().to_string(); + let delete_result = client.delete_document(&fake_id).await; + + // Should return 401 error + assert!(delete_result.is_err(), "Unauthenticated deletion should fail"); + let error_msg = delete_result.unwrap_err().to_string(); + assert!(error_msg.contains("401") || error_msg.contains("Unauthorized"), + "Should return 401/Unauthorized error"); + + println!("✅ Unauthorized deletion test passed"); +} + +#[tokio::test] +async fn test_cross_user_deletion_protection() { + let mut client1 = DocumentDeletionTestClient::new(); + let mut client2 = DocumentDeletionTestClient::new(); + skip_if_server_down!(client1); + + let user1_suffix = Uuid::new_v4(); + let user2_suffix = Uuid::new_v4(); + + // Register and login as user 1 + client1.register_and_login( + &format!("testuser1_{}", user1_suffix), + &format!("testuser1_{}@example.com", user1_suffix), + "password123", + None + ).await.expect("Failed to register user 1"); + + // Register and login as user 2 + client2.register_and_login( + &format!("testuser2_{}", user2_suffix), + &format!("testuser2_{}@example.com", user2_suffix), + "password123", + None + ).await.expect("Failed to register user 2"); + + // User 1 uploads a document + let test_content = b"This is user 1's document."; + let user1_document = client1.upload_document(test_content, "user1_doc.txt") + .await.expect("Failed to upload document as user 1"); + + // User 2 tries to delete user 1's document + let delete_result = client2.delete_document(&user1_document.id.to_string()).await; + + // Should return 404 (document not found for user 2) + assert!(delete_result.is_err(), "Cross-user deletion should fail"); + let error_msg = delete_result.unwrap_err().to_string(); + assert!(error_msg.contains("404"), "Should return 404 for document not owned by user"); + + // Verify user 1's document still exists + let retrieved_doc = client1.get_document(&user1_document.id.to_string()) + .await.expect("Failed to check document existence"); + assert!(retrieved_doc.is_some(), "User 1's document should still exist after failed cross-user deletion"); + + println!("✅ Cross-user deletion protection test passed"); +} + +#[tokio::test] +async fn test_admin_can_delete_any_document() { + let mut user_client = DocumentDeletionTestClient::new(); + let mut admin_client = DocumentDeletionTestClient::new(); + skip_if_server_down!(user_client); + + let user_suffix = Uuid::new_v4(); + let admin_suffix = Uuid::new_v4(); + + // Register and login as regular user + user_client.register_and_login( + &format!("regularuser_{}", user_suffix), + &format!("regularuser_{}@example.com", user_suffix), + "password123", + None + ).await.expect("Failed to register regular user"); + + // Register and login as admin + admin_client.register_and_login( + &format!("adminuser_{}", admin_suffix), + &format!("adminuser_{}@example.com", admin_suffix), + "adminpass123", + Some(UserRole::Admin) + ).await.expect("Failed to register admin user"); + + // Regular user uploads a document + let test_content = b"This is a regular user's document."; + let user_document = user_client.upload_document(test_content, "user_doc.txt") + .await.expect("Failed to upload document as user"); + + // Admin deletes user's document + let delete_result = admin_client.delete_document(&user_document.id.to_string()) + .await.expect("Admin should be able to delete any document"); + + // Verify deletion response + assert_eq!(delete_result["success"], true); + assert_eq!(delete_result["document_id"], user_document.id.to_string()); + + // Verify document no longer exists + let retrieved_doc = user_client.get_document(&user_document.id.to_string()) + .await.expect("Failed to check document existence"); + assert!(retrieved_doc.is_none(), "Document should be deleted by admin"); + + println!("✅ Admin can delete any document test passed"); +} + +#[tokio::test] +async fn test_document_count_updates_after_deletion() { + let mut client = DocumentDeletionTestClient::new(); + skip_if_server_down!(client); + + // Register and login + client.register_and_login( + &format!("testuser_count_{}", Uuid::new_v4()), + &format!("testuser_count_{}@example.com", Uuid::new_v4()), + "password123", + None + ).await.expect("Failed to register and login"); + + // Get initial document count + let initial_list = client.list_documents() + .await.expect("Failed to list documents"); + let initial_count = initial_list["pagination"]["total"].as_i64().unwrap(); + + // Upload documents + let mut document_ids = Vec::new(); + for i in 1..=3 { + let test_content = format!("Test document {}", i); + let document = client.upload_document(test_content.as_bytes(), &format!("count_test_{}.txt", i)) + .await.expect("Failed to upload document"); + document_ids.push(document.id.to_string()); + } + + // Verify count increased + let after_upload_list = client.list_documents() + .await.expect("Failed to list documents"); + let after_upload_count = after_upload_list["pagination"]["total"].as_i64().unwrap(); + assert_eq!(after_upload_count, initial_count + 3, "Document count should increase after uploads"); + + // Delete one document + client.delete_document(&document_ids[0]) + .await.expect("Failed to delete document"); + + // Verify count decreased by 1 + let after_single_delete_list = client.list_documents() + .await.expect("Failed to list documents"); + let after_single_delete_count = after_single_delete_list["pagination"]["total"].as_i64().unwrap(); + assert_eq!(after_single_delete_count, initial_count + 2, "Document count should decrease after single deletion"); + + // Bulk delete remaining documents + let remaining_ids = vec![document_ids[1].clone(), document_ids[2].clone()]; + client.bulk_delete_documents(&remaining_ids) + .await.expect("Failed to bulk delete documents"); + + // Verify count is back to initial + let final_list = client.list_documents() + .await.expect("Failed to list documents"); + let final_count = final_list["pagination"]["total"].as_i64().unwrap(); + assert_eq!(final_count, initial_count, "Document count should be back to initial after bulk deletion"); + + println!("✅ Document count updates after deletion test passed"); +} \ No newline at end of file