feat(tests): add deletion unit tests

This commit is contained in:
aaldebs99 2025-06-20 16:09:27 +00:00
parent 1507532083
commit e3c276226a
5 changed files with 2588 additions and 0 deletions

View File

@ -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

View File

@ -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());
}
}

View File

@ -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;

View File

@ -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");
}