Compare commits

..

2 Commits

Author SHA1 Message Date
Zhengchao An f12f3c47e1 test(e2e): finish the serial sweep, leaving only markers that mean something (#6240)
Removes the remaining 230 no-op `#[serial]` markers across 77 files, plus the 77 imports that went with them. Stacked on the six-suite batch; together they take `crates/e2e_test` from 345 markers to 36.

The reasoning is unchanged from #6209: `#[serial]` is an in-process mutex and cargo-nextest gives every test its own process, so only a `[test-groups]` binding with `max-threads = 1` serializes anything.

Everything still carrying a marker now has a reason to:

  inline_fast_path_cluster_test     14   test-group e2e-inline-boundaries
  policy/policy_variables_test       6   binds a fixed port
  kms/kms_vault_test                 5   test-group e2e-vault
  reliability_disk_fault_test        4   test-group e2e-reliability
  degraded_read_eof_regression_test  3   test-group e2e-reliability
  replacement_privileged_e2e_test    2   test-group e2e-reliability
  protocols/webdav_core              1   binds fixed ports
  policy/test_runner                 1   binds a fixed port

The three fixed-port files are held back rather than swept, because their markers are not merely useless: `PolicyTestEnvironment::with_address("127.0.0.1:9000")` and webdav_core's `127.0.0.1:9080`/`:9010` bind fixed ports, which an in-process mutex cannot protect against a second test process. They need a test-group, which is a config change rather than a deletion, so it is filed on the issue instead.

`security_boundary_test` was checked and swept: its `127.0.0.1:8080` strings are SSRF targets fed to AddTier in a negative test, not ports it binds.

Refs backlog#1846 (T1)
2026-08-19 10:29:36 +08:00
overtrue 83bf7649b1 test(e2e): drop 79 no-op serial markers from six more suites
serial_test's `#[serial]` is an in-process mutex. cargo-nextest, this repo's authoritative runner, gives every test its own process, so the mutex is never contended; cross-process serialization comes only from a `[test-groups]` entry with `max-threads = 1`. This continues #6209 and #6213.

Six more suites, none of them bound to a test group:

  object_lambda_test                  16
  special_chars_test                  14
  quota_test                          14
  archive_download_integrity_test     13
  list_objects_v2_pagination_test     12
  version_id_regression_test          10

Five of these do appear in `.config/nextest.toml`, but only in a lane's `default-filter` or a `slow-timeout` override — neither confers serialization. `inline_fast_path_cluster_test` is left alone precisely because it *is* bound to a group.

Each suite self-isolates: every test builds its own server through `RustFSTestEnvironment::new()` (UUID temp dir, allocated port), quota_test wrapping it in `QuotaTestEnv::new()` with a UUID bucket per test. No test mutates process env — quota_test passes its variables to the child server via `start_rustfs_server_with_env` — and the only literal addresses are `127.0.0.1:0`, which asks the kernel for a free port.

All 79 markers were bare `#[serial]`, no named groups, so the `use serial_test::serial;` import goes with the last marker in each of the six files and stays untouched in the 266 markers still spread across the crate.

Pure deletion, no test renamed and no behaviour changed.

Refs backlog#1846 (T1)
2026-08-19 10:07:21 +08:00
92 changed files with 760 additions and 1318 deletions
-5
View File
@@ -38,7 +38,6 @@ mod tests {
use rustfs_signer::constants::UNSIGNED_PAYLOAD;
use rustfs_signer::sign_v4;
use s3s::Body;
use serial_test::serial;
use std::error::Error;
use std::io::Read;
use std::process::{Command, Stdio};
@@ -162,7 +161,6 @@ mod tests {
/// A fully authenticated but non-admin credential must be rejected with
/// `403 AccessDenied` on an admin API, while the root credential succeeds.
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn non_admin_credential_denied_on_admin_api() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -198,7 +196,6 @@ mod tests {
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn non_admin_credential_denied_on_manual_transition_run() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -326,7 +323,6 @@ mod tests {
/// credential is accepted and the old one is rejected, on both the S3 data
/// plane and the admin plane.
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn root_credential_rotation_takes_effect() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -389,7 +385,6 @@ mod tests {
/// runtime. We capture the child's stdout/stderr directly (the shared
/// harness inherits stdio) and poll for the warning until it appears.
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn default_credentials_emit_startup_warning() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -33,7 +33,6 @@ use aws_sdk_s3::config::{Credentials, Region};
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::{Client, Config};
use reqwest::StatusCode;
use serial_test::serial;
use std::error::Error;
use tokio::time::{Duration, sleep};
@@ -178,7 +177,6 @@ async fn assert_admin_status(
}
#[tokio::test]
#[serial]
async fn test_update_service_account_enforces_owner_and_parent_scope() -> TestResult {
init_logging();
@@ -348,7 +346,6 @@ async fn test_update_service_account_enforces_owner_and_parent_scope() -> TestRe
/// Full user -> policy -> service-account lifecycle, proving each management
/// call takes effect on the data plane, not just that the endpoint answers 200.
#[tokio::test]
#[serial]
async fn test_admin_user_policy_service_account_crud_lifecycle() -> TestResult {
init_logging();
@@ -573,7 +570,6 @@ async fn test_admin_user_policy_service_account_crud_lifecycle() -> TestResult {
/// non-admin credential with 403 AccessDenied (sec-4 assertion pattern; the
/// gate implementation itself is owned by sec-4 / admin_auth_test).
#[tokio::test]
#[serial]
async fn test_admin_iam_endpoints_deny_non_admin_credential() -> TestResult {
init_logging();
@@ -21,7 +21,6 @@ use rustfs_signer::constants::UNSIGNED_PAYLOAD;
use rustfs_signer::sign_v4;
use s3s::Body;
use serde::Deserialize;
use serial_test::serial;
use std::error::Error;
use std::process::Command;
use tokio::time::{Duration, sleep, timeout};
@@ -100,7 +99,6 @@ fn offline_server_count(info: &InfoMessage) -> usize {
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn test_single_admin_timeout_does_not_immediately_mark_peer_offline() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -18,7 +18,6 @@
use crate::common::{RustFSTestEnvironment, init_logging, local_http_client};
use aws_sdk_s3::types::PublicAccessBlockConfiguration;
use serial_test::serial;
use tracing::info;
async fn setup_public_bucket(
@@ -73,7 +72,6 @@ async fn anonymous_get_object(
/// Issue #2036: Anonymous GetObject should succeed when bucket policy allows it
/// and no PublicAccessBlock configuration exists (ConfigNotFound).
#[tokio::test]
#[serial]
async fn test_anonymous_access_allowed_when_public_access_block_missing() -> Result<(), Box<dyn std::error::Error + Send + Sync>>
{
init_logging();
@@ -100,7 +98,6 @@ async fn test_anonymous_access_allowed_when_public_access_block_missing() -> Res
/// Anonymous GetObject should be denied when RestrictPublicBuckets is true.
#[tokio::test]
#[serial]
async fn test_anonymous_access_denied_when_restrict_public_buckets_enabled()
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
@@ -137,7 +134,6 @@ async fn test_anonymous_access_denied_when_restrict_public_buckets_enabled()
/// Anonymous GetObject should succeed when PublicAccessBlock exists but
/// RestrictPublicBuckets is explicitly false.
#[tokio::test]
#[serial]
async fn test_anonymous_access_allowed_when_restrict_public_buckets_disabled()
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
@@ -176,7 +172,6 @@ async fn test_anonymous_access_allowed_when_restrict_public_buckets_disabled()
/// reaches authorization through a fallback branch, and that branch has to apply the
/// same public-access gate as a direct grant.
#[tokio::test]
#[serial]
async fn ghsa_x298_anonymous_list_object_versions_denied_when_restrict_public_buckets_enabled()
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
@@ -18,13 +18,11 @@
//! completely inert with default configuration.
use crate::common::{RustFSTestEnvironment, init_logging, local_http_client};
use serial_test::serial;
use tracing::info;
type TestResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;
#[tokio::test]
#[serial]
async fn api_rate_limit_enforces_429_with_retry_after_when_enabled() -> TestResult {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -87,7 +85,6 @@ async fn api_rate_limit_enforces_429_with_retry_after_when_enabled() -> TestResu
}
#[tokio::test]
#[serial]
async fn api_rate_limit_bucket_dimension_throttles_per_bucket() -> TestResult {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -133,7 +130,6 @@ async fn api_rate_limit_bucket_dimension_throttles_per_bucket() -> TestResult {
}
#[tokio::test]
#[serial]
async fn api_rate_limit_stays_inert_by_default() -> TestResult {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -24,7 +24,6 @@ mod tests {
use rustfs_signer::constants::UNSIGNED_PAYLOAD;
use rustfs_signer::{pre_sign_v4, sign_v4};
use s3s::Body;
use serial_test::serial;
use sha2::{Digest, Sha256};
use std::error::Error;
use std::io::{Cursor, Write};
@@ -339,7 +338,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_archive_put_allows_content_encoding_by_default() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -367,7 +365,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_archive_put_rejects_content_encoding_when_strict_mode_enabled() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -391,7 +388,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_archive_put_with_aws_chunked_does_not_persist_content_encoding_by_default()
-> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -427,7 +423,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_archive_put_with_aws_chunked_and_effective_encoding_roundtrips_by_default()
-> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -463,7 +458,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_archive_put_with_aws_chunked_allowed_when_strict_mode_enabled() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -498,7 +492,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_archive_put_with_aws_chunked_and_effective_encoding_rejects_when_strict_mode_enabled()
-> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -529,7 +522,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_archive_download_roundtrip_with_http_compression_enabled() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -591,7 +583,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_archive_multipart_roundtrip_preserves_bytes() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -687,7 +678,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_multipart_get_ignores_empty_conditional_etag_headers() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -723,7 +713,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_archive_multipart_with_aws_chunked_and_effective_encoding_roundtrips_by_default()
-> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -753,7 +742,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_archive_multipart_with_aws_chunked_allowed_when_strict_mode_enabled() -> Result<(), Box<dyn Error + Send + Sync>>
{
init_logging();
@@ -783,7 +771,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_archive_multipart_with_aws_chunked_and_effective_encoding_rejects_when_strict_mode_enabled()
-> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -816,7 +803,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_presigned_get_and_reverse_proxy_preserve_multipart_bytes() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -24,11 +24,9 @@ mod tests {
};
use http::Method;
use http::header::CONTENT_TYPE;
use serial_test::serial;
use tracing::info;
#[tokio::test]
#[serial]
async fn test_dummy_bucket_compatibility_endpoints() {
init_logging();
info!("Starting test: dummy-compat bucket APIs should match S3-compatible behavior");
@@ -236,7 +234,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_dummy_bucket_compatibility_endpoints_no_such_bucket() {
init_logging();
info!("Starting test: dummy-compat bucket APIs should return NoSuchBucket for missing bucket");
@@ -392,7 +389,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_dummy_bucket_endpoints_http_contracts() {
init_logging();
info!("Starting test: dummy-compat bucket API HTTP contracts");
@@ -18,7 +18,6 @@
use crate::common::{RustFSTestEnvironment, init_logging};
use aws_sdk_s3::config::{Credentials, Region};
use aws_sdk_s3::{Client, Config};
use serial_test::serial;
use tracing::info;
async fn create_user(
@@ -51,7 +50,6 @@ fn create_user_client(env: &RustFSTestEnvironment, access_key: &str, secret_key:
}
#[tokio::test]
#[serial]
async fn test_bucket_policy_authenticated_user() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if !crate::common::awscurl_available() {
@@ -35,7 +35,6 @@ mod tests {
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::{BucketVersioningStatus, VersioningConfiguration};
use rustfs_data_usage::DataUsageInfo;
use serial_test::serial;
use std::error::Error;
use tokio::time::{Duration, sleep};
use tracing::info;
@@ -59,7 +58,6 @@ mod tests {
/// 3. Query admin data usage API
/// 4. Verify object count > 0
#[tokio::test]
#[serial]
async fn test_bucket_object_count_updates_after_put() -> TestResult {
init_logging();
info!("RT-09: bucket object count updates after PUT");
@@ -126,7 +124,6 @@ mod tests {
/// Regression pattern: stats remain unchanged after objects are deleted
/// (rustfs#5615).
#[tokio::test]
#[serial]
async fn test_bucket_object_count_updates_after_delete() -> TestResult {
init_logging();
info!("RT-09b: bucket object count updates after DELETE");
@@ -220,7 +217,6 @@ mod tests {
/// Regression pattern: DataUsageInfo undercounts versioned bucket versions
/// and delete markers (rustfs#3898).
#[tokio::test]
#[serial]
async fn test_versioned_bucket_stats_count_all_versions() -> TestResult {
init_logging();
info!("RT-09c: versioned bucket stats count all versions");
@@ -26,7 +26,6 @@ mod tests {
use base64::Engine;
use md5::{Digest as Md5Digest, Md5};
use rustfs_rio::{Checksum, ChecksumType as RioChecksumType};
use serial_test::serial;
use sha2::Sha256;
use tracing::info;
@@ -90,7 +89,6 @@ mod tests {
/// PutObject with Content-MD5: upload succeeds and GetObject returns same content.
#[tokio::test]
#[serial]
async fn test_put_object_with_content_md5() {
init_logging();
info!("TEST: PutObject with Content-MD5");
@@ -126,7 +124,6 @@ mod tests {
/// PutObject with x-amz-checksum-sha256: upload succeeds and GetObject returns same content.
#[tokio::test]
#[serial]
async fn test_put_object_with_checksum_sha256() {
init_logging();
info!("TEST: PutObject with x-amz-checksum-sha256");
@@ -164,7 +161,6 @@ mod tests {
/// PutObject with a SHA256 checksum that does NOT match the body must be
/// rejected (BadDigest / checksum mismatch), NOT accepted with HTTP 200.
#[tokio::test]
#[serial]
async fn test_put_object_rejects_mismatched_sha256() {
init_logging();
info!("TEST: PutObject rejects mismatched x-amz-checksum-sha256 (issue #4341)");
@@ -212,7 +208,6 @@ mod tests {
/// After PutObject with a correct SHA256 checksum, HeadObject with
/// ChecksumMode=ENABLED must return that stored base64 SHA256 digest.
#[tokio::test]
#[serial]
async fn test_head_object_returns_stored_sha256() {
init_logging();
info!("TEST: HeadObject returns stored SHA256 with ChecksumMode=ENABLED (issue #4341)");
@@ -258,7 +253,6 @@ mod tests {
/// Multipart upload with checksum: CreateMultipartUpload, UploadPart(s) with checksum_sha256, CompleteMultipartUpload; then GetObject verifies content.
/// Uses part size >= 5MB (server minimum) for two parts.
#[tokio::test]
#[serial]
async fn test_multipart_upload_with_checksum() {
init_logging();
info!("TEST: MultipartUpload with checksum (checksum_sha256 on parts)");
@@ -356,7 +350,6 @@ mod tests {
/// Regression test for issue #2282:
/// CRC64NVME full-object checksum should match between direct PutObject and multipart upload.
#[tokio::test]
#[serial]
async fn test_crc64nvme_matches_between_put_object_and_multipart_upload() {
init_logging();
info!("TEST: CRC64NVME matches between direct PutObject and multipart upload");
@@ -492,7 +485,6 @@ mod tests {
/// value is rejected with BadDigest and nothing is stored. Full HEAD/GET header
/// echo round-trip is additionally exercised by the boto3+awscrt e2e.
#[tokio::test]
#[serial]
async fn test_additional_checksums_verify_on_write() {
init_logging();
info!("TEST: additional checksums (XXHash3/64/128, SHA-512, MD5) verify-on-write");
@@ -16,7 +16,6 @@ use crate::common::RustFSTestClusterEnvironment;
use aws_sdk_s3::Client;
use aws_sdk_s3::error::SdkError;
use bytes::Bytes;
use serial_test::serial;
use std::sync::Arc;
use tokio::sync::Barrier;
use tracing::{info, warn};
@@ -135,7 +134,6 @@ async fn run_race_iteration(
}
#[tokio::test]
#[serial]
async fn test_conditional_put_race_cluster() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
crate::common::init_logging();
info!("Starting conditional PUT race test with auto cluster");
@@ -192,7 +190,6 @@ async fn test_conditional_put_race_cluster() -> Result<(), Box<dyn std::error::E
}
#[tokio::test]
#[serial]
async fn test_conditional_put_basic_cluster() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
crate::common::init_logging();
info!("Starting basic conditional PUT test with auto cluster");
@@ -31,7 +31,6 @@
//! (toxiproxy / socket proxy) and 5GiB large-object budgets.
use crate::common::{ClusterTopology, RustFSTestClusterEnvironment};
use serial_test::serial;
type TestResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;
@@ -58,7 +57,6 @@ async fn put_get_roundtrip(cluster: &RustFSTestClusterEnvironment, key: &str, pa
/// 4 nodes x 2 drives, single pool: the multi-drive layout boots and round-trips.
#[tokio::test]
#[serial]
async fn cluster_multidrive_single_pool_smoke() -> TestResult {
crate::common::init_logging();
@@ -81,7 +79,6 @@ async fn cluster_multidrive_single_pool_smoke() -> TestResult {
/// Two single-node pools, 2 drives each: the multi-pool layout boots and
/// round-trips. Every pool is a distinct erasure pool (`pool_idx` 0 and 1).
#[tokio::test]
#[serial]
async fn cluster_two_pool_smoke() -> TestResult {
crate::common::init_logging();
-7
View File
@@ -3,7 +3,6 @@
use crate::common::{RustFSTestEnvironment, init_logging, rustfs_binary_path};
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::{CompletedMultipartUpload, CompletedPart};
use serial_test::serial;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
@@ -102,7 +101,6 @@ async fn start_rustfs_with_compression(env: &mut RustFSTestEnvironment) -> Resul
}
#[tokio::test]
#[serial]
async fn test_compression_roundtrip() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("Starting compression roundtrip test");
@@ -230,7 +228,6 @@ async fn fetch_range(
/// (rustfs/rustfs#5957: multipart uploads previously bypassed disk compression
/// entirely).
#[tokio::test]
#[serial]
async fn test_compression_multipart_roundtrip() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("Starting multipart compression roundtrip test");
@@ -349,7 +346,6 @@ const MPU_HIGH_RATIO_BUCKET: &str = "compression-mpu-high-ratio-bucket";
/// reproduced the mid-payload Pending truncation (rustfs/rustfs#5957). Every GET shape must return
/// the exact original bytes, and the stored size must show the data really was compressed.
#[tokio::test]
#[serial]
async fn test_compression_multipart_high_ratio_binary_roundtrip() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("Starting multipart high-ratio binary compression roundtrip test");
@@ -446,7 +442,6 @@ const MPU_COPY_RANGE_LEN: usize = 5 * 1024 * 1024;
/// range must be decompressed on read and re-compressed into the destination part, so the final
/// object has to match "source prefix + uploaded tail" byte for byte.
#[tokio::test]
#[serial]
async fn test_compression_multipart_upload_part_copy_roundtrip() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("Starting multipart upload-part-copy compression roundtrip test");
@@ -570,7 +565,6 @@ const MPU_THREE_PARTS_TAIL_SIZE: usize = 512 * 1024;
/// Three-part upload with uneven part sizes: each partNumber GET must map back to exactly one
/// compressed part stream, and a suffix range must resolve inside the trailing part.
#[tokio::test]
#[serial]
async fn test_compression_multipart_three_parts_part_number_gets() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("Starting three-part multipart compression partNumber test");
@@ -689,7 +683,6 @@ async fn start_rustfs_with_compression_and_sse(
/// shape must still return the original plaintext bytes. Physical size must shrink because the
/// compression runs before encryption.
#[tokio::test]
#[serial]
async fn test_compression_multipart_sse_s3_roundtrip() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
use aws_sdk_s3::types::ServerSideEncryption;
@@ -18,7 +18,6 @@
//! concurrency — a queued connection is served only after a held one closes.
use crate::common::{RustFSTestEnvironment, init_logging};
use serial_test::serial;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
@@ -57,7 +56,6 @@ async fn read_response_head(stream: &mut TcpStream, dur: Duration) -> Option<Str
}
#[tokio::test]
#[serial]
async fn connection_cap_releases_permits_on_close() -> TestResult {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -89,7 +87,6 @@ async fn open_and_stall(addr: &str) -> std::io::Result<TcpStream> {
}
#[tokio::test]
#[serial]
async fn connection_cap_blocks_excess_connections_until_permits_free() -> TestResult {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -33,7 +33,6 @@
//! serve the unauthenticated console endpoints at all.
use crate::common::{RustFSTestEnvironment, init_logging, local_http_client};
use serial_test::serial;
use std::error::Error;
use tokio::time::{Duration, sleep};
@@ -58,7 +57,6 @@ async fn wait_for_console_ready(console_base: &str) -> Result<reqwest::Response,
}
#[tokio::test]
#[serial]
async fn test_console_over_the_wire_smoke() -> TestResult {
init_logging();
@@ -22,12 +22,10 @@
mod tests {
use crate::common::{RustFSTestEnvironment, init_logging};
use aws_sdk_s3::primitives::ByteStream;
use serial_test::serial;
use tracing::info;
/// Verify Content-Encoding header roundtrips through PUT, GET, and HEAD operations
#[tokio::test]
#[serial]
async fn test_content_encoding_roundtrip() {
init_logging();
info!("Starting Content-Encoding roundtrip test");
@@ -105,7 +103,6 @@ mod tests {
/// Issue #1857: Content-Encoding "aws-chunked" is used by SigV4 streaming clients and must
/// not be stored or returned. Upload with aws-chunked and verify GET/HEAD do not return it.
#[tokio::test]
#[serial]
async fn test_content_encoding_aws_chunked_not_returned_issue_1857() {
init_logging();
info!("Issue #1857: aws-chunked must not be persisted or returned");
@@ -161,7 +158,6 @@ mod tests {
/// Issue #2475 / Route A: when aws-chunked is combined with an effective object encoding,
/// only the effective encoding should roundtrip through GET/HEAD.
#[tokio::test]
#[serial]
async fn test_content_encoding_aws_chunked_with_effective_encoding_roundtrip() {
init_logging();
info!("aws-chunked,gzip should persist only gzip");
@@ -30,7 +30,6 @@ mod tests {
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64;
use rustfs_rio::{Checksum, ChecksumType as RioChecksumType};
use serial_test::serial;
use sha2::{Digest, Sha256};
use tracing::info;
@@ -114,7 +113,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_copy_supports_all_checksum_algorithms() {
init_logging();
@@ -196,7 +194,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_copy_without_algorithm_preserves_every_supported_source_checksum() {
init_logging();
@@ -262,7 +259,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_copy_without_algorithm_preserves_composite_checksum_type() {
init_logging();
@@ -352,7 +348,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_copy_rejects_unknown_algorithm_without_destination_mutation() {
init_logging();
@@ -453,7 +448,6 @@ mod tests {
/// bytes, return it in `CopyObjectResult.ChecksumSHA256`, and persist it so a checksum-mode
/// HEAD on the destination returns the identical value.
#[tokio::test]
#[serial]
async fn test_copy_with_checksum_algorithm_returns_and_persists_sha256() {
init_logging();
info!("Issue #4996: CopyObject with ChecksumAlgorithm=SHA256 must return and persist the checksum");
@@ -523,7 +517,6 @@ mod tests {
/// No algorithm requested: when the source object already carries a checksum, the copy must
/// preserve it on the destination (AWS default), visible via a checksum-mode HEAD.
#[tokio::test]
#[serial]
async fn test_copy_without_algorithm_preserves_source_checksum() {
init_logging();
info!("Issue #4996: CopyObject without ChecksumAlgorithm must preserve the source object's checksum");
@@ -603,7 +596,6 @@ mod tests {
/// checksum-not-inherited path, and exercises the CRC32 code path (a different branch of
/// ChecksumType::from_string than SHA256).
#[tokio::test]
#[serial]
async fn test_copy_requested_algorithm_overrides_source_checksum() {
init_logging();
info!("Issue #4996: a requested CopyObject checksum algorithm must override the source object's algorithm");
@@ -22,11 +22,9 @@ mod tests {
use aws_sdk_s3::types::{
BucketVersioningStatus, CompletedMultipartUpload, CompletedPart, MetadataDirective, StorageClass, VersioningConfiguration,
};
use serial_test::serial;
use tracing::info;
#[tokio::test]
#[serial]
async fn copy_object_standard_metadata_copy_replace_and_clear() {
init_logging();
info!("Issue #2789: self-copy metadata replacement must preserve object data");
@@ -300,7 +298,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn copy_object_replace_accepts_each_standard_field_independently() {
init_logging();
let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
@@ -416,7 +413,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn copy_object_replace_handles_versioned_multipart_source() {
init_logging();
let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
@@ -530,7 +526,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn invalid_replacement_metadata_does_not_mutate_destination() {
init_logging();
let mut env = RustFSTestEnvironment::new().await.expect("Failed to create test environment");
@@ -21,7 +21,6 @@ mod tests {
use aws_sdk_s3::error::ProvideErrorMetadata;
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::{BucketVersioningStatus, MetadataDirective, TaggingDirective, VersioningConfiguration};
use serial_test::serial;
use std::collections::BTreeMap;
async fn object_tags(client: &Client, bucket: &str, key: &str) -> BTreeMap<String, String> {
@@ -39,7 +38,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn copy_object_applies_copy_replace_and_empty_tagging_directives() {
init_logging();
let mut env = RustFSTestEnvironment::new()
@@ -305,7 +303,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn copy_object_tag_replacement_honors_request_tag_policy_denial() -> Result<(), Box<dyn std::error::Error + Send + Sync>>
{
init_logging();
@@ -21,11 +21,9 @@ mod tests {
use crate::common::{RustFSTestEnvironment, init_logging};
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::{BucketVersioningStatus, VersioningConfiguration};
use serial_test::serial;
use tracing::info;
#[tokio::test]
#[serial]
async fn test_self_copy_of_historical_version_restores_data_and_metadata() {
init_logging();
info!("Issue #4238: self-copy of a historical version must be allowed and preserve metadata");
@@ -165,7 +163,6 @@ mod tests {
/// version copied via `x-amz-copy-source-version-id` (SDK `CopySourceVersionId`), kept distinct
/// from the newly created destination `x-amz-version-id`.
#[tokio::test]
#[serial]
async fn test_copy_of_non_latest_source_version_returns_copy_source_version_id() {
init_logging();
info!("Issue #4976: versioned CopyObject must return x-amz-copy-source-version-id for the exact source version");
@@ -47,7 +47,6 @@ mod tests {
use rustfs_signer::constants::UNSIGNED_PAYLOAD;
use rustfs_signer::sign_v4;
use s3s::Body;
use serial_test::serial;
use std::error::Error;
/// Signed raw `PUT` copy request with an explicit copy-source conditional
@@ -84,7 +83,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_copy_source_if_unmodified_since_valid_and_invalid() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -25,13 +25,11 @@
mod tests {
use crate::common::{RustFSTestEnvironment, init_logging};
use aws_sdk_s3::types::{BucketLocationConstraint, CreateBucketConfiguration};
use serial_test::serial;
use std::error::Error;
/// `CreateBucket` with a `LocationConstraint` body must pass SigV4 validation
/// and create the bucket, mirroring `minio-go` `MakeBucket(bucket, "us-east-1")`.
#[tokio::test]
#[serial]
async fn test_create_bucket_with_us_east_1_location_constraint() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -62,7 +60,6 @@ mod tests {
/// A plain `CreateBucket` (no body) must also succeed; guards against a
/// regression where an empty body would be hashed incorrectly during SigV4.
#[tokio::test]
#[serial]
async fn test_create_bucket_without_location_constraint() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
-3
View File
@@ -15,7 +15,6 @@
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::{BucketVersioningStatus, VersioningConfiguration};
use rustfs_data_usage::DataUsageInfo;
use serial_test::serial;
use tokio::time::{Duration, sleep};
use crate::common::{FAST_DATA_USAGE_SCANNER_ENV, RustFSTestEnvironment, TEST_BUCKET, awscurl_get, init_logging};
@@ -60,7 +59,6 @@ where
/// Regression test for data usage accuracy (issue #1012).
/// Launches rustfs, writes 1000 objects, then asserts admin data usage reports the full count.
#[tokio::test(flavor = "multi_thread")]
#[serial]
#[ignore = "Starts a rustfs server and requires awscurl; enable when running full E2E"]
async fn data_usage_reports_all_objects() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
@@ -118,7 +116,6 @@ async fn data_usage_reports_all_objects() -> Result<(), Box<dyn std::error::Erro
/// Regression test for issue #3898.
/// Versioned buckets should expose versions and delete markers through admin data usage.
#[tokio::test(flavor = "multi_thread")]
#[serial]
#[ignore = "Starts a rustfs server and requires awscurl; enable when running full E2E"]
async fn data_usage_reports_versioned_objects_and_delete_markers() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
@@ -18,7 +18,6 @@ mod tests {
use aws_sdk_s3::Client;
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::{BucketVersioningStatus, VersioningConfiguration};
use serial_test::serial;
async fn create_versioned_bucket(client: &Client, bucket: &str) {
client
@@ -72,7 +71,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_versioning_only_delete_marker_has_minio_compatible_visibility_for_migration_proof() {
init_logging();
let mut env = RustFSTestEnvironment::new().await.expect("create test environment");
@@ -113,7 +111,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_versioning_delete_marker_plus_history_remains_visible_for_migration_proof() {
init_logging();
let mut env = RustFSTestEnvironment::new().await.expect("create test environment");
@@ -24,7 +24,6 @@ mod tests {
use rustfs_signer::constants::UNSIGNED_PAYLOAD;
use rustfs_signer::sign_v4;
use s3s::Body;
use serial_test::serial;
use std::error::Error;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
@@ -92,7 +91,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_delete_object_version_without_content_length_succeeds() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
info!("🧪 TEST: signed DELETE Object?versionId succeeds without Content-Length");
@@ -29,7 +29,6 @@ mod tests {
use crate::common::{RustFSTestEnvironment, init_logging};
use aws_sdk_s3::Client;
use aws_sdk_s3::types::{BucketVersioningStatus, Delete, ObjectIdentifier, VersioningConfiguration};
use serial_test::serial;
use tracing::info;
fn create_s3_client(env: &RustFSTestEnvironment) -> Client {
@@ -42,7 +41,6 @@ mod tests {
/// a versioned bucket, calling `list_object_versions` **immediately** (with
/// no sleep) returns the newly-created DeleteMarker with `is_latest = true`.
#[tokio::test]
#[serial]
async fn test_delete_objects_delete_marker_immediately_visible() {
init_logging();
info!("🧪 TEST: DeleteMarker from delete_objects is immediately visible via list_object_versions");
@@ -190,7 +188,6 @@ mod tests {
/// a single `delete_objects` call all have their delete markers visible
/// immediately afterwards.
#[tokio::test]
#[serial]
async fn test_delete_objects_multiple_keys_delete_markers_immediately_visible() {
init_logging();
info!("🧪 TEST: Multiple delete markers from delete_objects are immediately visible");
@@ -33,7 +33,6 @@ mod tests {
use crate::common::{RustFSTestEnvironment, init_logging};
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::{BucketVersioningStatus, Delete, ObjectIdentifier, VersioningConfiguration};
use serial_test::serial;
use std::error::Error;
use tracing::info;
@@ -51,7 +50,6 @@ mod tests {
/// 4. Verify the object is NOT in LIST
/// 5. Verify HEAD returns 404
#[tokio::test]
#[serial]
async fn test_delete_removes_object_from_list() -> TestResult {
init_logging();
info!("RT-05: delete removes object from list");
@@ -132,7 +130,6 @@ mod tests {
/// Regression pattern: batch delete returns success but some objects
/// remain in LIST.
#[tokio::test]
#[serial]
async fn test_batch_delete_removes_all_objects() -> TestResult {
init_logging();
info!("RT-05c: batch delete removes all objects");
@@ -212,7 +209,6 @@ mod tests {
/// Covers the pattern where permanent deletion of a specific version
/// fails with FileAccessDenied (rustfs#4978).
#[tokio::test]
#[serial]
async fn test_versioned_permanent_delete() -> TestResult {
init_logging();
info!("RT-05d: versioned permanent delete");
@@ -283,7 +279,6 @@ mod tests {
/// Covers the pattern where creating a delete marker and then listing
/// versions shows incorrect state (rustfs#760).
#[tokio::test]
#[serial]
async fn test_versioned_delete_marker_and_list_consistency() -> TestResult {
init_logging();
info!("RT-05e: versioned delete marker and list consistency");
@@ -379,7 +374,6 @@ mod tests {
/// Regression pattern: after delete, the object data files remain on disk
/// (rustfs#5029: Node Does Not Remove Files After Reconnect).
#[tokio::test]
#[serial]
async fn test_delete_removes_object_head_returns_404() -> TestResult {
init_logging();
info!("RT-05f: delete → HEAD 404 consistency");
@@ -32,7 +32,6 @@
mod tests {
use crate::common::{RustFSTestClusterEnvironment, init_logging};
use aws_sdk_s3::primitives::ByteStream;
use serial_test::serial;
use std::error::Error;
use tokio::time::{Duration, sleep};
use tracing::info;
@@ -50,7 +49,6 @@ mod tests {
/// 3. Verify all nodes report healthy
/// 4. Verify S3 operations work through any node
#[tokio::test]
#[serial]
async fn test_four_node_cluster_startup_and_health() -> TestResult {
init_logging();
info!("RT-10: 4-node cluster startup and health");
@@ -103,7 +101,6 @@ mod tests {
/// Regression pattern: after a node restart, it cannot rejoin the cluster
/// or enters a faulty state (rustfs#2601).
#[tokio::test]
#[serial]
async fn test_cluster_survives_node_restart() -> TestResult {
init_logging();
info!("RT-10b: cluster survives node restart");
@@ -168,7 +165,6 @@ mod tests {
/// Regression pattern: bucket metadata is not replicated to all nodes,
/// causing NoSuchBucket errors on some nodes (rustfs#3191).
#[tokio::test]
#[serial]
async fn test_bucket_visible_from_all_nodes() -> TestResult {
init_logging();
info!("RT-10c: bucket visible from all nodes");
@@ -23,7 +23,6 @@ use aws_sdk_s3::config::{Credentials, Region};
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::{Delete, ObjectIdentifier, Tag, Tagging};
use aws_sdk_s3::{Client, Config};
use serial_test::serial;
use tracing::info;
use uuid::Uuid;
@@ -174,7 +173,6 @@ async fn cleanup_bucket_and_object(admin: &Client, bucket: &str, key: &str) {
/// IAM identity policy: GetObject allowed only when `s3:ExistingObjectTag/security` == `public`.
#[tokio::test]
#[serial]
async fn test_e2e_iam_policy_existing_object_tag_get_object() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if !awscurl_available() {
@@ -233,7 +231,6 @@ async fn test_e2e_iam_policy_existing_object_tag_get_object() -> Result<(), Box<
/// Bucket policy: same `ExistingObjectTag` condition; user has no canned IAM policy attached.
#[tokio::test]
#[serial]
async fn test_e2e_bucket_policy_existing_object_tag_get_object() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if !awscurl_available() {
@@ -295,7 +292,6 @@ async fn test_e2e_bucket_policy_existing_object_tag_get_object() -> Result<(), B
/// STS `AssumeRole` with inline `Policy` (session policy): GetObject only when `ExistingObjectTag/security` is `public`.
#[tokio::test]
#[serial]
async fn test_e2e_sts_assume_role_session_policy_existing_object_tag() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if !awscurl_available() {
@@ -372,7 +368,6 @@ async fn test_e2e_sts_assume_role_session_policy_existing_object_tag() -> Result
/// STS inline session policy: DeleteObjects must evaluate `s3:DeleteObject` per requested object key.
#[tokio::test]
#[serial]
async fn test_e2e_sts_session_policy_delete_objects_object_prefix_only() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if !awscurl_available() {
@@ -66,7 +66,6 @@ mod tests {
use aws_sdk_s3::error::ProvideErrorMetadata;
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::{CompletedMultipartUpload, CompletedPart};
use serial_test::serial;
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
use std::error::Error;
@@ -277,7 +276,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn codec_streaming_matches_legacy_duplex_body_and_headers() -> TestResult {
init_logging();
-4
View File
@@ -17,7 +17,6 @@
use crate::common::{RustFSTestEnvironment, admin_request, awscurl_delete, awscurl_get, awscurl_put, init_logging};
use aws_sdk_s3::config::{Credentials, Region};
use aws_sdk_s3::{Client, Config};
use serial_test::serial;
use tracing::info;
fn create_user_s3_client(env: &RustFSTestEnvironment, access_key: &str, secret_key: &str) -> Client {
@@ -84,7 +83,6 @@ async fn update_group_members_rejects_invalid_new_group_names() -> Result<(), Bo
/// Test that deleting a group with members fails, and deleting an empty group succeeds.
#[tokio::test(flavor = "multi_thread")]
#[serial]
#[ignore = "requires awscurl and spawns a real RustFS server"]
async fn test_delete_group_requires_empty_membership() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
@@ -144,7 +142,6 @@ async fn test_delete_group_requires_empty_membership() -> Result<(), Box<dyn std
/// Test that a user with only group membership (no explicit user policy) gets group policies
/// and can perform actions allowed by the group (regression test for #2028.1).
#[tokio::test(flavor = "multi_thread")]
#[serial]
#[ignore = "requires awscurl and spawns a real RustFS server"]
async fn test_user_with_only_group_gets_group_policies() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
@@ -212,7 +209,6 @@ async fn test_user_with_only_group_gets_group_policies() -> Result<(), Box<dyn s
/// Test that after deleting a user who was the only member of a group, the group can be deleted
/// (regression test for #2028.2: delete group uses backend membership, not stale cache).
#[tokio::test(flavor = "multi_thread")]
#[serial]
#[ignore = "requires awscurl and spawns a real RustFS server"]
async fn test_delete_group_after_deleting_user() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
@@ -16,7 +16,6 @@ use crate::common::{RustFSTestEnvironment, init_logging, local_http_client};
use aws_sdk_s3::presigning::PresigningConfig;
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::{CompletedMultipartUpload, CompletedPart};
use serial_test::serial;
use std::time::Duration;
use tracing::info;
@@ -29,7 +28,6 @@ fn list_contains_key(output: &aws_sdk_s3::operation::list_objects_v2::ListObject
}
#[tokio::test]
#[serial]
async fn head_object_consistency_after_write_and_multipart_and_presigned_head()
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
@@ -1,6 +1,5 @@
use crate::common::{RustFSTestEnvironment, init_logging};
use aws_sdk_s3::primitives::ByteStream;
use serial_test::serial;
use tracing::info;
const RANGE_HEAD_BUCKET: &str = "range-head-test-bucket";
@@ -8,7 +7,6 @@ const RANGE_HEAD_KEY: &str = "range-head-object.bin";
const ACCEPT_RANGES_BYTES: &str = "bytes";
#[tokio::test]
#[serial]
async fn head_object_advertises_accept_ranges() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("Starting HeadObject Accept-Ranges regression test");
@@ -19,7 +19,6 @@ mod tests {
use crate::chaos::signed_admin_post;
use crate::common::{RustFSTestClusterEnvironment, RustFSTestEnvironment, init_logging};
use aws_sdk_s3::primitives::ByteStream;
use serial_test::serial;
use std::collections::HashSet;
use std::error::Error;
use std::path::{Path, PathBuf};
@@ -63,7 +62,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_auto_heal_rebuilds_runtime_wiped_disk_without_restart() {
init_logging();
info!("Issue #1533: auto heal should rebuild a runtime-wiped disk in a 4-disk single-node erasure set without restart");
@@ -182,7 +180,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_admin_deep_heal_rebuilds_cleared_disk_in_single_node_erasure_set() {
init_logging();
info!("Discussion #2964: admin deep heal should rebuild a wiped disk in a 4-disk single-node erasure set");
@@ -332,7 +329,6 @@ mod tests {
}
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn test_cluster_root_heal_rebuilds_replaced_remote_disk() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
info!("Root recursive heal should rebuild data on a remote node after its disk is replaced and the node rejoins");
@@ -444,7 +440,6 @@ mod tests {
/// topology early-return or the merge hard-fail) turns the down-window
/// response into a 500 and fails this test.
#[tokio::test]
#[serial]
async fn test_background_heal_status_degrades_while_peer_down_and_recovers_after_rejoin()
-> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -101,7 +101,6 @@ use rustfs_config::{
};
use rustfs_protos::canonical_make_volume_request_body;
use rustfs_protos::proto_gen::node_service::{MakeVolumeRequest, MakeVolumeResponse, PingRequest, PingResponse};
use serial_test::serial;
use sha2::{Digest, Sha256};
use std::error::Error;
use tonic::{Code, Request, Response, Status};
@@ -397,7 +396,6 @@ fn assert_rejected(result: Result<MakeVolumeResponse, Status>, expected: Code, e
/// Grouped into one server start because each case is independent and spawning
/// a `rustfs` process per assertion would dominate the runtime.
#[tokio::test]
#[serial]
async fn internode_rpc_signature_default_posture_e2e() -> TestResult {
init_logging();
align_rpc_secret_with_server();
@@ -424,7 +422,6 @@ async fn internode_rpc_signature_default_posture_e2e() -> TestResult {
/// epoch is learned from a real response, then the same server is restarted in place to prove its
/// replacement epoch rejects the captured request even though the nonce cache is necessarily new.
#[tokio::test]
#[serial]
async fn replay_scope_rejects_replay_path_transplant_and_stale_epoch_e2e() -> TestResult {
init_logging();
align_rpc_secret_with_server();
@@ -497,7 +494,6 @@ async fn replay_scope_rejects_replay_path_transplant_and_stale_epoch_e2e() -> Te
/// A mutating v2 request cannot use that lane; once the epoch proof is returned, the first v3
/// mutation succeeds. This protects a server restart without reopening a general downgrade path.
#[tokio::test]
#[serial]
async fn replay_scope_strict_requires_v3_after_ping_bootstrap_e2e() -> TestResult {
init_logging();
align_rpc_secret_with_server();
@@ -704,7 +700,6 @@ async fn legacy_only_signature_is_accepted_in_default_posture(url: &str) {
///
/// The paired v2 positive control rules out "strict simply breaks everything".
#[tokio::test]
#[serial]
async fn signature_strict_rejects_legacy_only_downgrade() -> TestResult {
init_logging();
align_rpc_secret_with_server();
@@ -741,7 +736,6 @@ async fn signature_strict_rejects_legacy_only_downgrade() -> TestResult {
/// takes the still-open legacy lane), which is what pins the rejection to the
/// handler's digest gate; the cited message confirms which check spoke.
#[tokio::test]
#[serial]
async fn body_digest_strict_rejects_digestless_mutation() -> TestResult {
init_logging();
align_rpc_secret_with_server();
@@ -27,12 +27,10 @@ use aws_sdk_s3::types::{
ServerSideEncryptionByDefault, ServerSideEncryptionConfiguration, ServerSideEncryptionRule,
};
use rustfs_rio::{Checksum, ChecksumType};
use serial_test::serial;
use tracing::{debug, info, warn};
/// Test 1: When bucket is configured with default SSE-S3 encryption, put_object should automatically apply encryption
#[tokio::test]
#[serial]
async fn test_bucket_default_sse_s3_put_object() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("Testing bucket default SSE-S3 encryption impact on put_object");
@@ -155,7 +153,6 @@ async fn test_bucket_default_sse_s3_put_object() -> Result<(), Box<dyn std::erro
/// Test 2: When bucket is configured with default SSE-KMS encryption, put_object should automatically apply encryption and use the specified KMS key
#[tokio::test]
#[serial]
async fn test_bucket_default_sse_kms_put_object() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("Testing bucket default SSE-KMS encryption impact on put_object");
@@ -275,7 +272,6 @@ async fn test_bucket_default_sse_kms_put_object() -> Result<(), Box<dyn std::err
/// Test 3: When bucket is configured with default encryption, create_multipart_upload should inherit the configuration
#[tokio::test]
#[serial]
async fn test_bucket_default_sse_kms_multipart_crc32() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("Testing bucket default encryption impact on create_multipart_upload");
@@ -473,7 +469,6 @@ async fn test_bucket_default_sse_kms_multipart_crc32() -> Result<(), Box<dyn std
/// Test 4: Explicitly specified encryption parameters in requests should override bucket default configuration
#[tokio::test]
#[serial]
async fn test_explicit_encryption_overrides_bucket_default() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("Testing explicitly specified encryption parameters override bucket default configuration");
@@ -569,7 +564,6 @@ async fn test_explicit_encryption_overrides_bucket_default() -> Result<(), Box<d
/// Test 5: Setting SSE-KMS without a specific key ID should auto-populate the
/// default KMS key ID so that GetBucketEncryption returns it (issue #3039).
#[tokio::test]
#[serial]
async fn test_sse_kms_without_key_id_populates_default() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("Testing SSE-KMS without explicit key ID populates default key");
@@ -20,7 +20,6 @@ use super::common::{
};
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::{BucketVersioningStatus, ServerSideEncryption, VersioningConfiguration};
use serial_test::serial;
use std::error::Error;
use uuid::Uuid;
@@ -386,7 +385,6 @@ async fn assert_versioned_sse_kms_roundtrip_and_cleanup(
}
#[tokio::test]
#[serial]
async fn test_configured_local_kms_admin_and_versioned_cleanup() -> TestResult {
let mut env = LocalKMSTestEnvironment::new().await?;
env.base_env.start_rustfs_server(Vec::new()).await?;
@@ -434,7 +432,6 @@ async fn test_configured_local_kms_admin_and_versioned_cleanup() -> TestResult {
}
#[tokio::test]
#[serial]
#[ignore = "requires a Vault binary"]
async fn test_configured_vault_kms_admin_and_versioned_cleanup() -> TestResult {
let mut env = VaultTestEnvironment::new().await?;
@@ -32,11 +32,9 @@ use aws_sdk_s3::types::{
MetadataDirective, ServerSideEncryption, ServerSideEncryptionByDefault, ServerSideEncryptionConfiguration,
ServerSideEncryptionRule,
};
use serial_test::serial;
use tracing::info;
#[tokio::test]
#[serial]
async fn test_metadata_replace_self_copy_of_sse_object_stays_decryptable() {
init_logging();
info!("same-key CopyObject with REPLACE metadata must not re-key an SSE-S3 object");
@@ -136,7 +134,6 @@ async fn test_metadata_replace_self_copy_of_sse_object_stays_decryptable() {
}
#[tokio::test]
#[serial]
async fn test_metadata_replace_self_copy_dropping_sse_rewrites_plaintext() {
init_logging();
info!("same-key CopyObject that drops SSE must rewrite the data, not orphan the ciphertext");
@@ -233,7 +230,6 @@ async fn test_metadata_replace_self_copy_dropping_sse_rewrites_plaintext() {
}
#[tokio::test]
#[serial]
async fn test_metadata_replace_self_copy_under_bucket_default_sse_stays_decryptable() {
init_logging();
info!("bucket default encryption must also keep a same-key copy off the metadata-only path");
@@ -25,11 +25,9 @@ use super::common::{LocalKMSTestEnvironment, create_key_with_specific_id};
use crate::common::init_logging;
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::{BucketVersioningStatus, ServerSideEncryption, VersioningConfiguration};
use serial_test::serial;
use tracing::info;
#[tokio::test]
#[serial]
async fn test_self_copy_of_historical_sse_s3_version_is_readable() {
init_logging();
info!("Issue #4238 (SSE): restoring an encrypted historical version must stay decryptable");
@@ -22,7 +22,6 @@ use aws_sdk_s3::types::{
CompletedMultipartUpload, CompletedPart, ServerSideEncryption, ServerSideEncryptionByDefault,
ServerSideEncryptionConfiguration, ServerSideEncryptionRule,
};
use serial_test::serial;
use std::collections::{HashMap, VecDeque};
use tracing::info;
@@ -82,7 +81,6 @@ pub(super) fn assert_storage_encrypted(storage_root: &std::path::Path, bucket: &
}
#[tokio::test]
#[serial]
async fn test_head_reports_managed_metadata_for_sse_s3() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("Validating SSE-S3 managed encryption metadata exposure");
@@ -143,7 +141,6 @@ async fn test_head_reports_managed_metadata_for_sse_s3() -> Result<(), Box<dyn s
}
#[tokio::test]
#[serial]
async fn test_head_reports_managed_metadata_for_sse_kms_and_copy() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("Validating SSE-KMS managed encryption metadata (including copy)");
@@ -247,7 +244,6 @@ async fn test_head_reports_managed_metadata_for_sse_kms_and_copy() -> Result<(),
}
#[tokio::test]
#[serial]
async fn test_multipart_upload_writes_encrypted_data() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("Validating ciphertext persistence for multipart SSE-KMS uploads");
@@ -35,7 +35,6 @@ use aws_sdk_s3::config::{Config, Credentials, Region};
use aws_sdk_s3::error::ProvideErrorMetadata;
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::ServerSideEncryption;
use serial_test::serial;
use std::time::Duration;
use tracing::info;
@@ -209,7 +208,6 @@ fn disable_body(key_id: &str) -> String {
/// Data-path matrix: SSE-KMS writes and reads are authorized against the resolved key.
#[tokio::test]
#[serial]
async fn sse_kms_per_key_authorization_negative_matrix() -> TestResult {
init_logging();
@@ -355,7 +353,6 @@ async fn sse_kms_per_key_authorization_negative_matrix() -> TestResult {
/// Runs without the SSE enforcement switch: admin scoping is unconditional, and
/// leaving the switch off proves the two planes are independent.
#[tokio::test]
#[serial]
async fn kms_admin_per_key_authorization_negative_matrix() -> TestResult {
init_logging();
@@ -24,13 +24,11 @@ use super::common::{
test_sse_kms_encryption, test_sse_s3_encryption,
};
use crate::common::{TEST_BUCKET, init_logging};
use serial_test::serial;
use tokio::time::{Duration, sleep};
use tracing::info;
/// Comprehensive test: Full KMS workflow with all encryption types
#[tokio::test]
#[serial]
async fn test_comprehensive_kms_full_workflow() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("🏁 Start the KMS full-featured synthesis test");
@@ -99,7 +97,6 @@ async fn test_mixed_encryption_workload(
/// Comprehensive stress test: Large dataset with multiple encryption types
#[tokio::test]
#[serial]
async fn test_comprehensive_stress_test() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("💪 Start the KMS stress test");
@@ -134,7 +131,6 @@ async fn test_comprehensive_stress_test() -> Result<(), Box<dyn std::error::Erro
/// Test encryption key isolation and security
#[tokio::test]
#[serial]
async fn test_comprehensive_key_isolation() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("🔐 Begin the comprehensive test of encryption key isolation");
@@ -206,7 +202,6 @@ async fn test_comprehensive_key_isolation() -> Result<(), Box<dyn std::error::Er
/// Test concurrent encryption operations
#[tokio::test]
#[serial]
async fn test_comprehensive_concurrent_operations() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("⚡ Started comprehensive testing of concurrent encryption operations");
@@ -252,7 +247,6 @@ async fn test_comprehensive_concurrent_operations() -> Result<(), Box<dyn std::e
/// Test encryption/decryption performance with different file sizes
#[tokio::test]
#[serial]
async fn test_comprehensive_performance_benchmark() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("📊 Start KMS performance benchmarking");
@@ -26,7 +26,6 @@ use crate::common::{TEST_BUCKET, init_logging};
use aws_sdk_s3::types::ServerSideEncryption;
use base64::Engine;
use md5::{Digest as Md5Digest, Md5};
use serial_test::serial;
use std::sync::Arc;
use tokio::sync::Semaphore;
use tracing::{info, warn};
@@ -39,7 +38,6 @@ fn md5_hex(input: impl AsRef<[u8]>) -> String {
/// Test encryption of zero-byte files (empty files)
#[tokio::test]
#[serial]
async fn test_kms_zero_byte_file_encryption() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("🧪 Testing KMS encryption with zero-byte files");
@@ -113,7 +111,6 @@ async fn test_kms_zero_byte_file_encryption() -> Result<(), Box<dyn std::error::
/// Test encryption of single-byte files
#[tokio::test]
#[serial]
async fn test_kms_single_byte_file_encryption() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("🧪 Testing KMS encryption with single-byte files");
@@ -206,7 +203,6 @@ async fn test_kms_single_byte_file_encryption() -> Result<(), Box<dyn std::error
/// Test multipart upload boundary conditions (minimum 5MB part size)
#[tokio::test]
#[serial]
async fn test_kms_multipart_boundary_conditions() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("🧪 Testing KMS multipart upload boundary conditions");
@@ -282,7 +278,6 @@ async fn test_kms_multipart_boundary_conditions() -> Result<(), Box<dyn std::err
/// Test invalid key scenarios and error handling
#[tokio::test]
#[serial]
async fn test_kms_invalid_key_scenarios() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("🧪 Testing KMS invalid key scenarios and error handling");
@@ -370,7 +365,6 @@ async fn test_kms_invalid_key_scenarios() -> Result<(), Box<dyn std::error::Erro
/// Test concurrent encryption operations
#[tokio::test]
#[serial]
async fn test_kms_concurrent_encryption() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("🧪 Testing KMS concurrent encryption operations");
@@ -478,7 +472,6 @@ async fn test_kms_concurrent_encryption() -> Result<(), Box<dyn std::error::Erro
/// Test key validation and security properties
#[tokio::test]
#[serial]
async fn test_kms_key_validation_security() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("🧪 Testing KMS key validation and security properties");
@@ -24,7 +24,6 @@
use super::common::LocalKMSTestEnvironment;
use crate::common::{TEST_BUCKET, init_logging};
use aws_sdk_s3::types::ServerSideEncryption;
use serial_test::serial;
use std::fs;
use std::time::Duration;
use tokio::time::sleep;
@@ -32,7 +31,6 @@ use tracing::{info, warn};
/// Test KMS behavior when key directory is temporarily unavailable
#[tokio::test]
#[serial]
async fn test_kms_key_directory_unavailable() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("🧪 Testing KMS behavior with unavailable key directory");
@@ -123,7 +121,6 @@ async fn test_kms_key_directory_unavailable() -> Result<(), Box<dyn std::error::
/// Test handling of corrupted key files
#[tokio::test]
#[serial]
async fn test_kms_corrupted_key_files() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("🧪 Testing KMS behavior with corrupted key files");
@@ -215,7 +212,6 @@ async fn test_kms_corrupted_key_files() -> Result<(), Box<dyn std::error::Error
/// Test multipart upload interruption and recovery
#[tokio::test]
#[serial]
async fn test_kms_multipart_upload_interruption() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("🧪 Testing KMS multipart upload interruption and recovery");
@@ -399,7 +395,6 @@ async fn test_kms_multipart_upload_interruption() -> Result<(), Box<dyn std::err
/// Test KMS resilience to temporary resource constraints
#[tokio::test]
#[serial]
async fn test_kms_resource_constraints() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("🧪 Testing KMS behavior under resource constraints");
@@ -51,7 +51,6 @@ use aws_sdk_s3::types::{
TransitionStorageClass,
};
use serde::Deserialize;
use serial_test::serial;
use std::time::{Duration as StdDuration, Instant};
use tracing::info;
@@ -424,7 +423,6 @@ async fn wait_for_restore_complete(client: &Client, bucket: &str, key: &str, dea
/// filter as the cause of the deletion and proves the encrypted bucket stays
/// readable end to end after the scanner has run.
#[tokio::test]
#[serial]
async fn ilm_expiration_on_sse_kms_bucket_under_enforcement() -> TestResult {
init_logging();
@@ -485,7 +483,6 @@ async fn ilm_expiration_on_sse_kms_bucket_under_enforcement() -> TestResult {
/// (the mechanism `reliant/tiering.rs` established), so the test does not
/// depend on scanner scheduling; the 1s scanner cycle stays on as a backstop.
#[tokio::test]
#[serial]
async fn ilm_transition_on_sse_kms_bucket_under_enforcement_reads_back() -> TestResult {
init_logging();
@@ -24,11 +24,9 @@ use super::common::{
test_kms_key_management, test_sse_c_encryption,
};
use crate::common::{TEST_BUCKET, init_logging};
use serial_test::serial;
use tracing::{error, info};
#[tokio::test]
#[serial]
async fn test_local_kms_end_to_end() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if skip_if_kms_admin_tool_unavailable("test_local_kms_end_to_end") {
@@ -114,7 +112,6 @@ async fn test_local_kms_end_to_end() -> Result<(), Box<dyn std::error::Error + S
}
#[tokio::test]
#[serial]
async fn test_local_kms_key_isolation() {
init_logging();
info!("Starting Local KMS Key Isolation Test");
@@ -215,7 +212,6 @@ async fn test_local_kms_key_isolation() {
}
#[tokio::test]
#[serial]
async fn test_local_kms_large_file() {
init_logging();
info!("Starting Local KMS Large File Test");
@@ -298,7 +294,6 @@ async fn test_local_kms_large_file() {
}
#[tokio::test]
#[serial]
async fn test_local_kms_multipart_upload() {
init_logging();
info!("Starting Local KMS Multipart Upload Test");
@@ -23,12 +23,10 @@
use super::common::{LocalKMSTestEnvironment, sse_customer_key_md5_base64};
use crate::common::{TEST_BUCKET, init_logging};
use serial_test::serial;
use tracing::{debug, info};
/// Step 1: Test the basic single-file encryption function (ensure that SSE-S3 works properly in non-sharded scenarios)
#[tokio::test]
#[serial]
async fn test_step1_basic_single_file_encryption() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("🧪 Step 1: Test the basic single-file encryption function");
@@ -85,7 +83,6 @@ async fn test_step1_basic_single_file_encryption() -> Result<(), Box<dyn std::er
/// Step 2: Test the unencrypted shard upload (make sure the shard upload base is working properly)
#[tokio::test]
#[serial]
async fn test_step2_basic_multipart_upload_without_encryption() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("🧪 Step 2: Test unencrypted shard uploads");
@@ -184,7 +181,6 @@ async fn test_step2_basic_multipart_upload_without_encryption() -> Result<(), Bo
/// Step 3: Test Shard Upload + SSE-S3 Encryption (Focus Test)
#[tokio::test]
#[serial]
async fn test_step3_multipart_upload_with_sse_s3() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("🧪 Step 3: Test Shard Upload + SSE-S3 Encryption");
@@ -308,7 +304,6 @@ async fn test_step3_multipart_upload_with_sse_s3() -> Result<(), Box<dyn std::er
/// Step 4: test larger multipart uploads (streaming encryption)
#[tokio::test]
#[serial]
async fn test_step4_large_multipart_upload_with_encryption() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("🧪 Step 4: test large-file multipart encryption");
@@ -434,7 +429,6 @@ async fn test_step4_large_multipart_upload_with_encryption() -> Result<(), Box<d
/// Step 5: test multipart uploads for every encryption mode
#[tokio::test]
#[serial]
async fn test_step5_all_encryption_types_multipart() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
info!("🧪 Step 5: test multipart uploads for every encryption mode");
-3
View File
@@ -19,7 +19,6 @@
//! filtering, and comprehensive reporting capabilities.
use crate::common::init_logging;
use serial_test::serial;
use std::time::Instant;
use tokio::time::{Duration, sleep};
use tracing::{debug, error, info, warn};
@@ -458,7 +457,6 @@ impl KMSTestSuite {
/// Quick test suite for critical tests only
#[tokio::test]
#[serial]
async fn test_kms_critical_suite() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let config = TestSuiteConfig {
categories: vec![TestCategory::CoreFunctionality, TestCategory::MultipartEncryption],
@@ -481,7 +479,6 @@ async fn test_kms_critical_suite() -> Result<(), Box<dyn std::error::Error + Sen
/// Full comprehensive test suite
#[tokio::test]
#[serial]
async fn test_kms_full_suite() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let suite = KMSTestSuite::new();
let results = suite.run_test_suite().await;
@@ -24,7 +24,6 @@ mod tests {
use crate::common::{RustFSTestEnvironment, init_logging};
use aws_sdk_s3::Client;
use aws_sdk_s3::primitives::ByteStream;
use serial_test::serial;
use std::error::Error;
use tracing::info;
@@ -36,7 +35,6 @@ mod tests {
/// PUT with a leading-slash key must succeed and the object must be
/// readable under the normalized key (leading slash stripped).
#[tokio::test]
#[serial]
async fn test_put_object_with_leading_slash_key() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
info!("Starting test: PUT object with leading slash in key (Issue #2427)");
@@ -94,7 +92,6 @@ mod tests {
/// Duplicate and repeated slashes after a leading slash collapse MinIO-style.
#[tokio::test]
#[serial]
async fn test_put_object_with_duplicate_slashes_normalized() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
info!("Starting test: duplicate slash normalization (Issue #2427)");
@@ -36,7 +36,6 @@ mod tests {
BucketLifecycleConfiguration, BucketVersioningStatus, ExpirationStatus, LifecycleExpiration, LifecycleRule,
LifecycleRuleFilter, NoncurrentVersionExpiration, VersioningConfiguration,
};
use serial_test::serial;
use std::error::Error;
use tracing::info;
@@ -80,7 +79,6 @@ mod tests {
///
/// This tests the rule persistence path (rustfs#4963: 3 days → 0 days).
#[tokio::test]
#[serial]
async fn test_lifecycle_expiration_rule_persists_correctly() -> TestResult {
init_logging();
info!("RT-03: lifecycle expiration rule persists correctly");
@@ -148,7 +146,6 @@ mod tests {
/// Covers the pattern where noncurrent version expiration rules are
/// accepted but old versions are never cleaned up.
#[tokio::test]
#[serial]
async fn test_lifecycle_noncurrent_version_expiration_rule_persists() -> TestResult {
init_logging();
info!("RT-03b: noncurrent version expiration rule persists");
@@ -233,7 +230,6 @@ mod tests {
/// after restart. Transition rules require a configured remote tier
/// (tested in reliant/tiering.rs), so this test uses expiration only.
#[tokio::test]
#[serial]
async fn test_lifecycle_prefix_rule_persists() -> TestResult {
init_logging();
info!("RT-04: lifecycle prefix rule persists");
@@ -294,7 +290,6 @@ mod tests {
/// Regression pattern: DELETE on a versioned object fails or does not
/// create a delete marker, or the delete marker is not visible in LIST.
#[tokio::test]
#[serial]
async fn test_delete_marker_creation_and_visibility() -> TestResult {
init_logging();
info!("RT-05b: delete marker creation and visibility");
@@ -27,7 +27,6 @@ mod tests {
use rustfs_signer::constants::UNSIGNED_PAYLOAD;
use rustfs_signer::sign_v4;
use s3s::Body;
use serial_test::serial;
use std::error::Error;
/// Sends a SigV4-signed `GET` where the signature is computed over `sign_path`
@@ -67,7 +66,6 @@ mod tests {
/// `GET /` (path-style service call) returns `ListBuckets`.
#[tokio::test]
#[serial]
async fn test_list_buckets_single_slash() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -86,7 +84,6 @@ mod tests {
/// compat layer rewrites `//` to `/` before `s3s` parses/verifies the request,
/// so both routing and signature verification operate on `/`.
#[tokio::test]
#[serial]
async fn test_list_buckets_double_slash_browser_compat() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -105,7 +102,6 @@ mod tests {
/// (`GET //bucket`) must be left untouched by the compat layer — it is not a
/// `ListBuckets` request and s3s continues to reject the empty bucket name.
#[tokio::test]
#[serial]
async fn test_double_slash_rewrite_is_narrowly_scoped() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -15,7 +15,6 @@
use crate::common::{RustFSTestEnvironment, admin_ok, build_test_s3_config, build_test_sts_client, init_logging};
use aws_sdk_s3::Client;
use aws_sdk_s3::error::ProvideErrorMetadata;
use serial_test::serial;
use tokio::time::{Duration, Instant};
fn user_client(env: &RustFSTestEnvironment, access_key: &str, secret_key: &str, session_token: Option<&str>) -> Client {
@@ -76,7 +75,6 @@ async fn create_service_account(
}
#[tokio::test]
#[serial]
async fn list_buckets_filters_with_iam_bucket_resources() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -24,7 +24,6 @@ mod tests {
use rustfs_signer::constants::UNSIGNED_PAYLOAD;
use rustfs_signer::sign_v4;
use s3s::Body;
use serial_test::serial;
use std::error::Error;
use tracing::info;
@@ -54,7 +53,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_list_object_versions_metadata_extension_returns_metadata_tags_and_internal()
-> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -21,7 +21,6 @@ mod tests {
use aws_sdk_s3::Client;
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::{BucketVersioningStatus, VersioningConfiguration};
use serial_test::serial;
use tracing::info;
fn create_s3_client(env: &RustFSTestEnvironment) -> Client {
@@ -29,7 +28,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_list_object_versions_immediately_returns_latest_put_after_delete_marker() {
init_logging();
info!("🧪 TEST: ListObjectVersions returns the newest version immediately after put -> delete -> put");
@@ -182,7 +180,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_list_object_versions_prefix_with_marker_object_returns_children() {
init_logging();
info!("🧪 TEST: ListObjectVersions returns prefix children when a marker object also exists");
@@ -17,7 +17,6 @@ mod tests {
use crate::common::{RustFSTestEnvironment, init_logging};
use aws_sdk_s3::Client;
use aws_sdk_s3::primitives::ByteStream;
use serial_test::serial;
use tracing::info;
/// Helper function to create an S3 client for testing
@@ -60,7 +59,6 @@ mod tests {
/// The bug was that "folder/" (the object) and "folder/" (derived prefix) were both added to CommonPrefixes
/// when delimiter was "/" because the deduplication check was explicitly skipped for "/" delimiter.
#[tokio::test]
#[serial]
async fn test_list_objects_v2_unique_common_prefixes() {
init_logging();
info!("Starting test: ListObjectsV2 should return unique CommonPrefixes");
@@ -140,7 +138,6 @@ mod tests {
/// When both "marker/subdir/" and "marker/subdir/file.txt" exist, listing with
/// Prefix="marker/" must not duplicate "marker/subdir/file.txt" in Contents.
#[tokio::test]
#[serial]
async fn test_list_objects_v2_unique_contents_with_explicit_directory_markers() {
init_logging();
info!("Starting test: ListObjectsV2 should return unique keys with explicit directory markers");
@@ -208,7 +205,6 @@ mod tests {
/// and never produce the prefix entry `a/`. Delimiter="/" listings then
/// returned Contents `a` but silently dropped CommonPrefix `a/`.
#[tokio::test]
#[serial]
async fn test_list_objects_v2_object_and_same_named_prefix_coexist() {
init_logging();
info!("Starting test: ListObjectsV2 should return both object `a` and CommonPrefix `a/`");
@@ -23,7 +23,6 @@ mod tests {
use rustfs_signer::constants::UNSIGNED_PAYLOAD;
use rustfs_signer::sign_v4;
use s3s::Body;
use serial_test::serial;
use std::error::Error;
use tracing::info;
@@ -53,7 +52,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_list_objects_v2_metadata_extension_returns_metadata_tags_and_internal()
-> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -30,7 +30,6 @@ mod tests {
use crate::common::{RustFSTestEnvironment, init_logging};
use aws_sdk_s3::Client;
use aws_sdk_s3::primitives::ByteStream;
use serial_test::serial;
use std::collections::HashSet;
use tracing::info;
@@ -61,7 +60,6 @@ mod tests {
/// Test for Issue #2775: continuation forwarding must not
/// skip a child directory when the prefix component repeats in the key.
#[tokio::test]
#[serial]
async fn test_list_objects_v2_repeated_prefix_continuation() {
init_logging();
info!("Starting test: ListObjectsV2 repeated-prefix continuation");
@@ -187,7 +185,6 @@ mod tests {
/// This is the core bug from issue #1596: the server was returning
/// IsTruncated=true even when all objects fit within the requested max_keys.
#[tokio::test]
#[serial]
async fn test_list_objects_v2_not_truncated_when_all_objects_returned() {
init_logging();
info!("Starting test: ListObjectsV2 should not be truncated when all objects fit within max_keys");
@@ -252,7 +249,6 @@ mod tests {
/// 2. NextContinuationToken is returned (not NextMarker)
/// 3. Using ContinuationToken fetches the remaining objects
#[tokio::test]
#[serial]
async fn test_list_objects_v2_pagination_with_continuation_token() {
init_logging();
info!("Starting test: ListObjectsV2 pagination with continuation token");
@@ -394,7 +390,6 @@ mod tests {
/// Edge case: when max_keys exactly equals the number of objects,
/// IsTruncated should be false.
#[tokio::test]
#[serial]
async fn test_list_objects_v2_max_keys_equals_object_count() {
init_logging();
info!("Starting test: ListObjectsV2 with max_keys equal to object count");
@@ -455,7 +450,6 @@ mod tests {
///
/// Edge case: IsTruncated should be false for empty bucket.
#[tokio::test]
#[serial]
async fn test_list_objects_v2_empty_bucket() {
init_logging();
info!("Starting test: ListObjectsV2 with empty bucket");
@@ -495,7 +489,6 @@ mod tests {
/// Test ListObjectsV2 caps max_keys above the service limit and still paginates.
#[tokio::test]
#[serial]
async fn test_list_objects_v2_max_keys_above_limit_returns_token() {
init_logging();
info!("Starting test: ListObjectsV2 with max_keys above limit");
@@ -563,7 +556,6 @@ mod tests {
/// S3 semantics: when max_keys is 0, the response should include no objects
/// and IsTruncated should be false.
#[tokio::test]
#[serial]
async fn test_list_objects_v2_max_keys_zero() {
init_logging();
info!("Starting test: ListObjectsV2 with max_keys=0");
@@ -620,7 +612,6 @@ mod tests {
/// With max_keys=1000, all 5 visible results (3 prefixes + 2 objects) fit in one
/// page, so IsTruncated must be false even though raw entry count is much larger.
#[tokio::test]
#[serial]
async fn test_list_objects_v2_delimiter_collapsed_prefix_no_false_truncation() {
init_logging();
info!("Starting test: ListObjectsV2 delimiter collapsed-prefix no false truncation");
@@ -744,7 +735,6 @@ mod tests {
/// Each page returns up to 50 CommonPrefixes. The server must correctly set
/// IsTruncated and provide a valid continuation token across all pages.
#[tokio::test]
#[serial]
async fn test_list_objects_v2_delimiter_small_page_traverses_all() {
init_logging();
info!("Starting test: ListObjectsV2 delimiter small page traverses all keys");
@@ -867,7 +857,6 @@ mod tests {
/// but after delimiter collapse only 10 CommonPrefixes are visible (10 < 1000).
/// IsTruncated must be false since there are no additional visible results.
#[tokio::test]
#[serial]
async fn test_list_objects_v2_raw_exceeds_maxkeys_but_visible_below() {
init_logging();
info!("Starting test: ListObjectsV2 raw > MaxKeys but visible < MaxKeys after collapse");
@@ -970,7 +959,6 @@ mod tests {
/// This complements test_list_objects_v2_max_keys_above_limit_returns_token which
/// tests the non-delimiter case.
#[tokio::test]
#[serial]
async fn test_list_objects_v2_maxkeys_above_limit_with_delimiter() {
init_logging();
info!("Starting test: ListObjectsV2 MaxKeys above limit with delimiter");
@@ -1041,7 +1029,6 @@ mod tests {
/// the next page: with keys `a`, `a.txt`, `zz` and max_keys=1, page 2
/// returned `zz` and `a.txt` was never listed.
#[tokio::test]
#[serial]
async fn test_list_objects_v2_continuation_keeps_keys_after_marker_stem() {
init_logging();
info!("Starting test: continuation must not skip keys sorting below the cursor tag");
@@ -31,7 +31,6 @@
mod tests {
use crate::common::{RustFSTestEnvironment, init_logging};
use aws_sdk_s3::primitives::ByteStream;
use serial_test::serial;
use std::collections::HashSet;
use std::error::Error;
use tracing::info;
@@ -49,7 +48,6 @@ mod tests {
/// 3. Verify all 100 keys are returned exactly once
/// 4. Verify no duplicates or skipped keys
#[tokio::test]
#[serial]
async fn test_list_objects_v2_completeness_100_objects() -> TestResult {
init_logging();
info!("RT-06: listing completeness with 100 objects");
@@ -133,7 +131,6 @@ mod tests {
/// Regression pattern: prefix filter returns empty or includes wrong keys
/// (rustfs#5051: empty results for shallow prefixes).
#[tokio::test]
#[serial]
async fn test_list_objects_v2_prefix_filter_correctness() -> TestResult {
init_logging();
info!("RT-06b: prefix filter correctness");
@@ -233,7 +230,6 @@ mod tests {
/// Regression pattern: delimiter handling produces incorrect CommonPrefixes
/// or misses objects at the delimiter boundary.
#[tokio::test]
#[serial]
async fn test_list_objects_v2_delimiter_common_prefixes() -> TestResult {
init_logging();
info!("RT-06c: delimiter and CommonPrefixes");
@@ -290,7 +286,6 @@ mod tests {
/// Regression pattern: IsTruncated=false when there are more objects
/// (rustfs#4810: walk_dir timeout truncation with false IsTruncated).
#[tokio::test]
#[serial]
async fn test_list_objects_v2_is_truncated_correctness() -> TestResult {
init_logging();
info!("RT-06d: IsTruncated correctness");
@@ -13,7 +13,6 @@
// limitations under the License.
use crate::common::{DEFAULT_ACCESS_KEY, DEFAULT_SECRET_KEY, RustFSTestEnvironment};
use serial_test::serial;
use std::path::Path;
use std::process::Command;
use std::time::Duration;
@@ -73,7 +72,6 @@ fn count_files(root: &Path) -> usize {
}
#[tokio::test]
#[serial]
async fn test_mc_mirror_small_bucket_completes_without_list_timeout() -> TestResult {
crate::common::init_logging();
info!("Starting issue #3107 mc mirror regression test");
+610 -235
View File
@@ -17,7 +17,6 @@
use crate::common::{RustFSTestEnvironment, init_logging, local_http_client};
use async_compression::tokio::write::{BzEncoder, XzEncoder};
use aws_sdk_s3::error::{ProvideErrorMetadata, SdkError};
use aws_sdk_s3::operation::head_object::HeadObjectOutput;
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::{
ServerSideEncryption, ServerSideEncryptionByDefault, ServerSideEncryptionConfiguration, ServerSideEncryptionRule,
@@ -349,71 +348,6 @@ async fn run_post_object_policy_case(
Ok(())
}
/// One accepted POST Object upload driven end-to-end (backlog#1838): starts a
/// fresh server, allows anonymous PutObject on `bucket`, posts an anonymous
/// POST Object form whose policy carries `policy_conditions` and whose form
/// carries `form_field` on top of the mandatory key+policy fields, then asserts
/// 204 with an empty body, that `read_stored` observes the submitted value on
/// the stored object, and that the object body round-tripped unchanged.
/// `case` prefixes every assertion message so a failing table row is
/// identifiable at a glance.
#[allow(clippy::too_many_arguments)]
async fn run_post_object_accept_case(
bucket: &str,
object_key: &str,
policy_conditions: Vec<serde_json::Value>,
form_field: (&str, &str),
file_mime: &str,
file_body: &[u8],
read_stored: fn(&HeadObjectOutput) -> Option<&str>,
case: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut env = RustFSTestEnvironment::new().await?;
env.start_rustfs_server(vec![]).await?;
let admin_client = env.create_s3_client();
admin_client.create_bucket().bucket(bucket).send().await?;
allow_anonymous_put_object(&admin_client, bucket).await?;
let policy = encode_post_policy(policy_conditions);
let (field_name, field_value) = form_field;
let post_form = reqwest::multipart::Form::new()
.text("key", object_key.to_string())
.text("policy", policy)
.text(field_name.to_string(), field_value.to_string())
.part(
"file",
reqwest::multipart::Part::bytes(file_body.to_vec())
.file_name("upload.txt")
.mime_str(file_mime)?,
);
let post_resp = local_http_client()
.post(format!("{}/{}", env.url, bucket))
.multipart(post_form)
.send()
.await?;
let status = post_resp.status();
let response_body = post_resp.text().await?;
assert_eq!(status, reqwest::StatusCode::NO_CONTENT, "[{case}] unexpected status");
assert!(
response_body.is_empty(),
"[{case}] 204 response should not contain a body, got: {response_body}"
);
let head = admin_client.head_object().bucket(bucket).key(object_key).send().await?;
assert_eq!(read_stored(&head), Some(field_value), "[{case}] stored {field_name} mismatch");
let get_out = admin_client.get_object().bucket(bucket).key(object_key).send().await?;
let uploaded = get_out.body.collect().await?.into_bytes();
assert_eq!(uploaded.as_ref(), file_body, "[{case}] uploaded body mismatch");
Ok(())
}
/// Table-driven fold of the nine `*_missing_from_policy_conditions` POST
/// Object tests (backlog#1838 PR1). Every row keeps its original test's exact
/// bucket, key, form field, file body, and expected error strings; the shared
@@ -1600,6 +1534,59 @@ async fn test_anonymous_post_object_accepts_sse_s3_missing_from_policy_condition
Ok(())
}
#[tokio::test]
async fn test_anonymous_post_object_accepts_storage_class_exact_policy_match()
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
env.start_rustfs_server(vec![]).await?;
let bucket = "anon-post-storage-class";
let object_key = "post-storage-class-object.txt";
let expected_body = b"post-storage-class-body".to_vec();
let storage_class = "REDUCED_REDUNDANCY";
let admin_client = env.create_s3_client();
admin_client.create_bucket().bucket(bucket).send().await?;
allow_anonymous_put_object(&admin_client, bucket).await?;
let policy = encode_post_policy(vec![
serde_json::json!({ "bucket": bucket }),
serde_json::json!({ "key": object_key }),
serde_json::json!({ "x-amz-storage-class": storage_class }),
serde_json::json!(["content-length-range", 0, 1024]),
]);
let post_form = reqwest::multipart::Form::new()
.text("key", object_key.to_string())
.text("policy", policy)
.text("x-amz-storage-class", storage_class)
.part(
"file",
reqwest::multipart::Part::bytes(expected_body.clone())
.file_name("upload.txt")
.mime_str("text/plain")?,
);
let post_resp = local_http_client()
.post(format!("{}/{}", env.url, bucket))
.multipart(post_form)
.send()
.await?;
assert_eq!(post_resp.status(), reqwest::StatusCode::NO_CONTENT);
let head = admin_client.head_object().bucket(bucket).key(object_key).send().await?;
assert_eq!(head.storage_class().map(|value| value.as_str()), Some(storage_class));
let uploaded = admin_client.get_object().bucket(bucket).key(object_key).send().await?;
let uploaded = uploaded.body.collect().await?.into_bytes();
assert_eq!(uploaded.as_ref(), expected_body.as_slice());
Ok(())
}
#[tokio::test]
async fn test_anonymous_post_object_rejects_storage_class_missing_from_policy_conditions()
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
@@ -2597,182 +2584,512 @@ async fn test_anonymous_post_object_rejects_success_action_redirect_missing_from
Ok(())
}
/// Table-driven fold of the eleven accepted POST Object form-field tests
/// (backlog#1838 PR4). Every row keeps its original test's exact bucket, key,
/// form field, submitted value, policy condition, file MIME type, and file
/// body; the shared shape is: the policy covers the field (exact condition or
/// `starts-with` prefix), the form submits it, the upload returns 204 with an
/// empty body, and the stored object echoes the submitted value back.
#[tokio::test]
async fn test_anonymous_post_object_accepts_fields_covered_by_policy_conditions()
async fn test_anonymous_post_object_accepts_metadata_field_covered_by_starts_with()
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
// (case, bucket, object_key, field, submitted value, `starts-with` prefix
// (`None` pins the field to an exact policy condition), file part MIME type,
// file body, stored-value accessor)
type Case = (
&'static str,
&'static str,
&'static str,
&'static str,
&'static str,
Option<&'static str>,
&'static str,
&'static [u8],
fn(&HeadObjectOutput) -> Option<&str>,
);
let cases: &[Case] = &[
(
"storage-class",
"anon-post-storage-class",
"post-storage-class-object.txt",
"x-amz-storage-class",
"REDUCED_REDUNDANCY",
None,
"text/plain",
b"post-storage-class-body",
|head: &HeadObjectOutput| head.storage_class().map(|value| value.as_str()),
),
(
"metadata-starts-with",
"anon-post-policy-meta-accept",
"uploads/meta-object.txt",
"x-amz-meta-project",
"alpha-demo",
Some("alpha-"),
"text/plain",
b"post-policy-meta-body",
|head: &HeadObjectOutput| head.metadata().and_then(|meta| meta.get("project")).map(String::as_str),
),
(
"content-type",
"anon-post-policy-content-type-accept",
"uploads/content-type-accept.txt",
"Content-Type",
"text/plain",
None,
"text/plain",
b"post-policy-content-type-accept",
|head: &HeadObjectOutput| head.content_type(),
),
(
"content-type-starts-with",
"anon-post-policy-content-type-accept",
"uploads/content-type-object.txt",
"Content-Type",
"image/png",
Some("image/"),
"image/png",
b"post-policy-content-type-body",
|head: &HeadObjectOutput| head.content_type(),
),
(
"content-disposition",
"anon-post-policy-disposition-accept",
"uploads/disposition-object.txt",
"Content-Disposition",
"attachment; filename=\"upload.txt\"",
None,
"text/plain",
b"post-policy-disposition-body",
|head: &HeadObjectOutput| head.content_disposition(),
),
(
"cache-control",
"anon-post-policy-cache-control-accept",
"uploads/cache-control-object.txt",
"Cache-Control",
"max-age=60",
None,
"text/plain",
b"post-policy-cache-control-body",
|head: &HeadObjectOutput| head.cache_control(),
),
(
"content-language",
"anon-post-policy-content-language-accept",
"uploads/content-language-object.txt",
"Content-Language",
"en-US",
None,
"text/plain",
b"post-policy-content-language-body",
|head: &HeadObjectOutput| head.content_language(),
),
(
"content-encoding",
"anon-post-policy-content-encoding-accept",
"uploads/content-encoding-object.txt",
"Content-Encoding",
"gzip",
None,
"text/plain",
b"post-policy-content-encoding-body",
|head: &HeadObjectOutput| head.content_encoding(),
),
(
"website-redirect-location",
"anon-post-policy-website-redirect-accept",
"uploads/website-redirect-object.txt",
"x-amz-website-redirect-location",
"/docs/landing.html",
None,
"text/plain",
b"post-policy-website-redirect-body",
|head: &HeadObjectOutput| head.website_redirect_location(),
),
(
"expires",
"anon-post-policy-expires-accept",
"uploads/expires-object.txt",
"Expires",
"Wed, 21 Oct 2037 07:28:00 GMT",
None,
"text/plain",
b"post-policy-expires-body",
|head: &HeadObjectOutput| head.expires_string(),
),
(
"metadata-exact",
"anon-post-policy-meta-exact-accept",
"uploads/meta-exact-accept-object.txt",
"x-amz-meta-project",
"alpha-demo",
None,
"text/plain",
b"post-policy-meta-exact-body",
|head: &HeadObjectOutput| head.metadata().and_then(|meta| meta.get("project")).map(String::as_str),
),
];
let mut env = RustFSTestEnvironment::new().await?;
env.start_rustfs_server(vec![]).await?;
for (case, bucket, object_key, field, value, starts_with_prefix, file_mime, file_body, read_stored) in cases {
let condition = match starts_with_prefix {
Some(prefix) => serde_json::json!(["starts-with", format!("${field}"), prefix]),
None => {
let mut exact = serde_json::Map::new();
exact.insert((*field).to_string(), serde_json::Value::String((*value).to_string()));
serde_json::Value::Object(exact)
}
};
let bucket = "anon-post-policy-meta-accept";
let object_key = "uploads/meta-object.txt";
let metadata_value = "alpha-demo";
let expected_body = b"post-policy-meta-body".to_vec();
run_post_object_accept_case(
bucket,
object_key,
vec![
serde_json::json!({ "bucket": bucket }),
serde_json::json!({ "key": object_key }),
condition,
serde_json::json!(["content-length-range", 0, 1024]),
],
(field, value),
file_mime,
file_body,
*read_stored,
case,
)
let admin_client = env.create_s3_client();
admin_client.create_bucket().bucket(bucket).send().await?;
allow_anonymous_put_object(&admin_client, bucket).await?;
let policy = encode_post_policy(vec![
serde_json::json!({ "bucket": bucket }),
serde_json::json!({ "key": object_key }),
serde_json::json!(["starts-with", "$x-amz-meta-project", "alpha-"]),
serde_json::json!(["content-length-range", 0, 1024]),
]);
let post_form = reqwest::multipart::Form::new()
.text("key", object_key.to_string())
.text("policy", policy)
.text("x-amz-meta-project", metadata_value)
.part(
"file",
reqwest::multipart::Part::bytes(expected_body.clone())
.file_name("upload.txt")
.mime_str("text/plain")?,
);
let post_resp = local_http_client()
.post(format!("{}/{}", env.url, bucket))
.multipart(post_form)
.send()
.await?;
}
let status = post_resp.status();
let response_body = post_resp.text().await?;
assert_eq!(status, reqwest::StatusCode::NO_CONTENT);
assert!(response_body.is_empty(), "204 response should not contain a body, got: {response_body}");
let head = admin_client.head_object().bucket(bucket).key(object_key).send().await?;
let metadata = head.metadata().expect("head_object should expose uploaded metadata");
assert_eq!(metadata.get("project").map(String::as_str), Some(metadata_value));
Ok(())
}
#[tokio::test]
async fn test_anonymous_post_object_accepts_content_type_field_exact_policy_match()
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
env.start_rustfs_server(vec![]).await?;
let bucket = "anon-post-policy-content-type-accept";
let object_key = "uploads/content-type-accept.txt";
let content_type = "text/plain";
let expected_body = b"post-policy-content-type-accept".to_vec();
let admin_client = env.create_s3_client();
admin_client.create_bucket().bucket(bucket).send().await?;
allow_anonymous_put_object(&admin_client, bucket).await?;
let policy = encode_post_policy(vec![
serde_json::json!({ "bucket": bucket }),
serde_json::json!({ "key": object_key }),
serde_json::json!({ "Content-Type": content_type }),
serde_json::json!(["content-length-range", 0, 1024]),
]);
let post_form = reqwest::multipart::Form::new()
.text("key", object_key.to_string())
.text("policy", policy)
.text("Content-Type", content_type)
.part(
"file",
reqwest::multipart::Part::bytes(expected_body.clone())
.file_name("upload.txt")
.mime_str(content_type)?,
);
let post_resp = local_http_client()
.post(format!("{}/{}", env.url, bucket))
.multipart(post_form)
.send()
.await?;
let status = post_resp.status();
let response_body = post_resp.text().await?;
assert_eq!(status, reqwest::StatusCode::NO_CONTENT);
assert!(response_body.is_empty(), "204 response should not contain a body, got: {response_body}");
let head = admin_client.head_object().bucket(bucket).key(object_key).send().await?;
assert_eq!(head.content_type(), Some(content_type));
let get_out = admin_client.get_object().bucket(bucket).key(object_key).send().await?;
let uploaded = get_out.body.collect().await?.into_bytes();
assert_eq!(uploaded.as_ref(), expected_body.as_slice());
Ok(())
}
#[tokio::test]
async fn test_anonymous_post_object_accepts_content_type_field_covered_by_starts_with()
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
env.start_rustfs_server(vec![]).await?;
let bucket = "anon-post-policy-content-type-accept";
let object_key = "uploads/content-type-object.txt";
let content_type = "image/png";
let expected_body = b"post-policy-content-type-body".to_vec();
let admin_client = env.create_s3_client();
admin_client.create_bucket().bucket(bucket).send().await?;
allow_anonymous_put_object(&admin_client, bucket).await?;
let policy = encode_post_policy(vec![
serde_json::json!({ "bucket": bucket }),
serde_json::json!({ "key": object_key }),
serde_json::json!(["starts-with", "$Content-Type", "image/"]),
serde_json::json!(["content-length-range", 0, 1024]),
]);
let post_form = reqwest::multipart::Form::new()
.text("key", object_key.to_string())
.text("policy", policy)
.text("Content-Type", content_type)
.part(
"file",
reqwest::multipart::Part::bytes(expected_body.clone())
.file_name("upload.txt")
.mime_str(content_type)?,
);
let post_resp = local_http_client()
.post(format!("{}/{}", env.url, bucket))
.multipart(post_form)
.send()
.await?;
let status = post_resp.status();
let response_body = post_resp.text().await?;
assert_eq!(status, reqwest::StatusCode::NO_CONTENT);
assert!(response_body.is_empty(), "204 response should not contain a body, got: {response_body}");
let head = admin_client.head_object().bucket(bucket).key(object_key).send().await?;
assert_eq!(head.content_type(), Some(content_type));
let get_out = admin_client.get_object().bucket(bucket).key(object_key).send().await?;
let uploaded = get_out.body.collect().await?.into_bytes();
assert_eq!(uploaded.as_ref(), expected_body.as_slice());
Ok(())
}
#[tokio::test]
async fn test_anonymous_post_object_accepts_content_disposition_field_exact_policy_match()
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
env.start_rustfs_server(vec![]).await?;
let bucket = "anon-post-policy-disposition-accept";
let object_key = "uploads/disposition-object.txt";
let content_disposition = "attachment; filename=\"upload.txt\"";
let expected_body = b"post-policy-disposition-body".to_vec();
let admin_client = env.create_s3_client();
admin_client.create_bucket().bucket(bucket).send().await?;
allow_anonymous_put_object(&admin_client, bucket).await?;
let policy = encode_post_policy(vec![
serde_json::json!({ "bucket": bucket }),
serde_json::json!({ "key": object_key }),
serde_json::json!({ "Content-Disposition": content_disposition }),
serde_json::json!(["content-length-range", 0, 1024]),
]);
let post_form = reqwest::multipart::Form::new()
.text("key", object_key.to_string())
.text("policy", policy)
.text("Content-Disposition", content_disposition)
.part(
"file",
reqwest::multipart::Part::bytes(expected_body.clone())
.file_name("upload.txt")
.mime_str("text/plain")?,
);
let post_resp = local_http_client()
.post(format!("{}/{}", env.url, bucket))
.multipart(post_form)
.send()
.await?;
let status = post_resp.status();
let response_body = post_resp.text().await?;
assert_eq!(status, reqwest::StatusCode::NO_CONTENT);
assert!(response_body.is_empty(), "204 response should not contain a body, got: {response_body}");
let head = admin_client.head_object().bucket(bucket).key(object_key).send().await?;
assert_eq!(head.content_disposition(), Some(content_disposition));
let get_out = admin_client.get_object().bucket(bucket).key(object_key).send().await?;
let uploaded = get_out.body.collect().await?.into_bytes();
assert_eq!(uploaded.as_ref(), expected_body.as_slice());
Ok(())
}
#[tokio::test]
async fn test_anonymous_post_object_accepts_cache_control_field_exact_policy_match()
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
env.start_rustfs_server(vec![]).await?;
let bucket = "anon-post-policy-cache-control-accept";
let object_key = "uploads/cache-control-object.txt";
let cache_control = "max-age=60";
let expected_body = b"post-policy-cache-control-body".to_vec();
let admin_client = env.create_s3_client();
admin_client.create_bucket().bucket(bucket).send().await?;
allow_anonymous_put_object(&admin_client, bucket).await?;
let policy = encode_post_policy(vec![
serde_json::json!({ "bucket": bucket }),
serde_json::json!({ "key": object_key }),
serde_json::json!({ "Cache-Control": cache_control }),
serde_json::json!(["content-length-range", 0, 1024]),
]);
let post_form = reqwest::multipart::Form::new()
.text("key", object_key.to_string())
.text("policy", policy)
.text("Cache-Control", cache_control)
.part(
"file",
reqwest::multipart::Part::bytes(expected_body.clone())
.file_name("upload.txt")
.mime_str("text/plain")?,
);
let post_resp = local_http_client()
.post(format!("{}/{}", env.url, bucket))
.multipart(post_form)
.send()
.await?;
let status = post_resp.status();
let response_body = post_resp.text().await?;
assert_eq!(status, reqwest::StatusCode::NO_CONTENT);
assert!(response_body.is_empty(), "204 response should not contain a body, got: {response_body}");
let head = admin_client.head_object().bucket(bucket).key(object_key).send().await?;
assert_eq!(head.cache_control(), Some(cache_control));
let get_out = admin_client.get_object().bucket(bucket).key(object_key).send().await?;
let uploaded = get_out.body.collect().await?.into_bytes();
assert_eq!(uploaded.as_ref(), expected_body.as_slice());
Ok(())
}
#[tokio::test]
async fn test_anonymous_post_object_accepts_content_language_field_exact_policy_match()
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
env.start_rustfs_server(vec![]).await?;
let bucket = "anon-post-policy-content-language-accept";
let object_key = "uploads/content-language-object.txt";
let content_language = "en-US";
let expected_body = b"post-policy-content-language-body".to_vec();
let admin_client = env.create_s3_client();
admin_client.create_bucket().bucket(bucket).send().await?;
allow_anonymous_put_object(&admin_client, bucket).await?;
let policy = encode_post_policy(vec![
serde_json::json!({ "bucket": bucket }),
serde_json::json!({ "key": object_key }),
serde_json::json!({ "Content-Language": content_language }),
serde_json::json!(["content-length-range", 0, 1024]),
]);
let post_form = reqwest::multipart::Form::new()
.text("key", object_key.to_string())
.text("policy", policy)
.text("Content-Language", content_language)
.part(
"file",
reqwest::multipart::Part::bytes(expected_body.clone())
.file_name("upload.txt")
.mime_str("text/plain")?,
);
let post_resp = local_http_client()
.post(format!("{}/{}", env.url, bucket))
.multipart(post_form)
.send()
.await?;
let status = post_resp.status();
let response_body = post_resp.text().await?;
assert_eq!(status, reqwest::StatusCode::NO_CONTENT);
assert!(response_body.is_empty(), "204 response should not contain a body, got: {response_body}");
let head = admin_client.head_object().bucket(bucket).key(object_key).send().await?;
assert_eq!(head.content_language(), Some(content_language));
let get_out = admin_client.get_object().bucket(bucket).key(object_key).send().await?;
let uploaded = get_out.body.collect().await?.into_bytes();
assert_eq!(uploaded.as_ref(), expected_body.as_slice());
Ok(())
}
#[tokio::test]
async fn test_anonymous_post_object_accepts_content_encoding_field_exact_policy_match()
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
env.start_rustfs_server(vec![]).await?;
let bucket = "anon-post-policy-content-encoding-accept";
let object_key = "uploads/content-encoding-object.txt";
let content_encoding = "gzip";
let expected_body = b"post-policy-content-encoding-body".to_vec();
let admin_client = env.create_s3_client();
admin_client.create_bucket().bucket(bucket).send().await?;
allow_anonymous_put_object(&admin_client, bucket).await?;
let policy = encode_post_policy(vec![
serde_json::json!({ "bucket": bucket }),
serde_json::json!({ "key": object_key }),
serde_json::json!({ "Content-Encoding": content_encoding }),
serde_json::json!(["content-length-range", 0, 1024]),
]);
let post_form = reqwest::multipart::Form::new()
.text("key", object_key.to_string())
.text("policy", policy)
.text("Content-Encoding", content_encoding)
.part(
"file",
reqwest::multipart::Part::bytes(expected_body.clone())
.file_name("upload.txt")
.mime_str("text/plain")?,
);
let post_resp = local_http_client()
.post(format!("{}/{}", env.url, bucket))
.multipart(post_form)
.send()
.await?;
let status = post_resp.status();
let response_body = post_resp.text().await?;
assert_eq!(status, reqwest::StatusCode::NO_CONTENT);
assert!(response_body.is_empty(), "204 response should not contain a body, got: {response_body}");
let head = admin_client.head_object().bucket(bucket).key(object_key).send().await?;
assert_eq!(head.content_encoding(), Some(content_encoding));
let get_out = admin_client.get_object().bucket(bucket).key(object_key).send().await?;
let uploaded = get_out.body.collect().await?.into_bytes();
assert_eq!(uploaded.as_ref(), expected_body.as_slice());
Ok(())
}
#[tokio::test]
async fn test_anonymous_post_object_accepts_website_redirect_location_exact_policy_match()
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
env.start_rustfs_server(vec![]).await?;
let bucket = "anon-post-policy-website-redirect-accept";
let object_key = "uploads/website-redirect-object.txt";
let website_redirect_location = "/docs/landing.html";
let expected_body = b"post-policy-website-redirect-body".to_vec();
let admin_client = env.create_s3_client();
admin_client.create_bucket().bucket(bucket).send().await?;
allow_anonymous_put_object(&admin_client, bucket).await?;
let policy = encode_post_policy(vec![
serde_json::json!({ "bucket": bucket }),
serde_json::json!({ "key": object_key }),
serde_json::json!({ "x-amz-website-redirect-location": website_redirect_location }),
serde_json::json!(["content-length-range", 0, 1024]),
]);
let post_form = reqwest::multipart::Form::new()
.text("key", object_key.to_string())
.text("policy", policy)
.text("x-amz-website-redirect-location", website_redirect_location)
.part(
"file",
reqwest::multipart::Part::bytes(expected_body.clone())
.file_name("upload.txt")
.mime_str("text/plain")?,
);
let post_resp = local_http_client()
.post(format!("{}/{}", env.url, bucket))
.multipart(post_form)
.send()
.await?;
let status = post_resp.status();
let response_body = post_resp.text().await?;
assert_eq!(status, reqwest::StatusCode::NO_CONTENT);
assert!(response_body.is_empty(), "204 response should not contain a body, got: {response_body}");
let head = admin_client.head_object().bucket(bucket).key(object_key).send().await?;
assert_eq!(head.website_redirect_location(), Some(website_redirect_location));
let get_out = admin_client.get_object().bucket(bucket).key(object_key).send().await?;
let uploaded = get_out.body.collect().await?.into_bytes();
assert_eq!(uploaded.as_ref(), expected_body.as_slice());
Ok(())
}
#[tokio::test]
async fn test_anonymous_post_object_accepts_expires_field_exact_policy_match()
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
env.start_rustfs_server(vec![]).await?;
let bucket = "anon-post-policy-expires-accept";
let object_key = "uploads/expires-object.txt";
let expires = "Wed, 21 Oct 2037 07:28:00 GMT";
let expected_body = b"post-policy-expires-body".to_vec();
let admin_client = env.create_s3_client();
admin_client.create_bucket().bucket(bucket).send().await?;
allow_anonymous_put_object(&admin_client, bucket).await?;
let policy = encode_post_policy(vec![
serde_json::json!({ "bucket": bucket }),
serde_json::json!({ "key": object_key }),
serde_json::json!({ "Expires": expires }),
serde_json::json!(["content-length-range", 0, 1024]),
]);
let post_form = reqwest::multipart::Form::new()
.text("key", object_key.to_string())
.text("policy", policy)
.text("Expires", expires)
.part(
"file",
reqwest::multipart::Part::bytes(expected_body.clone())
.file_name("upload.txt")
.mime_str("text/plain")?,
);
let post_resp = local_http_client()
.post(format!("{}/{}", env.url, bucket))
.multipart(post_form)
.send()
.await?;
let status = post_resp.status();
let response_body = post_resp.text().await?;
assert_eq!(status, reqwest::StatusCode::NO_CONTENT);
assert!(response_body.is_empty(), "204 response should not contain a body, got: {response_body}");
let head = admin_client.head_object().bucket(bucket).key(object_key).send().await?;
assert_eq!(head.expires_string(), Some(expires));
let get_out = admin_client.get_object().bucket(bucket).key(object_key).send().await?;
let uploaded = get_out.body.collect().await?.into_bytes();
assert_eq!(uploaded.as_ref(), expected_body.as_slice());
Ok(())
}
@@ -3123,6 +3440,64 @@ async fn test_anonymous_post_object_accepts_tagging_field_exact_policy_match()
Ok(())
}
#[tokio::test]
async fn test_anonymous_post_object_accepts_metadata_field_exact_policy_match()
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
env.start_rustfs_server(vec![]).await?;
let bucket = "anon-post-policy-meta-exact-accept";
let object_key = "uploads/meta-exact-accept-object.txt";
let metadata_value = "alpha-demo";
let expected_body = b"post-policy-meta-exact-body".to_vec();
let admin_client = env.create_s3_client();
admin_client.create_bucket().bucket(bucket).send().await?;
allow_anonymous_put_object(&admin_client, bucket).await?;
let policy = encode_post_policy(vec![
serde_json::json!({ "bucket": bucket }),
serde_json::json!({ "key": object_key }),
serde_json::json!({ "x-amz-meta-project": metadata_value }),
serde_json::json!(["content-length-range", 0, 1024]),
]);
let post_form = reqwest::multipart::Form::new()
.text("key", object_key.to_string())
.text("policy", policy)
.text("x-amz-meta-project", metadata_value)
.part(
"file",
reqwest::multipart::Part::bytes(expected_body.clone())
.file_name("upload.txt")
.mime_str("text/plain")?,
);
let post_resp = local_http_client()
.post(format!("{}/{}", env.url, bucket))
.multipart(post_form)
.send()
.await?;
let status = post_resp.status();
let response_body = post_resp.text().await?;
assert_eq!(status, reqwest::StatusCode::NO_CONTENT);
assert!(response_body.is_empty(), "204 response should not contain a body, got: {response_body}");
let head = admin_client.head_object().bucket(bucket).key(object_key).send().await?;
let metadata = head.metadata().expect("head_object should expose uploaded metadata");
assert_eq!(metadata.get("project").map(String::as_str), Some(metadata_value));
let get_out = admin_client.get_object().bucket(bucket).key(object_key).send().await?;
let uploaded = get_out.body.collect().await?.into_bytes();
assert_eq!(uploaded.as_ref(), expected_body.as_slice());
Ok(())
}
#[tokio::test]
async fn test_anonymous_post_object_allows_x_ignore_fields_outside_policy_conditions()
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
@@ -16,7 +16,6 @@ use crate::common::RustFSTestClusterEnvironment;
use aws_sdk_s3::Client;
use aws_sdk_s3::error::SdkError;
use bytes::Bytes;
use serial_test::serial;
use std::sync::Arc;
use tokio::sync::Barrier;
use tracing::{info, warn};
@@ -51,7 +50,6 @@ fn format_s3_error(err: SdkError<aws_sdk_s3::operation::put_object::PutObjectErr
}
#[tokio::test]
#[serial]
async fn test_concurrent_cluster_overwrites_do_not_fail_namespace_lock_quorum() -> TestResult {
crate::common::init_logging();
info!("Starting namespace lock quorum regression test with auto cluster");
@@ -128,7 +126,6 @@ async fn test_concurrent_cluster_overwrites_do_not_fail_namespace_lock_quorum()
/// `StorageError::other(...)` → `StorageError::Io(...)`, which fell through to
/// `S3ErrorCode::InternalError` (500) in the error mapping.
#[tokio::test]
#[serial]
async fn test_concurrent_put_same_key_never_returns_500() -> TestResult {
crate::common::init_logging();
info!("Starting concurrent PUT 500 regression test");
@@ -37,7 +37,6 @@ use crate::common::{RustFSTestEnvironment, init_logging, local_http_client};
use aws_sdk_s3::primitives::ByteStream;
use rustfs_signer::constants::UNSIGNED_PAYLOAD;
use rustfs_signer::request_signature_v4::{SIGN_V4_ALGORITHM, get_scope, get_signature, get_signing_key};
use serial_test::serial;
use std::fmt::Write as _;
use time::macros::format_description;
use time::{Duration, OffsetDateTime};
@@ -183,7 +182,6 @@ async fn setup(env: &mut RustFSTestEnvironment) -> Result<(), Box<dyn std::error
/// this, every negative assertion below could pass for the wrong reason (a
/// broken signer that never produces a valid signature).
#[tokio::test]
#[serial]
async fn valid_header_sigv4_request_succeeds() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -214,7 +212,6 @@ async fn valid_header_sigv4_request_succeeds() -> Result<(), Box<dyn std::error:
/// (a) Tampering the `Signature=` component must be rejected with
/// SignatureDoesNotMatch / 403.
#[tokio::test]
#[serial]
async fn tampered_signature_returns_signature_does_not_match() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -254,7 +251,6 @@ async fn tampered_signature_returns_signature_does_not_match() -> Result<(), Box
/// (b) A valid AccessKeyId paired with the wrong secret key must be rejected
/// with SignatureDoesNotMatch / 403.
#[tokio::test]
#[serial]
async fn wrong_secret_key_returns_signature_does_not_match() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -279,7 +275,6 @@ async fn wrong_secret_key_returns_signature_does_not_match() -> Result<(), Box<d
/// signature itself is valid (it covers the *declared* hash), so the server is
/// forced to detect the payload/hash mismatch while streaming the body.
#[tokio::test]
#[serial]
async fn tampered_payload_is_rejected() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -320,7 +315,6 @@ async fn tampered_payload_is_rejected() -> Result<(), Box<dyn std::error::Error
/// x-amz-date both derive from the same skewed timestamp, so skew — not a
/// signature mismatch — is the failure.
#[tokio::test]
#[serial]
async fn skewed_date_returns_request_time_too_skewed() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -344,7 +338,6 @@ async fn skewed_date_returns_request_time_too_skewed() -> Result<(), Box<dyn std
/// structurally invalid SigV4 header that must be rejected before any
/// credential/service handling.
#[tokio::test]
#[serial]
async fn malformed_authorization_header_returns_clean_4xx() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -32,7 +32,6 @@
mod tests {
use crate::common::{RustFSTestEnvironment, init_logging};
use aws_sdk_s3::types::{BucketVersioningStatus, VersioningConfiguration};
use serial_test::serial;
use std::error::Error;
use tracing::info;
@@ -47,7 +46,6 @@ mod tests {
/// starts successfully with notification enabled and can serve S3 requests.
/// A full webhook delivery test is in notification_webhook_test.rs.
#[tokio::test]
#[serial]
async fn test_notification_enabled_server_starts_cleanly() -> TestResult {
init_logging();
info!("RT-01: notification enabled server starts cleanly");
@@ -92,7 +90,6 @@ mod tests {
/// 3. Restart server
/// 4. Verify notification config still exists
#[tokio::test]
#[serial]
async fn test_notification_config_survives_restart() -> TestResult {
init_logging();
info!("RT-02: notification config survives restart");
@@ -47,7 +47,6 @@ use rustfs_utils::egress::ENV_OUTBOUND_ALLOW_ORIGINS;
use rustfs_utils::http::headers::{AMZ_REQUEST_ID, REQUEST_ID_HEADER};
use s3s::Body;
use serde_json::Value;
use serial_test::serial;
use std::error::Error;
use std::io::Cursor;
use std::path::Path;
@@ -625,7 +624,6 @@ fn assert_generated_request_id_correlation(record: &Value, request_id: &str) {
/// RUSTFS_NOTIFY_ENABLE, an HTTPS webhook using a configured CA must become
/// online and receive a real S3 event POST.
#[tokio::test]
#[serial]
async fn test_https_webhook_target_delivers_event_with_notify_env_enabled() -> TestResult {
init_logging();
@@ -680,7 +678,6 @@ async fn test_https_webhook_target_delivers_event_with_notify_env_enabled() -> T
/// PUT / multipart-complete / DELETE each deliver one event with correct fields,
/// and the prefix/suffix filter drops non-matching keys.
#[tokio::test]
#[serial]
async fn test_webhook_event_delivery_and_filtering() -> TestResult {
init_logging();
@@ -900,7 +897,6 @@ async fn test_webhook_event_delivery_and_filtering() -> TestResult {
/// An event queued while the target endpoint rejects delivery survives on the
/// durable store and is redelivered once the endpoint comes back.
#[tokio::test]
#[serial]
async fn test_webhook_redelivers_event_after_target_recovers() -> TestResult {
init_logging();
-17
View File
@@ -20,7 +20,6 @@ use rustfs_signer::constants::UNSIGNED_PAYLOAD;
use rustfs_signer::{pre_sign_v4, sign_v4};
use rustfs_utils::egress::ENV_OUTBOUND_ALLOW_ORIGINS;
use s3s::Body;
use serial_test::serial;
use std::collections::HashMap;
use std::error::Error;
use time::OffsetDateTime;
@@ -548,7 +547,6 @@ async fn read_listen_notification_event(
}
#[tokio::test]
#[serial]
async fn test_notification_target_persists_across_restart_and_delete() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -608,7 +606,6 @@ async fn test_notification_target_persists_across_restart_and_delete() -> Result
}
#[tokio::test]
#[serial]
async fn test_notification_target_with_path_is_online_via_transport_probe() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -641,7 +638,6 @@ async fn test_notification_target_with_path_is_online_via_transport_probe() -> R
}
#[tokio::test]
#[serial]
async fn test_get_object_lambda_accepts_presigned_requests() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -682,7 +678,6 @@ async fn test_get_object_lambda_accepts_presigned_requests() -> Result<(), Box<d
}
#[tokio::test]
#[serial]
async fn test_get_object_lambda_accepts_named_webhook_target_arn() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -722,7 +717,6 @@ async fn test_get_object_lambda_accepts_named_webhook_target_arn() -> Result<(),
}
#[tokio::test]
#[serial]
async fn test_get_object_lambda_invokes_runtime_webhook_target() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -790,7 +784,6 @@ async fn test_get_object_lambda_invokes_runtime_webhook_target() -> Result<(), B
}
#[tokio::test]
#[serial]
async fn test_get_object_lambda_passthroughs_non_success_webhook_response() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -850,7 +843,6 @@ async fn test_get_object_lambda_passthroughs_non_success_webhook_response() -> R
}
#[tokio::test]
#[serial]
async fn test_get_object_lambda_rejects_success_response_without_auth_headers() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -896,7 +888,6 @@ async fn test_get_object_lambda_rejects_success_response_without_auth_headers()
}
#[tokio::test]
#[serial]
async fn test_get_object_lambda_rejects_success_response_with_mismatched_auth_headers() -> Result<(), Box<dyn Error + Send + Sync>>
{
init_logging();
@@ -943,7 +934,6 @@ async fn test_get_object_lambda_rejects_success_response_with_mismatched_auth_he
}
#[tokio::test]
#[serial]
async fn test_get_object_lambda_rejects_unsupported_target_type() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -980,7 +970,6 @@ async fn test_get_object_lambda_rejects_unsupported_target_type() -> Result<(),
}
#[tokio::test]
#[serial]
async fn test_get_object_lambda_rejects_unconfigured_target() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -1017,7 +1006,6 @@ async fn test_get_object_lambda_rejects_unconfigured_target() -> Result<(), Box<
}
#[tokio::test]
#[serial]
async fn test_get_object_lambda_rejects_disabled_target() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -1063,7 +1051,6 @@ async fn test_get_object_lambda_rejects_disabled_target() -> Result<(), Box<dyn
}
#[tokio::test]
#[serial]
async fn test_configure_object_lambda_target_rejects_invalid_endpoint() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -1106,7 +1093,6 @@ async fn test_configure_object_lambda_target_rejects_invalid_endpoint() -> Resul
}
#[tokio::test]
#[serial]
async fn test_configure_object_lambda_notify_webhook_rejects_response_header_timeout_key()
-> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -1140,7 +1126,6 @@ async fn test_configure_object_lambda_notify_webhook_rejects_response_header_tim
}
#[tokio::test]
#[serial]
async fn test_listen_notification_emits_after_put_object() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -1184,7 +1169,6 @@ async fn test_listen_notification_emits_after_put_object() -> Result<(), Box<dyn
}
#[tokio::test]
#[serial]
async fn test_listen_notification_emits_on_empty_bucket_when_notify_disabled() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -1219,7 +1203,6 @@ async fn test_listen_notification_emits_on_empty_bucket_when_notify_disabled() -
}
#[tokio::test]
#[serial]
async fn test_listen_notification_fans_in_remote_node_events() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -13,7 +13,6 @@
// limitations under the License.
use aws_sdk_s3::primitives::ByteStream;
use serial_test::serial;
use std::path::{Path, PathBuf};
use uuid::Uuid;
@@ -24,7 +23,6 @@ const TEST_OBJECT: &str = "large-object.bin";
const PAYLOAD_SIZE: usize = 512 * 1024;
#[tokio::test(flavor = "multi_thread")]
#[serial]
async fn unversioned_overwrite_removes_previous_physical_data_dir() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
@@ -43,7 +43,6 @@ use aws_sdk_s3::presigning::{PresignedRequest, PresigningConfig};
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::{Client, Config};
use aws_smithy_http_client::Builder as SmithyHttpClientBuilder;
use serial_test::serial;
use std::time::{Duration, SystemTime};
use tracing::info;
@@ -157,7 +156,6 @@ async fn setup(env: &mut RustFSTestEnvironment) -> Result<(), Box<dyn std::error
/// stored bytes. Without this, every negative assertion could pass for the
/// wrong reason (a server that rejects all presigned URLs).
#[tokio::test]
#[serial]
async fn valid_presigned_get_succeeds() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -182,7 +180,6 @@ async fn valid_presigned_get_succeeds() -> Result<(), Box<dyn std::error::Error
/// Positive control (PUT): a valid presigned PUT must store the object, which we
/// verify with a follow-up authenticated HEAD.
#[tokio::test]
#[serial]
async fn valid_presigned_put_succeeds() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -211,7 +208,6 @@ async fn valid_presigned_put_succeeds() -> Result<(), Box<dyn std::error::Error
/// ("Request has expired"). s3s checks expiry BEFORE the signature, so the
/// signature here is otherwise valid — only the elapsed window is at fault.
#[tokio::test]
#[serial]
async fn expired_presigned_get_is_rejected() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -236,7 +232,6 @@ async fn expired_presigned_get_is_rejected() -> Result<(), Box<dyn std::error::E
/// (b) Tampering the `X-Amz-Signature` query value must be rejected with 403 /
/// SignatureDoesNotMatch.
#[tokio::test]
#[serial]
async fn tampered_signature_returns_signature_does_not_match() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -262,7 +257,6 @@ async fn tampered_signature_returns_signature_does_not_match() -> Result<(), Box
/// (c) A presigned URL generated with the WRONG secret (but the real access key
/// id) must be rejected with 403 / SignatureDoesNotMatch.
#[tokio::test]
#[serial]
async fn wrong_secret_key_returns_signature_does_not_match() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -290,7 +284,6 @@ async fn wrong_secret_key_returns_signature_does_not_match() -> Result<(), Box<d
/// check runs during auth, before any object lookup, so the swapped key need
/// not even exist.
#[tokio::test]
#[serial]
async fn tampered_target_key_returns_signature_does_not_match() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -325,7 +318,6 @@ async fn tampered_target_key_returns_signature_does_not_match() -> Result<(), Bo
/// (e / acceptance 4 negative half) Tampering the signature of a presigned PUT
/// must be rejected with 403 / SignatureDoesNotMatch — the write must not land.
#[tokio::test]
#[serial]
async fn tampered_presigned_put_returns_signature_does_not_match() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -22,7 +22,6 @@ use crate::protocols::sftp_compliance::{
};
use crate::protocols::sftp_core::{test_sftp_core_operations, test_sftp_idle_timeout_disconnects};
use crate::protocols::webdav_core::test_webdav_core_operations;
use serial_test::serial;
use std::time::Instant;
use tokio::time::{Duration, sleep};
use tracing::{error, info};
@@ -229,7 +228,6 @@ fn all_protocol_tests() -> Vec<TestDefinition> {
/// Test suite
#[tokio::test]
#[serial]
async fn test_protocol_core_suite() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let suite = ProtocolTestSuite::new();
let results = suite.run_test_suite().await;
-15
View File
@@ -15,7 +15,6 @@
use crate::common::{RustFSTestEnvironment, admin_request, awscurl_delete, awscurl_get, awscurl_post, awscurl_put, init_logging};
use aws_sdk_s3::Client;
use http::{Method, StatusCode};
use serial_test::serial;
use tokio::time::{Duration, sleep, timeout};
use tracing::{debug, info};
@@ -255,7 +254,6 @@ mod integration_tests {
use aws_sdk_s3::error::ProvideErrorMetadata;
#[tokio::test]
#[serial]
async fn test_quota_basic_operations() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if skip_without_awscurl() {
@@ -300,7 +298,6 @@ mod integration_tests {
/// with 400 UnexpectedContent, and an over-quota aws-chunked PUT must still get the quota
/// rejection.
#[tokio::test]
#[serial]
async fn test_quota_admission_aws_chunked_declared_encoding() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if skip_without_awscurl() {
@@ -352,7 +349,6 @@ mod integration_tests {
}
#[tokio::test]
#[serial]
async fn test_quota_update_and_clear() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if skip_without_awscurl() {
@@ -388,7 +384,6 @@ mod integration_tests {
}
#[tokio::test]
#[serial]
async fn test_quota_delete_operations() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if skip_without_awscurl() {
@@ -425,7 +420,6 @@ mod integration_tests {
}
#[tokio::test]
#[serial]
async fn test_quota_usage_tracking() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if skip_without_awscurl() {
@@ -464,7 +458,6 @@ mod integration_tests {
}
#[tokio::test]
#[serial]
async fn test_quota_statistics() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if skip_without_awscurl() {
@@ -498,7 +491,6 @@ mod integration_tests {
}
#[tokio::test]
#[serial]
async fn test_quota_check_api() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if skip_without_awscurl() {
@@ -539,7 +531,6 @@ mod integration_tests {
}
#[tokio::test]
#[serial]
async fn test_quota_multiple_buckets() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if skip_without_awscurl() {
@@ -580,7 +571,6 @@ mod integration_tests {
}
#[tokio::test]
#[serial]
async fn test_quota_error_handling() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if skip_without_awscurl() {
@@ -616,7 +606,6 @@ mod integration_tests {
}
#[tokio::test]
#[serial]
async fn test_quota_http_endpoints() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if skip_without_awscurl() {
@@ -682,7 +671,6 @@ mod integration_tests {
/// Test that a normal user with `readwrite` policy can read quota but cannot set/clear quota.
#[tokio::test]
#[serial]
async fn test_quota_normal_user_permissions() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if skip_without_awscurl() {
@@ -738,7 +726,6 @@ mod integration_tests {
}
#[tokio::test]
#[serial]
async fn test_quota_copy_operations() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if skip_without_awscurl() {
@@ -784,7 +771,6 @@ mod integration_tests {
}
#[tokio::test]
#[serial]
async fn test_quota_batch_delete() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if skip_without_awscurl() {
@@ -843,7 +829,6 @@ mod integration_tests {
}
#[tokio::test]
#[serial]
async fn test_quota_multipart_upload() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
if skip_without_awscurl() {
@@ -6,7 +6,6 @@ use aws_sdk_s3::config::{Credentials, Region};
use aws_sdk_s3::error::SdkError;
use aws_sdk_s3::types::{CompletedMultipartUpload, CompletedPart};
use bytes::Bytes;
use serial_test::serial;
use std::error::Error;
const ENDPOINT: &str = "http://localhost:9000";
@@ -89,7 +88,6 @@ fn generate_test_key(prefix: &str) -> String {
}
#[tokio::test]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
async fn test_conditional_put_okay() -> Result<(), Box<dyn std::error::Error>> {
let client = create_aws_s3_client().await?;
@@ -132,7 +130,6 @@ async fn test_conditional_put_okay() -> Result<(), Box<dyn std::error::Error>> {
}
#[tokio::test]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
async fn test_conditional_put_failed() -> Result<(), Box<dyn std::error::Error>> {
let client = create_aws_s3_client().await?;
@@ -195,7 +192,6 @@ async fn test_conditional_put_failed() -> Result<(), Box<dyn std::error::Error>>
}
#[tokio::test]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
async fn test_conditional_put_when_object_does_not_exist() -> Result<(), Box<dyn std::error::Error>> {
let client = create_aws_s3_client().await?;
@@ -240,7 +236,6 @@ async fn test_conditional_put_when_object_does_not_exist() -> Result<(), Box<dyn
}
#[tokio::test]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
async fn test_conditional_multi_part_upload() -> Result<(), Box<dyn std::error::Error>> {
let client = create_aws_s3_client().await?;
@@ -24,7 +24,6 @@ use aws_sdk_s3::Client;
use aws_sdk_s3::config::{Credentials, Region};
use aws_sdk_s3::error::SdkError;
use bytes::Bytes;
use serial_test::serial;
use std::error::Error;
use tracing::info;
@@ -70,7 +69,6 @@ async fn setup_test_bucket(client: &Client) -> Result<(), Box<dyn Error>> {
}
#[tokio::test]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
async fn test_get_deleted_object_returns_nosuchkey() -> Result<(), Box<dyn std::error::Error>> {
// Initialize logging
@@ -144,7 +142,6 @@ async fn test_get_deleted_object_returns_nosuchkey() -> Result<(), Box<dyn std::
/// Test that HeadObject on a deleted object also returns NoSuchKey
#[tokio::test]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
async fn test_head_deleted_object_returns_nosuchkey() -> Result<(), Box<dyn std::error::Error>> {
let _ = tracing_subscriber::fmt()
@@ -196,7 +193,6 @@ async fn test_head_deleted_object_returns_nosuchkey() -> Result<(), Box<dyn std:
/// Test GetObject with non-existent key (never existed)
#[tokio::test]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
async fn test_get_nonexistent_object_returns_nosuchkey() -> Result<(), Box<dyn std::error::Error>> {
let _ = tracing_subscriber::fmt()
@@ -233,7 +229,6 @@ async fn test_get_nonexistent_object_returns_nosuchkey() -> Result<(), Box<dyn s
/// Test multiple consecutive GetObject calls on deleted object
/// This ensures the fix is stable and doesn't have race conditions
#[tokio::test]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
async fn test_multiple_gets_deleted_object() -> Result<(), Box<dyn std::error::Error>> {
let _ = tracing_subscriber::fmt()
@@ -25,7 +25,6 @@ use aws_sdk_s3::config::{Credentials, Region};
use aws_sdk_s3::error::SdkError;
use aws_sdk_s3::types::{BucketVersioningStatus, VersioningConfiguration};
use bytes::Bytes;
use serial_test::serial;
use std::error::Error;
use tracing::info;
@@ -85,7 +84,6 @@ async fn setup_test_bucket(client: &Client) -> Result<(), Box<dyn Error>> {
/// Test that HeadObject on a deleted object returns NoSuchKey when versioning is enabled
#[tokio::test]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
async fn test_head_deleted_object_versioning_returns_nosuchkey() -> Result<(), Box<dyn std::error::Error>> {
let _ = tracing_subscriber::fmt()
@@ -30,7 +30,6 @@ use reqwest::{Certificate, Client, Response, StatusCode};
use rustfs_signer::constants::UNSIGNED_PAYLOAD;
use rustfs_signer::sign_v4;
use s3s::Body;
use serial_test::serial;
use std::error::Error;
use std::path::Path;
use std::process::Command;
@@ -157,7 +156,6 @@ async fn start_tls_rustfs_server(env: &mut RustFSTestEnvironment, tls_dir: &Path
}
#[tokio::test]
#[serial]
async fn test_head_missing_object_over_tls_http2_is_bodyless() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
-8
View File
@@ -20,7 +20,6 @@ use aws_sdk_s3::types::{
CsvInput, CsvOutput, ExpressionType, FileHeaderInfo, InputSerialization, JsonInput, JsonOutput, JsonType, OutputSerialization,
};
use bytes::Bytes;
use serial_test::serial;
use std::error::Error;
const ENDPOINT: &str = "http://localhost:9000";
@@ -118,7 +117,6 @@ async fn process_select_response(
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
async fn test_select_object_content_csv_basic() -> Result<(), Box<dyn Error>> {
let client = create_aws_s3_client().await?;
@@ -160,7 +158,6 @@ async fn test_select_object_content_csv_basic() -> Result<(), Box<dyn Error>> {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
async fn test_select_object_content_csv_aggregation() -> Result<(), Box<dyn Error>> {
let client = create_aws_s3_client().await?;
@@ -206,7 +203,6 @@ async fn test_select_object_content_csv_aggregation() -> Result<(), Box<dyn Erro
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
async fn test_select_object_content_json_basic() -> Result<(), Box<dyn Error>> {
let client = create_aws_s3_client().await?;
@@ -248,7 +244,6 @@ async fn test_select_object_content_json_basic() -> Result<(), Box<dyn Error>> {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
async fn test_select_object_content_csv_limit() -> Result<(), Box<dyn Error>> {
let client = create_aws_s3_client().await?;
@@ -291,7 +286,6 @@ async fn test_select_object_content_csv_limit() -> Result<(), Box<dyn Error>> {
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
async fn test_select_object_content_csv_order_by() -> Result<(), Box<dyn Error>> {
let client = create_aws_s3_client().await?;
@@ -337,7 +331,6 @@ async fn test_select_object_content_csv_order_by() -> Result<(), Box<dyn Error>>
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
async fn test_select_object_content_error_handling() -> Result<(), Box<dyn Error>> {
let client = create_aws_s3_client().await?;
@@ -373,7 +366,6 @@ async fn test_select_object_content_error_handling() -> Result<(), Box<dyn Error
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
#[serial]
#[ignore = "requires running RustFS server at localhost:9000"]
async fn test_select_object_content_nonexistent_object() -> Result<(), Box<dyn Error>> {
let client = create_aws_s3_client().await?;
@@ -60,7 +60,6 @@ use rustfs_signer::constants::UNSIGNED_PAYLOAD;
use rustfs_signer::sign_v4;
use s3s::Body;
use s3s::header::X_AMZ_REPLICATION_STATUS;
use serial_test::serial;
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
use std::convert::Infallible;
@@ -4630,7 +4629,6 @@ async fn test_bucket_replication_sse_c_multipart_passthrough() -> TestResult {
/// fails without any PUT reaching the target (capability cache, proven from
/// the target journal); plaintext objects still replicate COMPLETED.
#[tokio::test]
#[serial]
async fn test_ssec_replication_fails_closed_when_target_drops_passthrough_headers() -> TestResult {
init_logging();
@@ -4740,7 +4738,6 @@ async fn test_ssec_replication_fails_closed_when_target_drops_passthrough_header
/// a capability limit, and a plaintext-only deployment against a MinIO-like
/// target must not turn red.
#[tokio::test]
#[serial]
async fn test_replication_check_flags_ssec_passthrough_dropping_target() -> TestResult {
init_logging();
@@ -4831,7 +4828,6 @@ async fn test_replication_check_flags_ssec_passthrough_dropping_target() -> Test
/// HEAD-back audit against the recovered RustFS target — and the replica must
/// be readable with the customer key.
#[tokio::test]
#[serial]
async fn test_bucket_replication_sse_c_heals_after_target_outage() -> TestResult {
init_logging();
@@ -4924,7 +4920,6 @@ async fn test_bucket_replication_sse_c_heals_after_target_outage() -> TestResult
/// the existing-object resync (`replicate_all` transport, N2-audited), land as
/// a REPLICA, and read back with the customer key.
#[tokio::test]
#[serial]
async fn test_bucket_replication_sse_c_existing_object_resync() -> TestResult {
init_logging();
@@ -8819,7 +8814,6 @@ async fn start_read_proxy_lab(
/// headers are forwarded verbatim, and an inbound request that was itself
/// proxied is answered locally (404) without touching the target.
#[tokio::test]
#[serial]
async fn test_get_and_head_proxy_unreplicated_object_to_replication_target() -> TestResult {
init_logging();
@@ -9002,7 +8996,6 @@ async fn test_get_and_head_proxy_unreplicated_object_to_replication_target() ->
/// proxied to the replication target with the anti-loop marker, mirroring
/// MinIO `proxyGetTaggingToRepTarget`.
#[tokio::test]
#[serial]
async fn test_get_object_tagging_proxies_unreplicated_object_to_replication_target() -> TestResult {
init_logging();
@@ -25,7 +25,6 @@ use crate::common::{RustFSTestEnvironment, awscurl_available, awscurl_put, init_
use aws_sdk_s3::error::ProvideErrorMetadata;
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::{CompletedMultipartUpload, CompletedPart, Tag, Tagging};
use serial_test::serial;
use std::error::Error;
use tracing::info;
@@ -36,7 +35,6 @@ use tracing::info;
/// far beyond that limit and assert the server rejects it with the specific
/// error, rather than accepting an arbitrarily large control-plane body.
#[tokio::test]
#[serial]
async fn test_large_xml_body_rejection() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -92,7 +90,6 @@ async fn test_large_xml_body_rejection() -> Result<(), Box<dyn Error + Send + Sy
/// Excessive multipart parts must be rejected.
#[tokio::test]
#[serial]
async fn test_excessive_multipart_parts() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -149,7 +146,6 @@ async fn test_excessive_multipart_parts() -> Result<(), Box<dyn Error + Send + S
/// (last-writer-wins, no torn/garbage state) and that it is absent after a
/// subsequent delete.
#[tokio::test]
#[serial]
async fn test_concurrent_object_operations() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
let mut env = RustFSTestEnvironment::new().await?;
@@ -232,7 +228,6 @@ async fn test_concurrent_object_operations() -> Result<(), Box<dyn Error + Send
/// pattern used by the other admin-API E2E tests in this crate; the test is
/// skipped when `awscurl` is not installed.
#[tokio::test]
#[serial]
async fn test_tiering_url_validation() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
if !awscurl_available() {
@@ -23,7 +23,6 @@
#[cfg(test)]
mod tests {
use crate::common::{RustFSTestEnvironment, init_logging, rustfs_binary_path};
use serial_test::serial;
use std::net::TcpListener;
use std::time::{Duration, Instant};
@@ -31,7 +30,6 @@ mod tests {
/// while :9001 is occupied: the server exits at startup, and the harness
/// must surface that promptly rather than waiting out the 60s timeout.
#[tokio::test]
#[serial]
async fn test_start_fails_fast_when_server_exits_during_startup() {
init_logging();
@@ -17,7 +17,6 @@ mod tests {
use crate::common::{RustFSTestEnvironment, init_logging};
use aws_sdk_s3::error::ProvideErrorMetadata;
use aws_sdk_s3::primitives::ByteStream;
use serial_test::serial;
use std::error::Error;
use std::io::Cursor;
@@ -101,7 +100,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn snowball_auto_extract_supports_minio_prefix_and_directory_markers() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -138,7 +136,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn snowball_auto_extract_supports_standard_headers_with_combined_extract_options()
-> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -229,7 +226,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn snowball_auto_extract_ignores_directories_when_requested() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -268,7 +264,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn snowball_auto_extract_ignores_invalid_entries_when_requested() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -304,7 +299,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn snowball_auto_extract_rejects_parent_dir_entry_without_cross_bucket_write()
-> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -347,7 +341,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn snowball_auto_extract_prefers_exact_minio_prefix_over_suffix_fallback() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
-15
View File
@@ -34,7 +34,6 @@ mod tests {
use rustfs_signer::constants::UNSIGNED_PAYLOAD;
use rustfs_signer::sign_v4;
use s3s::Body;
use serial_test::serial;
use std::error::Error;
use tracing::{debug, info};
@@ -93,7 +92,6 @@ mod tests {
/// mc cp README.md "local/dummy/a%20f+/b/c/3/README.md"
/// ```
#[tokio::test]
#[serial]
async fn test_object_with_space_in_path() {
init_logging();
info!("Starting test: object with space in path");
@@ -175,7 +173,6 @@ mod tests {
/// /test/data/org_main-org/dashboards/ES+net/LHC+Data+Challenge/firefly-details.json
/// ```
#[tokio::test]
#[serial]
async fn test_object_with_plus_in_path() {
init_logging();
info!("Starting test: object with plus sign in path");
@@ -245,7 +242,6 @@ mod tests {
/// Test with mixed special characters
#[tokio::test]
#[serial]
async fn test_object_with_mixed_special_chars() {
init_logging();
info!("Starting test: object with mixed special characters");
@@ -305,7 +301,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_signed_get_missing_object_with_trailing_equals_returns_no_such_key() -> Result<(), Box<dyn Error + Send + Sync>>
{
init_logging();
@@ -334,7 +329,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_signed_get_existing_object_with_trailing_equals_returns_content() -> Result<(), Box<dyn Error + Send + Sync>> {
init_logging();
@@ -373,7 +367,6 @@ mod tests {
/// Test DELETE operation with special characters
#[tokio::test]
#[serial]
async fn test_delete_object_with_special_chars() {
init_logging();
info!("Starting test: DELETE object with special characters");
@@ -421,7 +414,6 @@ mod tests {
/// Test exact scenario from the issue
#[tokio::test]
#[serial]
async fn test_issue_scenario_exact() {
init_logging();
info!("Starting test: Exact scenario from GitHub issue");
@@ -494,7 +486,6 @@ mod tests {
/// Test HEAD object with special characters
#[tokio::test]
#[serial]
async fn test_head_object_with_special_chars() {
init_logging();
info!("Starting test: HEAD object with special characters");
@@ -538,7 +529,6 @@ mod tests {
/// Test COPY object with special characters in both source and destination
#[tokio::test]
#[serial]
async fn test_copy_object_with_special_chars() {
init_logging();
info!("Starting test: COPY object with special characters");
@@ -597,7 +587,6 @@ mod tests {
/// Test Unicode characters in object keys
#[tokio::test]
#[serial]
async fn test_unicode_characters_in_path() {
init_logging();
info!("Starting test: Unicode characters in object paths");
@@ -661,7 +650,6 @@ mod tests {
/// Test special characters in different parts of the path
#[tokio::test]
#[serial]
async fn test_special_chars_in_different_path_positions() {
init_logging();
info!("Starting test: Special characters in different path positions");
@@ -719,7 +707,6 @@ mod tests {
/// Test that control characters are properly rejected
#[tokio::test]
#[serial]
async fn test_control_characters_rejected() {
init_logging();
info!("Starting test: Control characters should be rejected");
@@ -769,7 +756,6 @@ mod tests {
/// Test LIST with various special character prefixes
#[tokio::test]
#[serial]
async fn test_list_with_special_char_prefixes() {
init_logging();
info!("Starting test: LIST with special character prefixes");
@@ -838,7 +824,6 @@ mod tests {
/// Test delimiter-based listing with special characters
#[tokio::test]
#[serial]
async fn test_list_with_delimiter_and_special_chars() {
init_logging();
info!("Starting test: LIST with delimiter and special characters");
@@ -16,7 +16,6 @@ use crate::common::{RustFSTestClusterEnvironment, init_logging};
use aws_sdk_s3::error::SdkError;
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::CompletedMultipartUpload;
use serial_test::serial;
use tokio::time::{Duration, sleep};
use tracing::info;
use uuid::Uuid;
@@ -101,7 +100,6 @@ async fn wait_for_cleanup_on_all_nodes(
}
#[tokio::test]
#[serial]
async fn test_stale_multipart_cleanup_removes_incomplete_upload_across_cluster()
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
@@ -25,7 +25,6 @@ use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper_util::rt::TokioIo;
use serde_json::Value;
use serial_test::serial;
use std::collections::BTreeSet;
use std::convert::Infallible;
use std::error::Error;
@@ -350,7 +349,6 @@ impl Drop for OpaMock {
}
#[tokio::test]
#[serial]
async fn test_sts_query_responses_are_aws_sdk_compatible() -> TestResult {
init_logging();
@@ -487,7 +485,6 @@ async fn test_sts_query_responses_are_aws_sdk_compatible() -> TestResult {
}
#[tokio::test]
#[serial]
async fn test_sts_assume_role_opa_contract() -> TestResult {
init_logging();
@@ -559,7 +556,6 @@ async fn test_sts_assume_role_opa_contract() -> TestResult {
}
#[tokio::test]
#[serial]
async fn test_list_buckets_opa_contract() -> TestResult {
init_logging();
@@ -645,7 +641,6 @@ async fn test_list_buckets_opa_contract() -> TestResult {
}
#[tokio::test]
#[serial]
async fn test_sts_and_list_buckets_fail_closed_while_opa_is_initializing() -> TestResult {
init_logging();
@@ -662,7 +657,6 @@ async fn test_sts_and_list_buckets_fail_closed_while_opa_is_initializing() -> Te
}
#[tokio::test]
#[serial]
async fn test_sts_and_list_buckets_fail_closed_after_opa_validation_failure() -> TestResult {
init_logging();
@@ -679,7 +673,6 @@ async fn test_sts_and_list_buckets_fail_closed_after_opa_validation_failure() ->
}
#[tokio::test]
#[serial]
async fn test_sts_query_rate_limit_error_is_aws_sdk_compatible() -> TestResult {
init_logging();
@@ -30,7 +30,6 @@
mod tests {
use crate::common::{RustFSTestEnvironment, admin_ok, init_logging};
use serde_json::Value;
use serial_test::serial;
use std::error::Error;
use tracing::info;
@@ -42,7 +41,6 @@ mod tests {
/// validates that an expiration-only rule (the persistence path) survives
/// a server restart.
#[tokio::test]
#[serial]
async fn test_lifecycle_rule_persists_after_restart() -> TestResult {
init_logging();
info!("RT-13: lifecycle rule persists after restart");
@@ -105,7 +103,6 @@ mod tests {
/// Regression pattern: tier add/verify/delete API fails or the tier
/// configuration is not persisted (rustfs#5218).
#[tokio::test]
#[serial]
async fn test_admin_tier_list_endpoint_returns_json() -> TestResult {
init_logging();
info!("RT-13b: admin tier list endpoint returns JSON");
@@ -135,7 +132,6 @@ mod tests {
/// is not persisted (rustfs#5013), causing the scanner to not run or
/// use stale settings.
#[tokio::test]
#[serial]
async fn test_scanner_config_persists_after_restart() -> TestResult {
init_logging();
info!("RT-13c: scanner config persists after restart");
@@ -34,7 +34,6 @@ use rcgen::generate_simple_self_signed;
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::{ClientConfig, ClientConnection, DigitallySignedStruct, Error as RustlsError, SignatureScheme, StreamOwned};
use serial_test::serial;
use sha2::{Digest, Sha256};
use std::error::Error;
use std::io::{Read, Write};
@@ -242,7 +241,6 @@ async fn roundtrip_and_return(mut session: TlsSession) -> Result<TlsSession, Box
}
#[tokio::test]
#[serial]
async fn test_tls_certificate_hot_reload_live_listener() -> TestResult {
init_logging();
// Install the process-wide rustls crypto provider (idempotent).
@@ -27,7 +27,6 @@ mod tests {
use aws_sdk_s3::Client;
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::types::{BucketVersioningStatus, CompletedMultipartUpload, CompletedPart, VersioningConfiguration};
use serial_test::serial;
use tracing::info;
fn create_s3_client(env: &RustFSTestEnvironment) -> Client {
@@ -86,7 +85,6 @@ mod tests {
/// Test 1: PutObject should return version_id when versioning is enabled
/// This directly addresses the Veeam issue from #1066
#[tokio::test]
#[serial]
async fn test_put_object_returns_version_id_with_versioning() {
init_logging();
info!("🧪 TEST: PutObject returns version_id with versioning enabled");
@@ -130,7 +128,6 @@ mod tests {
/// Test 2: CopyObject should return version_id when versioning is enabled
#[tokio::test]
#[serial]
async fn test_copy_object_returns_version_id_with_versioning() {
init_logging();
info!("🧪 TEST: CopyObject returns version_id with versioning enabled");
@@ -185,7 +182,6 @@ mod tests {
/// Test 3: CompleteMultipartUpload should return version_id when versioning is enabled
#[tokio::test]
#[serial]
async fn test_multipart_upload_returns_version_id_with_versioning() {
init_logging();
info!("🧪 TEST: CompleteMultipartUpload returns version_id with versioning enabled");
@@ -260,7 +256,6 @@ mod tests {
/// Test 4: PutObject should NOT return version_id when versioning is NOT enabled
/// This ensures we didn't break non-versioned buckets
#[tokio::test]
#[serial]
async fn test_put_object_without_versioning() {
init_logging();
info!("🧪 TEST: PutObject behavior without versioning (no regression)");
@@ -296,7 +291,6 @@ mod tests {
/// Test 5: Basic S3 operations still work correctly (no regression)
#[tokio::test]
#[serial]
async fn test_basic_s3_operations_no_regression() {
init_logging();
info!("🧪 TEST: Basic S3 operations work correctly (no regression)");
@@ -363,7 +357,6 @@ mod tests {
/// Test 6: Veeam-specific scenario simulation
/// Simulates the exact workflow that Veeam uses when backing up data
#[tokio::test]
#[serial]
async fn test_veeam_backup_workflow_simulation() {
init_logging();
info!("🧪 TEST: Veeam VBR backup workflow simulation (Issue #1066)");
@@ -413,7 +406,6 @@ mod tests {
}
#[tokio::test]
#[serial]
async fn test_terraform_put_after_delete() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
init_logging();
@@ -456,7 +448,6 @@ mod tests {
/// Test 7: PutObject should omit version_id when versioning is Suspended
#[tokio::test]
#[serial]
async fn test_put_object_omits_version_id_with_suspended_versioning() {
init_logging();
info!("🧪 TEST: PutObject omits version_id with versioning suspended");
@@ -500,7 +491,6 @@ mod tests {
/// Test 8: CopyObject should omit version_id when versioning is Suspended
#[tokio::test]
#[serial]
async fn test_copy_object_omits_version_id_with_suspended_versioning() {
init_logging();
info!("🧪 TEST: CopyObject omits version_id with versioning suspended");
@@ -551,7 +541,6 @@ mod tests {
/// Test 9: CompleteMultipartUpload should omit version_id when versioning is Suspended
#[tokio::test]
#[serial]
async fn test_multipart_upload_omits_version_id_with_suspended_versioning() {
init_logging();
info!("🧪 TEST: CompleteMultipartUpload omits version_id with versioning suspended");
@@ -157,219 +157,4 @@ mod tests {
)
.await;
}
#[cfg(target_os = "linux")]
mod linux_privileged_tests {
use super::*;
use std::error::Error;
use std::path::Path;
use std::process::Command;
const ENABLE_ENV: &str = "RUSTFS_PRIVILEGED_MOUNT_READINESS_TESTS";
const NAMESPACE_ENV: &str = "RUSTFS_PRIVILEGED_MOUNT_READINESS_TESTS_IN_NAMESPACE";
const MOUNT_SIZE: &str = "size=32m,mode=0700";
struct MountGuard {
mounts: Vec<std::path::PathBuf>,
}
impl MountGuard {
fn new() -> Result<Self, Box<dyn Error + Send + Sync>> {
run_command("mount", &["--make-rprivate", "/"])?;
Ok(Self { mounts: Vec::new() })
}
fn mount_tmpfs(&mut self, target: &Path, label: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
mount_tmpfs(target, label)?;
self.mounts.push(target.to_path_buf());
Ok(())
}
fn mount_bind(&mut self, source: &Path, target: &Path) -> Result<(), Box<dyn Error + Send + Sync>> {
mount_bind(source, target)?;
self.mounts.push(target.to_path_buf());
Ok(())
}
}
impl Drop for MountGuard {
fn drop(&mut self) {
for mount in self.mounts.iter().rev() {
let _ = detach_mount(mount);
}
}
}
fn run_command(program: &str, args: &[&str]) -> Result<(), Box<dyn Error + Send + Sync>> {
let output = Command::new(program).args(args).output()?;
if output.status.success() {
return Ok(());
}
Err(format!(
"{program} {} failed with status {}: stdout={} stderr={}",
args.join(" "),
output.status,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
.into())
}
fn path_to_string(path: &Path, label: &str) -> Result<String, Box<dyn Error + Send + Sync>> {
path.to_str()
.map(str::to_owned)
.ok_or_else(|| format!("{label} path is not UTF-8: {path:?}").into())
}
fn mount_tmpfs(target: &Path, label: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
let target = path_to_string(target, "tmpfs target")?;
run_command("mount", &["-t", "tmpfs", "-o", MOUNT_SIZE, label, &target])
}
fn mount_bind(source: &Path, target: &Path) -> Result<(), Box<dyn Error + Send + Sync>> {
let source = path_to_string(source, "bind source")?;
let target = path_to_string(target, "bind target")?;
run_command("mount", &["--bind", &source, &target])
}
fn detach_mount(target: &Path) -> Result<(), Box<dyn Error + Send + Sync>> {
let target = path_to_string(target, "umount target")?;
run_command("umount", &[&target])
}
fn privileged_enabled() -> Result<bool, Box<dyn Error + Send + Sync>> {
let enabled = std::env::var(ENABLE_ENV)
.ok()
.is_some_and(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES"));
if !enabled {
return Ok(false);
}
Ok(true)
}
fn run_current_test_in_mount_namespace() -> Result<(), Box<dyn Error + Send + Sync>> {
let test_name = std::thread::current()
.name()
.ok_or("privileged mount readiness test thread is unnamed")?
.to_owned();
let test_binary = std::env::current_exe()?;
let status = Command::new("unshare")
.arg("--mount")
.arg("--propagation")
.arg("private")
.arg("--")
.arg(test_binary)
.arg("--exact")
.arg(test_name)
.arg("--ignored")
.arg("--nocapture")
.env(NAMESPACE_ENV, "1")
.status()?;
if status.success() {
return Ok(());
}
Err(format!("{ENABLE_ENV}=1 requires Linux root or CAP_SYS_ADMIN; unshare exited with status {status}").into())
}
fn run_privileged_mount_test<F, Fut>(test: F) -> Result<(), Box<dyn Error + Send + Sync>>
where
F: FnOnce(MountGuard) -> Fut + Send + 'static,
Fut: std::future::Future<Output = Result<(), Box<dyn Error + Send + Sync>>> + 'static,
{
if !privileged_enabled()? {
return Ok(());
}
if std::env::var_os(NAMESPACE_ENV).is_none() {
return run_current_test_in_mount_namespace();
}
let guard = MountGuard::new()?;
let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build()?;
runtime.block_on(test(guard))
}
#[test]
#[ignore = "requires Linux root/CAP_SYS_ADMIN and RUSTFS_PRIVILEGED_MOUNT_READINESS_TESTS=1"]
fn auto_replacement_readiness_accepts_an_independent_mount() -> Result<(), Box<dyn Error + Send + Sync>> {
run_privileged_mount_test(|mut mounts| async move {
let temp = TempDir::new().expect("temporary replacement roots should be created");
let target = temp.path().join("target");
let sibling = temp.path().join("sibling");
std::fs::create_dir(&target).expect("target mountpoint should be created");
std::fs::create_dir(&sibling).expect("sibling mountpoint should be created");
mounts.mount_tmpfs(&target, "rustfs-readiness-target")?;
mounts.mount_tmpfs(&sibling, "rustfs-readiness-sibling")?;
let target_endpoint = Endpoint::try_from(target.to_string_lossy().as_ref())?;
let sibling_endpoint = Endpoint::try_from(sibling.to_string_lossy().as_ref())?;
let target_disk = new_disk(
&target_endpoint,
&DiskOption {
cleanup: false,
health_check: false,
},
)
.await?;
let sibling_disk = new_disk(
&sibling_endpoint,
&DiskOption {
cleanup: false,
health_check: false,
},
)
.await?;
let identity = auto_replacement_target_identity(&target_disk, &[target_disk.clone(), sibling_disk.clone()]).await;
assert!(
identity.is_some(),
"a separately mounted replacement target with no sibling device overlap must be admitted"
);
Ok(())
})
}
#[test]
#[ignore = "requires Linux root/CAP_SYS_ADMIN and RUSTFS_PRIVILEGED_MOUNT_READINESS_TESTS=1"]
fn auto_replacement_readiness_rejects_a_same_device_sibling_bind_mount() -> Result<(), Box<dyn Error + Send + Sync>> {
run_privileged_mount_test(|mut mounts| async move {
let temp = TempDir::new().expect("temporary replacement roots should be created");
let source = temp.path().join("source");
let target = temp.path().join("target");
let sibling = temp.path().join("sibling");
std::fs::create_dir(&source).expect("source mountpoint should be created");
std::fs::create_dir(&target).expect("target mountpoint should be created");
std::fs::create_dir(&sibling).expect("sibling mountpoint should be created");
mounts.mount_tmpfs(&source, "rustfs-readiness-shared-source")?;
mounts.mount_bind(&source, &target)?;
mounts.mount_bind(&source, &sibling)?;
let target_endpoint = Endpoint::try_from(target.to_string_lossy().as_ref())?;
let sibling_endpoint = Endpoint::try_from(sibling.to_string_lossy().as_ref())?;
let target_disk = new_disk(
&target_endpoint,
&DiskOption {
cleanup: false,
health_check: false,
},
)
.await?;
let sibling_disk = new_disk(
&sibling_endpoint,
&DiskOption {
cleanup: false,
health_check: false,
},
)
.await?;
assert!(
auto_replacement_target_identity(&target_disk, &[target_disk.clone(), sibling_disk.clone()])
.await
.is_none(),
"replacement readiness must reject a target sharing its physical device with a sibling endpoint"
);
Ok(())
})
}
}
}
+1 -1
View File
@@ -63,7 +63,7 @@
| list_objects_v2_metadata_extension_test | 1 | |
| list_objects_v2_pagination_test | 12 | ✅ |
| mc_mirror_small_bucket_test | 1 | |
| multipart_auth_test | 75 | |
| multipart_auth_test | 85 | |
| multipart_storage_class_test | 3 | ✅ |
| namespace_lock_quorum_test | 2 | |
| negative_sigv4_test | 6 | ✅ |
+17 -32
View File
@@ -14,7 +14,7 @@
use crate::admin::storage_api::cluster::{CapabilityState, CapabilityStatus, ObservabilitySnapshot, TopologySnapshot};
use crate::admin::{
auth::authorize_admin_request,
auth::validate_admin_request,
router::{AdminOperation, Operation, S3Router},
runtime_sources::default_admin_usecase,
storage_api::cluster::{
@@ -24,10 +24,11 @@ use crate::admin::{
},
system,
};
use crate::auth::{check_key_valid, get_session_token};
use crate::cluster_snapshot::{
ClusterReadOnlySnapshot, ClusterRuntimeReadinessState, ClusterRuntimeStatusSnapshot, cluster_has_actionable_pressure,
};
use crate::server::{ADMIN_PREFIX, ReadinessDegradedReason};
use crate::server::{ADMIN_PREFIX, ReadinessDegradedReason, RemoteAddr};
use http::{HeaderMap, HeaderValue, StatusCode};
use hyper::Method;
use matchit::Params;
@@ -65,15 +66,23 @@ pub(crate) struct ClusterSnapshotDiscoveryResponse {
pub components: Option<ClusterComponentStatusView>,
}
/// The pre-check keeps this endpoint's historical missing-credentials message;
/// the shared gate reports "get cred failed".
async fn authorize_cluster_snapshot_request(req: &S3Request<Body>) -> S3Result<()> {
if req.credentials.is_none() {
let Some(input_cred) = &req.credentials else {
return Err(s3_error!(InvalidRequest, "authentication required"));
}
};
authorize_admin_request(req, vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)]).await?;
Ok(())
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
)
.await
}
fn build_json_response(
@@ -944,30 +953,6 @@ mod tests {
);
}
/// This endpoint authorizes through the shared admin gate, which reports
/// "get cred failed" for a credential-less request. The pre-check keeps the
/// message it has always returned (rustfs/backlog#1829).
#[tokio::test]
async fn cluster_snapshot_gate_keeps_its_missing_credentials_message() {
let req = s3s::S3Request {
input: s3s::Body::from(String::new()),
method: http::Method::GET,
uri: http::Uri::from_static("/rustfs/admin/v4/cluster/snapshot"),
headers: http::HeaderMap::new(),
extensions: http::Extensions::new(),
credentials: None,
region: None,
service: None,
trailing_headers: None,
};
let err = super::authorize_cluster_snapshot_request(&req)
.await
.expect_err("a request without credentials must be rejected");
assert_eq!(err.code(), &s3s::S3ErrorCode::InvalidRequest);
assert_eq!(err.message(), Some("authentication required"));
}
#[test]
fn cluster_snapshot_response_serializes_none_snapshot() {
let value = serde_json::to_value(ClusterSnapshotResponse { snapshot: None }).expect("serialize response");
+31 -44
View File
@@ -14,7 +14,7 @@
use crate::admin::storage_api::cluster::CapabilityStatus;
use crate::admin::{
auth::authorize_admin_request,
auth::validate_admin_request,
handlers::{cluster_snapshot, plugins_instances, system},
plugin_contract::{
PluginContractDomain, PluginInstanceDiagnosticCode, PluginInstanceDiagnosticCount, PluginInstanceEntry,
@@ -22,7 +22,8 @@ use crate::admin::{
},
router::{AdminOperation, Operation, S3Router},
};
use crate::server::ADMIN_PREFIX;
use crate::auth::{check_key_valid, get_session_token};
use crate::server::{ADMIN_PREFIX, RemoteAddr};
use http::{HeaderMap, HeaderValue, StatusCode};
use hyper::Method;
use matchit::Params;
@@ -182,26 +183,42 @@ fn map_extension_instance(instance: PluginInstanceEntry) -> ExtensionInstanceEnt
}
}
/// The pre-check keeps this endpoint's historical missing-credentials message;
/// the shared gate reports "get cred failed".
async fn authorize_extension_catalog_request(req: &S3Request<Body>) -> S3Result<()> {
if req.credentials.is_none() {
let Some(input_cred) = &req.credentials else {
return Err(s3_error!(InvalidRequest, "authentication required"));
}
};
authorize_admin_request(req, vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)]).await?;
Ok(())
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
)
.await
}
/// The pre-check keeps this endpoint's historical missing-credentials message;
/// the shared gate reports "get cred failed".
async fn authorize_extension_instance_request(req: &S3Request<Body>) -> S3Result<()> {
if req.credentials.is_none() {
let Some(input_cred) = &req.credentials else {
return Err(s3_error!(InvalidRequest, "authentication required"));
}
};
authorize_admin_request(req, vec![Action::AdminAction(AdminAction::GetBucketTargetAction)]).await?;
Ok(())
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::GetBucketTargetAction)],
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
)
.await
}
fn build_json_response(
@@ -303,36 +320,6 @@ mod tests {
);
}
/// Both extension gates authorize through the shared admin gate, which reports
/// "get cred failed" for a credential-less request. The pre-check keeps the
/// message these endpoints have always returned (rustfs/backlog#1829).
#[tokio::test]
async fn extension_gates_keep_their_missing_credentials_message() {
let credential_less_request = || s3s::S3Request {
input: s3s::Body::from(String::new()),
method: http::Method::GET,
uri: http::Uri::from_static("/rustfs/admin/v4/extensions/catalog"),
headers: http::HeaderMap::new(),
extensions: http::Extensions::new(),
credentials: None,
region: None,
service: None,
trailing_headers: None,
};
for err in [
super::authorize_extension_catalog_request(&credential_less_request())
.await
.expect_err("a request without credentials must be rejected"),
super::authorize_extension_instance_request(&credential_less_request())
.await
.expect_err("a request without credentials must be rejected"),
] {
assert_eq!(err.code(), &s3s::S3ErrorCode::InvalidRequest);
assert_eq!(err.message(), Some("authentication required"));
}
}
#[test]
fn builtin_ops_schemas_register_cleanly_in_runtime_registries() {
let mut diagnostics_registry = rustfs_targets::OpsDiagnosticsRegistry::new();
+12 -32
View File
@@ -21,11 +21,12 @@
//! that bucket, and with `bucket`+`object` it flushes that one identity — the
//! only remediation for a poisoned entry short of a node restart.
use crate::admin::auth::authorize_admin_request;
use crate::admin::auth::validate_admin_request;
use crate::admin::router::{AdminOperation, Operation, S3Router};
use crate::admin::runtime_sources::current_object_data_cache;
use crate::app::object_data_cache::ObjectDataCacheAdapter;
use crate::server::ADMIN_PREFIX;
use crate::auth::{check_key_valid, get_session_token};
use crate::server::{ADMIN_PREFIX, RemoteAddr};
use http::{HeaderMap, HeaderValue};
use hyper::{Method, StatusCode};
use matchit::Params;
@@ -75,14 +76,17 @@ pub fn register_object_data_cache_route(r: &mut S3Router<AdminOperation>) -> std
Ok(())
}
/// The pre-check keeps these endpoints' historical missing-credentials message;
/// the shared gate reports "get cred failed".
async fn authorize(req: &S3Request<Body>, action: AdminAction) -> S3Result<()> {
if req.credentials.is_none() {
let Some(input_cred) = req.credentials.as_ref() else {
return Err(s3_error!(InvalidRequest, "missing credentials"));
}
authorize_admin_request(req, vec![Action::AdminAction(action)]).await?;
Ok(())
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
let remote_addr = req
.extensions
.get::<Option<RemoteAddr>>()
.and_then(|opt| opt.map(|addr| addr.0));
validate_admin_request(&req.headers, &cred, owner, false, vec![Action::AdminAction(action)], remote_addr).await
}
fn json_response<T: Serialize>(body: &T) -> S3Result<S3Response<(StatusCode, Body)>> {
@@ -204,30 +208,6 @@ mod tests {
assert_eq!(invalidation_outcome(&ObjectDataCacheInvalidationResult::NoOp), ("noop", 0));
}
/// These endpoints authorize through the shared admin gate, which reports
/// "get cred failed" for a credential-less request. The pre-check keeps the
/// message they have always returned (rustfs/backlog#1829).
#[tokio::test]
async fn authorize_keeps_its_missing_credentials_message() {
let req = S3Request {
input: Body::from(String::new()),
method: Method::GET,
uri: "/rustfs/admin/v3/object-data-cache/stats".parse().expect("uri should parse"),
headers: HeaderMap::new(),
extensions: http::Extensions::new(),
credentials: None,
region: None,
service: None,
trailing_headers: None,
};
let err = authorize(&req, AdminAction::ServerInfoAdminAction)
.await
.expect_err("a request without credentials must be rejected");
assert_eq!(err.code(), &S3ErrorCode::InvalidRequest);
assert_eq!(err.message(), Some("missing credentials"));
}
#[test]
fn stats_handler_requires_server_info_action() {
// Guard the auth contract: the stats endpoint is a read, the flush
+17 -32
View File
@@ -13,7 +13,7 @@
// limitations under the License.
use crate::admin::{
auth::authorize_admin_request,
auth::validate_admin_request,
plugin_contract::{
PluginCatalogAdminDiscovery, PluginCatalogDomainEntry, PluginCatalogEntry, PluginCatalogResponse, PluginContractDomain,
PluginContractEntrypointKind, PluginContractPackaging, PluginDistributionContract, PluginRuntimeContract,
@@ -21,7 +21,8 @@ use crate::admin::{
router::{AdminOperation, Operation, S3Router},
runtime_sources::default_admin_usecase,
};
use crate::server::ADMIN_PREFIX;
use crate::auth::{check_key_valid, get_session_token};
use crate::server::{ADMIN_PREFIX, RemoteAddr};
use http::{HeaderMap, HeaderValue, StatusCode};
use hyper::Method;
use matchit::Params;
@@ -113,15 +114,23 @@ fn merge_catalog_descriptor(plugins: &mut HashMap<&'static str, PluginCatalogEnt
}
}
/// The pre-check keeps this endpoint's historical missing-credentials message;
/// the shared gate reports "get cred failed".
async fn authorize_plugin_catalog_request(req: &S3Request<Body>) -> S3Result<()> {
if req.credentials.is_none() {
let Some(input_cred) = &req.credentials else {
return Err(s3_error!(InvalidRequest, "authentication required"));
}
};
authorize_admin_request(req, vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)]).await?;
Ok(())
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
)
.await
}
fn build_json_response(
@@ -166,30 +175,6 @@ mod tests {
);
}
/// This endpoint authorizes through the shared admin gate, which reports
/// "get cred failed" for a credential-less request. The pre-check keeps the
/// message it has always returned (rustfs/backlog#1829).
#[tokio::test]
async fn plugin_catalog_gate_keeps_its_missing_credentials_message() {
let req = s3s::S3Request {
input: s3s::Body::from(String::new()),
method: http::Method::GET,
uri: http::Uri::from_static("/rustfs/admin/v4/plugins/catalog"),
headers: http::HeaderMap::new(),
extensions: http::Extensions::new(),
credentials: None,
region: None,
service: None,
trailing_headers: None,
};
let err = super::authorize_plugin_catalog_request(&req)
.await
.expect_err("a request without credentials must be rejected");
assert_eq!(err.code(), &s3s::S3ErrorCode::InvalidRequest);
assert_eq!(err.message(), Some("authentication required"));
}
#[test]
fn plugin_catalog_contains_representative_builtin_targets() {
let response = build_catalog_response();
+32 -45
View File
@@ -13,7 +13,7 @@
// limitations under the License.
use crate::admin::{
auth::authorize_admin_request,
auth::validate_admin_request,
handlers::audit_runtime_config::{load_server_config_from_store, remove_audit_target_config, set_audit_target_config},
handlers::notify_runtime_access::{
load_notification_config_snapshot, remove_notification_target_config, set_notification_target_config,
@@ -29,9 +29,10 @@ use crate::admin::{
},
router::{AdminOperation, Operation, S3Router},
};
use crate::auth::{check_key_valid, get_session_token};
use crate::server::{
ADMIN_PREFIX, is_audit_module_enabled, is_notify_module_enabled, refresh_audit_module_enabled, refresh_notify_module_enabled,
refresh_persisted_module_switches_from_store,
ADMIN_PREFIX, RemoteAddr, is_audit_module_enabled, is_notify_module_enabled, refresh_audit_module_enabled,
refresh_notify_module_enabled, refresh_persisted_module_switches_from_store,
};
use hyper::{Method, StatusCode};
use matchit::Params;
@@ -562,26 +563,42 @@ fn plugin_instance_matches_query(instance: &PluginInstanceEntry, query: &str) ->
.any(|field| field.to_ascii_lowercase().contains(&query))
}
/// The pre-check keeps this endpoint's historical missing-credentials message;
/// the shared gate reports "get cred failed".
async fn authorize_plugin_instance_request(req: &S3Request<Body>) -> S3Result<()> {
if req.credentials.is_none() {
let Some(input_cred) = &req.credentials else {
return Err(s3_error!(InvalidRequest, "authentication required"));
}
};
authorize_admin_request(req, vec![Action::AdminAction(AdminAction::GetBucketTargetAction)]).await?;
Ok(())
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::GetBucketTargetAction)],
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
)
.await
}
/// The pre-check keeps this endpoint's historical missing-credentials message;
/// the shared gate reports "get cred failed".
async fn authorize_plugin_instance_write_request(req: &S3Request<Body>) -> S3Result<()> {
if req.credentials.is_none() {
let Some(input_cred) = &req.credentials else {
return Err(s3_error!(InvalidRequest, "authentication required"));
}
};
authorize_admin_request(req, vec![Action::AdminAction(AdminAction::SetBucketTargetAction)]).await?;
Ok(())
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::SetBucketTargetAction)],
req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0)),
)
.await
}
fn plugin_instance_mutation_block_reason(
@@ -925,36 +942,6 @@ mod tests {
);
}
/// Both instance gates authorize through the shared admin gate, which reports
/// "get cred failed" for a credential-less request. The pre-check keeps the
/// message these endpoints have always returned (rustfs/backlog#1829).
#[tokio::test]
async fn plugin_instance_gates_keep_their_missing_credentials_message() {
let credential_less_request = || S3Request {
input: Body::from(String::new()),
method: Method::GET,
uri: Uri::from_static("/rustfs/admin/v4/plugins/instances"),
headers: HeaderMap::new(),
extensions: Extensions::new(),
credentials: None,
region: None,
service: None,
trailing_headers: None,
};
for err in [
super::authorize_plugin_instance_request(&credential_less_request())
.await
.expect_err("a request without credentials must be rejected"),
super::authorize_plugin_instance_write_request(&credential_less_request())
.await
.expect_err("a request without credentials must be rejected"),
] {
assert_eq!(err.code(), &s3s::S3ErrorCode::InvalidRequest);
assert_eq!(err.message(), Some("authentication required"));
}
}
#[test]
fn configured_instance_without_runtime_appears_offline() {
let config = Config(HashMap::from([(
+40 -290
View File
@@ -747,12 +747,11 @@ pub struct S3ErrorMessageCompatService<S> {
inner: S,
}
impl<S, ReqBody, RestBody, GrpcBody> Service<HttpRequest<ReqBody>> for S3ErrorMessageCompatService<S>
impl<S, RestBody, GrpcBody> Service<HttpRequest<Incoming>> for S3ErrorMessageCompatService<S>
where
S: Service<HttpRequest<ReqBody>, Response = Response<HybridBody<RestBody, GrpcBody>>> + Clone + Send + 'static,
S: Service<HttpRequest<Incoming>, Response = Response<HybridBody<RestBody, GrpcBody>>> + Clone + Send + 'static,
S::Future: Send + 'static,
S::Error: Send + 'static,
ReqBody: Send + 'static,
RestBody: Body<Data = Bytes> + From<Bytes> + Send + 'static,
RestBody::Error: Into<S::Error> + Send + 'static,
GrpcBody: Send + 'static,
@@ -765,27 +764,28 @@ where
self.inner.poll_ready(cx)
}
fn call(&mut self, req: HttpRequest<ReqBody>) -> Self::Future {
fn call(&mut self, req: HttpRequest<Incoming>) -> Self::Future {
let is_sts_query =
req.method() == Method::POST && req.uri().path() == "/" && req.extensions().get::<StsQueryRequest>().is_some();
let mut inner = self.inner.clone();
Box::pin(async move {
let response = inner.call(req).await?;
if is_sts_query || response.status() != StatusCode::FORBIDDEN || !is_xml_response(response.headers()) {
return Ok(response);
}
let (parts, body) = response.into_parts();
let should_fix = !is_sts_query && parts.status == StatusCode::FORBIDDEN && is_xml_response(&parts.headers);
let response = match body {
HybridBody::Rest { rest_body } => {
let (rest_body, changed) = fix_s3_error_message_in_xml(rest_body).await.map_err(Into::into)?;
let mut parts = parts;
if changed {
parts.headers.remove(http::header::CONTENT_LENGTH);
if !should_fix {
Response::from_parts(parts, HybridBody::Rest { rest_body })
} else {
let (rest_body, changed) = fix_s3_error_message_in_xml(rest_body).await.map_err(Into::into)?;
let mut parts = parts;
if changed {
parts.headers.remove(http::header::CONTENT_LENGTH);
}
Response::from_parts(parts, HybridBody::Rest { rest_body })
}
Response::from_parts(parts, HybridBody::Rest { rest_body })
}
HybridBody::Grpc { grpc_body } => Response::from_parts(parts, HybridBody::Grpc { grpc_body }),
};
@@ -886,12 +886,11 @@ pub struct IcebergRestErrorCompatService<S> {
inner: S,
}
impl<S, ReqBody, RestBody, GrpcBody> Service<HttpRequest<ReqBody>> for IcebergRestErrorCompatService<S>
impl<S, RestBody, GrpcBody> Service<HttpRequest<Incoming>> for IcebergRestErrorCompatService<S>
where
S: Service<HttpRequest<ReqBody>, Response = Response<HybridBody<RestBody, GrpcBody>>> + Clone + Send + 'static,
S: Service<HttpRequest<Incoming>, Response = Response<HybridBody<RestBody, GrpcBody>>> + Clone + Send + 'static,
S::Future: Send + 'static,
S::Error: Send + 'static,
ReqBody: Send + 'static,
RestBody: Body<Data = Bytes> + From<Bytes> + Send + 'static,
RestBody::Error: Into<S::Error> + Send + 'static,
GrpcBody: Send + 'static,
@@ -904,21 +903,18 @@ where
self.inner.poll_ready(cx)
}
fn call(&mut self, req: HttpRequest<ReqBody>) -> Self::Future {
fn call(&mut self, req: HttpRequest<Incoming>) -> Self::Future {
let catalog_path =
(req.method() != Method::HEAD && is_table_catalog_path(req.uri().path())).then(|| req.uri().path().to_string());
let mut inner = self.inner.clone();
Box::pin(async move {
let response = inner.call(req).await?;
if catalog_path.is_none() || response.status().is_success() || !is_xml_response(response.headers()) {
return Ok(response);
}
let (parts, body) = response.into_parts();
let should_convert = catalog_path.is_some() && !parts.status.is_success() && is_xml_response(&parts.headers);
let response = match body {
HybridBody::Rest { rest_body } => {
HybridBody::Rest { rest_body } if should_convert => {
let (rest_body, converted_status) = convert_iceberg_error_in_xml(
rest_body,
parts.status,
@@ -936,6 +932,7 @@ where
}
Response::from_parts(parts, HybridBody::Rest { rest_body })
}
HybridBody::Rest { rest_body } => Response::from_parts(parts, HybridBody::Rest { rest_body }),
HybridBody::Grpc { grpc_body } => Response::from_parts(parts, HybridBody::Grpc { grpc_body }),
};
@@ -1048,12 +1045,11 @@ pub struct ObjectAttributesEtagFixService<S> {
inner: S,
}
impl<S, ReqBody, RestBody, GrpcBody> Service<HttpRequest<ReqBody>> for ObjectAttributesEtagFixService<S>
impl<S, RestBody, GrpcBody> Service<HttpRequest<Incoming>> for ObjectAttributesEtagFixService<S>
where
S: Service<HttpRequest<ReqBody>, Response = Response<HybridBody<RestBody, GrpcBody>>> + Clone + Send + 'static,
S: Service<HttpRequest<Incoming>, Response = Response<HybridBody<RestBody, GrpcBody>>> + Clone + Send + 'static,
S::Future: Send + 'static,
S::Error: Send + 'static,
ReqBody: Send + 'static,
RestBody: Body<Data = Bytes> + From<Bytes> + Send + 'static,
RestBody::Error: Into<S::Error> + Send + 'static,
GrpcBody: Send + 'static,
@@ -1066,26 +1062,27 @@ where
self.inner.poll_ready(cx)
}
fn call(&mut self, req: HttpRequest<ReqBody>) -> Self::Future {
fn call(&mut self, req: HttpRequest<Incoming>) -> Self::Future {
let is_target = is_object_attributes_request(&req);
let mut inner = self.inner.clone();
Box::pin(async move {
let response = inner.call(req).await?;
if !is_target || !response.status().is_success() || !is_xml_response(response.headers()) {
return Ok(response);
}
let (parts, body) = response.into_parts();
let should_fix = is_target && parts.status.is_success() && is_xml_response(&parts.headers);
let response = match body {
HybridBody::Rest { rest_body } => {
let rest_body = fix_object_attributes_etag_in_xml(rest_body).await.map_err(Into::into)?;
if !should_fix {
Response::from_parts(parts, HybridBody::Rest { rest_body })
} else {
let rest_body = fix_object_attributes_etag_in_xml(rest_body).await.map_err(Into::into)?;
let mut parts = parts;
parts.headers.remove(http::header::CONTENT_LENGTH);
let mut parts = parts;
parts.headers.remove(http::header::CONTENT_LENGTH);
Response::from_parts(parts, HybridBody::Rest { rest_body })
Response::from_parts(parts, HybridBody::Rest { rest_body })
}
}
HybridBody::Grpc { grpc_body } => Response::from_parts(parts, HybridBody::Grpc { grpc_body }),
};
@@ -1147,11 +1144,12 @@ where
Box::pin(async move {
let response = inner.call(req).await?;
if !is_bodyless_status(response.status()) {
return Ok(response);
let (mut parts, body) = response.into_parts();
if !is_bodyless_status(parts.status) {
return Ok(Response::from_parts(parts, body));
}
let (mut parts, body) = response.into_parts();
let response = match body {
HybridBody::Rest { .. } => {
parts.headers.remove(http::header::CONTENT_LENGTH);
@@ -1804,7 +1802,7 @@ fn strip_quotes_from_first_etag(xml: String) -> String {
fixed
}
fn is_object_attributes_request<B>(req: &HttpRequest<B>) -> bool {
fn is_object_attributes_request(req: &HttpRequest<Incoming>) -> bool {
if req.method() != Method::GET {
return false;
}
@@ -1969,12 +1967,11 @@ fn apply_bucket_cors_result(response_headers: &mut HeaderMap, bucket_cors_header
}
}
impl<S, ReqBody, ResBody> Service<HttpRequest<ReqBody>> for ConditionalCorsService<S>
impl<S, ResBody> Service<HttpRequest<Incoming>> for ConditionalCorsService<S>
where
S: Service<HttpRequest<ReqBody>, Response = Response<ResBody>> + Clone + Send + 'static,
S: Service<HttpRequest<Incoming>, Response = Response<ResBody>> + Clone + Send + 'static,
S::Future: Send + 'static,
S::Error: Into<Box<dyn std::error::Error + Send + Sync>> + Send + 'static,
ReqBody: Send + 'static,
ResBody: Default + Send + 'static,
{
type Response = Response<ResBody>;
@@ -1985,14 +1982,7 @@ where
self.inner.poll_ready(cx).map_err(Into::into)
}
fn call(&mut self, req: HttpRequest<ReqBody>) -> Self::Future {
let is_options = req.method() == Method::OPTIONS;
let has_origin = req.headers().contains_key(cors::standard::ORIGIN);
if !is_options && !has_origin {
let mut inner = self.inner.clone();
return Box::pin(async move { inner.call(req).await.map_err(Into::into) });
}
fn call(&mut self, req: HttpRequest<Incoming>) -> Self::Future {
let path = req.uri().path().to_string();
let method = req.method().clone();
let request_headers = req.headers().clone();
@@ -2000,7 +1990,7 @@ where
let is_s3 = ConditionalCorsLayer::is_s3_path(&path);
let is_root = path == "/";
if is_options {
if method == Method::OPTIONS {
let has_acrm = request_headers.contains_key(cors::request::ACCESS_CONTROL_REQUEST_METHOD);
if is_root {
@@ -2202,7 +2192,6 @@ mod tests {
use futures::future::{Ready, ready};
use http::Request;
use http_body_util::BodyExt;
use http_body_util::Empty;
use http_body_util::Full;
use opentelemetry::global;
use opentelemetry_sdk::propagation::TraceContextPropagator;
@@ -3794,188 +3783,6 @@ mod tests {
assert_eq!(bytes, input);
}
#[derive(Clone)]
struct FixedHybridResponse {
status: StatusCode,
body: Bytes,
content_type: &'static str,
}
impl<B: Send + 'static> Service<Request<B>> for FixedHybridResponse {
type Response = Response<HybridBody<Full<Bytes>, Empty<Bytes>>>;
type Error = Infallible;
type Future = Ready<Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _req: Request<B>) -> Self::Future {
let body = self.body.clone();
ready(Ok(Response::builder()
.status(self.status)
.header(http::header::CONTENT_TYPE, self.content_type)
.header(http::header::CONTENT_LENGTH, body.len().to_string())
.body(HybridBody::Rest {
rest_body: Full::from(body),
})
.expect("fixed hybrid response")))
}
}
async fn collect_hybrid_response(
response: Response<HybridBody<Full<Bytes>, Empty<Bytes>>>,
) -> (StatusCode, HeaderMap, String) {
let status = response.status();
let headers = response.headers().clone();
let body = BodyExt::collect(response.into_body())
.await
.expect("collect hybrid body")
.to_bytes();
(
status,
headers,
String::from_utf8(body.to_vec()).expect("hybrid response body should be UTF-8"),
)
}
#[tokio::test]
async fn s3_error_message_compat_fixes_regular_forbidden_xml() {
let body = Bytes::from_static(b"<Error><Code>SignatureDoesNotMatch</Code></Error>");
let mut service = S3ErrorMessageCompatLayer.layer(FixedHybridResponse {
status: StatusCode::FORBIDDEN,
body,
content_type: "application/xml",
});
let request = Request::builder()
.method(Method::GET)
.uri("/bucket/object")
.body(())
.expect("request");
let response = service.call(request).await.expect("service response");
let (status, headers, body) = collect_hybrid_response(response).await;
assert_eq!(status, StatusCode::FORBIDDEN);
assert!(headers.get(http::header::CONTENT_LENGTH).is_none());
assert!(body.contains("<Message>"));
}
#[tokio::test]
async fn s3_error_message_compat_leaves_sts_query_response_unchanged() {
let input = Bytes::from_static(b"<Error><Code>SignatureDoesNotMatch</Code></Error>");
let mut service = S3ErrorMessageCompatLayer.layer(FixedHybridResponse {
status: StatusCode::FORBIDDEN,
body: input.clone(),
content_type: "application/xml",
});
let mut request = Request::builder().method(Method::POST).uri("/").body(()).expect("request");
request.extensions_mut().insert(StsQueryRequest);
let response = service.call(request).await.expect("service response");
let (_status, headers, body) = collect_hybrid_response(response).await;
let expected_len = input.len().to_string();
assert_eq!(
headers
.get(http::header::CONTENT_LENGTH)
.and_then(|value| value.to_str().ok()),
Some(expected_len.as_str())
);
assert_eq!(body.as_bytes(), input.as_ref());
}
#[tokio::test]
async fn iceberg_rest_error_compat_converts_catalog_xml_errors() {
let mut service = IcebergRestErrorCompatLayer.layer(FixedHybridResponse {
status: StatusCode::NOT_FOUND,
body: Bytes::from_static(b"<Error><Code>NoSuchTableException</Code><Message>missing</Message></Error>"),
content_type: "application/xml",
});
let request = Request::builder()
.method(Method::GET)
.uri("/iceberg/v1/warehouse/namespaces/ns/tables/events")
.body(())
.expect("request");
let response = service.call(request).await.expect("service response");
let (status, headers, body) = collect_hybrid_response(response).await;
assert_eq!(status, StatusCode::NOT_FOUND);
assert_eq!(headers.get(http::header::CONTENT_TYPE).unwrap(), "application/json");
assert!(headers.get(http::header::CONTENT_LENGTH).is_none());
assert!(body.contains("\"type\":\"NoSuchTableException\""));
}
#[tokio::test]
async fn iceberg_rest_error_compat_leaves_non_catalog_errors_unchanged() {
let input = Bytes::from_static(b"<Error><Code>NoSuchKey</Code><Message>missing</Message></Error>");
let mut service = IcebergRestErrorCompatLayer.layer(FixedHybridResponse {
status: StatusCode::NOT_FOUND,
body: input.clone(),
content_type: "application/xml",
});
let request = Request::builder()
.method(Method::GET)
.uri("/bucket/object")
.body(())
.expect("request");
let response = service.call(request).await.expect("service response");
let (status, headers, body) = collect_hybrid_response(response).await;
assert_eq!(status, StatusCode::NOT_FOUND);
assert_eq!(headers.get(http::header::CONTENT_TYPE).unwrap(), "application/xml");
assert_eq!(body.as_bytes(), input.as_ref());
}
#[tokio::test]
async fn object_attributes_etag_fix_rewrites_target_response() {
let mut service = ObjectAttributesEtagFixLayer.layer(FixedHybridResponse {
status: StatusCode::OK,
body: Bytes::from_static(b"<GetObjectAttributesOutput><ETag>\"abc\"</ETag></GetObjectAttributesOutput>"),
content_type: "application/xml",
});
let request = Request::builder()
.method(Method::GET)
.uri("/bucket/object?attributes")
.body(())
.expect("request");
let response = service.call(request).await.expect("service response");
let (_status, headers, body) = collect_hybrid_response(response).await;
assert!(headers.get(http::header::CONTENT_LENGTH).is_none());
assert!(body.contains("<ETag>abc</ETag>"));
}
#[tokio::test]
async fn object_attributes_etag_fix_leaves_regular_get_unchanged() {
let input = Bytes::from_static(b"<GetObjectAttributesOutput><ETag>\"abc\"</ETag></GetObjectAttributesOutput>");
let mut service = ObjectAttributesEtagFixLayer.layer(FixedHybridResponse {
status: StatusCode::OK,
body: input.clone(),
content_type: "application/xml",
});
let request = Request::builder()
.method(Method::GET)
.uri("/bucket/object")
.body(())
.expect("request");
let response = service.call(request).await.expect("service response");
let (_status, headers, body) = collect_hybrid_response(response).await;
let expected_len = input.len().to_string();
assert_eq!(
headers
.get(http::header::CONTENT_LENGTH)
.and_then(|value| value.to_str().ok()),
Some(expected_len.as_str())
);
assert_eq!(body.as_bytes(), input.as_ref());
}
#[derive(Clone)]
struct FixedStsResponse {
status: StatusCode,
@@ -4463,63 +4270,6 @@ mod tests {
});
}
#[derive(Clone)]
struct CorsOkService;
impl<B> Service<Request<B>> for CorsOkService {
type Response = Response<Empty<Bytes>>;
type Error = Infallible;
type Future = Ready<Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _req: Request<B>) -> Self::Future {
ready(Ok(Response::builder()
.status(StatusCode::OK)
.body(Empty::new())
.expect("response")))
}
}
#[tokio::test]
async fn conditional_cors_passthrough_without_origin() {
let layer = ConditionalCorsLayer {
cors_origins: Some("*".to_string()),
};
let mut service = layer.layer(CorsOkService);
let request = Request::builder()
.method(Method::GET)
.uri("/bucket/object")
.body(())
.expect("request");
let response = service.call(request).await.expect("response");
assert_eq!(response.status(), StatusCode::OK);
assert!(response.headers().get(cors::response::ACCESS_CONTROL_ALLOW_ORIGIN).is_none());
}
#[tokio::test]
async fn conditional_cors_applies_origin_headers() {
let layer = ConditionalCorsLayer {
cors_origins: Some("*".to_string()),
};
let mut service = layer.layer(CorsOkService);
let request = Request::builder()
.method(Method::GET)
.uri("/bucket/object")
.header(cors::standard::ORIGIN, "https://example.com")
.body(())
.expect("request");
let response = service.call(request).await.expect("response");
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.headers().get(cors::response::ACCESS_CONTROL_ALLOW_ORIGIN).unwrap(), "*");
}
#[test]
fn request_context_layer_populates_context_without_mutating_signed_headers() {
let mut service = RequestContextLayer.layer(CaptureService);