//! E2E integration tests using real MinIO //! //! Prerequisites: //! - MinIO must be running: docker-compose up -d minio //! - Credentials: foxhunt_test / foxhunt_test_password //! - Endpoint: http://localhost:9000 //! //! These tests verify the full storage stack against a real S3-compatible backend. use std::sync::Arc; use storage::object_store_backend::ObjectStoreBackend; use storage::Storage; const TEST_BUCKET: &str = "test-bucket"; /// Helper to create MinIO backend for testing async fn create_minio_backend() -> ObjectStoreBackend { storage::object_store_backend::test_helpers::new_for_minio_testing(TEST_BUCKET.to_string()) .await .expect("Failed to create MinIO backend - is MinIO running?") } /// Test basic store and retrieve operations #[tokio::test] #[ignore = "Run with: cargo test --test minio_e2e_tests -- --ignored"] async fn test_minio_store_and_retrieve() { let backend = create_minio_backend().await; let test_data = b"Hello MinIO!"; let path = "test/basic.txt"; // Store backend .store(path, test_data) .await .expect("Failed to store data"); // Retrieve let retrieved = backend .retrieve(path) .await .expect("Failed to retrieve data"); assert_eq!( retrieved, test_data, "Retrieved data should match stored data" ); // Cleanup let deleted = backend.delete(path).await.expect("Failed to delete"); assert!(deleted, "File should be deleted"); } /// Test exists operation #[tokio::test] #[ignore = "Requires MinIO - run with --ignored"] async fn test_minio_exists() { let backend = create_minio_backend().await; let path = "test/exists.txt"; // Should not exist initially let exists_before = backend.exists(path).await.expect("exists() failed"); assert!(!exists_before, "File should not exist before storing"); // Store file backend.store(path, b"test").await.expect("store() failed"); // Should exist now let exists_after = backend.exists(path).await.expect("exists() failed"); assert!(exists_after, "File should exist after storing"); // Cleanup backend.delete(path).await.expect("delete() failed"); } /// Test delete operation #[tokio::test] #[ignore = "Requires MinIO - run with --ignored"] async fn test_minio_delete() { let backend = create_minio_backend().await; let path = "test/delete.txt"; // Store file backend .store(path, b"to be deleted") .await .expect("store() failed"); // Delete should succeed let deleted = backend.delete(path).await.expect("delete() failed"); assert!(deleted, "Delete should return true for existing file"); // Delete non-existent should return false let deleted_again = backend.delete(path).await.expect("delete() failed"); assert!( !deleted_again, "Delete should return false for non-existent file" ); } /// Test list operation #[tokio::test] #[ignore = "Requires MinIO - run with --ignored"] async fn test_minio_list() { let backend = create_minio_backend().await; let prefix = "test/list/"; // Store multiple files for i in 0..3 { let path = format!("{}file{}.txt", prefix, i); backend .store(&path, format!("content {}", i).as_bytes()) .await .expect("store() failed"); } // List files let files = backend.list(prefix).await.expect("list() failed"); assert!(files.len() >= 3, "Should list at least 3 files"); // Cleanup for file in files { backend.delete(&file).await.expect("delete() failed"); } } /// Test metadata operation #[tokio::test] #[ignore = "Requires MinIO - run with --ignored"] async fn test_minio_metadata() { let backend = create_minio_backend().await; let path = "test/metadata.txt"; let test_data = b"metadata test content"; // Store file backend .store(path, test_data) .await .expect("store() failed"); // Get metadata let metadata = backend.metadata(path).await.expect("metadata() failed"); assert_eq!(metadata.path, path); assert_eq!(metadata.size, test_data.len() as u64); assert!(metadata.etag.is_some(), "ETag should be present"); // Cleanup backend.delete(path).await.expect("delete() failed"); } /// Test large file upload and download #[tokio::test] #[ignore = "Requires MinIO - run with --ignored"] async fn test_minio_large_file() { let backend = create_minio_backend().await; let path = "test/large.bin"; // 5MB test file let test_data: Vec = (0..5_000_000).map(|i| (i % 256) as u8).collect(); // Store backend .store(path, &test_data) .await .expect("store() failed"); // Retrieve let retrieved = backend.retrieve(path).await.expect("retrieve() failed"); assert_eq!(retrieved.len(), test_data.len(), "Size should match"); assert_eq!(retrieved, test_data, "Content should match"); // Cleanup backend.delete(path).await.expect("delete() failed"); } /// Test download with progress callback #[tokio::test] #[ignore = "Requires MinIO - run with --ignored"] async fn test_minio_download_with_progress() { let backend = create_minio_backend().await; let path = "test/progress.txt"; let test_data = b"Progress tracking test"; // Store file backend .store(path, test_data) .await .expect("store() failed"); // Track progress let progress_calls = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let progress_calls_clone = Arc::clone(&progress_calls); let callback = Arc::new(move |downloaded: u64, total: u64| { progress_calls_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst); println!("Downloaded: {} / {} bytes", downloaded, total); }); // Download with progress let retrieved = backend .download_with_progress(path, Some(callback)) .await .expect("download_with_progress() failed"); assert_eq!(retrieved, test_data); assert!( progress_calls.load(std::sync::atomic::Ordering::SeqCst) >= 2, "Progress callback should be called at least twice" ); // Cleanup backend.delete(path).await.expect("delete() failed"); } /// Test streaming download with progress #[tokio::test] #[ignore = "Requires MinIO - run with --ignored"] async fn test_minio_stream_download_with_progress() { let backend = create_minio_backend().await; let path = "test/stream.txt"; let test_data = b"Streaming download test data"; // Store file backend .store(path, test_data) .await .expect("store() failed"); // Track progress let progress_calls = Arc::new(std::sync::atomic::AtomicUsize::new(0)); let progress_calls_clone = Arc::clone(&progress_calls); let callback = Arc::new(move |downloaded: u64, total: u64| { progress_calls_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst); println!("Stream downloaded: {} / {} bytes", downloaded, total); }); // Stream download with progress let retrieved = backend .stream_download_with_progress(path, 1024, callback) .await .expect("stream_download_with_progress() failed"); assert_eq!(retrieved, test_data); assert!( progress_calls.load(std::sync::atomic::Ordering::SeqCst) > 0, "Progress callback should be called" ); // Cleanup backend.delete(path).await.expect("delete() failed"); } /// Test parallel download #[tokio::test] #[ignore = "Requires MinIO - run with --ignored"] async fn test_minio_parallel_download() { let backend = create_minio_backend().await; let prefix = "test/parallel/"; // Store multiple files let mut paths = Vec::new(); for i in 0..5 { let path = format!("{}file{}.txt", prefix, i); backend .store(&path, format!("content {}", i).as_bytes()) .await .expect("store() failed"); paths.push(path); } // Parallel download let results = backend .parallel_download(paths.clone(), None) .await .expect("parallel_download() failed"); assert_eq!(results.len(), 5, "Should download all 5 files"); for (i, (path, data)) in results.iter().enumerate() { let expected_content = format!("content {}", i); assert_eq!( std::str::from_utf8(data).unwrap(), expected_content, "Content should match for {}", path ); } // Cleanup for path in paths { backend.delete(&path).await.expect("delete() failed"); } } /// Test path helpers #[tokio::test] #[ignore = "Requires MinIO - run with --ignored"] async fn test_minio_path_helpers() { let backend = create_minio_backend().await; // Test model path let model_path = backend.get_model_path("mamba2", "v1.0", "model.safetensors"); assert_eq!(model_path, "models/mamba2/v1.0/model.safetensors"); // Test checkpoint path let checkpoint_path = backend.get_checkpoint_path("dqn", "checkpoint_001"); assert_eq!(checkpoint_path, "models/dqn/checkpoints/checkpoint_001"); // Test metadata path let metadata_path = backend.get_metadata_path("tft", "v2.1"); assert_eq!(metadata_path, "models/tft/v2.1/metadata.json"); } /// Test error handling - non-existent file #[tokio::test] #[ignore = "Requires MinIO - run with --ignored"] async fn test_minio_error_not_found() { let backend = create_minio_backend().await; let result = backend.retrieve("nonexistent/file.txt").await; assert!(result.is_err(), "Should fail for non-existent file"); } /// Test concurrent operations #[tokio::test] #[ignore = "Requires MinIO - run with --ignored"] async fn test_minio_concurrent_operations() { let backend = Arc::new(create_minio_backend().await); let mut handles = vec![]; for i in 0..10 { let backend = Arc::clone(&backend); let handle = tokio::spawn(async move { let path = format!("test/concurrent/file{}.txt", i); let data = format!("content {}", i); // Store backend.store(&path, data.as_bytes()).await?; // Retrieve let retrieved = backend.retrieve(&path).await?; assert_eq!(retrieved, data.as_bytes()); // Delete backend.delete(&path).await?; Ok::<(), storage::error::StorageError>(()) }); handles.push(handle); } // Wait for all operations for handle in handles { handle .await .expect("Task panicked") .expect("Operation failed"); } } /// Test empty file handling #[tokio::test] #[ignore = "Requires MinIO - run with --ignored"] async fn test_minio_empty_file() { let backend = create_minio_backend().await; let path = "test/empty.txt"; let empty_data: &[u8] = b""; // Store empty file backend .store(path, empty_data) .await .expect("store() failed"); // Retrieve empty file let retrieved = backend.retrieve(path).await.expect("retrieve() failed"); assert_eq!( retrieved, empty_data, "Empty file should be handled correctly" ); // Cleanup backend.delete(path).await.expect("delete() failed"); }