mirror of
https://github.com/readur/readur.git
synced 2025-12-30 11:00:17 -06:00
2702 lines
112 KiB
Rust
2702 lines
112 KiB
Rust
use anyhow::Result;
|
|
use readur::models::{Document, DocumentResponse};
|
|
use readur::test_utils::{TestContext, TestAuthHelper};
|
|
use chrono::Utc;
|
|
use serde_json::Value;
|
|
use uuid::Uuid;
|
|
|
|
#[cfg(test)]
|
|
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: 1024000,
|
|
mime_type: "application/pdf".to_string(),
|
|
content: Some("Test document content".to_string()),
|
|
ocr_text: Some("This is extracted OCR text from the test document.".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(), "document".to_string()],
|
|
created_at: Utc::now(),
|
|
updated_at: Utc::now(),
|
|
user_id,
|
|
file_hash: Some(format!("{:x}", Uuid::new_v4().as_u128())),
|
|
original_created_at: None,
|
|
original_modified_at: None,
|
|
source_path: None,
|
|
source_type: None,
|
|
source_id: None,
|
|
file_permissions: None,
|
|
file_owner: None,
|
|
file_group: None,
|
|
source_metadata: None,
|
|
ocr_retry_count: None,
|
|
ocr_failure_reason: None,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn create_test_document_without_ocr(user_id: Uuid) -> Document {
|
|
Document {
|
|
id: Uuid::new_v4(),
|
|
filename: "test_no_ocr.txt".to_string(),
|
|
original_filename: "test_no_ocr.txt".to_string(),
|
|
file_path: "/uploads/test_no_ocr.txt".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("pending".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("fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321".to_string()),
|
|
original_created_at: None,
|
|
original_modified_at: None,
|
|
source_path: None,
|
|
source_type: None,
|
|
source_id: None,
|
|
file_permissions: None,
|
|
file_owner: None,
|
|
file_group: None,
|
|
source_metadata: None,
|
|
ocr_retry_count: None,
|
|
ocr_failure_reason: None,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
fn create_test_document_with_ocr_error(user_id: Uuid) -> Document {
|
|
Document {
|
|
id: Uuid::new_v4(),
|
|
filename: "test_error.pdf".to_string(),
|
|
original_filename: "test_error.pdf".to_string(),
|
|
file_path: "/uploads/test_error.pdf".to_string(),
|
|
file_size: 2048000,
|
|
mime_type: "application/pdf".to_string(),
|
|
content: None,
|
|
ocr_text: None,
|
|
ocr_confidence: None,
|
|
ocr_word_count: None,
|
|
ocr_processing_time_ms: Some(5000),
|
|
ocr_status: Some("failed".to_string()),
|
|
ocr_error: Some("Failed to process document: corrupted file".to_string()),
|
|
ocr_completed_at: Some(Utc::now()),
|
|
tags: vec!["error".to_string()],
|
|
created_at: Utc::now(),
|
|
updated_at: Utc::now(),
|
|
user_id,
|
|
file_hash: Some("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890".to_string()),
|
|
original_created_at: None,
|
|
original_modified_at: None,
|
|
source_path: None,
|
|
source_type: None,
|
|
source_id: None,
|
|
file_permissions: None,
|
|
file_owner: None,
|
|
file_group: None,
|
|
source_metadata: None,
|
|
ocr_retry_count: None,
|
|
ocr_failure_reason: None,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_document_response_conversion() {
|
|
let user_id = Uuid::new_v4();
|
|
let document = create_test_document(user_id);
|
|
|
|
let response: DocumentResponse = document.clone().into();
|
|
|
|
assert_eq!(response.id, document.id);
|
|
assert_eq!(response.filename, document.filename);
|
|
assert_eq!(response.original_filename, document.original_filename);
|
|
assert_eq!(response.file_size, document.file_size);
|
|
assert_eq!(response.mime_type, document.mime_type);
|
|
assert_eq!(response.tags, document.tags);
|
|
assert_eq!(response.has_ocr_text, true);
|
|
assert_eq!(response.ocr_confidence, document.ocr_confidence);
|
|
assert_eq!(response.ocr_word_count, document.ocr_word_count);
|
|
assert_eq!(response.ocr_processing_time_ms, document.ocr_processing_time_ms);
|
|
assert_eq!(response.ocr_status, document.ocr_status);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_document_response_conversion_no_ocr() {
|
|
let user_id = Uuid::new_v4();
|
|
let document = create_test_document_without_ocr(user_id);
|
|
|
|
let response: DocumentResponse = document.clone().into();
|
|
|
|
assert_eq!(response.has_ocr_text, false);
|
|
assert_eq!(response.ocr_confidence, None);
|
|
assert_eq!(response.ocr_word_count, None);
|
|
}
|
|
|
|
#[test]
|
|
fn test_ocr_response_structure() {
|
|
let user_id = Uuid::new_v4();
|
|
let document = create_test_document(user_id);
|
|
|
|
// Test that OCR response fields match expected structure
|
|
let ocr_response = serde_json::json!({
|
|
"id": document.id.to_string(),
|
|
"filename": document.filename,
|
|
"has_ocr_text": document.ocr_text.is_some(),
|
|
"ocr_text": document.ocr_text,
|
|
"ocr_confidence": document.ocr_confidence,
|
|
"ocr_word_count": document.ocr_word_count,
|
|
"ocr_processing_time_ms": document.ocr_processing_time_ms,
|
|
"ocr_status": document.ocr_status,
|
|
"ocr_error": document.ocr_error,
|
|
"ocr_completed_at": document.ocr_completed_at
|
|
});
|
|
|
|
assert!(ocr_response.is_object());
|
|
assert_eq!(ocr_response["id"], document.id.to_string());
|
|
assert_eq!(ocr_response["filename"], document.filename);
|
|
assert_eq!(ocr_response["has_ocr_text"], true);
|
|
assert_eq!(ocr_response["ocr_text"], document.ocr_text.unwrap());
|
|
assert_eq!(ocr_response["ocr_confidence"], document.ocr_confidence.unwrap());
|
|
assert_eq!(ocr_response["ocr_word_count"], document.ocr_word_count.unwrap());
|
|
assert_eq!(ocr_response["ocr_processing_time_ms"], document.ocr_processing_time_ms.unwrap());
|
|
assert_eq!(ocr_response["ocr_status"], document.ocr_status.unwrap());
|
|
assert_eq!(ocr_response["ocr_error"], Value::Null);
|
|
}
|
|
|
|
#[test]
|
|
fn test_ocr_response_with_error() {
|
|
let user_id = Uuid::new_v4();
|
|
let document = create_test_document_with_ocr_error(user_id);
|
|
|
|
let ocr_response = serde_json::json!({
|
|
"document_id": document.id,
|
|
"filename": document.filename,
|
|
"has_ocr_text": document.ocr_text.is_some(),
|
|
"ocr_text": document.ocr_text,
|
|
"ocr_confidence": document.ocr_confidence,
|
|
"ocr_word_count": document.ocr_word_count,
|
|
"ocr_processing_time_ms": document.ocr_processing_time_ms,
|
|
"ocr_status": document.ocr_status,
|
|
"ocr_error": document.ocr_error,
|
|
"ocr_completed_at": document.ocr_completed_at
|
|
});
|
|
|
|
assert_eq!(ocr_response["has_ocr_text"], false);
|
|
assert_eq!(ocr_response["ocr_text"], Value::Null);
|
|
assert_eq!(ocr_response["ocr_status"], "failed");
|
|
assert_eq!(ocr_response["ocr_error"], "Failed to process document: corrupted file");
|
|
}
|
|
|
|
#[test]
|
|
fn test_ocr_confidence_validation() {
|
|
let user_id = Uuid::new_v4();
|
|
let mut document = create_test_document(user_id);
|
|
|
|
// Test valid confidence range
|
|
document.ocr_confidence = Some(95.5);
|
|
assert!(document.ocr_confidence.unwrap() >= 0.0 && document.ocr_confidence.unwrap() <= 100.0);
|
|
|
|
// Test edge cases
|
|
document.ocr_confidence = Some(0.0);
|
|
assert!(document.ocr_confidence.unwrap() >= 0.0);
|
|
|
|
document.ocr_confidence = Some(100.0);
|
|
assert!(document.ocr_confidence.unwrap() <= 100.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_ocr_word_count_validation() {
|
|
let user_id = Uuid::new_v4();
|
|
let mut document = create_test_document(user_id);
|
|
|
|
// Test positive word count
|
|
document.ocr_word_count = Some(150);
|
|
assert!(document.ocr_word_count.unwrap() > 0);
|
|
|
|
// Test zero word count (valid for empty documents)
|
|
document.ocr_word_count = Some(0);
|
|
assert!(document.ocr_word_count.unwrap() >= 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_ocr_processing_time_validation() {
|
|
let user_id = Uuid::new_v4();
|
|
let mut document = create_test_document(user_id);
|
|
|
|
// Test positive processing time
|
|
document.ocr_processing_time_ms = Some(1200);
|
|
assert!(document.ocr_processing_time_ms.unwrap() > 0);
|
|
|
|
// Test very fast processing
|
|
document.ocr_processing_time_ms = Some(50);
|
|
assert!(document.ocr_processing_time_ms.unwrap() > 0);
|
|
|
|
// Test slow processing
|
|
document.ocr_processing_time_ms = Some(30000); // 30 seconds
|
|
assert!(document.ocr_processing_time_ms.unwrap() > 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_ocr_status_values() {
|
|
let user_id = Uuid::new_v4();
|
|
let mut document = create_test_document(user_id);
|
|
|
|
// Test valid status values
|
|
let valid_statuses = vec!["pending", "processing", "completed", "failed"];
|
|
|
|
for status in valid_statuses {
|
|
document.ocr_status = Some(status.to_string());
|
|
assert!(matches!(
|
|
document.ocr_status.as_deref(),
|
|
Some("pending") | Some("processing") | Some("completed") | Some("failed")
|
|
));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_document_with_complete_ocr_data() {
|
|
let user_id = Uuid::new_v4();
|
|
let document = create_test_document(user_id);
|
|
|
|
// Verify all OCR fields are properly set for a completed document
|
|
assert!(document.ocr_text.is_some());
|
|
assert!(document.ocr_confidence.is_some());
|
|
assert!(document.ocr_word_count.is_some());
|
|
assert!(document.ocr_processing_time_ms.is_some());
|
|
assert_eq!(document.ocr_status.as_deref(), Some("completed"));
|
|
assert!(document.ocr_error.is_none());
|
|
assert!(document.ocr_completed_at.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_document_with_failed_ocr() {
|
|
let user_id = Uuid::new_v4();
|
|
let document = create_test_document_with_ocr_error(user_id);
|
|
|
|
// Verify failed OCR document has appropriate fields
|
|
assert!(document.ocr_text.is_none());
|
|
assert!(document.ocr_confidence.is_none());
|
|
assert!(document.ocr_word_count.is_none());
|
|
assert_eq!(document.ocr_status.as_deref(), Some("failed"));
|
|
assert!(document.ocr_error.is_some());
|
|
assert!(document.ocr_completed_at.is_some()); // Should still have completion time
|
|
}
|
|
|
|
#[test]
|
|
fn test_document_mime_type_ocr_eligibility() {
|
|
let user_id = Uuid::new_v4();
|
|
|
|
// Test OCR-eligible file types
|
|
let ocr_eligible_types = vec![
|
|
"application/pdf",
|
|
"image/png",
|
|
"image/jpeg",
|
|
"image/jpg",
|
|
"image/tiff",
|
|
"image/bmp"
|
|
];
|
|
|
|
for mime_type in ocr_eligible_types {
|
|
let mut document = create_test_document(user_id);
|
|
document.mime_type = mime_type.to_string();
|
|
|
|
// These types should typically have OCR processing
|
|
assert!(document.mime_type.contains("pdf") || document.mime_type.starts_with("image/"));
|
|
}
|
|
|
|
// Test non-OCR file types
|
|
let mut text_document = create_test_document_without_ocr(user_id);
|
|
text_document.mime_type = "text/plain".to_string();
|
|
|
|
// Text files typically don't need OCR
|
|
assert_eq!(text_document.mime_type, "text/plain");
|
|
assert!(text_document.ocr_text.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_ocr_text_content_validation() {
|
|
let user_id = Uuid::new_v4();
|
|
let document = create_test_document(user_id);
|
|
|
|
if let Some(ocr_text) = &document.ocr_text {
|
|
// Test that OCR text is not empty
|
|
assert!(!ocr_text.trim().is_empty());
|
|
|
|
// Test that OCR text is reasonable length
|
|
assert!(ocr_text.len() > 0);
|
|
assert!(ocr_text.len() < 100000); // Reasonable upper limit
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod document_deletion_tests {
|
|
use super::*;
|
|
use readur::test_utils::TestContext;
|
|
use readur::models::{UserRole, User, Document, AuthProvider, CreateUser};
|
|
use chrono::Utc;
|
|
use uuid::Uuid;
|
|
|
|
#[tokio::test]
|
|
async fn test_delete_document_as_owner() {
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let db = &ctx.state.db;
|
|
|
|
// Create test user and document
|
|
let user_data = CreateUser {
|
|
username: format!("testuser_{}", Uuid::new_v4()),
|
|
email: format!("test_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let user = db.create_user(user_data).await.expect("Failed to create user");
|
|
let document = super::create_test_document(user.id);
|
|
let document = db.create_document(document).await.expect("Failed to create document");
|
|
|
|
// Delete document as owner
|
|
let result = db
|
|
.delete_document(document.id, user.id, user.role)
|
|
.await
|
|
.expect("Failed to delete document");
|
|
|
|
// Verify document was deleted
|
|
assert!(result);
|
|
|
|
// Verify document no longer exists in database
|
|
let found_doc = db
|
|
.get_document_by_id(document.id, user.id, user.role)
|
|
.await
|
|
.expect("Database query failed");
|
|
assert!(found_doc.is_none());
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_delete_document_as_admin() {
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let db = &ctx.state.db;
|
|
|
|
// Create regular user and their document
|
|
let user_data = CreateUser {
|
|
username: format!("testuser_{}", Uuid::new_v4()),
|
|
email: format!("test_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let user = db.create_user(user_data).await.expect("Failed to create user");
|
|
let document = super::create_test_document(user.id);
|
|
let document = db.create_document(document).await.expect("Failed to create document");
|
|
|
|
// Create admin user
|
|
let admin_data = CreateUser {
|
|
username: format!("adminuser_{}", Uuid::new_v4()),
|
|
email: format!("admin_{}@example.com", Uuid::new_v4()),
|
|
password: "adminpass123".to_string(),
|
|
role: Some(UserRole::Admin),
|
|
};
|
|
let admin = db.create_user(admin_data).await.expect("Failed to create admin");
|
|
|
|
// Delete document as admin
|
|
let result = db
|
|
.delete_document(document.id, admin.id, admin.role)
|
|
.await
|
|
.expect("Failed to delete document as admin");
|
|
|
|
// Verify document was deleted
|
|
assert!(result);
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_delete_document_unauthorized() {
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let db = &ctx.state.db;
|
|
|
|
// Create two regular users
|
|
let user1_data = CreateUser {
|
|
username: format!("testuser1_{}", Uuid::new_v4()),
|
|
email: format!("test1_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let user1 = db.create_user(user1_data).await.expect("Failed to create user1");
|
|
|
|
let user2_data = CreateUser {
|
|
username: format!("testuser2_{}", Uuid::new_v4()),
|
|
email: format!("test2_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let user2 = db.create_user(user2_data).await.expect("Failed to create user2");
|
|
|
|
// Create document owned by user1
|
|
let document = super::create_test_document(user1.id);
|
|
let document = db.create_document(document).await.expect("Failed to create document");
|
|
|
|
// Try to delete document as user2 (should fail)
|
|
let result = db
|
|
.delete_document(document.id, user2.id, user2.role)
|
|
.await
|
|
.expect("Database query failed");
|
|
|
|
// Verify document was not deleted
|
|
assert!(!result);
|
|
|
|
// Verify document still exists
|
|
let found_doc = db
|
|
.get_document_by_id(document.id, user1.id, user1.role)
|
|
.await
|
|
.expect("Database query failed");
|
|
assert!(found_doc.is_some());
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_delete_nonexistent_document() {
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let auth_helper = TestAuthHelper::new(ctx.app.clone());
|
|
let user = auth_helper.create_test_user().await;
|
|
|
|
let nonexistent_id = Uuid::new_v4();
|
|
|
|
// Try to delete nonexistent document
|
|
let result = ctx.state.db
|
|
.delete_document(nonexistent_id, user.user_response.id, user.user_response.role)
|
|
.await
|
|
.expect("Database query failed");
|
|
|
|
// Verify nothing was deleted
|
|
assert!(!result);
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bulk_delete_documents_as_owner() {
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let auth_helper = TestAuthHelper::new(ctx.app.clone());
|
|
let user = auth_helper.create_test_user().await;
|
|
|
|
// Create multiple documents
|
|
let doc1 = create_test_document(user.user_response.id);
|
|
let doc1 = ctx.state.db.create_document(doc1).await.expect("Failed to create document");
|
|
let doc2 = create_test_document(user.user_response.id);
|
|
let doc2 = ctx.state.db.create_document(doc2).await.expect("Failed to create document");
|
|
let doc3 = create_test_document(user.user_response.id);
|
|
let doc3 = ctx.state.db.create_document(doc3).await.expect("Failed to create document");
|
|
|
|
let document_ids = vec![doc1.id, doc2.id, doc3.id];
|
|
|
|
// Delete documents as owner
|
|
let result = ctx.state.db
|
|
.bulk_delete_documents(&document_ids, user.user_response.id, user.user_response.role)
|
|
.await
|
|
.expect("Failed to bulk delete documents");
|
|
|
|
// Verify all documents were deleted
|
|
let (deleted_ids, failed_ids) = result;
|
|
assert_eq!(deleted_ids.len(), 3);
|
|
assert_eq!(failed_ids.len(), 0);
|
|
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 = ctx.state.db
|
|
.get_document_by_id(doc_id, user.user_response.id, user.user_response.role)
|
|
.await
|
|
.expect("Database query failed");
|
|
assert!(found_doc.is_none());
|
|
}
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bulk_delete_documents_as_admin() {
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let auth_helper = TestAuthHelper::new(ctx.app.clone());
|
|
|
|
// Create regular user and their documents
|
|
let user = auth_helper.create_test_user().await;
|
|
let doc1 = create_test_document(user.user_response.id);
|
|
let doc1 = ctx.state.db.create_document(doc1).await.expect("Failed to create document");
|
|
let doc2 = create_test_document(user.user_response.id);
|
|
let doc2 = ctx.state.db.create_document(doc2).await.expect("Failed to create document");
|
|
|
|
// Create admin user
|
|
let admin = auth_helper.create_admin_user().await;
|
|
|
|
let document_ids = vec![doc1.id, doc2.id];
|
|
|
|
// Delete documents as admin
|
|
let result = ctx.state.db
|
|
.bulk_delete_documents(&document_ids, admin.user_response.id, admin.user_response.role)
|
|
.await
|
|
.expect("Failed to bulk delete documents as admin");
|
|
|
|
// Verify all documents were deleted
|
|
let (deleted_ids, failed_ids) = result;
|
|
assert_eq!(deleted_ids.len(), 2);
|
|
assert_eq!(failed_ids.len(), 0);
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bulk_delete_documents_mixed_ownership() {
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let db = &ctx.state.db;
|
|
|
|
// Create two regular users
|
|
let user1_data = CreateUser {
|
|
username: format!("testuser1_{}", Uuid::new_v4()),
|
|
email: format!("test1_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let user1 = db.create_user(user1_data).await.expect("Failed to create user1");
|
|
|
|
let user2_data = CreateUser {
|
|
username: format!("testuser2_{}", Uuid::new_v4()),
|
|
email: format!("test2_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let user2 = db.create_user(user2_data).await.expect("Failed to create user2");
|
|
|
|
// Create documents for both users
|
|
let doc1_user1 = create_test_document(user1.id);
|
|
let doc1_user1 = ctx.state.db.create_document(doc1_user1).await.expect("Failed to create document");
|
|
let doc2_user1 = create_test_document(user1.id);
|
|
let doc2_user1 = ctx.state.db.create_document(doc2_user1).await.expect("Failed to create document");
|
|
let doc1_user2 = create_test_document(user2.id);
|
|
let doc1_user2 = ctx.state.db.create_document(doc1_user2).await.expect("Failed to create document");
|
|
|
|
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 = ctx.state.db
|
|
.bulk_delete_documents(&document_ids, user1.id, user1.role)
|
|
.await
|
|
.expect("Failed to bulk delete documents");
|
|
|
|
// Verify only user1's documents were deleted
|
|
let (deleted_ids, failed_ids) = result;
|
|
assert_eq!(deleted_ids.len(), 2);
|
|
assert_eq!(failed_ids.len(), 1);
|
|
assert!(deleted_ids.contains(&doc1_user1.id));
|
|
assert!(deleted_ids.contains(&doc2_user1.id));
|
|
assert!(failed_ids.contains(&doc1_user2.id));
|
|
|
|
// Verify user2's document still exists
|
|
let found_doc = ctx.state.db
|
|
.get_document_by_id(doc1_user2.id, user2.id, user2.role)
|
|
.await
|
|
.expect("Database query failed");
|
|
assert!(found_doc.is_some());
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bulk_delete_documents_empty_list() {
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let auth_helper = TestAuthHelper::new(ctx.app.clone());
|
|
|
|
let user = auth_helper.create_test_user().await;
|
|
let empty_ids: Vec<Uuid> = vec![];
|
|
|
|
// Delete empty list of documents
|
|
let result = ctx.state.db
|
|
.bulk_delete_documents(&empty_ids, user.user_response.id, user.user_response.role)
|
|
.await
|
|
.expect("Failed to bulk delete empty list");
|
|
|
|
// Verify empty result
|
|
let (deleted_ids, failed_ids) = result;
|
|
assert_eq!(deleted_ids.len(), 0);
|
|
assert_eq!(failed_ids.len(), 0);
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bulk_delete_documents_nonexistent_ids() {
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let auth_helper = TestAuthHelper::new(ctx.app.clone());
|
|
|
|
let user = auth_helper.create_test_user().await;
|
|
|
|
// Create one real document
|
|
let real_doc = create_test_document(user.user_response.id);
|
|
let real_doc = ctx.state.db.create_document(real_doc).await.expect("Failed to create document");
|
|
|
|
// 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 = ctx.state.db
|
|
.bulk_delete_documents(&document_ids, user.user_response.id, user.user_response.role)
|
|
.await
|
|
.expect("Failed to bulk delete documents");
|
|
|
|
// Verify only the real document was deleted
|
|
let (deleted_ids, failed_ids) = result;
|
|
assert_eq!(deleted_ids.len(), 1);
|
|
assert_eq!(failed_ids.len(), 2);
|
|
assert!(deleted_ids.contains(&real_doc.id));
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bulk_delete_documents_partial_authorization() {
|
|
|
|
// Create regular user and admin
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let auth_helper = TestAuthHelper::new(ctx.app.clone());
|
|
let user = auth_helper.create_test_user().await;
|
|
let admin = auth_helper.create_admin_user().await;
|
|
|
|
// Create documents for both users
|
|
let user_doc_doc = create_test_document(user.user_response.id);
|
|
let user_doc = ctx.state.db.create_document(user_doc_doc).await.expect("Failed to create document");
|
|
let admin_doc_doc = create_test_document(admin.user_response.id);
|
|
let admin_doc = ctx.state.db.create_document(admin_doc_doc).await.expect("Failed to create document");
|
|
|
|
let document_ids = vec![user_doc.id, admin_doc.id];
|
|
|
|
// Admin should be able to delete both
|
|
let result = ctx.state.db
|
|
.bulk_delete_documents(&document_ids, admin.user_response.id, admin.user_response.role)
|
|
.await
|
|
.expect("Failed to bulk delete documents as admin");
|
|
|
|
let (deleted_ids, failed_ids) = result;
|
|
assert_eq!(deleted_ids.len(), 2);
|
|
assert_eq!(failed_ids.len(), 0);
|
|
|
|
// Recreate documents for user test
|
|
let user_doc2_doc = create_test_document(user.user_response.id);
|
|
let user_doc2 = ctx.state.db.create_document(user_doc2_doc).await.expect("Failed to create document");
|
|
let admin_doc2_doc = create_test_document(admin.user_response.id);
|
|
let admin_doc2 = ctx.state.db.create_document(admin_doc2_doc).await.expect("Failed to create document");
|
|
|
|
let document_ids2 = vec![user_doc2.id, admin_doc2.id];
|
|
|
|
// Regular user should only delete their own
|
|
let result2 = ctx.state.db
|
|
.bulk_delete_documents(&document_ids2, user.user_response.id, user.user_response.role)
|
|
.await
|
|
.expect("Failed to bulk delete documents as user");
|
|
|
|
let (deleted_ids2, failed_ids2) = result2;
|
|
assert_eq!(deleted_ids2.len(), 1);
|
|
assert_eq!(failed_ids2.len(), 1);
|
|
assert!(deleted_ids2.contains(&user_doc2.id));
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod rbac_deletion_tests {
|
|
use super::*;
|
|
use readur::test_utils::TestContext;
|
|
use readur::models::{UserRole, CreateUser};
|
|
use uuid::Uuid;
|
|
|
|
#[tokio::test]
|
|
async fn test_user_can_delete_own_document() {
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let db = &ctx.state.db;
|
|
|
|
let user_data = CreateUser {
|
|
username: format!("testuser_{}", Uuid::new_v4()),
|
|
email: format!("test_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let user = db.create_user(user_data).await.expect("Failed to create user");
|
|
let document = super::create_test_document(user.id);
|
|
let document = db.create_document(document).await.expect("Failed to create document");
|
|
|
|
// User should be able to delete their own document
|
|
let result = db
|
|
.delete_document(document.id, user.id, user.role)
|
|
.await
|
|
.expect("Failed to delete document");
|
|
|
|
assert!(result);
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_user_cannot_delete_other_user_document() {
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let db = &ctx.state.db;
|
|
|
|
// Create users using direct database approach
|
|
let user1_data = CreateUser {
|
|
username: format!("testuser1_{}", Uuid::new_v4()),
|
|
email: format!("test1_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let user1 = db.create_user(user1_data).await.expect("Failed to create user1");
|
|
|
|
let user2_data = CreateUser {
|
|
username: format!("testuser2_{}", Uuid::new_v4()),
|
|
email: format!("test2_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let user2 = db.create_user(user2_data).await.expect("Failed to create user2");
|
|
|
|
let document = create_test_document(user1.id);
|
|
let document = ctx.state.db.create_document(document).await.expect("Failed to create document");
|
|
|
|
// User2 should NOT be able to delete user1's document
|
|
let result = ctx.state.db
|
|
.delete_document(document.id, user2.id, user2.role)
|
|
.await
|
|
.expect("Database query failed");
|
|
|
|
assert!(!result);
|
|
|
|
// Verify document still exists
|
|
let found_doc = ctx.state.db
|
|
.get_document_by_id(document.id, user1.id, user1.role)
|
|
.await
|
|
.expect("Database query failed");
|
|
assert!(found_doc.is_some());
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_admin_can_delete_any_document() {
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let db = &ctx.state.db;
|
|
|
|
// Create users using direct database approach
|
|
let user_data = CreateUser {
|
|
username: format!("testuser_{}", Uuid::new_v4()),
|
|
email: format!("test_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let user = db.create_user(user_data).await.expect("Failed to create user");
|
|
|
|
let admin_data = CreateUser {
|
|
username: format!("testadmin_{}", Uuid::new_v4()),
|
|
email: format!("admin_{}@example.com", Uuid::new_v4()),
|
|
password: "adminpass123".to_string(),
|
|
role: Some(UserRole::Admin),
|
|
};
|
|
let admin = db.create_user(admin_data).await.expect("Failed to create admin");
|
|
|
|
let user_document = create_test_document(user.id);
|
|
let user_document = ctx.state.db.create_document(user_document).await.expect("Failed to create document");
|
|
let admin_document = create_test_document(admin.id);
|
|
let admin_document = ctx.state.db.create_document(admin_document).await.expect("Failed to create document");
|
|
|
|
// Admin should be able to delete user's document
|
|
let result1 = ctx.state.db
|
|
.delete_document(user_document.id, admin.id, admin.role)
|
|
.await
|
|
.expect("Failed to delete user document as admin");
|
|
|
|
assert!(result1);
|
|
|
|
// Admin should be able to delete their own document
|
|
let result2 = ctx.state.db
|
|
.delete_document(admin_document.id, admin.id, admin.role)
|
|
.await
|
|
.expect("Failed to delete admin document as admin");
|
|
|
|
assert!(result2);
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bulk_delete_respects_ownership() {
|
|
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let db = &ctx.state.db;
|
|
|
|
// Create users using direct database approach
|
|
let user1_data = CreateUser {
|
|
username: format!("testuser1_{}", Uuid::new_v4()),
|
|
email: format!("test1_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let user1 = db.create_user(user1_data).await.expect("Failed to create user1");
|
|
|
|
let user2_data = CreateUser {
|
|
username: format!("testuser2_{}", Uuid::new_v4()),
|
|
email: format!("test2_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let user2 = db.create_user(user2_data).await.expect("Failed to create user2");
|
|
|
|
// Create documents for both users
|
|
let user1_doc1_doc = create_test_document(user1.id);
|
|
let user1_doc1 = ctx.state.db.create_document(user1_doc1_doc).await.expect("Failed to create document");
|
|
let user1_doc2_doc = create_test_document(user1.id);
|
|
let user1_doc2 = ctx.state.db.create_document(user1_doc2_doc).await.expect("Failed to create document");
|
|
let user2_doc1_doc = create_test_document(user2.id);
|
|
let user2_doc1 = ctx.state.db.create_document(user2_doc1_doc).await.expect("Failed to create document");
|
|
let user2_doc2_doc = create_test_document(user2.id);
|
|
let user2_doc2 = ctx.state.db.create_document(user2_doc2_doc).await.expect("Failed to create document");
|
|
|
|
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 = ctx.state.db
|
|
.bulk_delete_documents(&all_document_ids, user1.id, user1.role)
|
|
.await
|
|
.expect("Failed to bulk delete documents");
|
|
|
|
// Should only delete user1's documents
|
|
let (deleted_ids, failed_ids) = result;
|
|
assert_eq!(deleted_ids.len(), 2);
|
|
assert_eq!(failed_ids.len(), 2);
|
|
assert!(deleted_ids.contains(&user1_doc1.id));
|
|
assert!(deleted_ids.contains(&user1_doc2.id));
|
|
assert!(failed_ids.contains(&user2_doc1.id));
|
|
assert!(failed_ids.contains(&user2_doc2.id));
|
|
|
|
// Verify user2's documents still exist
|
|
let user2_doc1_exists = ctx.state.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 = ctx.state.db
|
|
.get_document_by_id(user2_doc2.id, user2.id, user2.role)
|
|
.await
|
|
.expect("Database query failed");
|
|
assert!(user2_doc2_exists.is_some());
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_admin_bulk_delete_all_documents() {
|
|
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let db = &ctx.state.db;
|
|
|
|
// Create users using direct database approach
|
|
let user1_data = CreateUser {
|
|
username: format!("testuser1_{}", Uuid::new_v4()),
|
|
email: format!("test1_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let user1 = db.create_user(user1_data).await.expect("Failed to create user1");
|
|
|
|
let user2_data = CreateUser {
|
|
username: format!("testuser2_{}", Uuid::new_v4()),
|
|
email: format!("test2_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let user2 = db.create_user(user2_data).await.expect("Failed to create user2");
|
|
|
|
let admin_data = CreateUser {
|
|
username: format!("testadmin_{}", Uuid::new_v4()),
|
|
email: format!("admin_{}@example.com", Uuid::new_v4()),
|
|
password: "adminpass123".to_string(),
|
|
role: Some(UserRole::Admin),
|
|
};
|
|
let admin = db.create_user(admin_data).await.expect("Failed to create admin");
|
|
|
|
// Create documents for all users
|
|
let user1_doc_doc = create_test_document(user1.id);
|
|
let user1_doc = ctx.state.db.create_document(user1_doc_doc).await.expect("Failed to create document");
|
|
let user2_doc_doc = create_test_document(user2.id);
|
|
let user2_doc = ctx.state.db.create_document(user2_doc_doc).await.expect("Failed to create document");
|
|
let admin_doc_doc = create_test_document(admin.id);
|
|
let admin_doc = ctx.state.db.create_document(admin_doc_doc).await.expect("Failed to create document");
|
|
|
|
let all_document_ids = vec![user1_doc.id, user2_doc.id, admin_doc.id];
|
|
|
|
// Admin should be able to delete all documents
|
|
let result = ctx.state.db
|
|
.bulk_delete_documents(&all_document_ids, admin.id, admin.role)
|
|
.await
|
|
.expect("Failed to bulk delete documents as admin");
|
|
|
|
// Should delete all documents
|
|
let (deleted_ids, failed_ids) = result;
|
|
assert_eq!(deleted_ids.len(), 3);
|
|
assert_eq!(failed_ids.len(), 0);
|
|
assert!(deleted_ids.contains(&user1_doc.id));
|
|
assert!(deleted_ids.contains(&user2_doc.id));
|
|
assert!(deleted_ids.contains(&admin_doc.id));
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_role_escalation_prevention() {
|
|
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let db = &ctx.state.db;
|
|
|
|
// Create users using direct database approach
|
|
let user_data = CreateUser {
|
|
username: format!("testuser_{}", Uuid::new_v4()),
|
|
email: format!("test_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let user = db.create_user(user_data).await.expect("Failed to create user");
|
|
|
|
let admin_data = CreateUser {
|
|
username: format!("testadmin_{}", Uuid::new_v4()),
|
|
email: format!("admin_{}@example.com", Uuid::new_v4()),
|
|
password: "adminpass123".to_string(),
|
|
role: Some(UserRole::Admin),
|
|
};
|
|
let admin = db.create_user(admin_data).await.expect("Failed to create admin");
|
|
|
|
let admin_document_doc = create_test_document(admin.id);
|
|
let admin_document = ctx.state.db.create_document(admin_document_doc).await.expect("Failed to create document");
|
|
|
|
// Regular user should NOT be able to delete admin's document
|
|
// even if they somehow know the document ID
|
|
let result = ctx.state.db
|
|
.delete_document(admin_document.id, user.id, user.role)
|
|
.await
|
|
.expect("Database query failed");
|
|
|
|
assert!(!result);
|
|
|
|
// Verify admin's document still exists
|
|
let found_doc = ctx.state.db
|
|
.get_document_by_id(admin_document.id, admin.id, admin.role)
|
|
.await
|
|
.expect("Database query failed");
|
|
assert!(found_doc.is_some());
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_cross_tenant_isolation() {
|
|
|
|
// Create users that could represent different tenants/organizations
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let db = &ctx.state.db;
|
|
|
|
// Create tenant users using direct database approach
|
|
let tenant1_user1_data = CreateUser {
|
|
username: format!("tenant1_user1_{}", Uuid::new_v4()),
|
|
email: format!("tenant1_user1_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let tenant1_user1 = db.create_user(tenant1_user1_data).await.expect("Failed to create tenant1_user1");
|
|
|
|
let tenant1_user2_data = CreateUser {
|
|
username: format!("tenant1_user2_{}", Uuid::new_v4()),
|
|
email: format!("tenant1_user2_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let tenant1_user2 = db.create_user(tenant1_user2_data).await.expect("Failed to create tenant1_user2");
|
|
|
|
let tenant2_user1_data = CreateUser {
|
|
username: format!("tenant2_user1_{}", Uuid::new_v4()),
|
|
email: format!("tenant2_user1_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let tenant2_user1 = db.create_user(tenant2_user1_data).await.expect("Failed to create tenant2_user1");
|
|
|
|
let tenant2_user2_data = CreateUser {
|
|
username: format!("tenant2_user2_{}", Uuid::new_v4()),
|
|
email: format!("tenant2_user2_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let tenant2_user2 = db.create_user(tenant2_user2_data).await.expect("Failed to create tenant2_user2");
|
|
|
|
// Create documents for each tenant
|
|
let tenant1_doc1_doc = create_test_document(tenant1_user1.id);
|
|
let tenant1_doc1 = ctx.state.db.create_document(tenant1_doc1_doc).await.expect("Failed to create document");
|
|
let tenant1_doc2_doc = create_test_document(tenant1_user2.id);
|
|
let tenant1_doc2 = ctx.state.db.create_document(tenant1_doc2_doc).await.expect("Failed to create document");
|
|
let tenant2_doc1_doc = create_test_document(tenant2_user1.id);
|
|
let tenant2_doc1 = ctx.state.db.create_document(tenant2_doc1_doc).await.expect("Failed to create document");
|
|
let tenant2_doc2_doc = create_test_document(tenant2_user2.id);
|
|
let tenant2_doc2 = ctx.state.db.create_document(tenant2_doc2_doc).await.expect("Failed to create document");
|
|
|
|
// Tenant1 user should not be able to delete tenant2 documents
|
|
let result1 = ctx.state.db
|
|
.delete_document(tenant2_doc1.id, tenant1_user1.id, tenant1_user1.role)
|
|
.await
|
|
.expect("Database query failed");
|
|
assert!(!result1);
|
|
|
|
let result2 = ctx.state.db
|
|
.delete_document(tenant2_doc2.id, tenant1_user2.id, tenant1_user2.role)
|
|
.await
|
|
.expect("Database query failed");
|
|
assert!(!result2);
|
|
|
|
// Tenant2 user should not be able to delete tenant1 documents
|
|
let result3 = ctx.state.db
|
|
.delete_document(tenant1_doc1.id, tenant2_user1.id, tenant2_user1.role)
|
|
.await
|
|
.expect("Database query failed");
|
|
assert!(!result3);
|
|
|
|
let result4 = ctx.state.db
|
|
.delete_document(tenant1_doc2.id, tenant2_user2.id, tenant2_user2.role)
|
|
.await
|
|
.expect("Database query failed");
|
|
assert!(!result4);
|
|
|
|
// 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 = ctx.state.db
|
|
.get_document_by_id(doc_id, owner_id, owner_role)
|
|
.await
|
|
.expect("Database query failed");
|
|
assert!(found_doc.is_some());
|
|
}
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_permission_consistency_single_vs_bulk() {
|
|
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let db = &ctx.state.db;
|
|
|
|
// Create users using direct database approach
|
|
let user1_data = CreateUser {
|
|
username: format!("testuser1_{}", Uuid::new_v4()),
|
|
email: format!("test1_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let user1 = db.create_user(user1_data).await.expect("Failed to create user1");
|
|
|
|
let user2_data = CreateUser {
|
|
username: format!("testuser2_{}", Uuid::new_v4()),
|
|
email: format!("test2_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let user2 = db.create_user(user2_data).await.expect("Failed to create user2");
|
|
|
|
let _user1_doc_doc = create_test_document(user1.id);
|
|
let _user1_doc = ctx.state.db.create_document(_user1_doc_doc).await.expect("Failed to create document");
|
|
let user2_doc_doc = create_test_document(user2.id);
|
|
let user2_doc = ctx.state.db.create_document(user2_doc_doc).await.expect("Failed to create document");
|
|
|
|
// Test single deletion permissions
|
|
let single_delete_result = ctx.state.db
|
|
.delete_document(user2_doc.id, user1.id, user1.role)
|
|
.await
|
|
.expect("Database query failed");
|
|
assert!(!single_delete_result); // Should fail
|
|
|
|
// Test bulk deletion permissions with same document
|
|
let user2_doc2_doc = create_test_document(user2.id);
|
|
let user2_doc2 = ctx.state.db.create_document(user2_doc2_doc).await.expect("Failed to create document");
|
|
let bulk_delete_result = ctx.state.db
|
|
.bulk_delete_documents(&vec![user2_doc2.id], user1.id, user1.role)
|
|
.await
|
|
.expect("Database query failed");
|
|
let (deleted_ids, failed_ids) = bulk_delete_result;
|
|
assert_eq!(deleted_ids.len(), 0); // Should delete nothing
|
|
assert_eq!(failed_ids.len(), 1);
|
|
|
|
// Verify both documents still exist
|
|
let doc1_exists = ctx.state.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 = ctx.state.db
|
|
.get_document_by_id(user2_doc2.id, user2.id, user2.role)
|
|
.await
|
|
.expect("Database query failed");
|
|
assert!(doc2_exists.is_some());
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_admin_permission_inheritance() {
|
|
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let db = &ctx.state.db;
|
|
|
|
// Create users using direct database approach
|
|
let user_data = CreateUser {
|
|
username: format!("testuser_{}", Uuid::new_v4()),
|
|
email: format!("test_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(UserRole::User),
|
|
};
|
|
let user = db.create_user(user_data).await.expect("Failed to create user");
|
|
|
|
let admin_data = CreateUser {
|
|
username: format!("testadmin_{}", Uuid::new_v4()),
|
|
email: format!("admin_{}@example.com", Uuid::new_v4()),
|
|
password: "adminpass123".to_string(),
|
|
role: Some(UserRole::Admin),
|
|
};
|
|
let admin = db.create_user(admin_data).await.expect("Failed to create admin");
|
|
|
|
let user_doc_doc = create_test_document(user.id);
|
|
let user_doc = ctx.state.db.create_document(user_doc_doc).await.expect("Failed to create document");
|
|
|
|
// 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 = ctx.state.db
|
|
.delete_document(user_doc.id, admin.id, admin.role)
|
|
.await
|
|
.expect("Failed to delete as admin");
|
|
assert!(admin_delete_result);
|
|
|
|
// Create another document to test admin's own document deletion
|
|
let admin_doc_doc = create_test_document(admin.id);
|
|
let admin_doc = ctx.state.db.create_document(admin_doc_doc).await.expect("Failed to create document");
|
|
let admin_own_delete_result = ctx.state.db
|
|
.delete_document(admin_doc.id, admin.id, admin.role)
|
|
.await
|
|
.expect("Failed to delete admin's own document");
|
|
assert!(admin_own_delete_result);
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[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 readur::test_utils::{TestContext, TestAuthHelper};
|
|
use uuid::Uuid;
|
|
|
|
#[tokio::test]
|
|
async fn test_delete_with_invalid_uuid() {
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let db = &ctx.state.db;
|
|
|
|
// Create user using direct database approach
|
|
let user_data = readur::models::CreateUser {
|
|
username: format!("testuser_{}", Uuid::new_v4()),
|
|
email: format!("test_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(readur::models::UserRole::User),
|
|
};
|
|
let user = db.create_user(user_data).await.expect("Failed to create user");
|
|
|
|
// Use malformed UUID (this test assumes the function handles UUID parsing)
|
|
let invalid_uuid = Uuid::nil(); // Use nil UUID as "invalid"
|
|
|
|
let result = ctx.state.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);
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_delete_with_sql_injection_attempt() {
|
|
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let db = &ctx.state.db;
|
|
|
|
// Create user using direct database approach
|
|
let user_data = readur::models::CreateUser {
|
|
username: format!("testuser_{}", Uuid::new_v4()),
|
|
email: format!("test_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(readur::models::UserRole::User),
|
|
};
|
|
let user = db.create_user(user_data).await.expect("Failed to create user");
|
|
|
|
let document_doc = create_test_document(user.id);
|
|
let document = ctx.state.db.create_document(document_doc).await.expect("Failed to create document");
|
|
|
|
// Test with legitimate document ID - SQLx should prevent injection
|
|
let result = ctx.state.db
|
|
.delete_document(document.id, user.id, user.role)
|
|
.await
|
|
.expect("Query should execute safely");
|
|
|
|
assert!(result);
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bulk_delete_with_duplicate_ids() {
|
|
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let db = &ctx.state.db;
|
|
|
|
// Create user using direct database approach
|
|
let user_data = readur::models::CreateUser {
|
|
username: format!("testuser_{}", Uuid::new_v4()),
|
|
email: format!("test_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(readur::models::UserRole::User),
|
|
};
|
|
let user = db.create_user(user_data).await.expect("Failed to create user");
|
|
|
|
let document_doc = create_test_document(user.id);
|
|
let document = ctx.state.db.create_document(document_doc).await.expect("Failed to create document");
|
|
|
|
// Include the same document ID multiple times
|
|
let duplicate_ids = vec![document.id, document.id, document.id];
|
|
|
|
let result = ctx.state.db
|
|
.bulk_delete_documents(&duplicate_ids, user.id, user.role)
|
|
.await
|
|
.expect("Bulk delete should handle duplicates");
|
|
|
|
// Should only delete the document once, but subsequent attempts fail
|
|
let (deleted_ids, failed_ids) = result;
|
|
assert_eq!(deleted_ids.len(), 1);
|
|
assert_eq!(failed_ids.len(), 2); // Two failed attempts on already-deleted document
|
|
assert!(deleted_ids.contains(&document.id));
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bulk_delete_with_extremely_large_request() {
|
|
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let auth_helper = TestAuthHelper::new(ctx.app.clone());
|
|
let user = auth_helper.create_test_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_doc = create_test_document(user.user_response.id);
|
|
let real_document = ctx.state.db.create_document(real_document_doc).await.expect("Failed to create document");
|
|
large_id_list.push(real_document.id);
|
|
|
|
// Add many fake UUIDs
|
|
for _ in 0..499 {
|
|
large_id_list.push(Uuid::new_v4());
|
|
}
|
|
|
|
let result = ctx.state.db
|
|
.bulk_delete_documents(&large_id_list, user.user_response.id, user.user_response.role)
|
|
.await
|
|
.expect("Should handle large requests");
|
|
|
|
// Should only delete the one real document
|
|
let (deleted_ids, failed_ids) = result;
|
|
assert_eq!(deleted_ids.len(), 1);
|
|
assert_eq!(failed_ids.len(), 499);
|
|
assert!(deleted_ids.contains(&real_document.id));
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_concurrent_deletion_same_document() {
|
|
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let auth_helper = TestAuthHelper::new(ctx.app.clone());
|
|
let user = auth_helper.create_test_user().await;
|
|
let document_doc = create_test_document(user.user_response.id);
|
|
let document = ctx.state.db.create_document(document_doc).await.expect("Failed to create document");
|
|
|
|
// Create multiple handles to the same database connection pool
|
|
let db1 = ctx.state.db.clone();
|
|
let db2 = ctx.state.db.clone();
|
|
|
|
// Attempt concurrent deletions
|
|
let doc_id = document.id;
|
|
let user_id = user.user_response.id;
|
|
let user_role = user.user_response.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 false
|
|
let success_count = [result1, result2]
|
|
.iter()
|
|
.filter(|&&x| x)
|
|
.count();
|
|
|
|
assert_eq!(success_count, 1, "Exactly one deletion should succeed");
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_delete_document_with_foreign_key_constraints() {
|
|
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let auth_helper = TestAuthHelper::new(ctx.app.clone());
|
|
let user = auth_helper.create_test_user().await;
|
|
let document_doc = create_test_document(user.user_response.id);
|
|
let document = ctx.state.db.create_document(document_doc).await.expect("Failed to create document");
|
|
|
|
// If there are foreign key relationships (like document_labels),
|
|
// test that CASCADE deletion works properly
|
|
|
|
// Delete the document
|
|
let result = ctx.state.db
|
|
.delete_document(document.id, user.user_response.id, user.user_response.role)
|
|
.await
|
|
.expect("Deletion should handle foreign key constraints");
|
|
|
|
assert!(result);
|
|
|
|
// Verify related records are also deleted (if any exist)
|
|
// This would depend on the actual schema relationships
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bulk_delete_with_mixed_permissions_and_errors() {
|
|
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
|
|
// Create users using direct database approach
|
|
let user1_data = readur::models::CreateUser {
|
|
username: format!("testuser1_{}", Uuid::new_v4()),
|
|
email: format!("test1_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(readur::models::UserRole::User),
|
|
};
|
|
let user1 = ctx.state.db.create_user(user1_data).await.expect("Failed to create user1");
|
|
|
|
let user2_data = readur::models::CreateUser {
|
|
username: format!("testuser2_{}", Uuid::new_v4()),
|
|
email: format!("test2_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(readur::models::UserRole::User),
|
|
};
|
|
let user2 = ctx.state.db.create_user(user2_data).await.expect("Failed to create user2");
|
|
|
|
// Create mix of documents
|
|
let user1_doc_doc = create_test_document(user1.id);
|
|
let user1_doc = ctx.state.db.create_document(user1_doc_doc).await.expect("Failed to create document");
|
|
let user2_doc_doc = create_test_document(user2.id);
|
|
let user2_doc = ctx.state.db.create_document(user2_doc_doc).await.expect("Failed to create document");
|
|
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 = ctx.state.db
|
|
.bulk_delete_documents(&mixed_ids, user1.id, user1.role)
|
|
.await
|
|
.expect("Should handle mixed permissions gracefully");
|
|
|
|
// Should only delete user1's document
|
|
let (deleted_ids, failed_ids) = result;
|
|
assert_eq!(deleted_ids.len(), 1);
|
|
assert_eq!(failed_ids.len(), 2);
|
|
assert!(deleted_ids.contains(&user1_doc.id));
|
|
|
|
// Verify user2's document still exists
|
|
let user2_doc_exists = ctx.state.db
|
|
.get_document_by_id(user2_doc.id, user2.id, user2.role)
|
|
.await
|
|
.expect("Query should succeed");
|
|
assert!(user2_doc_exists.is_some());
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[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]
|
|
async fn test_delete_after_user_deletion() {
|
|
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let auth_helper = TestAuthHelper::new(ctx.app.clone());
|
|
let user = auth_helper.create_test_user().await;
|
|
let document_doc = create_test_document(user.user_response.id);
|
|
let document = ctx.state.db.create_document(document_doc).await.expect("Failed to create document");
|
|
|
|
// Delete the user first (simulating cascade deletion scenarios)
|
|
sqlx::query("DELETE FROM users WHERE id = $1")
|
|
.bind(user.user_response.id)
|
|
.execute(&ctx.state.db.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 = ctx.state.db
|
|
.delete_document(document.id, user.user_response.id, user.user_response.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(true) => {
|
|
// Document was deleted successfully
|
|
},
|
|
Ok(false) => {
|
|
// Document not found (possibly already cascade deleted)
|
|
},
|
|
Err(_) => {
|
|
// Error occurred (foreign key constraint issue)
|
|
// This might be expected behavior
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bulk_delete_empty_and_null_scenarios() {
|
|
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let auth_helper = TestAuthHelper::new(ctx.app.clone());
|
|
let user = auth_helper.create_test_user().await;
|
|
|
|
// Test empty list
|
|
let empty_result = ctx.state.db
|
|
.bulk_delete_documents(&vec![], user.user_response.id, user.user_response.role)
|
|
.await
|
|
.expect("Empty list should be handled gracefully");
|
|
let (deleted_ids, failed_ids) = empty_result;
|
|
assert_eq!(deleted_ids.len(), 0);
|
|
assert_eq!(failed_ids.len(), 0);
|
|
|
|
// Test with only nil UUIDs
|
|
let nil_uuids = vec![Uuid::nil(), Uuid::nil()];
|
|
let nil_result = ctx.state.db
|
|
.bulk_delete_documents(&nil_uuids, user.user_response.id, user.user_response.role)
|
|
.await
|
|
.expect("Nil UUIDs should be handled gracefully");
|
|
let (deleted_ids, failed_ids) = nil_result;
|
|
assert_eq!(deleted_ids.len(), 0);
|
|
assert_eq!(failed_ids.len(), 2);
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
|
|
#[tokio::test]
|
|
async fn test_transaction_rollback_simulation() {
|
|
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let auth_helper = TestAuthHelper::new(ctx.app.clone());
|
|
let user = auth_helper.create_test_user().await;
|
|
let document_doc = create_test_document(user.user_response.id);
|
|
let document = ctx.state.db.create_document(document_doc).await.expect("Failed to create document");
|
|
|
|
// Verify document exists before deletion
|
|
let exists_before = ctx.state.db
|
|
.get_document_by_id(document.id, user.user_response.id, user.user_response.role)
|
|
.await
|
|
.expect("Query should succeed");
|
|
assert!(exists_before.is_some());
|
|
|
|
// Perform deletion
|
|
let deletion_result = ctx.state.db
|
|
.delete_document(document.id, user.user_response.id, user.user_response.role)
|
|
.await
|
|
.expect("Deletion should succeed");
|
|
assert!(deletion_result);
|
|
|
|
// Verify document no longer exists
|
|
let exists_after = ctx.state.db
|
|
.get_document_by_id(document.id, user.user_response.id, user.user_response.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
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
mod low_confidence_deletion_db_tests {
|
|
use super::*;
|
|
use readur::models::UserRole;
|
|
|
|
#[cfg(test)]
|
|
fn create_test_document_with_confidence(user_id: Uuid, confidence: f32) -> Document {
|
|
Document {
|
|
id: Uuid::new_v4(),
|
|
filename: format!("test_conf_{}.pdf", confidence),
|
|
original_filename: format!("test_conf_{}.pdf", confidence),
|
|
file_path: format!("/uploads/test_conf_{}.pdf", confidence),
|
|
file_size: 1024,
|
|
mime_type: "application/pdf".to_string(),
|
|
content: Some("Test document content".to_string()),
|
|
ocr_text: Some("Test OCR text".to_string()),
|
|
ocr_confidence: Some(confidence),
|
|
ocr_word_count: Some(50),
|
|
ocr_processing_time_ms: Some(1000),
|
|
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("test_hash_123456789abcdef123456789abcdef123456789abcdef123456789abcdef".to_string()),
|
|
original_created_at: None,
|
|
original_modified_at: None,
|
|
source_path: None,
|
|
source_type: None,
|
|
source_id: None,
|
|
file_permissions: None,
|
|
file_owner: None,
|
|
file_group: None,
|
|
source_metadata: None,
|
|
ocr_retry_count: None,
|
|
ocr_failure_reason: None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_confidence_filtering_logic() {
|
|
let user_id = Uuid::new_v4();
|
|
|
|
let documents = vec![
|
|
create_test_document_with_confidence(user_id, 95.0), // Should not be deleted
|
|
create_test_document_with_confidence(user_id, 75.0), // Should not be deleted
|
|
create_test_document_with_confidence(user_id, 45.0), // Should not be deleted
|
|
create_test_document_with_confidence(user_id, 25.0), // Should be deleted (< 30)
|
|
create_test_document_with_confidence(user_id, 15.0), // Should be deleted (< 30)
|
|
create_test_document_with_confidence(user_id, 5.0), // Should be deleted (< 30)
|
|
];
|
|
|
|
let threshold = 30.0;
|
|
let low_confidence_docs: Vec<_> = documents.iter()
|
|
.filter(|doc| {
|
|
doc.ocr_confidence.is_some() &&
|
|
doc.ocr_confidence.unwrap() < threshold
|
|
})
|
|
.collect();
|
|
|
|
assert_eq!(low_confidence_docs.len(), 3);
|
|
assert_eq!(low_confidence_docs[0].ocr_confidence.unwrap(), 25.0);
|
|
assert_eq!(low_confidence_docs[1].ocr_confidence.unwrap(), 15.0);
|
|
assert_eq!(low_confidence_docs[2].ocr_confidence.unwrap(), 5.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_documents_without_ocr_confidence_excluded() {
|
|
let user_id = Uuid::new_v4();
|
|
|
|
let mut doc_no_confidence = create_test_document_with_confidence(user_id, 20.0);
|
|
doc_no_confidence.ocr_confidence = None;
|
|
|
|
let doc_with_confidence = create_test_document_with_confidence(user_id, 20.0);
|
|
|
|
let documents = vec![doc_no_confidence, doc_with_confidence];
|
|
let threshold = 30.0;
|
|
|
|
let low_confidence_docs: Vec<_> = documents.iter()
|
|
.filter(|doc| {
|
|
doc.ocr_confidence.is_some() &&
|
|
doc.ocr_confidence.unwrap() < threshold
|
|
})
|
|
.collect();
|
|
|
|
// Only the document with confidence should be included
|
|
assert_eq!(low_confidence_docs.len(), 1);
|
|
assert!(low_confidence_docs[0].ocr_confidence.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_user_role_authorization_in_filtering() {
|
|
let user1_id = Uuid::new_v4();
|
|
let user2_id = Uuid::new_v4();
|
|
|
|
let user1_doc = create_test_document_with_confidence(user1_id, 20.0);
|
|
let user2_doc = create_test_document_with_confidence(user2_id, 15.0);
|
|
|
|
// Regular user should only see their own documents
|
|
let user_role = UserRole::User;
|
|
let admin_role = UserRole::Admin;
|
|
|
|
// User1 should only access their own document
|
|
let user1_can_access_own = user1_doc.user_id == user1_id || user_role == UserRole::Admin;
|
|
let user1_can_access_other = user2_doc.user_id == user1_id || user_role == UserRole::Admin;
|
|
|
|
assert!(user1_can_access_own);
|
|
assert!(!user1_can_access_other);
|
|
|
|
// Admin should access all documents
|
|
let admin_can_access_user1 = user1_doc.user_id == user1_id || admin_role == UserRole::Admin;
|
|
let admin_can_access_user2 = user2_doc.user_id == user1_id || admin_role == UserRole::Admin;
|
|
|
|
assert!(admin_can_access_user1);
|
|
assert!(admin_can_access_user2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_boundary_conditions_for_confidence_thresholds() {
|
|
let user_id = Uuid::new_v4();
|
|
|
|
let test_cases = vec![
|
|
(0.0, 10.0, true), // 0% < 10% threshold
|
|
(10.0, 10.0, false), // 10% = 10% threshold (not less than)
|
|
(10.1, 10.0, false), // 10.1% > 10% threshold
|
|
(29.9, 30.0, true), // 29.9% < 30% threshold
|
|
(30.0, 30.0, false), // 30% = 30% threshold (not less than)
|
|
(30.1, 30.0, false), // 30.1% > 30% threshold
|
|
(99.9, 100.0, true), // 99.9% < 100% threshold
|
|
(100.0, 100.0, false), // 100% = 100% threshold (not less than)
|
|
];
|
|
|
|
for (doc_confidence, threshold, should_be_included) in test_cases {
|
|
let doc = create_test_document_with_confidence(user_id, doc_confidence);
|
|
let is_included = doc.ocr_confidence.is_some() &&
|
|
doc.ocr_confidence.unwrap() < threshold;
|
|
|
|
assert_eq!(is_included, should_be_included,
|
|
"Document with {}% confidence vs {}% threshold",
|
|
doc_confidence, threshold);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_performance_considerations_for_large_datasets() {
|
|
let user_id = Uuid::new_v4();
|
|
|
|
// Create a large number of test documents
|
|
let mut documents = Vec::new();
|
|
for i in 0..1000 {
|
|
let confidence = (i as f32) / 10.0; // 0.0 to 99.9
|
|
documents.push(create_test_document_with_confidence(user_id, confidence));
|
|
}
|
|
|
|
let threshold = 50.0;
|
|
let start_time = std::time::Instant::now();
|
|
|
|
let low_confidence_docs: Vec<_> = documents.iter()
|
|
.filter(|doc| {
|
|
doc.ocr_confidence.is_some() &&
|
|
doc.ocr_confidence.unwrap() < threshold
|
|
})
|
|
.collect();
|
|
|
|
let elapsed = start_time.elapsed();
|
|
|
|
// Verify the filtering works correctly for large datasets
|
|
assert_eq!(low_confidence_docs.len(), 500); // 0.0 to 49.9
|
|
|
|
// Performance should be reasonable (under 10ms for 1000 documents in memory)
|
|
assert!(elapsed.as_millis() < 10,
|
|
"Filtering 1000 documents took too long: {:?}", elapsed);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sql_query_structure_expectations() {
|
|
// Test that our expected SQL query structure would work
|
|
let user_id = Uuid::new_v4();
|
|
let confidence_threshold = 30.0;
|
|
|
|
// This tests the logical structure we expect in the actual SQL query
|
|
let expected_where_conditions = vec![
|
|
"ocr_confidence IS NOT NULL",
|
|
"ocr_confidence < $1", // $1 = confidence_threshold
|
|
"user_id = $2", // $2 = user_id (for non-admin users)
|
|
];
|
|
|
|
// Verify our test documents would match the expected query logic
|
|
let test_doc = create_test_document_with_confidence(user_id, 25.0);
|
|
|
|
// Simulate the SQL conditions
|
|
let confidence_not_null = test_doc.ocr_confidence.is_some();
|
|
let confidence_below_threshold = test_doc.ocr_confidence.unwrap() < confidence_threshold;
|
|
let user_matches = test_doc.user_id == user_id;
|
|
|
|
assert!(confidence_not_null);
|
|
assert!(confidence_below_threshold);
|
|
assert!(user_matches);
|
|
|
|
// This document should be included in results
|
|
let would_be_selected = confidence_not_null && confidence_below_threshold && user_matches;
|
|
assert!(would_be_selected);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deletion_ordering_expectations() {
|
|
let user_id = Uuid::new_v4();
|
|
|
|
let mut documents = vec![
|
|
create_test_document_with_confidence(user_id, 25.0),
|
|
create_test_document_with_confidence(user_id, 5.0),
|
|
create_test_document_with_confidence(user_id, 15.0),
|
|
create_test_document_with_confidence(user_id, 35.0), // Above threshold
|
|
];
|
|
|
|
let threshold = 30.0;
|
|
let mut low_confidence_docs: Vec<_> = documents.iter()
|
|
.filter(|doc| {
|
|
doc.ocr_confidence.is_some() &&
|
|
doc.ocr_confidence.unwrap() < threshold
|
|
})
|
|
.collect();
|
|
|
|
// Sort by confidence ascending (lowest first) then by creation date descending (newest first)
|
|
low_confidence_docs.sort_by(|a, b| {
|
|
let conf_a = a.ocr_confidence.unwrap();
|
|
let conf_b = b.ocr_confidence.unwrap();
|
|
conf_a.partial_cmp(&conf_b).unwrap()
|
|
.then_with(|| b.created_at.cmp(&a.created_at))
|
|
});
|
|
|
|
assert_eq!(low_confidence_docs.len(), 3);
|
|
assert_eq!(low_confidence_docs[0].ocr_confidence.unwrap(), 5.0); // Lowest confidence first
|
|
assert_eq!(low_confidence_docs[1].ocr_confidence.unwrap(), 15.0);
|
|
assert_eq!(low_confidence_docs[2].ocr_confidence.unwrap(), 25.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_error_handling_scenarios() {
|
|
let user_id = Uuid::new_v4();
|
|
|
|
// Test invalid threshold values (these would be caught by the API handler)
|
|
let invalid_thresholds = vec![-1.0, 101.0, f32::NAN, f32::INFINITY];
|
|
|
|
for threshold in invalid_thresholds {
|
|
// The database query itself should handle these gracefully
|
|
// Invalid thresholds should either match no documents or be rejected
|
|
let test_doc = create_test_document_with_confidence(user_id, 50.0);
|
|
|
|
if threshold.is_finite() {
|
|
let would_match = test_doc.ocr_confidence.is_some() &&
|
|
test_doc.ocr_confidence.unwrap() < threshold;
|
|
|
|
if threshold < 0.0 {
|
|
assert!(!would_match, "Negative threshold should match no documents");
|
|
}
|
|
if threshold > 100.0 {
|
|
// Documents with confidence > 100 shouldn't exist, but if they did,
|
|
// they should still be considered for deletion if threshold > 100
|
|
assert!(would_match, "Threshold > 100 should match normal documents");
|
|
}
|
|
} else {
|
|
// NaN and infinity comparisons
|
|
let would_match = test_doc.ocr_confidence.is_some() &&
|
|
test_doc.ocr_confidence.unwrap() < threshold;
|
|
|
|
if threshold.is_nan() {
|
|
// NaN comparisons should always be false
|
|
assert!(!would_match, "NaN threshold should match no documents");
|
|
} else if threshold == f32::INFINITY {
|
|
// Positive infinity should match all finite numbers
|
|
assert!(would_match, "Positive infinity threshold should match finite documents");
|
|
} else {
|
|
// Other invalid values like negative infinity
|
|
assert!(!would_match, "Invalid threshold should match no documents");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_find_failed_ocr_documents() {
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let auth_helper = TestAuthHelper::new(ctx.app.clone());
|
|
let database = &ctx.state.db;
|
|
|
|
// Create actual users in the database
|
|
let user = auth_helper.create_test_user().await;
|
|
let admin_user = auth_helper.create_test_admin().await;
|
|
let user_id = user.user_response.id;
|
|
let admin_user_id = admin_user.user_response.id;
|
|
|
|
// Create test documents with different OCR statuses
|
|
let mut success_doc = create_test_document(user_id);
|
|
success_doc.ocr_status = Some("completed".to_string());
|
|
success_doc.ocr_confidence = Some(85.0);
|
|
success_doc.ocr_text = Some("Successfully extracted text".to_string());
|
|
|
|
let mut failed_doc = create_test_document(user_id);
|
|
failed_doc.ocr_status = Some("failed".to_string());
|
|
failed_doc.ocr_confidence = None;
|
|
failed_doc.ocr_text = None;
|
|
failed_doc.ocr_error = Some("OCR processing failed due to corrupted image".to_string());
|
|
|
|
let mut null_confidence_doc = create_test_document(user_id);
|
|
null_confidence_doc.ocr_status = Some("completed".to_string());
|
|
null_confidence_doc.ocr_confidence = None; // NULL confidence but not failed
|
|
null_confidence_doc.ocr_text = Some("Text extracted but no confidence".to_string());
|
|
|
|
let mut pending_doc = create_test_document(user_id);
|
|
pending_doc.ocr_status = Some("pending".to_string());
|
|
pending_doc.ocr_confidence = None;
|
|
pending_doc.ocr_text = None;
|
|
|
|
let mut processing_doc = create_test_document(user_id);
|
|
processing_doc.ocr_status = Some("processing".to_string());
|
|
processing_doc.ocr_confidence = None;
|
|
processing_doc.ocr_text = None;
|
|
|
|
// Different user's failed document
|
|
let mut other_user_failed_doc = create_test_document(admin_user_id);
|
|
other_user_failed_doc.ocr_status = Some("failed".to_string());
|
|
other_user_failed_doc.ocr_confidence = None;
|
|
|
|
// Insert all documents
|
|
let success_id = ctx.state.db.create_document(success_doc).await.unwrap().id;
|
|
let failed_id = ctx.state.db.create_document(failed_doc).await.unwrap().id;
|
|
let null_confidence_id = ctx.state.db.create_document(null_confidence_doc).await.unwrap().id;
|
|
let pending_id = ctx.state.db.create_document(pending_doc).await.unwrap().id;
|
|
let processing_id = ctx.state.db.create_document(processing_doc).await.unwrap().id;
|
|
let other_user_failed_id = ctx.state.db.create_document(other_user_failed_doc).await.unwrap().id;
|
|
|
|
// Test as regular user
|
|
let failed_docs = database
|
|
.find_failed_ocr_documents(user_id, readur::models::UserRole::User, 100, 0)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Should find: only failed_doc (null_confidence_doc has status 'completed')
|
|
assert_eq!(failed_docs.len(), 1);
|
|
let failed_ids: Vec<Uuid> = failed_docs.iter().map(|d| d.id).collect();
|
|
assert!(failed_ids.contains(&failed_id));
|
|
assert!(!failed_ids.contains(&null_confidence_id)); // This has status 'completed'
|
|
assert!(!failed_ids.contains(&success_id));
|
|
assert!(!failed_ids.contains(&pending_id));
|
|
assert!(!failed_ids.contains(&processing_id));
|
|
assert!(!failed_ids.contains(&other_user_failed_id)); // Different user
|
|
|
|
// Test as admin
|
|
let admin_failed_docs = database
|
|
.find_failed_ocr_documents(admin_user_id, readur::models::UserRole::Admin, 100, 0)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Should find all failed documents (from all users)
|
|
assert!(admin_failed_docs.len() >= 2); // At least our 2 failed docs
|
|
let admin_failed_ids: Vec<Uuid> = admin_failed_docs.iter().map(|d| d.id).collect();
|
|
assert!(admin_failed_ids.contains(&failed_id));
|
|
assert!(!admin_failed_ids.contains(&null_confidence_id)); // This has status 'completed'
|
|
assert!(admin_failed_ids.contains(&other_user_failed_id));
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_find_low_confidence_and_failed_documents() {
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let auth_helper = TestAuthHelper::new(ctx.app.clone());
|
|
let database = &ctx.state.db;
|
|
|
|
// Create actual user in the database
|
|
let user = auth_helper.create_test_user().await;
|
|
let user_id = user.user_response.id;
|
|
|
|
// Create test documents with different confidence levels
|
|
let mut high_confidence_doc = create_test_document(user_id);
|
|
high_confidence_doc.ocr_confidence = Some(95.0);
|
|
high_confidence_doc.ocr_status = Some("completed".to_string());
|
|
|
|
let mut medium_confidence_doc = create_test_document(user_id);
|
|
medium_confidence_doc.ocr_confidence = Some(65.0);
|
|
medium_confidence_doc.ocr_status = Some("completed".to_string());
|
|
|
|
let mut low_confidence_doc = create_test_document(user_id);
|
|
low_confidence_doc.ocr_confidence = Some(25.0);
|
|
low_confidence_doc.ocr_status = Some("completed".to_string());
|
|
|
|
let mut failed_doc = create_test_document(user_id);
|
|
failed_doc.ocr_status = Some("failed".to_string());
|
|
failed_doc.ocr_confidence = None;
|
|
failed_doc.ocr_error = Some("Processing failed".to_string());
|
|
|
|
let mut null_confidence_doc = create_test_document(user_id);
|
|
null_confidence_doc.ocr_status = Some("completed".to_string());
|
|
null_confidence_doc.ocr_confidence = None;
|
|
|
|
let mut pending_doc = create_test_document(user_id);
|
|
pending_doc.ocr_status = Some("pending".to_string());
|
|
pending_doc.ocr_confidence = None;
|
|
|
|
// Insert all documents
|
|
let high_id = ctx.state.db.create_document(high_confidence_doc).await.unwrap().id;
|
|
let medium_id = ctx.state.db.create_document(medium_confidence_doc).await.unwrap().id;
|
|
let low_id = ctx.state.db.create_document(low_confidence_doc).await.unwrap().id;
|
|
let failed_id = ctx.state.db.create_document(failed_doc).await.unwrap().id;
|
|
let null_confidence_id = ctx.state.db.create_document(null_confidence_doc).await.unwrap().id;
|
|
let pending_id = ctx.state.db.create_document(pending_doc).await.unwrap().id;
|
|
|
|
// Test with threshold of 50% - should include low confidence and failed only
|
|
let threshold_50_docs = database
|
|
.find_low_confidence_and_failed_documents(user_id, readur::models::UserRole::User, 50.0, 100, 0)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(threshold_50_docs.len(), 2);
|
|
let threshold_50_ids: Vec<Uuid> = threshold_50_docs.iter().map(|d| d.id).collect();
|
|
assert!(threshold_50_ids.contains(&low_id)); // 25% confidence
|
|
assert!(threshold_50_ids.contains(&failed_id)); // failed status
|
|
assert!(!threshold_50_ids.contains(&null_confidence_id)); // NULL confidence excluded
|
|
assert!(!threshold_50_ids.contains(&high_id)); // 95% confidence
|
|
assert!(!threshold_50_ids.contains(&medium_id)); // 65% confidence
|
|
assert!(!threshold_50_ids.contains(&pending_id)); // pending status
|
|
|
|
// Test with threshold of 70% - should include low and medium confidence and failed only
|
|
let threshold_70_docs = database
|
|
.find_low_confidence_and_failed_documents(user_id, readur::models::UserRole::User, 70.0, 100, 0)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(threshold_70_docs.len(), 3);
|
|
let threshold_70_ids: Vec<Uuid> = threshold_70_docs.iter().map(|d| d.id).collect();
|
|
assert!(threshold_70_ids.contains(&low_id)); // 25% confidence
|
|
assert!(threshold_70_ids.contains(&medium_id)); // 65% confidence
|
|
assert!(threshold_70_ids.contains(&failed_id)); // failed status
|
|
assert!(!threshold_70_ids.contains(&null_confidence_id)); // NULL confidence excluded
|
|
assert!(!threshold_70_ids.contains(&high_id)); // 95% confidence
|
|
assert!(!threshold_70_ids.contains(&pending_id)); // pending status
|
|
|
|
// Test with threshold of 100% - should include all confidence levels and failed only
|
|
let threshold_100_docs = database
|
|
.find_low_confidence_and_failed_documents(user_id, readur::models::UserRole::User, 100.0, 100, 0)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(threshold_100_docs.len(), 4);
|
|
let threshold_100_ids: Vec<Uuid> = threshold_100_docs.iter().map(|d| d.id).collect();
|
|
assert!(threshold_100_ids.contains(&high_id)); // 95% confidence
|
|
assert!(threshold_100_ids.contains(&medium_id)); // 65% confidence
|
|
assert!(threshold_100_ids.contains(&low_id)); // 25% confidence
|
|
assert!(threshold_100_ids.contains(&failed_id)); // failed status
|
|
assert!(!threshold_100_ids.contains(&null_confidence_id)); // NULL confidence excluded
|
|
assert!(!threshold_100_ids.contains(&pending_id)); // pending status
|
|
|
|
// Test with threshold of 0% - should only include failed documents
|
|
let threshold_0_docs = database
|
|
.find_low_confidence_and_failed_documents(user_id, readur::models::UserRole::User, 0.0, 100, 0)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(threshold_0_docs.len(), 1);
|
|
let threshold_0_ids: Vec<Uuid> = threshold_0_docs.iter().map(|d| d.id).collect();
|
|
assert!(threshold_0_ids.contains(&failed_id)); // failed status
|
|
assert!(!threshold_0_ids.contains(&null_confidence_id)); // NULL confidence excluded
|
|
assert!(!threshold_0_ids.contains(&high_id)); // 95% confidence
|
|
assert!(!threshold_0_ids.contains(&medium_id)); // 65% confidence
|
|
assert!(!threshold_0_ids.contains(&low_id)); // 25% confidence
|
|
assert!(!threshold_0_ids.contains(&pending_id)); // pending status
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_find_documents_by_confidence_threshold_original_behavior() {
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let auth_helper = TestAuthHelper::new(ctx.app.clone());
|
|
let database = &ctx.state.db;
|
|
|
|
// Create actual user in the database
|
|
let user = auth_helper.create_test_user().await;
|
|
let user_id = user.user_response.id;
|
|
|
|
// Create test documents to verify original behavior is preserved
|
|
let mut high_confidence_doc = create_test_document(user_id);
|
|
high_confidence_doc.ocr_confidence = Some(90.0);
|
|
high_confidence_doc.ocr_status = Some("completed".to_string());
|
|
|
|
let mut low_confidence_doc = create_test_document(user_id);
|
|
low_confidence_doc.ocr_confidence = Some(40.0);
|
|
low_confidence_doc.ocr_status = Some("completed".to_string());
|
|
|
|
let mut null_confidence_doc = create_test_document(user_id);
|
|
null_confidence_doc.ocr_confidence = None;
|
|
null_confidence_doc.ocr_status = Some("completed".to_string());
|
|
|
|
let mut failed_doc = create_test_document(user_id);
|
|
failed_doc.ocr_confidence = None;
|
|
failed_doc.ocr_status = Some("failed".to_string());
|
|
|
|
// Insert documents
|
|
let high_id = ctx.state.db.create_document(high_confidence_doc).await.unwrap().id;
|
|
let low_id = ctx.state.db.create_document(low_confidence_doc).await.unwrap().id;
|
|
let null_confidence_id = ctx.state.db.create_document(null_confidence_doc).await.unwrap().id;
|
|
let failed_id = ctx.state.db.create_document(failed_doc).await.unwrap().id;
|
|
|
|
// Test original method - should only find documents with explicit confidence below threshold
|
|
let original_results = database
|
|
.find_documents_by_confidence_threshold(user_id, readur::models::UserRole::User, 50.0, 100, 0)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Should only include low_confidence_doc (40%), not NULL confidence or failed docs
|
|
assert_eq!(original_results.len(), 1);
|
|
assert_eq!(original_results[0].id, low_id);
|
|
|
|
let original_ids: Vec<Uuid> = original_results.iter().map(|d| d.id).collect();
|
|
assert!(!original_ids.contains(&high_id)); // 90% > 50%
|
|
assert!(!original_ids.contains(&null_confidence_id)); // NULL confidence excluded
|
|
assert!(!original_ids.contains(&failed_id)); // NULL confidence excluded
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_confidence_query_ordering() {
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let database = &ctx.state.db;
|
|
|
|
// Create user using direct database approach
|
|
let user_data = readur::models::CreateUser {
|
|
username: format!("testuser_{}", Uuid::new_v4()),
|
|
email: format!("test_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(readur::models::UserRole::User),
|
|
};
|
|
let user = database.create_user(user_data).await.expect("Failed to create user");
|
|
let user_id = user.id;
|
|
|
|
// Create documents with different confidence levels and statuses
|
|
let mut confidence_10_doc = create_test_document(user_id);
|
|
confidence_10_doc.ocr_confidence = Some(10.0);
|
|
confidence_10_doc.ocr_status = Some("completed".to_string());
|
|
|
|
let mut confidence_30_doc = create_test_document(user_id);
|
|
confidence_30_doc.ocr_confidence = Some(30.0);
|
|
confidence_30_doc.ocr_status = Some("completed".to_string());
|
|
|
|
let mut failed_doc = create_test_document(user_id);
|
|
failed_doc.ocr_confidence = None;
|
|
failed_doc.ocr_status = Some("failed".to_string());
|
|
|
|
let mut null_confidence_doc = create_test_document(user_id);
|
|
null_confidence_doc.ocr_confidence = None;
|
|
null_confidence_doc.ocr_status = Some("completed".to_string());
|
|
|
|
// Insert documents
|
|
let id_10 = ctx.state.db.create_document(confidence_10_doc).await.unwrap().id;
|
|
let id_30 = ctx.state.db.create_document(confidence_30_doc).await.unwrap().id;
|
|
let failed_id = ctx.state.db.create_document(failed_doc).await.unwrap().id;
|
|
let null_id = ctx.state.db.create_document(null_confidence_doc).await.unwrap().id;
|
|
|
|
// Test ordering in combined query
|
|
let results = database
|
|
.find_low_confidence_and_failed_documents(user_id, readur::models::UserRole::User, 50.0, 100, 0)
|
|
.await
|
|
.unwrap();
|
|
|
|
// The function returns documents that are either:
|
|
// 1. Low confidence (< threshold)
|
|
// 2. Failed status
|
|
// A completed document with NULL confidence is not considered "failed"
|
|
assert_eq!(results.len(), 3); // Update expectation based on actual behavior
|
|
|
|
// Check that documents with actual confidence are ordered by confidence (ascending)
|
|
// and NULL confidence documents come first (due to CASE WHEN ordering)
|
|
let confidence_values: Vec<Option<f32>> = results.iter().map(|d| d.ocr_confidence).collect();
|
|
|
|
// With 3 documents: 1 failed (NULL confidence), 2 low confidence documents
|
|
// First should be NULL confidence (failed)
|
|
assert!(confidence_values[0].is_none());
|
|
|
|
// Next should be lowest confidence
|
|
assert_eq!(confidence_values[1], Some(10.0));
|
|
|
|
// Last should be higher confidence
|
|
assert_eq!(confidence_values[2], Some(30.0));
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_user_isolation_in_confidence_queries() {
|
|
let ctx = TestContext::new().await;
|
|
|
|
// Ensure cleanup happens even if test fails
|
|
let result: Result<()> = async {
|
|
let database = &ctx.state.db;
|
|
|
|
// Create users using direct database approach
|
|
let user1_data = readur::models::CreateUser {
|
|
username: format!("testuser1_{}", Uuid::new_v4()),
|
|
email: format!("test1_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(readur::models::UserRole::User),
|
|
};
|
|
let user1 = database.create_user(user1_data).await.expect("Failed to create user1");
|
|
let user1_id = user1.id;
|
|
|
|
let user2_data = readur::models::CreateUser {
|
|
username: format!("testuser2_{}", Uuid::new_v4()),
|
|
email: format!("test2_{}@example.com", Uuid::new_v4()),
|
|
password: "password123".to_string(),
|
|
role: Some(readur::models::UserRole::User),
|
|
};
|
|
let user2 = database.create_user(user2_data).await.expect("Failed to create user2");
|
|
let user2_id = user2.id;
|
|
|
|
// Create documents for user1
|
|
let mut user1_low_doc = create_test_document(user1_id);
|
|
user1_low_doc.ocr_confidence = Some(20.0);
|
|
|
|
let mut user1_failed_doc = create_test_document(user1_id);
|
|
user1_failed_doc.ocr_status = Some("failed".to_string());
|
|
user1_failed_doc.ocr_confidence = None;
|
|
|
|
// Create documents for user2
|
|
let mut user2_low_doc = create_test_document(user2_id);
|
|
user2_low_doc.ocr_confidence = Some(25.0);
|
|
|
|
let mut user2_failed_doc = create_test_document(user2_id);
|
|
user2_failed_doc.ocr_status = Some("failed".to_string());
|
|
user2_failed_doc.ocr_confidence = None;
|
|
|
|
// Insert documents
|
|
let user1_low_id: Uuid = ctx.state.db.create_document(user1_low_doc).await.unwrap().id;
|
|
let user1_failed_id: Uuid = ctx.state.db.create_document(user1_failed_doc).await.unwrap().id;
|
|
let user2_low_id: Uuid = ctx.state.db.create_document(user2_low_doc).await.unwrap().id;
|
|
let user2_failed_id: Uuid = ctx.state.db.create_document(user2_failed_doc).await.unwrap().id;
|
|
|
|
// Test user1 can only see their documents
|
|
let user1_results = database
|
|
.find_low_confidence_and_failed_documents(user1_id, readur::models::UserRole::User, 50.0, 100, 0)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(user1_results.len(), 2);
|
|
let user1_ids: Vec<Uuid> = user1_results.iter().map(|d| d.id).collect();
|
|
assert!(user1_ids.contains(&user1_low_id));
|
|
assert!(user1_ids.contains(&user1_failed_id));
|
|
assert!(!user1_ids.contains(&user2_low_id));
|
|
assert!(!user1_ids.contains(&user2_failed_id));
|
|
|
|
// Test user2 can only see their documents
|
|
let user2_results = database
|
|
.find_low_confidence_and_failed_documents(user2_id, readur::models::UserRole::User, 50.0, 100, 0)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(user2_results.len(), 2);
|
|
let user2_ids: Vec<Uuid> = user2_results.iter().map(|d| d.id).collect();
|
|
assert!(user2_ids.contains(&user2_low_id));
|
|
assert!(user2_ids.contains(&user2_failed_id));
|
|
assert!(!user2_ids.contains(&user1_low_id));
|
|
assert!(!user2_ids.contains(&user1_failed_id));
|
|
|
|
// Test admin can see all documents
|
|
let admin_results = database
|
|
.find_low_confidence_and_failed_documents(user1_id, readur::models::UserRole::Admin, 50.0, 100, 0)
|
|
.await
|
|
.unwrap();
|
|
|
|
assert!(admin_results.len() >= 4); // At least our 4 test documents
|
|
let admin_ids: Vec<Uuid> = admin_results.iter().map(|d| d.id).collect();
|
|
assert!(admin_ids.contains(&user1_low_id));
|
|
assert!(admin_ids.contains(&user1_failed_id));
|
|
assert!(admin_ids.contains(&user2_low_id));
|
|
assert!(admin_ids.contains(&user2_failed_id));
|
|
|
|
Ok(())
|
|
}.await;
|
|
|
|
// Always cleanup database connections and test data
|
|
if let Err(e) = ctx.cleanup_and_close().await {
|
|
eprintln!("Warning: Test cleanup failed: {}", e);
|
|
}
|
|
|
|
result.unwrap();
|
|
}
|
|
} |