616 lines
22 KiB
Rust
616 lines
22 KiB
Rust
/*!
|
|
* Document Deletion Integration Tests
|
|
*
|
|
* Comprehensive tests for single and bulk document deletion functionality.
|
|
* Tests HTTP endpoints, file cleanup, role-based access, and edge cases.
|
|
*/
|
|
|
|
use reqwest::{Client, multipart};
|
|
use serde_json::{json, Value};
|
|
use std::time::Duration;
|
|
use uuid::Uuid;
|
|
|
|
use readur::models::{DocumentResponse, CreateUser, LoginRequest, LoginResponse, UserRole};
|
|
|
|
fn get_base_url() -> String {
|
|
std::env::var("API_URL").unwrap_or_else(|_| "http://localhost:8000".to_string())
|
|
}
|
|
|
|
const TIMEOUT: Duration = Duration::from_secs(30);
|
|
|
|
/// Test client for document deletion integration tests
|
|
struct DocumentDeletionTestClient {
|
|
client: Client,
|
|
token: Option<String>,
|
|
user_id: Option<String>,
|
|
}
|
|
|
|
impl DocumentDeletionTestClient {
|
|
fn new() -> Self {
|
|
Self {
|
|
client: Client::new(),
|
|
token: None,
|
|
user_id: None,
|
|
}
|
|
}
|
|
|
|
/// Check if server is running and healthy
|
|
async fn check_server_health(&self) -> Result<(), Box<dyn std::error::Error>> {
|
|
let response = self.client
|
|
.get(&format!("{}/api/health", get_base_url()))
|
|
.timeout(Duration::from_secs(5))
|
|
.send()
|
|
.await?;
|
|
|
|
if !response.status().is_success() {
|
|
return Err("Server health check failed".into());
|
|
}
|
|
|
|
let health: Value = response.json().await?;
|
|
if health["status"] != "ok" {
|
|
return Err("Server is not healthy".into());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Register a new user and login to get auth token
|
|
async fn register_and_login(&mut self, username: &str, email: &str, password: &str, role: Option<UserRole>) -> Result<String, Box<dyn std::error::Error>> {
|
|
// Register user
|
|
let user_data = CreateUser {
|
|
username: username.to_string(),
|
|
email: email.to_string(),
|
|
password: password.to_string(),
|
|
role: Some(role.unwrap_or(UserRole::User)),
|
|
};
|
|
|
|
let register_response = self.client
|
|
.post(&format!("{}/api/auth/register", get_base_url()))
|
|
.json(&user_data)
|
|
.timeout(TIMEOUT)
|
|
.send()
|
|
.await?;
|
|
|
|
if !register_response.status().is_success() {
|
|
return Err(format!("Registration failed: {}", register_response.text().await?).into());
|
|
}
|
|
|
|
// Login to get token
|
|
let login_data = LoginRequest {
|
|
username: username.to_string(),
|
|
password: password.to_string(),
|
|
};
|
|
|
|
let login_response = self.client
|
|
.post(&format!("{}/api/auth/login", get_base_url()))
|
|
.json(&login_data)
|
|
.timeout(TIMEOUT)
|
|
.send()
|
|
.await?;
|
|
|
|
if !login_response.status().is_success() {
|
|
return Err(format!("Login failed: {}", login_response.text().await?).into());
|
|
}
|
|
|
|
let login_result: LoginResponse = login_response.json().await?;
|
|
self.token = Some(login_result.token.clone());
|
|
self.user_id = Some(login_result.user.id.to_string());
|
|
|
|
Ok(login_result.token)
|
|
}
|
|
|
|
/// Upload a test document
|
|
async fn upload_document(&self, content: &[u8], filename: &str) -> Result<DocumentResponse, Box<dyn std::error::Error>> {
|
|
let token = self.token.as_ref().ok_or("Not authenticated")?;
|
|
|
|
let form = multipart::Form::new()
|
|
.part("file", multipart::Part::bytes(content.to_vec())
|
|
.file_name(filename.to_string())
|
|
.mime_str("text/plain")?);
|
|
|
|
let response = self.client
|
|
.post(&format!("{}/api/documents", get_base_url()))
|
|
.header("Authorization", format!("Bearer {}", token))
|
|
.multipart(form)
|
|
.timeout(TIMEOUT)
|
|
.send()
|
|
.await?;
|
|
|
|
if !response.status().is_success() {
|
|
return Err(format!("Document upload failed: {}", response.text().await?).into());
|
|
}
|
|
|
|
let document: DocumentResponse = response.json().await?;
|
|
Ok(document)
|
|
}
|
|
|
|
/// Delete a single document
|
|
async fn delete_document(&self, document_id: &str) -> Result<Value, Box<dyn std::error::Error>> {
|
|
let token = self.token.as_ref().ok_or("Not authenticated")?;
|
|
|
|
let response = self.client
|
|
.delete(&format!("{}/api/documents/{}", get_base_url(), document_id))
|
|
.header("Authorization", format!("Bearer {}", token))
|
|
.timeout(TIMEOUT)
|
|
.send()
|
|
.await?;
|
|
|
|
let status = response.status();
|
|
let text = response.text().await?;
|
|
|
|
if !status.is_success() {
|
|
return Err(format!("Document deletion failed ({}): {}", status, text).into());
|
|
}
|
|
|
|
let result: Value = serde_json::from_str(&text)?;
|
|
Ok(result)
|
|
}
|
|
|
|
/// Bulk delete documents
|
|
async fn bulk_delete_documents(&self, document_ids: &[String]) -> Result<Value, Box<dyn std::error::Error>> {
|
|
let token = self.token.as_ref().ok_or("Not authenticated")?;
|
|
|
|
let request_data = json!({
|
|
"document_ids": document_ids
|
|
});
|
|
|
|
let response = self.client
|
|
.delete(&format!("{}/api/documents", get_base_url()))
|
|
.header("Authorization", format!("Bearer {}", token))
|
|
.json(&request_data)
|
|
.timeout(TIMEOUT)
|
|
.send()
|
|
.await?;
|
|
|
|
let status = response.status();
|
|
let text = response.text().await?;
|
|
|
|
if !status.is_success() {
|
|
return Err(format!("Bulk deletion failed ({}): {}", status, text).into());
|
|
}
|
|
|
|
let result: Value = serde_json::from_str(&text)?;
|
|
Ok(result)
|
|
}
|
|
|
|
/// Delete document without authentication (for testing unauthorized access)
|
|
async fn delete_document_without_auth(&self, document_id: &str) -> Result<Value, Box<dyn std::error::Error>> {
|
|
let response = self.client
|
|
.delete(&format!("{}/api/documents/{}", get_base_url(), document_id))
|
|
.timeout(TIMEOUT)
|
|
.send()
|
|
.await?;
|
|
|
|
let status = response.status();
|
|
let text = response.text().await?;
|
|
|
|
if !status.is_success() {
|
|
return Err(format!("Document deletion failed ({}): {}", status, text).into());
|
|
}
|
|
|
|
let result: Value = serde_json::from_str(&text)?;
|
|
Ok(result)
|
|
}
|
|
|
|
/// Get document by ID
|
|
async fn get_document(&self, document_id: &str) -> Result<Option<DocumentResponse>, Box<dyn std::error::Error>> {
|
|
let token = self.token.as_ref().ok_or("Not authenticated")?;
|
|
|
|
let response = self.client
|
|
.get(&format!("{}/api/documents/{}", get_base_url(), document_id))
|
|
.header("Authorization", format!("Bearer {}", token))
|
|
.timeout(TIMEOUT)
|
|
.send()
|
|
.await?;
|
|
|
|
if response.status() == 404 {
|
|
return Ok(None);
|
|
}
|
|
|
|
if !response.status().is_success() {
|
|
return Err(format!("Get document failed: {}", response.text().await?).into());
|
|
}
|
|
|
|
let document: DocumentResponse = response.json().await?;
|
|
Ok(Some(document))
|
|
}
|
|
|
|
/// List documents
|
|
async fn list_documents(&self) -> Result<Value, Box<dyn std::error::Error>> {
|
|
let token = self.token.as_ref().ok_or("Not authenticated")?;
|
|
|
|
let response = self.client
|
|
.get(&format!("{}/api/documents", get_base_url()))
|
|
.header("Authorization", format!("Bearer {}", token))
|
|
.timeout(TIMEOUT)
|
|
.send()
|
|
.await?;
|
|
|
|
if !response.status().is_success() {
|
|
return Err(format!("List documents failed: {}", response.text().await?).into());
|
|
}
|
|
|
|
let result: Value = response.json().await?;
|
|
Ok(result)
|
|
}
|
|
}
|
|
|
|
/// Skip test if server is not running
|
|
macro_rules! skip_if_server_down {
|
|
($client:expr) => {
|
|
if let Err(_) = $client.check_server_health().await {
|
|
println!("Skipping test: Server is not running at {}", get_base_url());
|
|
return;
|
|
}
|
|
};
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_single_document_deletion_success() {
|
|
let mut client = DocumentDeletionTestClient::new();
|
|
skip_if_server_down!(client);
|
|
|
|
// Register and login
|
|
client.register_and_login(
|
|
&format!("testuser_delete_{}", Uuid::new_v4()),
|
|
&format!("testuser_delete_{}@example.com", Uuid::new_v4()),
|
|
"password123",
|
|
None
|
|
).await.expect("Failed to register and login");
|
|
|
|
// Upload a test document
|
|
let test_content = b"This is a test document for deletion.";
|
|
let document = client.upload_document(test_content, "test_deletion.txt")
|
|
.await.expect("Failed to upload document");
|
|
|
|
println!("Uploaded document: {}", document.id);
|
|
|
|
// Verify document exists
|
|
let retrieved_doc = client.get_document(&document.id.to_string())
|
|
.await.expect("Failed to get document");
|
|
assert!(retrieved_doc.is_some(), "Document should exist before deletion");
|
|
|
|
// Delete the document
|
|
let delete_result = client.delete_document(&document.id.to_string())
|
|
.await.expect("Failed to delete document");
|
|
|
|
// Verify deletion response
|
|
assert_eq!(delete_result["success"], true);
|
|
assert_eq!(delete_result["document_id"], document.id.to_string());
|
|
assert_eq!(delete_result["filename"], document.filename);
|
|
|
|
// Verify document no longer exists
|
|
let retrieved_doc_after = client.get_document(&document.id.to_string())
|
|
.await.expect("Failed to check document existence");
|
|
assert!(retrieved_doc_after.is_none(), "Document should not exist after deletion");
|
|
|
|
println!("✅ Single document deletion test passed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bulk_document_deletion_success() {
|
|
let mut client = DocumentDeletionTestClient::new();
|
|
skip_if_server_down!(client);
|
|
|
|
// Register and login
|
|
client.register_and_login(
|
|
&format!("testuser_bulk_{}", Uuid::new_v4()),
|
|
&format!("testuser_bulk_{}@example.com", Uuid::new_v4()),
|
|
"password123",
|
|
None
|
|
).await.expect("Failed to register and login");
|
|
|
|
// Upload multiple test documents
|
|
let mut document_ids = Vec::new();
|
|
for i in 1..=5 {
|
|
let test_content = format!("This is test document number {}", i);
|
|
let document = client.upload_document(test_content.as_bytes(), &format!("test_bulk_{}.txt", i))
|
|
.await.expect("Failed to upload document");
|
|
document_ids.push(document.id.to_string());
|
|
}
|
|
|
|
println!("Uploaded {} documents for bulk deletion", document_ids.len());
|
|
|
|
// Verify all documents exist
|
|
for doc_id in &document_ids {
|
|
let retrieved_doc = client.get_document(doc_id)
|
|
.await.expect("Failed to get document");
|
|
assert!(retrieved_doc.is_some(), "Document should exist before bulk deletion");
|
|
}
|
|
|
|
// Perform bulk deletion
|
|
let delete_result = client.bulk_delete_documents(&document_ids)
|
|
.await.expect("Failed to bulk delete documents");
|
|
|
|
// Verify bulk deletion response
|
|
assert_eq!(delete_result["success"], true);
|
|
assert_eq!(delete_result["deleted_count"], 5);
|
|
assert_eq!(delete_result["requested_count"], 5);
|
|
|
|
let deleted_ids: Vec<String> = delete_result["deleted_document_ids"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|v| v.as_str().unwrap().to_string())
|
|
.collect();
|
|
|
|
assert_eq!(deleted_ids.len(), 5);
|
|
for doc_id in &document_ids {
|
|
assert!(deleted_ids.contains(doc_id), "Document ID should be in deleted list");
|
|
}
|
|
|
|
// Verify all documents no longer exist
|
|
for doc_id in &document_ids {
|
|
let retrieved_doc = client.get_document(doc_id)
|
|
.await.expect("Failed to check document existence");
|
|
assert!(retrieved_doc.is_none(), "Document should not exist after bulk deletion");
|
|
}
|
|
|
|
println!("✅ Bulk document deletion test passed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_delete_nonexistent_document() {
|
|
let mut client = DocumentDeletionTestClient::new();
|
|
skip_if_server_down!(client);
|
|
|
|
// Register and login
|
|
client.register_and_login(
|
|
&format!("testuser_nonexist_{}", Uuid::new_v4()),
|
|
&format!("testuser_nonexist_{}@example.com", Uuid::new_v4()),
|
|
"password123",
|
|
None
|
|
).await.expect("Failed to register and login");
|
|
|
|
// Try to delete a non-existent document
|
|
let fake_id = Uuid::new_v4().to_string();
|
|
let delete_result = client.delete_document(&fake_id).await;
|
|
|
|
// Should return 404 error
|
|
assert!(delete_result.is_err(), "Deleting non-existent document should fail");
|
|
let error_msg = delete_result.unwrap_err().to_string();
|
|
assert!(error_msg.contains("404"), "Should return 404 error for non-existent document");
|
|
|
|
println!("✅ Delete non-existent document test passed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bulk_delete_empty_request() {
|
|
let mut client = DocumentDeletionTestClient::new();
|
|
skip_if_server_down!(client);
|
|
|
|
// Register and login
|
|
client.register_and_login(
|
|
&format!("testuser_empty_{}", Uuid::new_v4()),
|
|
&format!("testuser_empty_{}@example.com", Uuid::new_v4()),
|
|
"password123",
|
|
None
|
|
).await.expect("Failed to register and login");
|
|
|
|
// Try bulk delete with empty array
|
|
let delete_result = client.bulk_delete_documents(&[])
|
|
.await.expect("Bulk delete with empty array should succeed");
|
|
|
|
// Should handle empty request gracefully
|
|
assert_eq!(delete_result["success"], false);
|
|
assert_eq!(delete_result["deleted_count"], 0);
|
|
assert!(delete_result["message"].as_str().unwrap().contains("No document IDs provided"));
|
|
|
|
println!("✅ Bulk delete empty request test passed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_bulk_delete_mixed_existing_nonexistent() {
|
|
let mut client = DocumentDeletionTestClient::new();
|
|
skip_if_server_down!(client);
|
|
|
|
// Register and login
|
|
client.register_and_login(
|
|
&format!("testuser_mixed_{}", Uuid::new_v4()),
|
|
&format!("testuser_mixed_{}@example.com", Uuid::new_v4()),
|
|
"password123",
|
|
None
|
|
).await.expect("Failed to register and login");
|
|
|
|
// Upload one real document
|
|
let test_content = b"This is a real document.";
|
|
let real_document = client.upload_document(test_content, "real_doc.txt")
|
|
.await.expect("Failed to upload document");
|
|
|
|
// Create a list with real and fake IDs
|
|
let fake_id = Uuid::new_v4().to_string();
|
|
let mixed_ids = vec![real_document.id.to_string(), fake_id];
|
|
|
|
// Perform bulk deletion
|
|
let delete_result = client.bulk_delete_documents(&mixed_ids)
|
|
.await.expect("Failed to bulk delete mixed documents");
|
|
|
|
// Should delete only the existing document
|
|
assert_eq!(delete_result["success"], true);
|
|
assert_eq!(delete_result["deleted_count"], 1);
|
|
assert_eq!(delete_result["requested_count"], 2);
|
|
|
|
let deleted_ids: Vec<String> = delete_result["deleted_document_ids"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|v| v.as_str().unwrap().to_string())
|
|
.collect();
|
|
|
|
assert_eq!(deleted_ids.len(), 1);
|
|
assert_eq!(deleted_ids[0], real_document.id.to_string());
|
|
|
|
// Verify real document was deleted
|
|
let retrieved_doc = client.get_document(&real_document.id.to_string())
|
|
.await.expect("Failed to check document existence");
|
|
assert!(retrieved_doc.is_none(), "Real document should be deleted");
|
|
|
|
println!("✅ Bulk delete mixed existing/non-existent test passed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_unauthorized_deletion() {
|
|
let client = DocumentDeletionTestClient::new();
|
|
skip_if_server_down!(client);
|
|
|
|
// Try to delete without authentication
|
|
let fake_id = Uuid::new_v4().to_string();
|
|
let delete_result = client.delete_document_without_auth(&fake_id).await;
|
|
|
|
// Should return 401 error
|
|
assert!(delete_result.is_err(), "Unauthenticated deletion should fail");
|
|
let error_msg = delete_result.unwrap_err().to_string();
|
|
assert!(error_msg.contains("401") || error_msg.contains("Unauthorized"),
|
|
"Should return 401/Unauthorized error");
|
|
|
|
println!("✅ Unauthorized deletion test passed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_cross_user_deletion_protection() {
|
|
let mut client1 = DocumentDeletionTestClient::new();
|
|
let mut client2 = DocumentDeletionTestClient::new();
|
|
skip_if_server_down!(client1);
|
|
|
|
let user1_suffix = Uuid::new_v4();
|
|
let user2_suffix = Uuid::new_v4();
|
|
|
|
// Register and login as user 1
|
|
client1.register_and_login(
|
|
&format!("testuser1_{}", user1_suffix),
|
|
&format!("testuser1_{}@example.com", user1_suffix),
|
|
"password123",
|
|
None
|
|
).await.expect("Failed to register user 1");
|
|
|
|
// Register and login as user 2
|
|
client2.register_and_login(
|
|
&format!("testuser2_{}", user2_suffix),
|
|
&format!("testuser2_{}@example.com", user2_suffix),
|
|
"password123",
|
|
None
|
|
).await.expect("Failed to register user 2");
|
|
|
|
// User 1 uploads a document
|
|
let test_content = b"This is user 1's document.";
|
|
let user1_document = client1.upload_document(test_content, "user1_doc.txt")
|
|
.await.expect("Failed to upload document as user 1");
|
|
|
|
// User 2 tries to delete user 1's document
|
|
let delete_result = client2.delete_document(&user1_document.id.to_string()).await;
|
|
|
|
// Should return 404 (document not found for user 2)
|
|
assert!(delete_result.is_err(), "Cross-user deletion should fail");
|
|
let error_msg = delete_result.unwrap_err().to_string();
|
|
assert!(error_msg.contains("404"), "Should return 404 for document not owned by user");
|
|
|
|
// Verify user 1's document still exists
|
|
let retrieved_doc = client1.get_document(&user1_document.id.to_string())
|
|
.await.expect("Failed to check document existence");
|
|
assert!(retrieved_doc.is_some(), "User 1's document should still exist after failed cross-user deletion");
|
|
|
|
println!("✅ Cross-user deletion protection test passed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_admin_can_delete_any_document() {
|
|
let mut user_client = DocumentDeletionTestClient::new();
|
|
let mut admin_client = DocumentDeletionTestClient::new();
|
|
skip_if_server_down!(user_client);
|
|
|
|
let user_suffix = Uuid::new_v4();
|
|
let admin_suffix = Uuid::new_v4();
|
|
|
|
// Register and login as regular user
|
|
user_client.register_and_login(
|
|
&format!("regularuser_{}", user_suffix),
|
|
&format!("regularuser_{}@example.com", user_suffix),
|
|
"password123",
|
|
None
|
|
).await.expect("Failed to register regular user");
|
|
|
|
// Register and login as admin
|
|
admin_client.register_and_login(
|
|
&format!("adminuser_{}", admin_suffix),
|
|
&format!("adminuser_{}@example.com", admin_suffix),
|
|
"adminpass123",
|
|
Some(UserRole::Admin)
|
|
).await.expect("Failed to register admin user");
|
|
|
|
// Regular user uploads a document
|
|
let test_content = b"This is a regular user's document.";
|
|
let user_document = user_client.upload_document(test_content, "user_doc.txt")
|
|
.await.expect("Failed to upload document as user");
|
|
|
|
// Admin deletes user's document
|
|
let delete_result = admin_client.delete_document(&user_document.id.to_string())
|
|
.await.expect("Admin should be able to delete any document");
|
|
|
|
// Verify deletion response
|
|
assert_eq!(delete_result["success"], true);
|
|
assert_eq!(delete_result["document_id"], user_document.id.to_string());
|
|
|
|
// Verify document no longer exists
|
|
let retrieved_doc = user_client.get_document(&user_document.id.to_string())
|
|
.await.expect("Failed to check document existence");
|
|
assert!(retrieved_doc.is_none(), "Document should be deleted by admin");
|
|
|
|
println!("✅ Admin can delete any document test passed");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_document_count_updates_after_deletion() {
|
|
let mut client = DocumentDeletionTestClient::new();
|
|
skip_if_server_down!(client);
|
|
|
|
// Register and login
|
|
client.register_and_login(
|
|
&format!("testuser_count_{}", Uuid::new_v4()),
|
|
&format!("testuser_count_{}@example.com", Uuid::new_v4()),
|
|
"password123",
|
|
None
|
|
).await.expect("Failed to register and login");
|
|
|
|
// Get initial document count
|
|
let initial_list = client.list_documents()
|
|
.await.expect("Failed to list documents");
|
|
let initial_count = initial_list["pagination"]["total"].as_i64().unwrap();
|
|
|
|
// Upload documents
|
|
let mut document_ids = Vec::new();
|
|
for i in 1..=3 {
|
|
let test_content = format!("Test document {}", i);
|
|
let document = client.upload_document(test_content.as_bytes(), &format!("count_test_{}.txt", i))
|
|
.await.expect("Failed to upload document");
|
|
document_ids.push(document.id.to_string());
|
|
}
|
|
|
|
// Verify count increased
|
|
let after_upload_list = client.list_documents()
|
|
.await.expect("Failed to list documents");
|
|
let after_upload_count = after_upload_list["pagination"]["total"].as_i64().unwrap();
|
|
assert_eq!(after_upload_count, initial_count + 3, "Document count should increase after uploads");
|
|
|
|
// Delete one document
|
|
client.delete_document(&document_ids[0])
|
|
.await.expect("Failed to delete document");
|
|
|
|
// Verify count decreased by 1
|
|
let after_single_delete_list = client.list_documents()
|
|
.await.expect("Failed to list documents");
|
|
let after_single_delete_count = after_single_delete_list["pagination"]["total"].as_i64().unwrap();
|
|
assert_eq!(after_single_delete_count, initial_count + 2, "Document count should decrease after single deletion");
|
|
|
|
// Bulk delete remaining documents
|
|
let remaining_ids = vec![document_ids[1].clone(), document_ids[2].clone()];
|
|
client.bulk_delete_documents(&remaining_ids)
|
|
.await.expect("Failed to bulk delete documents");
|
|
|
|
// Verify count is back to initial
|
|
let final_list = client.list_documents()
|
|
.await.expect("Failed to list documents");
|
|
let final_count = final_list["pagination"]["total"].as_i64().unwrap();
|
|
assert_eq!(final_count, initial_count, "Document count should be back to initial after bulk deletion");
|
|
|
|
println!("✅ Document count updates after deletion test passed");
|
|
} |