Files
readur/tests/integration_source_errors_db_tests.rs

818 lines
30 KiB
Rust

/// Integration tests for source error database functions
/// Tests the complete lifecycle of source scan failure tracking including:
/// - Recording failures with proper enum type casting
/// - Querying failures with various filters
/// - Resolving and resetting failures
/// - Statistics aggregation
///
/// These tests specifically ensure that PostgreSQL enum types are handled correctly
/// to prevent runtime SQL errors like "function resolve_source_scan_failure does not exist"
/// or "operator does not exist: source_error_source_type = text"
#[cfg(test)]
mod tests {
use anyhow::Result;
use readur::test_utils::TestContext;
use readur::models::{
CreateSourceScanFailure, ErrorSourceType, SourceErrorType,
SourceErrorSeverity, ListFailuresQuery, SourceScanFailure
};
use uuid::Uuid;
use serde_json::json;
#[tokio::test]
async fn test_record_source_scan_failure_with_enum_types() {
let ctx = TestContext::new().await;
let result: Result<()> = async {
let db = &ctx.state.db;
let user_id = Uuid::new_v4();
// Create test user
let unique_suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let username = format!("test_source_error_user_{}", unique_suffix);
let email = format!("test_source_error_{}@example.com", unique_suffix);
sqlx::query(
"INSERT INTO users (id, username, email, password_hash, role)
VALUES ($1, $2, $3, $4, $5)"
)
.bind(user_id)
.bind(&username)
.bind(&email)
.bind("hash")
.bind("user")
.execute(db.get_pool())
.await?;
// Test recording a source scan failure with all enum types
let failure = CreateSourceScanFailure {
user_id,
source_type: ErrorSourceType::WebDAV,
source_id: None, // No source_id to avoid foreign key constraint
resource_path: "/test/path/to/file.pdf".to_string(),
error_type: SourceErrorType::PermissionDenied,
error_message: "Access denied to resource".to_string(),
error_code: Some("403".to_string()),
http_status_code: Some(403),
response_time_ms: Some(250),
response_size_bytes: Some(1024),
resource_size_bytes: Some(2048),
diagnostic_data: Some(json!({
"headers": {
"content-type": "text/html"
}
})),
};
// This should succeed with proper enum type casting
let failure_id = db.record_source_scan_failure(&failure).await?;
assert!(!failure_id.is_nil());
// Test with different enum values
let failure2 = CreateSourceScanFailure {
user_id,
source_type: ErrorSourceType::S3,
source_id: None,
resource_path: "bucket/key/object.txt".to_string(),
error_type: SourceErrorType::Timeout,
error_message: "Request timed out".to_string(),
error_code: None,
http_status_code: None,
response_time_ms: Some(30000),
response_size_bytes: None,
resource_size_bytes: None,
diagnostic_data: None,
};
let failure_id2 = db.record_source_scan_failure(&failure2).await?;
assert!(!failure_id2.is_nil());
Ok(())
}.await;
if let Err(e) = ctx.cleanup_and_close().await {
eprintln!("Warning: Test cleanup failed: {}", e);
}
result.unwrap();
}
#[tokio::test]
async fn test_list_source_scan_failures_with_enum_filters() {
let ctx = TestContext::new().await;
let result: Result<()> = async {
let db = &ctx.state.db;
let user_id = Uuid::new_v4();
// Create test user
let unique_suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let username = format!("test_list_failures_user_{}", unique_suffix);
let email = format!("test_list_failures_{}@example.com", unique_suffix);
sqlx::query(
"INSERT INTO users (id, username, email, password_hash, role)
VALUES ($1, $2, $3, $4, $5)"
)
.bind(user_id)
.bind(&username)
.bind(&email)
.bind("hash")
.bind("user")
.execute(db.get_pool())
.await?;
// Create multiple failures with different enum values
let failures_data = vec![
(ErrorSourceType::WebDAV, SourceErrorType::PermissionDenied),
(ErrorSourceType::WebDAV, SourceErrorType::Timeout),
(ErrorSourceType::S3, SourceErrorType::NetworkError),
(ErrorSourceType::Local, SourceErrorType::PathTooLong),
];
for (source_type, error_type) in failures_data {
let failure = CreateSourceScanFailure {
user_id,
source_type: source_type.clone(),
source_id: None,
resource_path: format!("/test/{}/{}", source_type, error_type),
error_type: error_type.clone(),
error_message: format!("Test error: {}", error_type),
error_code: None,
http_status_code: None,
response_time_ms: None,
response_size_bytes: None,
resource_size_bytes: None,
diagnostic_data: None,
};
db.record_source_scan_failure(&failure).await?;
}
// Test querying with source_type filter (tests enum comparison)
let query = ListFailuresQuery {
source_type: Some(ErrorSourceType::WebDAV),
source_id: None,
error_type: None,
severity: None,
include_resolved: Some(false),
include_excluded: Some(false),
ready_for_retry: None,
limit: None,
offset: None,
};
let webdav_failures = db.list_source_scan_failures(user_id, &query).await?;
assert_eq!(webdav_failures.len(), 2);
// Test querying with error_type filter
let query = ListFailuresQuery {
source_type: None,
source_id: None,
error_type: Some(SourceErrorType::Timeout),
severity: None,
include_resolved: Some(false),
include_excluded: Some(false),
ready_for_retry: None,
limit: None,
offset: None,
};
let timeout_failures = db.list_source_scan_failures(user_id, &query).await?;
assert_eq!(timeout_failures.len(), 1);
// Test querying with multiple filters
let query = ListFailuresQuery {
source_type: Some(ErrorSourceType::S3),
source_id: None,
error_type: Some(SourceErrorType::NetworkError),
severity: None,
include_resolved: Some(false),
include_excluded: Some(false),
ready_for_retry: None,
limit: None,
offset: None,
};
let s3_network_failures = db.list_source_scan_failures(user_id, &query).await?;
assert_eq!(s3_network_failures.len(), 1);
Ok(())
}.await;
if let Err(e) = ctx.cleanup_and_close().await {
eprintln!("Warning: Test cleanup failed: {}", e);
}
result.unwrap();
}
#[tokio::test]
async fn test_resolve_source_scan_failure_with_enum_casting() {
let ctx = TestContext::new().await;
let result: Result<()> = async {
let db = &ctx.state.db;
let user_id = Uuid::new_v4();
// Create test user
let unique_suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let username = format!("test_resolve_user_{}", unique_suffix);
let email = format!("test_resolve_{}@example.com", unique_suffix);
sqlx::query(
"INSERT INTO users (id, username, email, password_hash, role)
VALUES ($1, $2, $3, $4, $5)"
)
.bind(user_id)
.bind(&username)
.bind(&email)
.bind("hash")
.bind("user")
.execute(db.get_pool())
.await?;
let source_id = None; // No source_id to avoid foreign key constraint
let resource_path = "/test/resolve/file.pdf";
// Create a failure
let failure = CreateSourceScanFailure {
user_id,
source_type: ErrorSourceType::WebDAV,
source_id,
resource_path: resource_path.to_string(),
error_type: SourceErrorType::ServerError,
error_message: "Internal server error".to_string(),
error_code: Some("500".to_string()),
http_status_code: Some(500),
response_time_ms: None,
response_size_bytes: None,
resource_size_bytes: None,
diagnostic_data: None,
};
db.record_source_scan_failure(&failure).await?;
// Test resolving the failure (this tests the function with enum type casting)
let resolved = db.resolve_source_scan_failure(
user_id,
ErrorSourceType::WebDAV,
source_id,
resource_path,
"manual"
).await?;
assert!(resolved);
// Verify it's marked as resolved
let query = ListFailuresQuery {
source_type: Some(ErrorSourceType::WebDAV),
source_id,
error_type: None,
severity: None,
include_resolved: Some(true), // Include resolved
include_excluded: Some(false),
ready_for_retry: None,
limit: None,
offset: None,
};
let failures = db.list_source_scan_failures(user_id, &query).await?;
assert_eq!(failures.len(), 1);
assert!(failures[0].resolved);
Ok(())
}.await;
if let Err(e) = ctx.cleanup_and_close().await {
eprintln!("Warning: Test cleanup failed: {}", e);
}
result.unwrap();
}
#[tokio::test]
async fn test_reset_source_scan_failure_with_enum_casting() {
let ctx = TestContext::new().await;
let result: Result<()> = async {
let db = &ctx.state.db;
let user_id = Uuid::new_v4();
// Create test user
let unique_suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let username = format!("test_reset_user_{}", unique_suffix);
let email = format!("test_reset_{}@example.com", unique_suffix);
sqlx::query(
"INSERT INTO users (id, username, email, password_hash, role)
VALUES ($1, $2, $3, $4, $5)"
)
.bind(user_id)
.bind(&username)
.bind(&email)
.bind("hash")
.bind("user")
.execute(db.get_pool())
.await?;
let resource_path = "/test/reset/file.pdf";
// Create a failure
let failure = CreateSourceScanFailure {
user_id,
source_type: ErrorSourceType::S3,
source_id: None,
resource_path: resource_path.to_string(),
error_type: SourceErrorType::RateLimited,
error_message: "Rate limit exceeded".to_string(),
error_code: Some("429".to_string()),
http_status_code: Some(429),
response_time_ms: None,
response_size_bytes: None,
resource_size_bytes: None,
diagnostic_data: None,
};
db.record_source_scan_failure(&failure).await?;
// Test resetting the failure
let reset = db.reset_source_scan_failure(
user_id,
ErrorSourceType::S3,
None,
resource_path
).await?;
assert!(reset);
Ok(())
}.await;
if let Err(e) = ctx.cleanup_and_close().await {
eprintln!("Warning: Test cleanup failed: {}", e);
}
result.unwrap();
}
#[tokio::test]
async fn test_is_source_known_failure_with_enum_comparison() {
let ctx = TestContext::new().await;
let result: Result<()> = async {
let db = &ctx.state.db;
let user_id = Uuid::new_v4();
// Create test user
let unique_suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let username = format!("test_known_failure_user_{}", unique_suffix);
let email = format!("test_known_failure_{}@example.com", unique_suffix);
sqlx::query(
"INSERT INTO users (id, username, email, password_hash, role)
VALUES ($1, $2, $3, $4, $5)"
)
.bind(user_id)
.bind(&username)
.bind(&email)
.bind("hash")
.bind("user")
.execute(db.get_pool())
.await?;
let resource_path = "/test/known/file.pdf";
// Create a critical failure that should be considered "known"
let failure = CreateSourceScanFailure {
user_id,
source_type: ErrorSourceType::Local,
source_id: None,
resource_path: resource_path.to_string(),
error_type: SourceErrorType::PathTooLong,
error_message: "Path exceeds maximum length".to_string(),
error_code: None,
http_status_code: None,
response_time_ms: None,
response_size_bytes: None,
resource_size_bytes: None,
diagnostic_data: None,
};
db.record_source_scan_failure(&failure).await?;
// Test checking if it's a known failure
let is_known = db.is_source_known_failure(
user_id,
ErrorSourceType::Local,
None,
resource_path
).await?;
// PathTooLong is a critical error, so it should be considered known
assert!(is_known);
// Test with different source type - should not be known
let is_known_different = db.is_source_known_failure(
user_id,
ErrorSourceType::S3, // Different source type
None,
resource_path
).await?;
assert!(!is_known_different);
Ok(())
}.await;
if let Err(e) = ctx.cleanup_and_close().await {
eprintln!("Warning: Test cleanup failed: {}", e);
}
result.unwrap();
}
#[tokio::test]
async fn test_exclude_source_from_scan_with_enum_casting() {
let ctx = TestContext::new().await;
let result: Result<()> = async {
let db = &ctx.state.db;
let user_id = Uuid::new_v4();
// Create test user
let unique_suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let username = format!("test_exclude_user_{}", unique_suffix);
let email = format!("test_exclude_{}@example.com", unique_suffix);
sqlx::query(
"INSERT INTO users (id, username, email, password_hash, role)
VALUES ($1, $2, $3, $4, $5)"
)
.bind(user_id)
.bind(&username)
.bind(&email)
.bind("hash")
.bind("user")
.execute(db.get_pool())
.await?;
let resource_path = "/test/exclude/file.pdf";
// Create a failure
let failure = CreateSourceScanFailure {
user_id,
source_type: ErrorSourceType::WebDAV,
source_id: None,
resource_path: resource_path.to_string(),
error_type: SourceErrorType::InvalidCharacters,
error_message: "Invalid characters in filename".to_string(),
error_code: None,
http_status_code: None,
response_time_ms: None,
response_size_bytes: None,
resource_size_bytes: None,
diagnostic_data: None,
};
db.record_source_scan_failure(&failure).await?;
// Test excluding the source from scan
let excluded = db.exclude_source_from_scan(
user_id,
ErrorSourceType::WebDAV,
None,
resource_path,
Some("User requested exclusion")
).await?;
assert!(excluded);
// Verify it's marked as excluded
let query = ListFailuresQuery {
source_type: Some(ErrorSourceType::WebDAV),
source_id: None,
error_type: None,
severity: None,
include_resolved: Some(false),
include_excluded: Some(true), // Include excluded
ready_for_retry: None,
limit: None,
offset: None,
};
let failures = db.list_source_scan_failures(user_id, &query).await?;
assert_eq!(failures.len(), 1);
assert!(failures[0].user_excluded);
Ok(())
}.await;
if let Err(e) = ctx.cleanup_and_close().await {
eprintln!("Warning: Test cleanup failed: {}", e);
}
result.unwrap();
}
#[tokio::test]
async fn test_get_source_retry_candidates_with_enum_filter() {
let ctx = TestContext::new().await;
let result: Result<()> = async {
let db = &ctx.state.db;
let user_id = Uuid::new_v4();
// Create test user
let unique_suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let username = format!("test_retry_candidates_user_{}", unique_suffix);
let email = format!("test_retry_candidates_{}@example.com", unique_suffix);
sqlx::query(
"INSERT INTO users (id, username, email, password_hash, role)
VALUES ($1, $2, $3, $4, $5)"
)
.bind(user_id)
.bind(&username)
.bind(&email)
.bind("hash")
.bind("user")
.execute(db.get_pool())
.await?;
// Create failures with different source types
for i in 0..3 {
let failure = CreateSourceScanFailure {
user_id,
source_type: if i == 0 { ErrorSourceType::WebDAV } else { ErrorSourceType::S3 },
source_id: None,
resource_path: format!("/test/retry/{}.pdf", i),
error_type: SourceErrorType::NetworkError,
error_message: "Network error".to_string(),
error_code: None,
http_status_code: None,
response_time_ms: None,
response_size_bytes: None,
resource_size_bytes: None,
diagnostic_data: None,
};
db.record_source_scan_failure(&failure).await?;
// Manually set next_retry_at to NOW() for testing
sqlx::query(
"UPDATE source_scan_failures
SET next_retry_at = NOW()
WHERE user_id = $1 AND resource_path = $2"
)
.bind(user_id)
.bind(format!("/test/retry/{}.pdf", i))
.execute(db.get_pool())
.await?;
}
// Test getting retry candidates filtered by source type
let webdav_candidates = db.get_source_retry_candidates(
user_id,
Some(ErrorSourceType::WebDAV),
10
).await?;
assert_eq!(webdav_candidates.len(), 1);
assert_eq!(webdav_candidates[0].source_type, ErrorSourceType::WebDAV);
// Test getting all retry candidates
let all_candidates = db.get_source_retry_candidates(
user_id,
None,
10
).await?;
assert_eq!(all_candidates.len(), 3);
Ok(())
}.await;
if let Err(e) = ctx.cleanup_and_close().await {
eprintln!("Warning: Test cleanup failed: {}", e);
}
result.unwrap();
}
#[tokio::test]
async fn test_get_source_scan_failure_stats_with_enum_filter() {
let ctx = TestContext::new().await;
let result: Result<()> = async {
let db = &ctx.state.db;
let user_id = Uuid::new_v4();
// Create test user
let unique_suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let username = format!("test_stats_user_{}", unique_suffix);
let email = format!("test_stats_{}@example.com", unique_suffix);
sqlx::query(
"INSERT INTO users (id, username, email, password_hash, role)
VALUES ($1, $2, $3, $4, $5)"
)
.bind(user_id)
.bind(&username)
.bind(&email)
.bind("hash")
.bind("user")
.execute(db.get_pool())
.await?;
// Create multiple failures with different characteristics
let failures_to_create = vec![
(ErrorSourceType::WebDAV, SourceErrorType::PermissionDenied, false),
(ErrorSourceType::WebDAV, SourceErrorType::Timeout, false),
(ErrorSourceType::S3, SourceErrorType::NetworkError, false),
(ErrorSourceType::S3, SourceErrorType::ServerError, true), // This one will be resolved
(ErrorSourceType::Local, SourceErrorType::PathTooLong, false),
];
for (i, (source_type, error_type, should_resolve)) in failures_to_create.iter().enumerate() {
let failure = CreateSourceScanFailure {
user_id,
source_type: source_type.clone(),
source_id: None,
resource_path: format!("/test/stats/{}.pdf", i),
error_type: error_type.clone(),
error_message: format!("Error: {}", error_type),
error_code: None,
http_status_code: None,
response_time_ms: None,
response_size_bytes: None,
resource_size_bytes: None,
diagnostic_data: None,
};
db.record_source_scan_failure(&failure).await?;
if *should_resolve {
db.resolve_source_scan_failure(
user_id,
source_type.clone(),
None,
&format!("/test/stats/{}.pdf", i),
"test"
).await?;
}
}
// Test getting stats for all source types
let all_stats = db.get_source_scan_failure_stats(user_id, None).await?;
assert_eq!(all_stats.active_failures, 4); // 5 created, 1 resolved
assert_eq!(all_stats.resolved_failures, 1);
assert!(all_stats.by_source_type.contains_key("webdav"));
assert!(all_stats.by_source_type.contains_key("s3"));
assert!(all_stats.by_source_type.contains_key("local"));
// Test getting stats filtered by source type
let webdav_stats = db.get_source_scan_failure_stats(
user_id,
Some(ErrorSourceType::WebDAV)
).await?;
assert_eq!(webdav_stats.active_failures, 2); // 2 WebDAV failures, none resolved
Ok(())
}.await;
if let Err(e) = ctx.cleanup_and_close().await {
eprintln!("Warning: Test cleanup failed: {}", e);
}
result.unwrap();
}
#[tokio::test]
async fn test_all_enum_values_are_supported() {
let ctx = TestContext::new().await;
let result: Result<()> = async {
let db = &ctx.state.db;
let user_id = Uuid::new_v4();
// Create test user
let unique_suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let username = format!("test_all_enums_user_{}", unique_suffix);
let email = format!("test_all_enums_{}@example.com", unique_suffix);
sqlx::query(
"INSERT INTO users (id, username, email, password_hash, role)
VALUES ($1, $2, $3, $4, $5)"
)
.bind(user_id)
.bind(&username)
.bind(&email)
.bind("hash")
.bind("user")
.execute(db.get_pool())
.await?;
// Test all ErrorSourceType values
let source_types = vec![
ErrorSourceType::WebDAV,
ErrorSourceType::S3,
ErrorSourceType::Local,
];
// Test all SourceErrorType values
let error_types = vec![
SourceErrorType::Timeout,
SourceErrorType::PermissionDenied,
SourceErrorType::NetworkError,
SourceErrorType::ServerError,
SourceErrorType::PathTooLong,
SourceErrorType::InvalidCharacters,
SourceErrorType::TooManyItems,
SourceErrorType::DepthLimit,
SourceErrorType::SizeLimit,
SourceErrorType::XmlParseError,
SourceErrorType::JsonParseError,
SourceErrorType::QuotaExceeded,
SourceErrorType::RateLimited,
SourceErrorType::NotFound,
SourceErrorType::Conflict,
SourceErrorType::UnsupportedOperation,
SourceErrorType::Unknown,
];
// Create a failure for each combination to ensure all enum values work
for source_type in &source_types {
for (i, error_type) in error_types.iter().enumerate() {
let failure = CreateSourceScanFailure {
user_id,
source_type: source_type.clone(),
source_id: None,
resource_path: format!("/test/enum/{}/{}.pdf", source_type, i),
error_type: error_type.clone(),
error_message: format!("Testing {} with {}", source_type, error_type),
error_code: None,
http_status_code: None,
response_time_ms: None,
response_size_bytes: None,
resource_size_bytes: None,
diagnostic_data: None,
};
// This should not panic or return an error
let result = db.record_source_scan_failure(&failure).await;
assert!(result.is_ok(), "Failed to record failure for {} / {}: {:?}",
source_type, error_type, result);
}
}
// Verify we can query all of them
let all_failures = db.list_source_scan_failures(user_id, &ListFailuresQuery {
source_type: None,
source_id: None,
error_type: None,
severity: None,
include_resolved: Some(false),
include_excluded: Some(false),
ready_for_retry: None,
limit: None,
offset: None,
}).await?;
assert_eq!(all_failures.len(), source_types.len() * error_types.len());
Ok(())
}.await;
if let Err(e) = ctx.cleanup_and_close().await {
eprintln!("Warning: Test cleanup failed: {}", e);
}
result.unwrap();
}
}