//! 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 ); } }