feat(tests): add deletion unit tests
This commit is contained in:
parent
1507532083
commit
e3c276226a
|
|
@ -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<Uuid, bool>,
|
||||
pub bulk_delete_results: std::collections::HashMap<Vec<Uuid>, Vec<Document>>,
|
||||
}
|
||||
|
||||
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<BulkDeleteRequest, _> = 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<BulkDeleteRequest, _> = 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<BulkDeleteRequest, _> = 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<BulkDeleteRequest, _> = serde_json::from_value(invalid_json);
|
||||
assert!(result.is_err());
|
||||
|
||||
// Test empty request
|
||||
let empty_json = json!({
|
||||
"document_ids": []
|
||||
});
|
||||
|
||||
let result: Result<BulkDeleteRequest, _> = 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"));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
user_id: Option<String>,
|
||||
}
|
||||
|
||||
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<dyn std::error::Error>> {
|
||||
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<UserRole>) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// 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<DocumentResponse, Box<dyn std::error::Error>> {
|
||||
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<Value, Box<dyn std::error::Error>> {
|
||||
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<Value, Box<dyn std::error::Error>> {
|
||||
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<Option<DocumentResponse>, Box<dyn std::error::Error>> {
|
||||
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<Value, Box<dyn std::error::Error>> {
|
||||
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<String> = 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<String> = 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");
|
||||
}
|
||||
Loading…
Reference in New Issue