#![cfg(feature = "server")]
mod common;
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::{Delete, ObjectIdentifier};
use common::{create_s3_client, init_tracing};
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tempfile::TempDir;
use tokio::net::TcpListener;
struct VersioningTestServer {
pub _addr: SocketAddr,
pub _temp_dir: TempDir,
_handle: tokio::task::JoinHandle<()>,
}
async fn setup_versioning_server() -> (aws_sdk_s3::Client, VersioningTestServer) {
init_tracing();
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("Failed to bind to address");
let addr = listener.local_addr().expect("Failed to get local address");
let storage_root = temp_dir.path().to_path_buf();
let storage = Arc::new(
rs3gw::storage::StorageEngine::new(storage_root.clone())
.expect("Failed to create storage engine"),
);
let metrics_handle = rs3gw::metrics::init_metrics().expect("Failed to initialize metrics");
let config = rs3gw::Config {
bind_addr: addr,
storage_root,
default_bucket: "default".to_string(),
access_key: String::new(),
secret_key: String::new(),
compression: rs3gw::storage::CompressionMode::None,
request_timeout_secs: 0,
max_concurrent_requests: 0,
tls: rs3gw::TlsConfig::default(),
connection_pool: rs3gw::ConnectionPoolConfig::default(),
cluster: rs3gw::cluster::ClusterConfig::default(),
dedup: rs3gw::storage::DedupConfig::disabled(),
zerocopy: rs3gw::storage::ZeroCopyConfig::default(),
select_cache: rs3gw::SelectCacheConfig::default(),
multipart_retention_hours: 168,
fsync: false,
};
let preprocessing_path = temp_dir.path().join("preprocessing");
let preprocessing_manager = std::sync::Arc::new(
rs3gw::storage::preprocessing::PreprocessingManager::new(preprocessing_path),
);
let predictive_analytics = std::sync::Arc::new(rs3gw::observability::PredictiveAnalytics::new(
10_000,
0.023,
0.09,
0.0004,
1_000_000_000_000,
));
let metrics_tracker = std::sync::Arc::new(rs3gw::observability::MetricsTracker::new());
let select_result_cache =
std::sync::Arc::new(rs3gw::api::SelectResultCache::new(100, 10 * 1024 * 1024));
#[cfg(feature = "formats")]
let query_intelligence = std::sync::Arc::new(rs3gw::api::QueryIntelligence::new());
let training_manager = std::sync::Arc::new(rs3gw::storage::TrainingManager::new(
temp_dir.path().join("training"),
));
let state = rs3gw::AppState {
config,
storage,
metrics_handle,
cache: None,
throttle: None,
quota: None,
event_broadcaster: rs3gw::api::EventBroadcaster::new(),
#[cfg(feature = "formats")]
query_plan_cache: None,
select_result_cache,
#[cfg(feature = "formats")]
query_intelligence,
advanced_replication: None,
preprocessing_manager,
predictive_analytics,
metrics_tracker,
usage_tracker: std::sync::Arc::new(rs3gw::observability::UsageTracker::new()),
training_manager,
start_time: std::time::Instant::now(),
verifier: None,
auth_failure_counts: std::sync::Arc::new(std::sync::Mutex::new(
std::collections::HashMap::new(),
)),
in_flight: rs3gw::InFlightTracker::new(),
encryption: std::sync::Arc::new(rs3gw::storage::encryption::EncryptionService::new(
std::sync::Arc::new(rs3gw::storage::encryption::LocalKeyProvider::default()),
)),
};
let app = axum::Router::new()
.merge(rs3gw::api::s3_router::routes())
.layer(tower_http::trace::TraceLayer::new_for_http())
.with_state(state);
let server_handle = tokio::spawn(async move {
axum::serve(listener, app)
.await
.expect("Server failed to start");
});
tokio::time::sleep(Duration::from_millis(100)).await;
let client = create_s3_client(addr).await;
let server = VersioningTestServer {
_addr: addr,
_temp_dir: temp_dir,
_handle: server_handle,
};
(client, server)
}
#[tokio::test]
async fn test_versioning_basic_operations() {
let (client, _server) = setup_versioning_server().await;
client
.create_bucket()
.bucket("test-versioning")
.send()
.await
.expect("Failed to create bucket");
let versioning_result = client
.get_bucket_versioning()
.bucket("test-versioning")
.send()
.await
.expect("Failed to get bucket versioning");
assert!(
versioning_result.status().is_none()
|| versioning_result.status().map(|s| s.as_str()) == Some("Suspended"),
"Versioning should be disabled by default"
);
client
.put_bucket_versioning()
.bucket("test-versioning")
.versioning_configuration(
aws_sdk_s3::types::VersioningConfiguration::builder()
.status(aws_sdk_s3::types::BucketVersioningStatus::Enabled)
.build(),
)
.send()
.await
.expect("Failed to enable versioning");
println!("✓ Basic versioning operations work correctly");
}
#[tokio::test]
async fn test_object_versions() {
let (client, _server) = setup_versioning_server().await;
client
.create_bucket()
.bucket("test-versions")
.send()
.await
.expect("Failed to create bucket");
let version1_data = b"This is version 1";
client
.put_object()
.bucket("test-versions")
.key("test-object")
.body(ByteStream::from(version1_data.to_vec()))
.send()
.await
.expect("Failed to upload version 1");
let version2_data = b"This is version 2";
client
.put_object()
.bucket("test-versions")
.key("test-object")
.body(ByteStream::from(version2_data.to_vec()))
.send()
.await
.expect("Failed to upload version 2");
let version3_data = b"This is version 3";
client
.put_object()
.bucket("test-versions")
.key("test-object")
.body(ByteStream::from(version3_data.to_vec()))
.send()
.await
.expect("Failed to upload version 3");
let result = client
.get_object()
.bucket("test-versions")
.key("test-object")
.send()
.await
.expect("Failed to get latest version");
let body = result.body.collect().await.expect("Failed to collect body");
assert_eq!(
body.into_bytes().as_ref(),
version3_data,
"Latest version should be version 3"
);
println!("✓ Multiple object versions work correctly");
}
#[tokio::test]
async fn test_list_object_versions() {
let (client, _server) = setup_versioning_server().await;
client
.create_bucket()
.bucket("test-list-versions")
.send()
.await
.expect("Failed to create bucket");
for i in 1..=5 {
let data = format!("Version {}", i);
client
.put_object()
.bucket("test-list-versions")
.key("versioned-object")
.body(ByteStream::from(data.into_bytes()))
.send()
.await
.expect("Failed to upload version");
}
let versions_result = client
.list_object_versions()
.bucket("test-list-versions")
.send()
.await
.expect("Failed to list object versions");
let versions = versions_result.versions();
assert!(!versions.is_empty(), "Should have at least one version");
println!("✓ Listed {} object versions successfully", versions.len());
}
#[tokio::test]
async fn test_delete_with_versioning() {
let (client, _server) = setup_versioning_server().await;
client
.create_bucket()
.bucket("test-delete-versions")
.send()
.await
.expect("Failed to create bucket");
let data = b"Test data for deletion";
client
.put_object()
.bucket("test-delete-versions")
.key("delete-test")
.body(ByteStream::from(data.to_vec()))
.send()
.await
.expect("Failed to upload object");
client
.delete_object()
.bucket("test-delete-versions")
.key("delete-test")
.send()
.await
.expect("Failed to delete object");
let get_result = client
.get_object()
.bucket("test-delete-versions")
.key("delete-test")
.send()
.await;
assert!(
get_result.is_err(),
"Object should not be accessible after deletion"
);
println!("✓ Delete with versioning creates delete marker correctly");
}
#[tokio::test]
async fn test_batch_delete_with_versions() {
let (client, _server) = setup_versioning_server().await;
client
.create_bucket()
.bucket("test-batch-delete")
.send()
.await
.expect("Failed to create bucket");
for i in 1..=5 {
client
.put_object()
.bucket("test-batch-delete")
.key(format!("object-{}", i))
.body(ByteStream::from(format!("Data {}", i).into_bytes()))
.send()
.await
.expect("Failed to upload object");
}
let objects_to_delete: Vec<ObjectIdentifier> = (1..=5)
.map(|i| {
ObjectIdentifier::builder()
.key(format!("object-{}", i))
.build()
.expect("Failed to build object identifier")
})
.collect();
client
.delete_objects()
.bucket("test-batch-delete")
.delete(
Delete::builder()
.set_objects(Some(objects_to_delete))
.build()
.expect("Failed to build delete request"),
)
.send()
.await
.expect("Failed to batch delete objects");
let list_result = client
.list_objects_v2()
.bucket("test-batch-delete")
.send()
.await
.expect("Failed to list objects");
let object_count = list_result.contents().len();
println!(
"✓ Batch delete processed successfully, {} objects remaining",
object_count
);
}
#[tokio::test]
async fn test_copy_with_versioning() {
let (client, _server) = setup_versioning_server().await;
client
.create_bucket()
.bucket("test-copy-versions")
.send()
.await
.expect("Failed to create bucket");
let source_data = b"Source object data";
client
.put_object()
.bucket("test-copy-versions")
.key("source-object")
.body(ByteStream::from(source_data.to_vec()))
.send()
.await
.expect("Failed to upload source object");
client
.copy_object()
.bucket("test-copy-versions")
.key("copied-object")
.copy_source("test-copy-versions/source-object")
.send()
.await
.expect("Failed to copy object");
let result = client
.get_object()
.bucket("test-copy-versions")
.key("copied-object")
.send()
.await
.expect("Failed to get copied object");
let body = result.body.collect().await.expect("Failed to collect body");
assert_eq!(
body.into_bytes().as_ref(),
source_data,
"Copied object content should match source"
);
println!("✓ Copy with versioning works correctly");
}
#[tokio::test]
async fn test_head_object_with_versioning() {
let (client, _server) = setup_versioning_server().await;
client
.create_bucket()
.bucket("test-head-versions")
.send()
.await
.expect("Failed to create bucket");
let data = b"Test data for head operation";
let _put_result = client
.put_object()
.bucket("test-head-versions")
.key("test-object")
.body(ByteStream::from(data.to_vec()))
.send()
.await
.expect("Failed to upload object");
let head_result = client
.head_object()
.bucket("test-head-versions")
.key("test-object")
.send()
.await
.expect("Failed to head object");
assert_eq!(
head_result.content_length().unwrap_or(0) as usize,
data.len(),
"Content length should match"
);
let version_id = head_result.version_id();
println!("✓ HeadObject returned version_id: {:?}", version_id);
}