use futures::future::join_all;
use s3tui::model::local_selected_item::LocalSelectedItem;
use s3tui::model::s3_selected_item::S3SelectedItem;
use s3tui::model::transfer_state::TransferState;
use s3tui::services::s3_data_fetcher::S3DataFetcher;
use s3tui::settings::file_credentials::FileCredential;
use std::io::Write;
use tempfile::NamedTempFile;
use testcontainers::runners::AsyncRunner;
use testcontainers::ContainerAsync;
use testcontainers_modules::minio::MinIO;
use tokio::sync::mpsc;
const MINIO_ACCESS_KEY: &str = "minioadmin";
const MINIO_SECRET_KEY: &str = "minioadmin";
fn create_minio_credential(port: u16) -> FileCredential {
FileCredential {
name: "minio-test".to_string(),
access_key: MINIO_ACCESS_KEY.to_string(),
secret_key: MINIO_SECRET_KEY.to_string(),
default_region: "us-east-1".to_string(),
selected: true,
endpoint_url: Some(format!("http://127.0.0.1:{}", port)),
force_path_style: true,
}
}
async fn setup_minio() -> (ContainerAsync<MinIO>, FileCredential) {
let container = MinIO::default()
.start()
.await
.expect("Failed to start MinIO container");
let port = container
.get_host_port_ipv4(9000)
.await
.expect("Failed to get MinIO port");
let creds = create_minio_credential(port);
(container, creds)
}
fn create_test_file(content: &[u8]) -> NamedTempFile {
let mut file = NamedTempFile::new().expect("Failed to create temp file");
file.write_all(content).expect("Failed to write to temp file");
file.flush().expect("Failed to flush temp file");
file
}
#[tokio::test]
#[ignore] async fn test_minio_list_buckets() {
let (_container, creds) = setup_minio().await;
let fetcher = S3DataFetcher::new(creds);
let buckets = fetcher
.list_current_location(None, None)
.await
.expect("Failed to list buckets");
assert!(buckets.is_empty(), "Expected no buckets initially");
}
#[tokio::test]
#[ignore] async fn test_minio_create_and_list_bucket() {
let (_container, creds) = setup_minio().await;
let fetcher = S3DataFetcher::new(creds);
let bucket_name = "test-bucket";
let result = fetcher
.create_bucket(bucket_name.to_string(), "us-east-1".to_string())
.await
.expect("Failed to create bucket");
assert!(result.is_none(), "Expected no error creating bucket");
let buckets = fetcher
.list_current_location(None, None)
.await
.expect("Failed to list buckets");
assert_eq!(buckets.len(), 1, "Expected one bucket");
assert_eq!(buckets[0].name, bucket_name);
assert!(buckets[0].is_bucket, "Expected item to be a bucket");
}
#[tokio::test]
#[ignore] async fn test_minio_upload_and_list_objects() {
let (_container, creds) = setup_minio().await;
let fetcher = S3DataFetcher::new(creds.clone());
let bucket_name = "upload-test-bucket";
fetcher
.create_bucket(bucket_name.to_string(), "us-east-1".to_string())
.await
.expect("Failed to create bucket");
let test_content = b"Hello, MinIO!";
let test_file = create_test_file(test_content);
let file_path = test_file.path().to_str().unwrap().to_string();
let (tx, mut rx) = mpsc::channel(100);
let upload_item = LocalSelectedItem {
name: "test-file.txt".to_string(),
path: file_path,
is_directory: false,
destination_bucket: bucket_name.to_string(),
destination_path: "test-file.txt".to_string(),
s3_creds: creds.clone(),
children: None,
transfer_state: TransferState::Pending,
job_id: None,
};
let result = fetcher.upload_item(upload_item, tx, None).await;
assert!(result.is_ok(), "Upload should succeed");
rx.close();
while rx.recv().await.is_some() {}
let objects = fetcher
.list_current_location(Some(bucket_name.to_string()), None)
.await
.expect("Failed to list objects");
assert_eq!(objects.len(), 1, "Expected one object");
assert_eq!(objects[0].name, "test-file.txt");
}
#[tokio::test]
#[ignore] async fn test_minio_upload_and_download() {
let (_container, creds) = setup_minio().await;
let fetcher = S3DataFetcher::new(creds.clone());
let bucket_name = "download-test-bucket";
fetcher
.create_bucket(bucket_name.to_string(), "us-east-1".to_string())
.await
.expect("Failed to create bucket");
let test_content = b"Test content for download verification";
let test_file = create_test_file(test_content);
let file_path = test_file.path().to_str().unwrap().to_string();
let (upload_tx, mut upload_rx) = mpsc::channel(100);
let upload_item = LocalSelectedItem {
name: "download-test.txt".to_string(),
path: file_path,
is_directory: false,
destination_bucket: bucket_name.to_string(),
destination_path: "download-test.txt".to_string(),
s3_creds: creds.clone(),
children: None,
transfer_state: TransferState::Pending,
job_id: None,
};
fetcher
.upload_item(upload_item, upload_tx, None)
.await
.expect("Upload failed");
upload_rx.close();
while upload_rx.recv().await.is_some() {}
let download_dir = tempfile::tempdir().expect("Failed to create temp dir");
let download_path = download_dir.path().to_str().unwrap().to_string();
let (download_tx, mut download_rx) = mpsc::channel(100);
let download_item = S3SelectedItem {
bucket: Some(bucket_name.to_string()),
name: "download-test.txt".to_string(),
path: Some("download-test.txt".to_string()),
is_directory: false,
is_bucket: false,
destination_dir: download_path.clone(),
s3_creds: creds,
children: None,
transfer_state: TransferState::Pending,
job_id: None,
};
fetcher
.download_item(download_item, download_tx, None)
.await
.expect("Download failed");
download_rx.close();
while download_rx.recv().await.is_some() {}
let downloaded_content =
std::fs::read(format!("{}/download-test.txt", download_path)).expect("Failed to read downloaded file");
assert_eq!(
downloaded_content, test_content,
"Downloaded content should match original"
);
}
#[tokio::test]
#[ignore] async fn test_minio_delete_object() {
let (_container, creds) = setup_minio().await;
let fetcher = S3DataFetcher::new(creds.clone());
let bucket_name = "delete-test-bucket";
fetcher
.create_bucket(bucket_name.to_string(), "us-east-1".to_string())
.await
.expect("Failed to create bucket");
let test_file = create_test_file(b"Delete me");
let file_path = test_file.path().to_str().unwrap().to_string();
let (tx, mut rx) = mpsc::channel(100);
let upload_item = LocalSelectedItem {
name: "to-delete.txt".to_string(),
path: file_path,
is_directory: false,
destination_bucket: bucket_name.to_string(),
destination_path: "to-delete.txt".to_string(),
s3_creds: creds.clone(),
children: None,
transfer_state: TransferState::Pending,
job_id: None,
};
fetcher.upload_item(upload_item, tx, None).await.expect("Upload failed");
rx.close();
while rx.recv().await.is_some() {}
let objects_before = fetcher
.list_current_location(Some(bucket_name.to_string()), None)
.await
.expect("Failed to list objects");
assert_eq!(objects_before.len(), 1);
let delete_result = fetcher
.delete_data(false, Some(bucket_name.to_string()), "to-delete.txt".to_string(), false)
.await
.expect("Delete failed");
assert!(delete_result.is_none(), "Expected no error on delete");
let objects_after = fetcher
.list_current_location(Some(bucket_name.to_string()), None)
.await
.expect("Failed to list objects");
assert!(objects_after.is_empty(), "Object should be deleted");
}
#[tokio::test]
#[ignore] async fn test_minio_list_objects_with_prefix() {
let (_container, creds) = setup_minio().await;
let fetcher = S3DataFetcher::new(creds.clone());
let bucket_name = "prefix-test-bucket";
fetcher
.create_bucket(bucket_name.to_string(), "us-east-1".to_string())
.await
.expect("Failed to create bucket");
let files = vec![
("folder1/file1.txt", b"content1" as &[u8]),
("folder1/file2.txt", b"content2"),
("folder2/file3.txt", b"content3"),
("root-file.txt", b"root content"),
];
for (key, content) in files {
let test_file = create_test_file(content);
let file_path = test_file.path().to_str().unwrap().to_string();
let (tx, mut rx) = mpsc::channel(100);
let upload_item = LocalSelectedItem {
name: key.split('/').last().unwrap().to_string(),
path: file_path,
is_directory: false,
destination_bucket: bucket_name.to_string(),
destination_path: key.to_string(),
s3_creds: creds.clone(),
children: None,
transfer_state: TransferState::Pending,
job_id: None,
};
fetcher.upload_item(upload_item, tx, None).await.expect("Upload failed");
rx.close();
while rx.recv().await.is_some() {}
}
let all_objects = fetcher
.list_current_location(Some(bucket_name.to_string()), None)
.await
.expect("Failed to list objects");
assert!(
all_objects.len() >= 2,
"Expected at least 2 items at root level, got {}",
all_objects.len()
);
let folder1_objects = fetcher
.list_current_location(Some(bucket_name.to_string()), Some("folder1/".to_string()))
.await
.expect("Failed to list folder1 objects");
assert_eq!(folder1_objects.len(), 2, "Expected 2 files in folder1/");
}
#[tokio::test]
#[ignore] async fn test_concurrent_transfers() {
let (_container, creds) = setup_minio().await;
let fetcher = S3DataFetcher::new(creds.clone());
let bucket_name = "concurrent-test-bucket";
fetcher
.create_bucket(bucket_name.to_string(), "us-east-1".to_string())
.await
.expect("Failed to create bucket");
let file_count = 5;
let mut temp_files = Vec::new();
let mut handles = Vec::new();
for i in 0..file_count {
let content = format!("Content for file {}", i);
let test_file = create_test_file(content.as_bytes());
let file_path = test_file.path().to_str().unwrap().to_string();
temp_files.push(test_file);
let creds_clone = creds.clone();
let bucket = bucket_name.to_string();
let fetcher_clone = S3DataFetcher::new(creds_clone.clone());
let handle = tokio::spawn(async move {
let (tx, mut rx) = mpsc::channel(100);
let upload_item = LocalSelectedItem {
name: format!("concurrent-file-{}.txt", i),
path: file_path,
is_directory: false,
destination_bucket: bucket,
destination_path: format!("concurrent-file-{}.txt", i),
s3_creds: creds_clone,
children: None,
transfer_state: TransferState::Pending,
job_id: None,
};
let result = fetcher_clone.upload_item(upload_item, tx, None).await;
rx.close();
while rx.recv().await.is_some() {}
result
});
handles.push(handle);
}
let results: Vec<_> = join_all(handles).await;
for (i, result) in results.into_iter().enumerate() {
assert!(
result.is_ok(),
"Task {} should not panic",
i
);
assert!(
result.unwrap().is_ok(),
"Upload {} should succeed",
i
);
}
let objects = fetcher
.list_current_location(Some(bucket_name.to_string()), None)
.await
.expect("Failed to list objects");
assert_eq!(
objects.len(),
file_count,
"Expected {} objects, got {}",
file_count,
objects.len()
);
}
#[tokio::test]
#[ignore] async fn test_error_recovery_nonexistent_bucket() {
let (_container, creds) = setup_minio().await;
let fetcher = S3DataFetcher::new(creds.clone());
let result = fetcher
.list_current_location(Some("nonexistent-bucket".to_string()), None)
.await;
assert!(result.is_err(), "Listing non-existent bucket should fail gracefully");
}
#[tokio::test]
#[ignore] async fn test_error_recovery_upload_to_nonexistent_bucket() {
let (_container, creds) = setup_minio().await;
let fetcher = S3DataFetcher::new(creds.clone());
let test_file = create_test_file(b"Test content");
let file_path = test_file.path().to_str().unwrap().to_string();
let (tx, mut rx) = mpsc::channel(100);
let upload_item = LocalSelectedItem {
name: "test-file.txt".to_string(),
path: file_path,
is_directory: false,
destination_bucket: "nonexistent-bucket".to_string(),
destination_path: "test-file.txt".to_string(),
s3_creds: creds,
children: None,
transfer_state: TransferState::Pending,
job_id: None,
};
let result = fetcher.upload_item(upload_item, tx, None).await;
rx.close();
while rx.recv().await.is_some() {}
assert!(result.is_err(), "Upload to non-existent bucket should fail gracefully");
}
#[tokio::test]
#[ignore] async fn test_error_recovery_download_nonexistent_object() {
let (_container, creds) = setup_minio().await;
let fetcher = S3DataFetcher::new(creds.clone());
let bucket_name = "error-recovery-bucket";
fetcher
.create_bucket(bucket_name.to_string(), "us-east-1".to_string())
.await
.expect("Failed to create bucket");
let download_dir = tempfile::tempdir().expect("Failed to create temp dir");
let download_path = download_dir.path().to_str().unwrap().to_string();
let (tx, mut rx) = mpsc::channel(100);
let download_item = S3SelectedItem {
bucket: Some(bucket_name.to_string()),
name: "nonexistent-file.txt".to_string(),
path: Some("nonexistent-file.txt".to_string()),
is_directory: false,
is_bucket: false,
destination_dir: download_path,
s3_creds: creds,
children: None,
transfer_state: TransferState::Pending,
job_id: None,
};
let result = fetcher.download_item(download_item, tx, None).await;
rx.close();
while rx.recv().await.is_some() {}
assert!(result.is_err(), "Download of non-existent object should fail gracefully");
}