Files
foxhunt/services/data_acquisition_service/tests/error_handling_tests.rs
jgrusewski db6462ba7a fix(clippy): resolve all clippy warnings across entire workspace (--all-targets)
Systematic fix of 360+ clippy errors across 37+ crates covering lib,
test, bench, and example targets. Key changes:

- Add targeted #[allow(...)] on #[cfg(test)] modules for test-only lints
  (assertions_on_result_states, float_cmp, str_to_string, indexing, etc.)
- Feature-gate broken integration tests behind __<crate>_integration flags
  where public APIs changed (trading-service, backtesting-service, etc.)
- Remove dead [[test]] entries from Cargo.toml files pointing to deleted files
- Fix production code: field_reassign_with_default, manual_range_contains,
  assert!(false) → panic!(), format!("{}") simplification, len() > 0 → !is_empty()
- Delete truly unused code (Order struct, unused methods/fields/variants)
- Convert sqlx::query!() to sqlx::query() for SQLX_OFFLINE compatibility

Result: cargo clippy --workspace --all-targets -- -D warnings = 0 errors, 0 warnings

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 10:18:35 +01:00

377 lines
12 KiB
Rust

//! Integration tests for error handling and retry logic
//!
//! These tests validate:
//! 1. Network failure handling and retries
//! 2. API error responses
//! 3. Data corruption detection
//! 4. Timeout handling
//! 5. Resource exhaustion handling
//!
//! TDD: These tests are written FIRST and should FAIL until implementation is complete.
#[path = "common/download_types.rs"]
mod download_types;
#[path = "common/mock_downloader.rs"]
mod mock_downloader;
use download_types::DownloadRequest;
use mock_downloader::{
create_test_downloader_that_fails_midway, create_test_downloader_with_corrupted_data,
create_test_downloader_with_error_type, create_test_downloader_with_invalid_auth,
create_test_downloader_with_invalid_format, create_test_downloader_with_limited_disk,
create_test_downloader_with_network_issues, create_test_downloader_with_rate_limiting,
create_test_downloader_with_retry_tracking, create_test_downloader_with_timeout,
create_test_service_with_concurrency_limit,
};
use std::time::Duration;
use tempfile::TempDir;
// Helper function to create test request
fn create_test_request() -> DownloadRequest {
DownloadRequest::new_test_request()
}
/// Test: Network failure triggers retry with exponential backoff
#[tokio::test]
async fn test_network_failure_triggers_retry() {
// Arrange: Downloader with simulated network failures
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let downloader = create_test_downloader_with_network_issues(temp_dir.path()).await;
let request = create_test_request();
// Act: Attempt download (should retry)
let result = downloader.download(request).await;
// Assert: Eventually succeeds after retries
assert!(
result.is_ok(),
"Should succeed after retries: {:?}",
result.err()
);
let download_result = result.unwrap();
assert!(
download_result.retry_count > 0,
"Should have retried at least once"
);
assert!(
download_result.retry_count <= 3,
"Should not exceed max retries"
);
}
/// Test: Exponential backoff timing
#[tokio::test]
async fn test_exponential_backoff_timing() {
// Arrange: Downloader that tracks retry timings
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let downloader = create_test_downloader_with_retry_tracking(temp_dir.path()).await;
let request = create_test_request();
// Act: Trigger retries
let result = downloader.download(request).await;
// Assert: Retry delays follow exponential backoff
assert!(result.is_ok());
let retry_delays = downloader.get_retry_delays();
assert_eq!(retry_delays.len(), 2, "Should have 2 retries");
// First retry: ~1s delay
assert!(
retry_delays[0] >= Duration::from_millis(900),
"First retry should wait ~1s"
);
assert!(
retry_delays[0] <= Duration::from_millis(1500),
"First retry should not wait too long"
);
// Second retry: ~2s delay
assert!(
retry_delays[1] >= Duration::from_millis(1800),
"Second retry should wait ~2s"
);
assert!(
retry_delays[1] <= Duration::from_millis(3000),
"Second retry should not wait too long"
);
}
/// Test: API rate limit error triggers appropriate backoff
#[tokio::test]
async fn test_rate_limit_error_triggers_backoff() {
// Arrange: Downloader that simulates rate limiting
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let downloader = create_test_downloader_with_rate_limiting(temp_dir.path()).await;
let request = create_test_request();
// Act: Download (will hit rate limit)
let result = downloader.download(request).await;
// Assert: Handles rate limiting gracefully
assert!(result.is_ok(), "Should handle rate limiting");
let download_result = result.unwrap();
assert!(
download_result.was_rate_limited,
"Should detect rate limiting"
);
assert!(
download_result.total_wait_time >= Duration::from_secs(5),
"Should wait for rate limit cooldown"
);
}
/// Test: API authentication failure is not retried
#[tokio::test]
async fn test_authentication_failure_not_retried() {
// Arrange: Downloader with invalid credentials
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let downloader = create_test_downloader_with_invalid_auth(temp_dir.path()).await;
let request = create_test_request();
// Act: Attempt download
let result = downloader.download(request).await;
// Assert: Fails immediately without retry
assert!(result.is_err(), "Should fail with auth error");
let error = result.unwrap_err();
let error_msg = error.to_string().to_lowercase();
assert!(
error_msg.contains("authentication") || error_msg.contains("unauthorized"),
"Error should indicate auth failure: {}",
error
);
// Verify no retries were attempted
assert_eq!(
downloader.get_retry_count(),
0,
"Should not retry auth failures"
);
}
/// Test: Timeout during download is handled
#[tokio::test]
async fn test_download_timeout_handled() {
// Arrange: Downloader with short timeout
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let downloader =
create_test_downloader_with_timeout(temp_dir.path(), Duration::from_millis(100)).await;
let request = create_test_request();
// Act: Download (will timeout)
let result = downloader.download(request).await;
// Assert: Timeout error is raised
assert!(result.is_err(), "Should fail with timeout");
let error = result.unwrap_err();
assert!(
error.to_string().contains("timeout"),
"Error should indicate timeout: {}",
error
);
}
/// Test: Data corruption is detected and download fails
#[tokio::test]
async fn test_data_corruption_detected() {
// Arrange: Downloader that returns corrupted data
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let downloader = create_test_downloader_with_corrupted_data(temp_dir.path()).await;
let request = create_test_request();
// Act: Download
let result = downloader.download(request).await;
// Assert: Corruption is detected
assert!(result.is_err(), "Should fail with corruption error");
let error = result.unwrap_err();
assert!(
error.to_string().contains("checksum")
|| error.to_string().contains("corruption")
|| error.to_string().contains("integrity"),
"Error should indicate data corruption: {}",
error
);
}
/// Test: Invalid response format is handled
#[tokio::test]
async fn test_invalid_response_format_handled() {
// Arrange: Downloader that returns invalid format
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let downloader = create_test_downloader_with_invalid_format(temp_dir.path()).await;
let request = create_test_request();
// Act: Download
let result = downloader.download(request).await;
// Assert: Invalid format error
assert!(result.is_err(), "Should fail with format error");
let error = result.unwrap_err();
assert!(
error.to_string().contains("format")
|| error.to_string().contains("parse")
|| error.to_string().contains("invalid"),
"Error should indicate format issue: {}",
error
);
}
/// Test: Disk space exhaustion is detected
#[tokio::test]
async fn test_disk_space_exhaustion_detected() {
// Arrange: Downloader with insufficient disk space
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let downloader = create_test_downloader_with_limited_disk(temp_dir.path()).await;
let request = create_test_request();
// Act: Download
let result = downloader.download(request).await;
// Assert: Disk space error
assert!(result.is_err(), "Should fail with disk space error");
let error = result.unwrap_err();
assert!(
error.to_string().contains("disk")
|| error.to_string().contains("space")
|| error.to_string().contains("storage"),
"Error should indicate disk space issue: {}",
error
);
}
/// Test: Partial download is cleaned up on failure
#[tokio::test]
async fn test_partial_download_cleaned_up() {
// Arrange: Downloader that fails mid-download
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let downloader = create_test_downloader_that_fails_midway(temp_dir.path()).await;
let request = create_test_request();
// Get initial file count
let initial_files = std::fs::read_dir(temp_dir.path())
.expect("Failed to read dir")
.count();
// Act: Download (will fail)
let result = downloader.download(request).await;
// Assert: Partial files are cleaned up
assert!(result.is_err(), "Should fail");
let final_files = std::fs::read_dir(temp_dir.path())
.expect("Failed to read dir")
.count();
assert_eq!(
initial_files, final_files,
"Partial files should be cleaned up"
);
}
/// Test: Concurrent download limits are enforced
#[tokio::test]
async fn test_concurrent_download_limits_enforced() {
// Arrange: Service with max 2 concurrent downloads
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let service = create_test_service_with_concurrency_limit(temp_dir.path(), 2).await;
// Act: Schedule 5 downloads
let mut job_ids = vec![];
for i in 0..5 {
let mut request = create_test_request();
request.description = format!("Download {}", i);
let response = service
.schedule_download(request)
.await
.expect("Schedule failed");
job_ids.push(response.job_id);
}
// Wait briefly for downloads to start
tokio::time::sleep(Duration::from_millis(100)).await;
// Assert: Only 2 are downloading, rest are pending
let mut downloading_count = 0;
let mut pending_count = 0;
for job_id in job_ids {
let status = service
.get_download_status(job_id)
.await
.expect("Get status failed");
match status.job_details.status {
2 => downloading_count += 1, // DOWNLOADING
1 => pending_count += 1, // PENDING
_ => {},
}
}
assert_eq!(
downloading_count, 2,
"Should have exactly 2 concurrent downloads"
);
assert!(pending_count >= 3, "Remaining should be pending");
}
/// Test: Error messages are descriptive
#[tokio::test]
async fn test_error_messages_are_descriptive() {
// Arrange: Various error scenarios
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let test_cases = vec![
("network", "Failed to connect to Databento API"),
("auth", "Authentication failed: invalid API key"),
("rate_limit", "Rate limit exceeded: retry after"),
("not_found", "Symbol not found in dataset"),
(
"invalid_date",
"Invalid date range: start_date must be before end_date",
),
];
for (error_type, expected_message_fragment) in test_cases {
let downloader = create_test_downloader_with_error_type(temp_dir.path(), error_type).await;
let request = create_test_request();
// Act: Trigger error
let result = downloader.download(request).await;
// Assert: Error message is descriptive
assert!(
result.is_err(),
"Should fail for error type: {}",
error_type
);
let error = result.unwrap_err();
assert!(
error.to_string().contains(expected_message_fragment),
"Error for {} should contain '{}', got: {}",
error_type,
expected_message_fragment,
error
);
}
}