mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-19 11:06:17 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb68ff4fbd | |||
| 1f8359537b | |||
| d7609b68a6 | |||
| 3958781320 | |||
| 5cb12300bc | |||
| bce0c05f3c | |||
| 07cef6789b | |||
| 05e6dc5f4a | |||
| 6f3f2f5f62 | |||
| b97fb02180 | |||
| f7073d0191 | |||
| d404e1bb8a | |||
| ceb6f779fb | |||
| e4eae22a70 | |||
| d030719dbc | |||
| 1741f79d7d | |||
| b8686b471a | |||
| 19cbba7ec8 | |||
| 13a9d19505 | |||
| aa6b9001f1 | |||
| 728efcec89 | |||
| 7b5389d2f9 | |||
| fa49f0ee4f | |||
| 09fe561443 | |||
| e3d7892404 | |||
| 7f2c0f1dfb | |||
| bde6736213 | |||
| cd9c96a03c | |||
| 4b676ef1ed | |||
| c7c5a8df6a | |||
| 612a5927b6 | |||
| 5355210070 | |||
| b648dea340 |
@@ -70,6 +70,11 @@ fips-wording-check: ## Check docs and crates/kms do not over-claim crypto capabi
|
||||
@echo "📣 Checking cryptographic capability wording guard..."
|
||||
./scripts/check_fips_wording.sh
|
||||
|
||||
.PHONY: embedded-secrets-check
|
||||
embedded-secrets-check: ## Check no private key material or credential literal is committed
|
||||
@echo "🔑 Checking embedded secret material guard..."
|
||||
./scripts/check_embedded_secrets.sh
|
||||
|
||||
.PHONY: log-analyzer-rules-check
|
||||
log-analyzer-rules-check: core-deps ## Check log-analyzer rule anchors still exist verbatim in source
|
||||
@echo "🩺 Checking log-analyzer rule anchors..."
|
||||
|
||||
@@ -19,13 +19,13 @@ planning-docs-check: ## Check that no planning-type documents are committed
|
||||
./scripts/check_no_planning_docs.sh
|
||||
|
||||
.PHONY: pre-commit
|
||||
pre-commit: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check s3s-footprint-check fips-wording-check doc-paths-check planning-docs-check quick-check ## Run fast pre-commit checks without clippy/full tests
|
||||
pre-commit: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check s3s-footprint-check fips-wording-check embedded-secrets-check doc-paths-check planning-docs-check quick-check ## Run fast pre-commit checks without clippy/full tests
|
||||
@echo "✅ All pre-commit checks passed!"
|
||||
|
||||
.PHONY: pre-pr
|
||||
pre-pr: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check s3s-footprint-check fips-wording-check doc-paths-check planning-docs-check log-analyzer-rules-check clippy-check test ## Run full pre-PR checks with clippy and tests
|
||||
pre-pr: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check s3s-footprint-check fips-wording-check embedded-secrets-check doc-paths-check planning-docs-check log-analyzer-rules-check clippy-check test ## Run full pre-PR checks with clippy and tests
|
||||
@echo "✅ All pre-PR checks passed!"
|
||||
|
||||
.PHONY: dev-check
|
||||
dev-check: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check s3s-footprint-check fips-wording-check doc-paths-check planning-docs-check quick-check ## Run fast local development checks
|
||||
dev-check: fmt-check unsafe-code-check architecture-migration-check logging-guardrails-check tokio-io-uring-check extension-schema-check body-cache-whitelist-check s3s-footprint-check fips-wording-check embedded-secrets-check doc-paths-check planning-docs-check quick-check ## Run fast local development checks
|
||||
@echo "✅ Fast development checks passed!"
|
||||
|
||||
@@ -34,6 +34,7 @@ script-tests: ## Run shell script tests
|
||||
./scripts/test_exact_1mib_handoff_abba.sh
|
||||
./scripts/test_pinned_paired_abba_bench.sh
|
||||
./scripts/test_manual_transition_runbooks.sh
|
||||
./scripts/check_embedded_secrets.sh --self-test
|
||||
bash -n ./scripts/validate_object_data_cache_cold_stampede.sh
|
||||
python3 ./scripts/check_object_data_cache_follower_samples.py --self-test
|
||||
./scripts/validate_object_data_cache_cold_stampede.sh --self-test
|
||||
|
||||
@@ -120,6 +120,9 @@ jobs:
|
||||
- name: Check cryptographic capability wording
|
||||
run: ./scripts/check_fips_wording.sh
|
||||
|
||||
- name: Check no embedded secret material
|
||||
run: ./scripts/check_embedded_secrets.sh
|
||||
|
||||
- name: Check no planning docs committed
|
||||
run: ./scripts/check_no_planning_docs.sh
|
||||
|
||||
|
||||
@@ -155,6 +155,9 @@ jobs:
|
||||
- name: Check cryptographic capability wording
|
||||
run: ./scripts/check_fips_wording.sh
|
||||
|
||||
- name: Check no embedded secret material
|
||||
run: ./scripts/check_embedded_secrets.sh
|
||||
|
||||
- name: Check no planning docs committed
|
||||
run: ./scripts/check_no_planning_docs.sh
|
||||
|
||||
|
||||
@@ -522,10 +522,16 @@ jobs:
|
||||
for f in "$DEB_FILE" "$RPM_FILE"; do
|
||||
if [[ -n "$f" && -f "$f" ]]; then
|
||||
base="$(basename "$f")"
|
||||
# Remove any stale entry, then append the fresh digest
|
||||
# GitHub stores release asset names with '~' normalized to '.'
|
||||
# (e.g. rustfs_1.0.0~rc.2_amd64.deb is stored as
|
||||
# rustfs_1.0.0.rc.2_amd64.deb), so checksum entries must
|
||||
# reference the name as stored on the release.
|
||||
github_base="${base//\~/.}"
|
||||
# Remove any stale entry (both naming variants), then append
|
||||
grep -Fv -- "$base" "$checksum_file" > "${checksum_file}.tmp" || true
|
||||
mv "${checksum_file}.tmp" "$checksum_file"
|
||||
(cd "$(dirname "$f")" && "$checksum_cmd" -- "$base") >> "$checksum_file"
|
||||
grep -Fv -- "$github_base" "${checksum_file}.tmp" > "${checksum_file}.tmp2" || true
|
||||
mv "${checksum_file}.tmp2" "$checksum_file"
|
||||
(cd "$(dirname "$f")" && "$checksum_cmd" -- "$github_base") >> "$checksum_file"
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
@@ -236,12 +236,19 @@ async fn audit_pipeline_reports_empty_runtime_snapshots() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn audit_runtime_facade_stops_empty_replay_workers() {
|
||||
async fn stopping_audit_replay_workers_is_a_no_op_when_there_are_none() {
|
||||
let registry = Arc::new(Mutex::new(AuditRegistry::new()));
|
||||
let replay_workers = Arc::new(RwLock::new(rustfs_targets::ReplayWorkerManager::new()));
|
||||
let facade = AuditRuntimeFacade::new(registry, replay_workers);
|
||||
let facade = AuditRuntimeFacade::new(registry, Arc::clone(&replay_workers));
|
||||
|
||||
facade.stop_replay_workers().await;
|
||||
|
||||
// The stop path takes the manager's workers and hands them to the adapter,
|
||||
// so an empty facade must leave it empty rather than wedge it, and a second
|
||||
// call — which shutdown paths make — must stay harmless (rustfs/backlog#1836).
|
||||
assert!(replay_workers.read().await.is_empty());
|
||||
facade.stop_replay_workers().await;
|
||||
assert!(replay_workers.read().await.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -228,15 +228,6 @@ pub const DEFAULT_SCANNER_MAX_CONCURRENT_DISK_SCANS: usize = 4;
|
||||
/// Default object interval for cooperative scanner yields.
|
||||
pub const DEFAULT_SCANNER_YIELD_EVERY_N_OBJECTS: u64 = 128;
|
||||
|
||||
/// Compatibility flag kept for Patch 3 rollback windows.
|
||||
///
|
||||
/// Inline scanner heal execution has been removed in favor of heal-candidate enqueue.
|
||||
/// When this flag is enabled, RustFS logs a warning and continues to use enqueue-based heal.
|
||||
pub const ENV_SCANNER_INLINE_HEAL_ENABLE: &str = "RUSTFS_SCANNER_INLINE_HEAL_ENABLE";
|
||||
|
||||
/// Default inline scanner heal compatibility mode.
|
||||
pub const DEFAULT_SCANNER_INLINE_HEAL_ENABLE: bool = false;
|
||||
|
||||
/// Scanner speed preset controlling throttling behavior.
|
||||
///
|
||||
/// Each preset defines three parameters:
|
||||
|
||||
@@ -203,14 +203,6 @@ mod tests {
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_source_does_not_embed_private_key() {
|
||||
let source = include_str!("license_token.rs");
|
||||
let forbidden = ["BEGIN", "PRIVATE KEY"].join(" ");
|
||||
|
||||
assert!(!source.contains(&forbidden));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_signed_license_token_rejects_invalid_token() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
@@ -317,15 +317,15 @@ pub struct SizeSummary {
|
||||
/// Number of delete markers
|
||||
pub delete_markers: usize,
|
||||
/// Replicated size
|
||||
pub replicated_size: usize,
|
||||
pub replicated_size: i64,
|
||||
/// Replicated count
|
||||
pub replicated_count: usize,
|
||||
/// Pending size
|
||||
pub pending_size: usize,
|
||||
pub pending_size: i64,
|
||||
/// Failed size
|
||||
pub failed_size: usize,
|
||||
pub failed_size: i64,
|
||||
/// Replica size
|
||||
pub replica_size: usize,
|
||||
pub replica_size: i64,
|
||||
/// Replica count
|
||||
pub replica_count: usize,
|
||||
/// Pending count
|
||||
@@ -334,19 +334,21 @@ pub struct SizeSummary {
|
||||
pub failed_count: usize,
|
||||
/// Replication target stats
|
||||
pub repl_target_stats: HashMap<String, ReplTargetSizeSummary>,
|
||||
/// Per-tier accounting, keyed by storage class or remote tier name
|
||||
pub tier_stats: HashMap<String, TierStats>,
|
||||
}
|
||||
|
||||
/// Replication target size summary
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ReplTargetSizeSummary {
|
||||
/// Replicated size
|
||||
pub replicated_size: usize,
|
||||
pub replicated_size: i64,
|
||||
/// Replicated count
|
||||
pub replicated_count: usize,
|
||||
/// Pending size
|
||||
pub pending_size: usize,
|
||||
pub pending_size: i64,
|
||||
/// Failed size
|
||||
pub failed_size: usize,
|
||||
pub failed_size: i64,
|
||||
/// Pending count
|
||||
pub pending_count: usize,
|
||||
/// Failed count
|
||||
@@ -710,28 +712,6 @@ impl DataUsageEntry {
|
||||
self.children.insert(hash.key());
|
||||
}
|
||||
|
||||
pub fn add_sizes(&mut self, summary: &SizeSummary) {
|
||||
self.size += summary.total_size;
|
||||
self.versions += summary.versions;
|
||||
self.delete_markers += summary.delete_markers;
|
||||
self.obj_sizes.add(summary.total_size as u64);
|
||||
self.obj_versions.add(summary.versions as u64);
|
||||
|
||||
let replication_stats = self.replication_stats.get_or_insert_with(ReplicationAllStats::default);
|
||||
replication_stats.replica_size += summary.replica_size as u64;
|
||||
replication_stats.replica_count += summary.replica_count as u64;
|
||||
|
||||
for (arn, st) in &summary.repl_target_stats {
|
||||
let tgt_stat = replication_stats.targets.entry(arn.to_string()).or_default();
|
||||
tgt_stat.pending_size += st.pending_size as u64;
|
||||
tgt_stat.failed_size += st.failed_size as u64;
|
||||
tgt_stat.replicated_size += st.replicated_size as u64;
|
||||
tgt_stat.replicated_count += st.replicated_count as u64;
|
||||
tgt_stat.failed_count += st.failed_count as u64;
|
||||
tgt_stat.pending_count += st.pending_count as u64;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, other: &DataUsageEntry) {
|
||||
self.objects += other.objects;
|
||||
self.versions += other.versions;
|
||||
@@ -1722,14 +1702,6 @@ impl BucketUsageInfo {
|
||||
}
|
||||
|
||||
/// Add size summary to this bucket usage
|
||||
pub fn add_size_summary(&mut self, summary: &SizeSummary) {
|
||||
self.size += summary.total_size as u64;
|
||||
self.versions_count += summary.versions as u64;
|
||||
self.delete_markers_count += summary.delete_markers as u64;
|
||||
self.replica_size += summary.replica_size as u64;
|
||||
self.replica_count += summary.replica_count as u64;
|
||||
}
|
||||
|
||||
/// Merge another BucketUsageInfo into this one
|
||||
pub fn merge(&mut self, other: &BucketUsageInfo) {
|
||||
self.size += other.size;
|
||||
@@ -1775,29 +1747,32 @@ impl SizeSummary {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Add another SizeSummary to this one
|
||||
/// Add another SizeSummary to this one.
|
||||
///
|
||||
/// Saturating throughout: a scan that overflows a counter should report the
|
||||
/// ceiling rather than panic in a debug build or wrap in a release one.
|
||||
pub fn add(&mut self, other: &SizeSummary) {
|
||||
self.total_size += other.total_size;
|
||||
self.versions += other.versions;
|
||||
self.delete_markers += other.delete_markers;
|
||||
self.replicated_size += other.replicated_size;
|
||||
self.replicated_count += other.replicated_count;
|
||||
self.pending_size += other.pending_size;
|
||||
self.failed_size += other.failed_size;
|
||||
self.replica_size += other.replica_size;
|
||||
self.replica_count += other.replica_count;
|
||||
self.pending_count += other.pending_count;
|
||||
self.failed_count += other.failed_count;
|
||||
self.total_size = self.total_size.saturating_add(other.total_size);
|
||||
self.versions = self.versions.saturating_add(other.versions);
|
||||
self.delete_markers = self.delete_markers.saturating_add(other.delete_markers);
|
||||
self.replicated_size = self.replicated_size.saturating_add(other.replicated_size);
|
||||
self.replicated_count = self.replicated_count.saturating_add(other.replicated_count);
|
||||
self.pending_size = self.pending_size.saturating_add(other.pending_size);
|
||||
self.failed_size = self.failed_size.saturating_add(other.failed_size);
|
||||
self.replica_size = self.replica_size.saturating_add(other.replica_size);
|
||||
self.replica_count = self.replica_count.saturating_add(other.replica_count);
|
||||
self.pending_count = self.pending_count.saturating_add(other.pending_count);
|
||||
self.failed_count = self.failed_count.saturating_add(other.failed_count);
|
||||
|
||||
// Merge replication target stats
|
||||
for (target, stats) in &other.repl_target_stats {
|
||||
let entry = self.repl_target_stats.entry(target.clone()).or_default();
|
||||
entry.replicated_size += stats.replicated_size;
|
||||
entry.replicated_count += stats.replicated_count;
|
||||
entry.pending_size += stats.pending_size;
|
||||
entry.failed_size += stats.failed_size;
|
||||
entry.pending_count += stats.pending_count;
|
||||
entry.failed_count += stats.failed_count;
|
||||
entry.replicated_size = entry.replicated_size.saturating_add(stats.replicated_size);
|
||||
entry.replicated_count = entry.replicated_count.saturating_add(stats.replicated_count);
|
||||
entry.pending_size = entry.pending_size.saturating_add(stats.pending_size);
|
||||
entry.failed_size = entry.failed_size.saturating_add(stats.failed_size);
|
||||
entry.pending_count = entry.pending_count.saturating_add(stats.pending_count);
|
||||
entry.failed_count = entry.failed_count.saturating_add(stats.failed_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2343,6 +2318,64 @@ mod tests {
|
||||
assert_eq!(usage1.versions_count, 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_summary_add_saturates_instead_of_overflowing() {
|
||||
// The scanner folds one summary per object into a per-prefix total, so a
|
||||
// counter at its ceiling must stay there rather than panic in a debug
|
||||
// build or wrap in a release one (backlog#1828).
|
||||
let mut summary = SizeSummary {
|
||||
total_size: usize::MAX,
|
||||
versions: usize::MAX,
|
||||
replicated_size: i64::MAX,
|
||||
pending_size: i64::MAX,
|
||||
failed_size: i64::MAX,
|
||||
replica_size: i64::MAX,
|
||||
..Default::default()
|
||||
};
|
||||
summary.repl_target_stats.insert(
|
||||
"arn".to_string(),
|
||||
ReplTargetSizeSummary {
|
||||
replicated_size: i64::MAX,
|
||||
pending_size: i64::MAX,
|
||||
failed_size: i64::MAX,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let mut increment = SizeSummary {
|
||||
total_size: 1,
|
||||
versions: 1,
|
||||
replicated_size: 1,
|
||||
pending_size: 1,
|
||||
failed_size: 1,
|
||||
replica_size: 1,
|
||||
..Default::default()
|
||||
};
|
||||
increment.repl_target_stats.insert(
|
||||
"arn".to_string(),
|
||||
ReplTargetSizeSummary {
|
||||
replicated_size: 1,
|
||||
pending_size: 1,
|
||||
failed_size: 1,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
summary.add(&increment);
|
||||
|
||||
assert_eq!(summary.total_size, usize::MAX);
|
||||
assert_eq!(summary.versions, usize::MAX);
|
||||
assert_eq!(summary.replicated_size, i64::MAX);
|
||||
assert_eq!(summary.pending_size, i64::MAX);
|
||||
assert_eq!(summary.failed_size, i64::MAX);
|
||||
assert_eq!(summary.replica_size, i64::MAX);
|
||||
|
||||
let target = summary.repl_target_stats.get("arn").expect("target survives the merge");
|
||||
assert_eq!(target.replicated_size, i64::MAX);
|
||||
assert_eq!(target.pending_size, i64::MAX);
|
||||
assert_eq!(target.failed_size, i64::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_size_summary_add() {
|
||||
let mut summary1 = SizeSummary::new();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
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,
|
||||
@@ -348,6 +349,71 @@ 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
|
||||
@@ -1534,59 +1600,6 @@ 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>> {
|
||||
@@ -2584,512 +2597,182 @@ 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_metadata_field_covered_by_starts_with()
|
||||
async fn test_anonymous_post_object_accepts_fields_covered_by_policy_conditions()
|
||||
-> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
init_logging();
|
||||
|
||||
let mut env = RustFSTestEnvironment::new().await?;
|
||||
env.start_rustfs_server(vec![]).await?;
|
||||
// (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 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();
|
||||
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 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()
|
||||
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,
|
||||
)
|
||||
.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(())
|
||||
}
|
||||
@@ -3440,64 +3123,6 @@ 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();
|
||||
|
||||
|
||||
@@ -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,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();
|
||||
|
||||
|
||||
@@ -168,6 +168,24 @@ async fn wait_for_version_expired(
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_key_versions_empty(client: &Client, bucket: &str, key: &str, deadline: StdDuration) -> TestResult {
|
||||
let start = std::time::Instant::now();
|
||||
loop {
|
||||
let listing = client.list_object_versions().bucket(bucket).prefix(key).send().await?;
|
||||
if listing.versions().is_empty() && listing.delete_markers().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
if start.elapsed() >= deadline {
|
||||
return Err(format!(
|
||||
"object {bucket}/{key} still had versions or delete markers after {}s: {listing:?}",
|
||||
deadline.as_secs()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
tokio::time::sleep(StdDuration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a prefix-scoped `Days`-based expiration rule.
|
||||
fn expiration_rule(id: &str, prefix: &str, days: i32) -> Result<LifecycleRule, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let rule = LifecycleRule::builder()
|
||||
@@ -193,6 +211,21 @@ fn noncurrent_expiration_rule(
|
||||
Ok(rule)
|
||||
}
|
||||
|
||||
fn noncurrent_expiration_with_delete_marker_cleanup_rule(
|
||||
id: &str,
|
||||
prefix: &str,
|
||||
days: i32,
|
||||
) -> Result<LifecycleRule, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let rule = LifecycleRule::builder()
|
||||
.id(id)
|
||||
.filter(LifecycleRuleFilter::builder().prefix(prefix).build())
|
||||
.expiration(LifecycleExpiration::builder().expired_object_delete_marker(true).build())
|
||||
.noncurrent_version_expiration(NoncurrentVersionExpiration::builder().noncurrent_days(days).build())
|
||||
.status(ExpirationStatus::Enabled)
|
||||
.build()?;
|
||||
Ok(rule)
|
||||
}
|
||||
|
||||
async fn put_expiration_config(client: &Client, bucket: &str, rule: LifecycleRule) -> TestResult {
|
||||
let lifecycle = BucketLifecycleConfiguration::builder().rules(rule).build()?;
|
||||
client
|
||||
@@ -412,6 +445,143 @@ async fn test_lifecycle_noncurrent_version_expiry_removes_only_old_version() ->
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A combined `NoncurrentDays=1` and `ExpiredObjectDeleteMarker=true` rule
|
||||
/// must remove a noncurrent data version and then its sole latest delete
|
||||
/// marker, without expiring current-only objects. A second prefix with only
|
||||
/// noncurrent expiry proves that marker cleanup comes from EODM.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_lifecycle_noncurrent_expiry_then_cleans_expired_delete_marker() -> TestResult {
|
||||
let mut env = RustFSTestEnvironment::new().await?;
|
||||
let mut extra_env = fast_lifecycle_env();
|
||||
extra_env.push(("RUSTFS_ILM_DEBUG_DAY_SECS", "2"));
|
||||
env.start_rustfs_server_with_env(vec![], &extra_env).await?;
|
||||
|
||||
let client = env.create_s3_client();
|
||||
let bucket = "ilm-expired-delete-marker";
|
||||
client.create_bucket().bucket(bucket).send().await?;
|
||||
client
|
||||
.put_bucket_versioning()
|
||||
.bucket(bucket)
|
||||
.versioning_configuration(
|
||||
VersioningConfiguration::builder()
|
||||
.status(BucketVersioningStatus::Enabled)
|
||||
.build(),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let cascade_key = "cascade/deleted.txt";
|
||||
let cascade_put = client
|
||||
.put_object()
|
||||
.bucket(bucket)
|
||||
.key(cascade_key)
|
||||
.body(ByteStream::from_static(b"cascade payload"))
|
||||
.send()
|
||||
.await?;
|
||||
let cascade_data_version = cascade_put
|
||||
.version_id()
|
||||
.map(str::to_string)
|
||||
.expect("cascade PUT returns a version id");
|
||||
let cascade_delete = client.delete_object().bucket(bucket).key(cascade_key).send().await?;
|
||||
let cascade_marker_version = cascade_delete
|
||||
.version_id()
|
||||
.map(str::to_string)
|
||||
.expect("cascade DELETE returns a marker version id");
|
||||
assert_eq!(cascade_delete.delete_marker(), Some(true));
|
||||
|
||||
let survivor_key = "cascade/current-only.txt";
|
||||
client
|
||||
.put_object()
|
||||
.bucket(bucket)
|
||||
.key(survivor_key)
|
||||
.body(ByteStream::from_static(b"current payload"))
|
||||
.send()
|
||||
.await?;
|
||||
let survivor_before = client.get_object().bucket(bucket).key(survivor_key).send().await?;
|
||||
assert_eq!(survivor_before.body.collect().await?.into_bytes().as_ref(), b"current payload");
|
||||
|
||||
let control_key = "nve-only/deleted.txt";
|
||||
let control_put = client
|
||||
.put_object()
|
||||
.bucket(bucket)
|
||||
.key(control_key)
|
||||
.body(ByteStream::from_static(b"control payload"))
|
||||
.send()
|
||||
.await?;
|
||||
let control_data_version = control_put
|
||||
.version_id()
|
||||
.map(str::to_string)
|
||||
.expect("control PUT returns a version id");
|
||||
let control_delete = client.delete_object().bucket(bucket).key(control_key).send().await?;
|
||||
let control_marker_version = control_delete
|
||||
.version_id()
|
||||
.map(str::to_string)
|
||||
.expect("control DELETE returns a marker version id");
|
||||
assert_eq!(control_delete.delete_marker(), Some(true));
|
||||
|
||||
let cascade_before = client
|
||||
.list_object_versions()
|
||||
.bucket(bucket)
|
||||
.prefix(cascade_key)
|
||||
.send()
|
||||
.await?;
|
||||
assert!(
|
||||
cascade_before
|
||||
.versions()
|
||||
.iter()
|
||||
.any(|version| version.version_id() == Some(cascade_data_version.as_str())),
|
||||
"cascade data version must exist before lifecycle is installed: {cascade_before:?}"
|
||||
);
|
||||
assert!(
|
||||
cascade_before
|
||||
.delete_markers()
|
||||
.iter()
|
||||
.any(|marker| { marker.version_id() == Some(cascade_marker_version.as_str()) && marker.is_latest() == Some(true) }),
|
||||
"cascade latest delete marker must exist before lifecycle is installed: {cascade_before:?}"
|
||||
);
|
||||
|
||||
let lifecycle = BucketLifecycleConfiguration::builder()
|
||||
.rules(noncurrent_expiration_with_delete_marker_cleanup_rule(
|
||||
"expire-and-clean-marker",
|
||||
"cascade/",
|
||||
1,
|
||||
)?)
|
||||
.rules(noncurrent_expiration_rule("expire-only", "nve-only/", 1)?)
|
||||
.build()?;
|
||||
client
|
||||
.put_bucket_lifecycle_configuration()
|
||||
.bucket(bucket)
|
||||
.lifecycle_configuration(lifecycle)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
wait_for_key_versions_empty(&client, bucket, cascade_key, StdDuration::from_secs(90)).await?;
|
||||
wait_for_version_expired(&client, bucket, control_key, &control_data_version, StdDuration::from_secs(90)).await?;
|
||||
|
||||
let survivor = client.get_object().bucket(bucket).key(survivor_key).send().await?;
|
||||
assert_eq!(survivor.body.collect().await?.into_bytes().as_ref(), b"current payload");
|
||||
|
||||
let control_after = client
|
||||
.list_object_versions()
|
||||
.bucket(bucket)
|
||||
.prefix(control_key)
|
||||
.send()
|
||||
.await?;
|
||||
assert!(
|
||||
control_after.versions().is_empty(),
|
||||
"NVE-only control must remove its data version: {control_after:?}"
|
||||
);
|
||||
assert!(
|
||||
control_after
|
||||
.delete_markers()
|
||||
.iter()
|
||||
.any(|marker| { marker.version_id() == Some(control_marker_version.as_str()) && marker.is_latest() == Some(true) }),
|
||||
"NVE-only control must preserve its latest delete marker: {control_after:?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `Days=0` expiration is invalid per S3 semantics (`Days` must be a positive
|
||||
/// integer >= 1). A `PutBucketLifecycleConfiguration` carrying a zero-day rule
|
||||
/// must be rejected with `InvalidArgument` (HTTP 400) - see crates/lifecycle
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -41,6 +41,10 @@ use bytes::Bytes;
|
||||
use futures::lock::Mutex;
|
||||
use metrics::counter;
|
||||
use rustfs_filemeta::{FileInfo, ObjectPartInfo, RawFileInfo};
|
||||
use rustfs_io_metrics::internode_metrics::{
|
||||
INTERNODE_STAGE_READ_VERSION_REQUEST_ENCODE, INTERNODE_STAGE_READ_VERSION_RESPONSE_DECODE,
|
||||
INTERNODE_STAGE_READ_VERSION_RPC_ROUNDTRIP,
|
||||
};
|
||||
use rustfs_protos::ChannelClass;
|
||||
use rustfs_protos::evict_failed_connection;
|
||||
use rustfs_protos::proto_gen::node_service::RenamePartRequest;
|
||||
@@ -64,7 +68,7 @@ use std::{
|
||||
atomic::{AtomicBool, AtomicU32, Ordering},
|
||||
},
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::time;
|
||||
use tokio::{
|
||||
@@ -1790,6 +1794,16 @@ fn decode_msgpack_or_json<T: DeserializeOwned>(binary: &[u8], json: &str, value_
|
||||
}
|
||||
}
|
||||
|
||||
fn read_version_stage_timer(attribution_enabled: bool) -> Option<Instant> {
|
||||
attribution_enabled.then(Instant::now)
|
||||
}
|
||||
|
||||
fn record_read_version_stage(stage: &'static str, started_at: Option<Instant>) {
|
||||
if let Some(started_at) = started_at {
|
||||
crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_read_version_stage(stage, started_at.elapsed());
|
||||
}
|
||||
}
|
||||
|
||||
/// Aggregate encoded size (bytes) of a `ReadMultiple` response, preferring the msgpack payloads
|
||||
/// and falling back to the JSON compatibility strings. Used to size the RPC for the payload
|
||||
/// histogram / large-payload alerting (grpc-optimization P0 instrumentation).
|
||||
@@ -2705,8 +2719,11 @@ impl DiskAPI for RemoteDisk {
|
||||
state = "started",
|
||||
"Remote disk RPC started"
|
||||
);
|
||||
let opts_str = compat_json(opts)?;
|
||||
let opts_bin = encode_msgpack(opts)?;
|
||||
let read_version_attribution_enabled = rustfs_io_metrics::get_stage_metrics_enabled();
|
||||
let encode_started = read_version_stage_timer(read_version_attribution_enabled);
|
||||
let encoded_opts = compat_json(opts).and_then(|opts_str| encode_msgpack(opts).map(|opts_bin| (opts_str, opts_bin)));
|
||||
record_read_version_stage(INTERNODE_STAGE_READ_VERSION_REQUEST_ENCODE, encode_started);
|
||||
let (opts_str, opts_bin) = encoded_opts?;
|
||||
|
||||
// Idempotent version read: eligible for the bounded transient-network retry so a single
|
||||
// reset-by-peer during the read-after-write window does not erode the metadata read
|
||||
@@ -2722,6 +2739,14 @@ impl DiskAPI for RemoteDisk {
|
||||
.get_client()
|
||||
.await
|
||||
.map_err(|err| Error::other(format!("can not get client, err: {err}")))?;
|
||||
let request_payload_bytes = read_version_attribution_enabled.then(|| {
|
||||
disk.len()
|
||||
.saturating_add(volume.len())
|
||||
.saturating_add(path.len())
|
||||
.saturating_add(version_id.len())
|
||||
.saturating_add(opts_str.len())
|
||||
.saturating_add(opts_bin.len())
|
||||
});
|
||||
let request = Request::new(ReadVersionRequest {
|
||||
disk,
|
||||
volume: volume.to_string(),
|
||||
@@ -2731,14 +2756,47 @@ impl DiskAPI for RemoteDisk {
|
||||
opts_bin: opts_bin.into(),
|
||||
});
|
||||
|
||||
let response = client.read_version(request).await?.into_inner();
|
||||
crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_read_version_request();
|
||||
if let Some(request_payload_bytes) = request_payload_bytes {
|
||||
crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_read_version_sent_bytes(request_payload_bytes);
|
||||
}
|
||||
let rpc_started = read_version_stage_timer(read_version_attribution_enabled);
|
||||
let response = match client.read_version(request).await {
|
||||
Ok(response) => {
|
||||
record_read_version_stage(INTERNODE_STAGE_READ_VERSION_RPC_ROUNDTRIP, rpc_started);
|
||||
response.into_inner()
|
||||
}
|
||||
Err(err) => {
|
||||
record_read_version_stage(INTERNODE_STAGE_READ_VERSION_RPC_ROUNDTRIP, rpc_started);
|
||||
crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_read_version_error();
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
|
||||
if !response.success {
|
||||
crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_read_version_error();
|
||||
return Err(response.error.unwrap_or_default().into());
|
||||
}
|
||||
|
||||
let file_info = decode_msgpack_or_json::<FileInfo>(&response.file_info_bin, &response.file_info, "FileInfo")?;
|
||||
validate_decoded_file_info(&file_info)?;
|
||||
crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_read_version_recv_bytes(
|
||||
response.file_info.len().saturating_add(response.file_info_bin.len()),
|
||||
);
|
||||
let decode_started = read_version_stage_timer(read_version_attribution_enabled);
|
||||
let file_info = match decode_msgpack_or_json::<FileInfo>(&response.file_info_bin, &response.file_info, "FileInfo")
|
||||
.and_then(|file_info| {
|
||||
validate_decoded_file_info(&file_info)?;
|
||||
Ok(file_info)
|
||||
}) {
|
||||
Ok(file_info) => {
|
||||
record_read_version_stage(INTERNODE_STAGE_READ_VERSION_RESPONSE_DECODE, decode_started);
|
||||
file_info
|
||||
}
|
||||
Err(err) => {
|
||||
record_read_version_stage(INTERNODE_STAGE_READ_VERSION_RESPONSE_DECODE, decode_started);
|
||||
crate::cluster::rpc::runtime_sources::record_remote_disk_grpc_read_version_error();
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(file_info)
|
||||
},
|
||||
@@ -7931,12 +7989,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn read_version_uses_the_metadata_timeout_on_a_stalled_peer() {
|
||||
runtime_sources::ensure_test_rpc_secret();
|
||||
let Some((base_addr, accept_task)) = spawn_stalled_grpc_peer().await else {
|
||||
return;
|
||||
};
|
||||
let remote_disk = remote_disk_for_addr(&base_addr).await;
|
||||
let metrics = rustfs_io_metrics::internode_metrics::global_internode_metrics();
|
||||
let previous_stage_metrics = rustfs_io_metrics::get_stage_metrics_enabled();
|
||||
metrics.reset_for_test();
|
||||
rustfs_io_metrics::set_get_stage_metrics_enabled(true);
|
||||
|
||||
temp_env::async_with_vars(
|
||||
[
|
||||
@@ -7960,6 +8023,18 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
|
||||
rustfs_io_metrics::set_get_stage_metrics_enabled(previous_stage_metrics);
|
||||
let snapshot = metrics.snapshot();
|
||||
assert!(
|
||||
snapshot.outgoing_requests_total >= 1,
|
||||
"ReadVersion call site should record outgoing attempts when attribution is enabled"
|
||||
);
|
||||
assert!(
|
||||
snapshot.sent_bytes_total > 0,
|
||||
"ReadVersion call site should record request payload bytes when attribution is enabled"
|
||||
);
|
||||
metrics.reset_for_test();
|
||||
|
||||
remote_disk.cancel_token.cancel();
|
||||
accept_task.abort();
|
||||
}
|
||||
|
||||
@@ -14,10 +14,11 @@
|
||||
|
||||
use rustfs_io_metrics::internode_metrics::{
|
||||
INTERNODE_MSGPACK_CODEC_JSON, INTERNODE_MSGPACK_CODEC_MSGPACK, INTERNODE_MSGPACK_DIRECTION_RESPONSE,
|
||||
INTERNODE_OPERATION_GRPC_READ_ALL, INTERNODE_OPERATION_GRPC_READ_MULTIPLE, INTERNODE_OPERATION_GRPC_WRITE_ALL,
|
||||
INTERNODE_OPERATION_PUT_FILE_STREAM, INTERNODE_OPERATION_READ_FILE_STREAM, INTERNODE_TRANSPORT_BACKEND_GRPC,
|
||||
INTERNODE_TRANSPORT_BACKEND_TCP_HTTP, global_internode_metrics,
|
||||
INTERNODE_OPERATION_GRPC_READ_ALL, INTERNODE_OPERATION_GRPC_READ_MULTIPLE, INTERNODE_OPERATION_GRPC_READ_VERSION,
|
||||
INTERNODE_OPERATION_GRPC_WRITE_ALL, INTERNODE_OPERATION_PUT_FILE_STREAM, INTERNODE_OPERATION_READ_FILE_STREAM,
|
||||
INTERNODE_TRANSPORT_BACKEND_GRPC, INTERNODE_TRANSPORT_BACKEND_TCP_HTTP, global_internode_metrics,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(test)]
|
||||
use rustfs_io_metrics::internode_metrics::InternodeMetricsSnapshot;
|
||||
@@ -82,6 +83,59 @@ pub(crate) fn record_remote_disk_grpc_read_all_request() {
|
||||
.record_outgoing_request_for_operation_and_backend(INTERNODE_OPERATION_GRPC_READ_ALL, INTERNODE_TRANSPORT_BACKEND_GRPC);
|
||||
}
|
||||
|
||||
pub(crate) fn record_remote_disk_grpc_read_version_request() {
|
||||
if !rustfs_io_metrics::get_stage_metrics_enabled() {
|
||||
return;
|
||||
}
|
||||
global_internode_metrics().record_outgoing_request_for_operation_and_backend(
|
||||
INTERNODE_OPERATION_GRPC_READ_VERSION,
|
||||
INTERNODE_TRANSPORT_BACKEND_GRPC,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn record_remote_disk_grpc_read_version_error() {
|
||||
if !rustfs_io_metrics::get_stage_metrics_enabled() {
|
||||
return;
|
||||
}
|
||||
global_internode_metrics()
|
||||
.record_error_for_operation_and_backend(INTERNODE_OPERATION_GRPC_READ_VERSION, INTERNODE_TRANSPORT_BACKEND_GRPC);
|
||||
}
|
||||
|
||||
pub(crate) fn record_remote_disk_grpc_read_version_sent_bytes(bytes: usize) {
|
||||
if !rustfs_io_metrics::get_stage_metrics_enabled() {
|
||||
return;
|
||||
}
|
||||
global_internode_metrics().record_sent_bytes_for_operation_and_backend(
|
||||
INTERNODE_OPERATION_GRPC_READ_VERSION,
|
||||
INTERNODE_TRANSPORT_BACKEND_GRPC,
|
||||
bytes,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn record_remote_disk_grpc_read_version_recv_bytes(bytes: usize) {
|
||||
if !rustfs_io_metrics::get_stage_metrics_enabled() {
|
||||
return;
|
||||
}
|
||||
global_internode_metrics().record_recv_bytes_for_operation_and_backend(
|
||||
INTERNODE_OPERATION_GRPC_READ_VERSION,
|
||||
INTERNODE_TRANSPORT_BACKEND_GRPC,
|
||||
bytes,
|
||||
);
|
||||
record_grpc_payload_size(INTERNODE_OPERATION_GRPC_READ_VERSION, bytes);
|
||||
}
|
||||
|
||||
pub(crate) fn record_remote_disk_grpc_read_version_stage(stage: &'static str, duration: Duration) {
|
||||
if !rustfs_io_metrics::get_stage_metrics_enabled() {
|
||||
return;
|
||||
}
|
||||
global_internode_metrics().record_stage_duration_for_operation_and_backend(
|
||||
INTERNODE_OPERATION_GRPC_READ_VERSION,
|
||||
INTERNODE_TRANSPORT_BACKEND_GRPC,
|
||||
stage,
|
||||
duration,
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn record_remote_disk_grpc_read_all_recv_bytes(bytes: usize) {
|
||||
global_internode_metrics().record_recv_bytes_for_operation_and_backend(
|
||||
INTERNODE_OPERATION_GRPC_READ_ALL,
|
||||
|
||||
@@ -1041,7 +1041,13 @@ fn should_count_decommission_version_complete(ignore: bool, cleanup_ignored: boo
|
||||
fn is_decommission_copy_cleanup_safe_error(err: &Error) -> bool {
|
||||
// DataMovementOverwriteErr only means source and destination pool resolved to
|
||||
// the same pool. Without a target equivalence check it is not cleanup-safe.
|
||||
is_err_object_not_found(err) || is_err_version_not_found(err)
|
||||
if is_err_object_not_found(err) || is_err_version_not_found(err) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A not-found surfacing from inside a data-movement stage is the same
|
||||
// condition once the wrapper is unwrapped (backlog#1827 T2).
|
||||
crate::data_movement::data_movement_stage_source(err).is_some_and(is_decommission_copy_cleanup_safe_error)
|
||||
}
|
||||
|
||||
fn is_decommission_target_capacity_error(err: &Error) -> bool {
|
||||
@@ -1049,6 +1055,13 @@ fn is_decommission_target_capacity_error(err: &Error) -> bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A stage failure keeps the error it wrapped, so classify by type rather
|
||||
// than by the rendered message (backlog#1827 T2). The substring fallback
|
||||
// stays for errors that reached here through some other wrapper.
|
||||
if let Some(source) = crate::data_movement::data_movement_stage_source(err) {
|
||||
return is_decommission_target_capacity_error(source);
|
||||
}
|
||||
|
||||
let message = err.to_string();
|
||||
let disk_full = Error::DiskFull.to_string();
|
||||
let storage_full = Error::StorageFull.to_string();
|
||||
@@ -4427,6 +4440,36 @@ mod tests {
|
||||
assert!(is_decommission_target_capacity_error(&Error::StorageFull));
|
||||
}
|
||||
|
||||
/// The decommission loop classifies errors that came back through a
|
||||
/// data-movement stage wrapper. Before backlog#1827 T2 the wrapper flattened
|
||||
/// everything into `Error::other(String)`, so these two classifiers had to
|
||||
/// match on rendered text; now the wrapped error is recoverable by type.
|
||||
#[test]
|
||||
fn decommission_classifiers_see_through_a_stage_wrapper() {
|
||||
let wrap = |inner: Error| {
|
||||
crate::data_movement::data_movement_stage_error_for_test(
|
||||
"decommission_object",
|
||||
"put_object",
|
||||
"bucket-a",
|
||||
"object-a",
|
||||
inner,
|
||||
)
|
||||
};
|
||||
|
||||
// Capacity: the target pool filling up must still stop the loop.
|
||||
assert!(is_decommission_target_capacity_error(&wrap(Error::DiskFull)));
|
||||
assert!(is_decommission_target_capacity_error(&wrap(Error::StorageFull)));
|
||||
assert!(!is_decommission_target_capacity_error(&wrap(Error::SlowDown)));
|
||||
|
||||
// Cleanup safety: a not-found surfacing from inside a stage is the same
|
||||
// condition as one surfacing directly, so the source entry stays
|
||||
// eligible for cleanup.
|
||||
let not_found = Error::ObjectNotFound("bucket-a".to_string(), "object-a".to_string());
|
||||
assert!(is_decommission_copy_cleanup_safe_error(¬_found));
|
||||
assert!(is_decommission_copy_cleanup_safe_error(&wrap(not_found)));
|
||||
assert!(!is_decommission_copy_cleanup_safe_error(&wrap(Error::SlowDown)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decommission_target_capacity_error_accepts_wrapped_capacity_errors() {
|
||||
let disk_full = Error::other(format!("decommission_object: put_object failed for bucket/object: {}", Error::DiskFull));
|
||||
|
||||
@@ -471,8 +471,60 @@ fn resolve_data_movement_abort_result(
|
||||
))
|
||||
}
|
||||
|
||||
fn data_movement_stage_error(op_label: &str, stage: &str, bucket: &str, object: &str, err: impl std::fmt::Display) -> Error {
|
||||
Error::other(format!("{op_label}: {stage} failed for {bucket}/{object}: {err}"))
|
||||
/// A data-movement stage failure that keeps the error it wrapped.
|
||||
///
|
||||
/// The rendered message is byte-identical to the `format!` this replaced, so
|
||||
/// logs and any message-matching callers are unaffected. What changes is that
|
||||
/// the original error stays reachable through `source()`, which is what lets
|
||||
/// the decommission loop classify by type instead of by substring
|
||||
/// (backlog#1827 T2).
|
||||
#[derive(Debug)]
|
||||
struct DataMovementStageError {
|
||||
rendered: String,
|
||||
source: Box<dyn std::error::Error + Send + Sync>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DataMovementStageError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.rendered)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DataMovementStageError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
Some(self.source.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
fn data_movement_stage_error<E>(op_label: &str, stage: &str, bucket: &str, object: &str, err: E) -> Error
|
||||
where
|
||||
E: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
let rendered = format!("{op_label}: {stage} failed for {bucket}/{object}: {err}");
|
||||
Error::other(DataMovementStageError {
|
||||
rendered,
|
||||
source: Box::new(err),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn data_movement_stage_error_for_test(op_label: &str, stage: &str, bucket: &str, object: &str, err: Error) -> Error {
|
||||
data_movement_stage_error(op_label, stage, bucket, object, err)
|
||||
}
|
||||
|
||||
/// Recover the error a [`data_movement_stage_error`] wrapped, if this is one.
|
||||
///
|
||||
/// `Error::other` boxes through `std::io::Error`, so the chain is
|
||||
/// `StorageError::Io` -> `DataMovementStageError` -> the original error.
|
||||
pub(crate) fn data_movement_stage_source(err: &Error) -> Option<&Error> {
|
||||
let Error::Io(io_err) = err else {
|
||||
return None;
|
||||
};
|
||||
io_err
|
||||
.get_ref()?
|
||||
.downcast_ref::<DataMovementStageError>()?
|
||||
.source
|
||||
.downcast_ref::<Error>()
|
||||
}
|
||||
|
||||
fn schedule_data_movement_multipart_abort_cleanup(
|
||||
@@ -1865,6 +1917,40 @@ mod tests {
|
||||
assert!(message.contains(Error::SlowDown.to_string().as_str()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stage_error_renders_exactly_as_the_format_it_replaced() {
|
||||
// The wrapper gained a source; its message must not have moved, or log
|
||||
// scrapers and any message-matching caller would break (backlog#1827 T2).
|
||||
// `Error::other` renders through `StorageError::Io`, which prefixes
|
||||
// "Io error: " — that was true of the `format!` this replaced too, so
|
||||
// the full string is what must stay stable.
|
||||
let err = data_movement_stage_error("rebalance_object", "put_object", "bucket-a", "object-a", Error::SlowDown);
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
format!("Io error: rebalance_object: put_object failed for bucket-a/object-a: {}", Error::SlowDown)
|
||||
);
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
Error::other(format!("rebalance_object: put_object failed for bucket-a/object-a: {}", Error::SlowDown)).to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stage_error_keeps_the_wrapped_error_recoverable() {
|
||||
for original in [Error::DiskFull, Error::StorageFull, Error::FileNotFound, Error::SlowDown] {
|
||||
let wrapped =
|
||||
data_movement_stage_error("decommission_object", "put_object", "bucket-a", "object-a", original.clone());
|
||||
let recovered = data_movement_stage_source(&wrapped).expect("the wrapped error must be recoverable");
|
||||
assert_eq!(recovered.to_string(), original.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stage_source_ignores_errors_it_did_not_wrap() {
|
||||
assert!(data_movement_stage_source(&Error::DiskFull).is_none());
|
||||
assert!(data_movement_stage_source(&Error::other("plain io error")).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_data_movement_part_stage_error_includes_stage_object_and_part() {
|
||||
let err =
|
||||
|
||||
@@ -19,7 +19,7 @@ pub mod local_snapshot;
|
||||
use crate::storage_api_contracts::{
|
||||
bucket::{BucketOperations as _, BucketOptions},
|
||||
list::{ListOperations as _, StorageListObjectVersionsInfo},
|
||||
object::{EcstoreObjectIO, HTTPPreconditions, ObjectIO as _, ObjectOperations as _},
|
||||
object::{EcstoreObjectIO, HTTPPreconditions, ObjectOperations as _},
|
||||
};
|
||||
use crate::{
|
||||
bucket::{metadata_sys::get_replication_config, versioning::VersioningApi as _, versioning_sys::BucketVersioningSys},
|
||||
@@ -2009,80 +2009,102 @@ pub async fn apply_bucket_usage_memory_overlay(data_usage_info: &mut DataUsageIn
|
||||
}
|
||||
|
||||
// Helper functions for DataUsageCache operations
|
||||
pub async fn load_data_usage_cache(store: &crate::set_disk::SetDisks, name: &str) -> crate::error::Result<DataUsageCache> {
|
||||
use crate::disk::{BUCKET_META_PREFIX, RUSTFS_META_BUCKET};
|
||||
|
||||
/// How many times `load_data_usage_cache` tries a read that failed for a
|
||||
/// transient reason before giving up.
|
||||
const DATA_USAGE_CACHE_LOAD_ATTEMPTS: usize = 5;
|
||||
const DATA_USAGE_CACHE_LOAD_BASE_DELAY: std::time::Duration = std::time::Duration::from_millis(100);
|
||||
const DATA_USAGE_CACHE_LOAD_MAX_DELAY: std::time::Duration = std::time::Duration::from_millis(1_000);
|
||||
|
||||
/// Result of one attempt at reading a data-usage cache object.
|
||||
enum DataUsageCacheRead {
|
||||
Loaded(DataUsageCache),
|
||||
/// The object is definitively not there, so retrying cannot turn the read
|
||||
/// into a hit.
|
||||
Absent,
|
||||
}
|
||||
|
||||
/// True when the error means the cache object does not exist, as opposed to a
|
||||
/// transient failure that is worth another attempt.
|
||||
///
|
||||
/// `SetDisks::get_object_reader` runs its failures through `to_object_err`,
|
||||
/// which rewrites `FileNotFound` to `ObjectNotFound` and `VolumeNotFound` to
|
||||
/// `BucketNotFound`, so those are the variants that actually arrive here. The
|
||||
/// raw pair is matched too because callers reading through a different layer
|
||||
/// can still surface it.
|
||||
fn is_data_usage_cache_absent(err: &Error) -> bool {
|
||||
matches!(
|
||||
err,
|
||||
Error::FileNotFound | Error::VolumeNotFound | Error::ObjectNotFound(..) | Error::BucketNotFound(..)
|
||||
)
|
||||
}
|
||||
|
||||
async fn read_data_usage_cache_object<S>(store: &S, key: &str) -> crate::error::Result<DataUsageCacheRead>
|
||||
where
|
||||
S: EcstoreObjectIO,
|
||||
{
|
||||
use crate::disk::RUSTFS_META_BUCKET;
|
||||
use crate::object_api::ObjectOptions;
|
||||
use http::HeaderMap;
|
||||
use rand::RngExt;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
let mut d = DataUsageCache::default();
|
||||
let mut retries = 0;
|
||||
while retries < 5 {
|
||||
let path = Path::new(BUCKET_META_PREFIX).join(name);
|
||||
match store
|
||||
.get_object_reader(
|
||||
RUSTFS_META_BUCKET,
|
||||
path.to_str().unwrap(),
|
||||
None,
|
||||
HeaderMap::new(),
|
||||
&ObjectOptions {
|
||||
no_lock: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(mut reader) => {
|
||||
if let Ok(info) = DataUsageCache::unmarshal(&reader.read_all().await?) {
|
||||
d = info
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(err) => match err {
|
||||
Error::FileNotFound | Error::VolumeNotFound => {
|
||||
match store
|
||||
.get_object_reader(
|
||||
RUSTFS_META_BUCKET,
|
||||
name,
|
||||
None,
|
||||
HeaderMap::new(),
|
||||
&ObjectOptions {
|
||||
no_lock: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(mut reader) => {
|
||||
if let Ok(info) = DataUsageCache::unmarshal(&reader.read_all().await?) {
|
||||
d = info
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(_) => match err {
|
||||
Error::FileNotFound | Error::VolumeNotFound => {
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
break;
|
||||
}
|
||||
match store
|
||||
.get_object_reader(
|
||||
RUSTFS_META_BUCKET,
|
||||
key,
|
||||
None,
|
||||
HeaderMap::new(),
|
||||
&ObjectOptions {
|
||||
no_lock: true,
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
retries += 1;
|
||||
let dur = {
|
||||
let mut rng = rand::rng();
|
||||
rng.random_range(0..1_000)
|
||||
};
|
||||
sleep(Duration::from_millis(dur)).await;
|
||||
)
|
||||
.await
|
||||
{
|
||||
// A cache object that fails to decode is treated as absent rather than
|
||||
// as an error: a corrupt cache should not stall the caller, and the
|
||||
// next scanner pass rewrites it.
|
||||
Ok(mut reader) => Ok(DataUsageCache::unmarshal(&reader.read_all().await?)
|
||||
.map(DataUsageCacheRead::Loaded)
|
||||
.unwrap_or(DataUsageCacheRead::Absent)),
|
||||
Err(err) if is_data_usage_cache_absent(&err) => Ok(DataUsageCacheRead::Absent),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// Load a data-usage cache, preferring the prefixed key and falling back to the
|
||||
/// legacy unprefixed one.
|
||||
///
|
||||
/// A cache that is absent under both keys yields an empty cache; a transient
|
||||
/// read failure is retried with capped, jittered backoff and surfaces as an
|
||||
/// error once the attempts are exhausted.
|
||||
pub(crate) async fn load_data_usage_cache<S>(store: &S, name: &str) -> crate::error::Result<DataUsageCache>
|
||||
where
|
||||
S: EcstoreObjectIO,
|
||||
{
|
||||
use crate::disk::BUCKET_META_PREFIX;
|
||||
use std::path::Path;
|
||||
|
||||
let prefixed = Path::new(BUCKET_META_PREFIX).join(name);
|
||||
let prefixed = prefixed
|
||||
.to_str()
|
||||
.ok_or_else(|| Error::other("data usage cache path is not valid UTF-8"))?
|
||||
.to_owned();
|
||||
|
||||
rustfs_utils::retry::retry_with_backoff(
|
||||
|| async {
|
||||
match read_data_usage_cache_object(store, &prefixed).await? {
|
||||
DataUsageCacheRead::Loaded(cache) => Ok(cache),
|
||||
DataUsageCacheRead::Absent => match read_data_usage_cache_object(store, name).await? {
|
||||
DataUsageCacheRead::Loaded(cache) => Ok(cache),
|
||||
DataUsageCacheRead::Absent => Ok(DataUsageCache::default()),
|
||||
},
|
||||
}
|
||||
},
|
||||
DATA_USAGE_CACHE_LOAD_ATTEMPTS,
|
||||
DATA_USAGE_CACHE_LOAD_BASE_DELAY,
|
||||
DATA_USAGE_CACHE_LOAD_MAX_DELAY,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Persist the current in-memory compression total to the backend.
|
||||
@@ -2220,6 +2242,7 @@ pub async fn init_compression_total_memory_from_backend(store: Arc<ECStore>) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::storage_api_contracts::object::ObjectIO as _;
|
||||
use rustfs_data_usage::BucketUsageInfo;
|
||||
use rustfs_lock::{LocalClient, LockRequest, LockType, NamespaceLock, ObjectKey};
|
||||
use serial_test::serial;
|
||||
@@ -2452,6 +2475,148 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal ObjectIO backing `load_data_usage_cache` tests: records the keys
|
||||
/// read and fails the first N reads with a transient (non-absence) error.
|
||||
#[derive(Debug, Default)]
|
||||
struct UsageCacheReadStore {
|
||||
transient_failures: Mutex<usize>,
|
||||
reads: Mutex<Vec<String>>,
|
||||
}
|
||||
|
||||
impl UsageCacheReadStore {
|
||||
fn failing_first(n: usize) -> Self {
|
||||
Self {
|
||||
transient_failures: Mutex::new(n),
|
||||
reads: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_keys(&self) -> Vec<String> {
|
||||
self.reads.lock().await.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::storage_api_contracts::object::ObjectIO for UsageCacheReadStore {
|
||||
type Error = Error;
|
||||
type RangeSpec = crate::storage_api_contracts::range::HTTPRangeSpec;
|
||||
type HeaderMap = http::HeaderMap;
|
||||
type ObjectOptions = ObjectOptions;
|
||||
type ObjectInfo = ObjectInfo;
|
||||
type GetObjectReader = crate::object_api::GetObjectReader;
|
||||
type PutObjectReader = PutObjReader;
|
||||
|
||||
async fn get_object_reader(
|
||||
&self,
|
||||
_bucket: &str,
|
||||
object: &str,
|
||||
_range: Option<Self::RangeSpec>,
|
||||
_h: Self::HeaderMap,
|
||||
_opts: &Self::ObjectOptions,
|
||||
) -> Result<Self::GetObjectReader, Self::Error> {
|
||||
self.reads.lock().await.push(object.to_string());
|
||||
let mut remaining = self.transient_failures.lock().await;
|
||||
if *remaining > 0 {
|
||||
*remaining -= 1;
|
||||
return Err(Error::other("transient read failure"));
|
||||
}
|
||||
// `SetDisks::get_object_reader` reports a missing object through
|
||||
// `to_object_err`, so the absence that reaches the caller is
|
||||
// `ObjectNotFound`, not the raw `FileNotFound`.
|
||||
Err(Error::ObjectNotFound(RUSTFS_META_BUCKET.to_string(), object.to_string()))
|
||||
}
|
||||
|
||||
async fn put_object(
|
||||
&self,
|
||||
_bucket: &str,
|
||||
_object: &str,
|
||||
_data: &mut Self::PutObjectReader,
|
||||
_opts: &Self::ObjectOptions,
|
||||
) -> Result<Self::ObjectInfo, Self::Error> {
|
||||
unimplemented!("load_data_usage_cache never writes")
|
||||
}
|
||||
}
|
||||
|
||||
fn prefixed_usage_key(name: &str) -> String {
|
||||
std::path::Path::new(crate::disk::BUCKET_META_PREFIX)
|
||||
.join(name)
|
||||
.to_str()
|
||||
.expect("utf-8 path")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_usage_cache_absence_covers_the_variants_that_actually_arrive() {
|
||||
// `to_object_err` rewrites the raw storage variants before they reach
|
||||
// `load_data_usage_cache`; classifying only the raw pair would treat a
|
||||
// missing cache as a transient failure and retry it.
|
||||
assert!(is_data_usage_cache_absent(&Error::ObjectNotFound(
|
||||
"bucket".to_string(),
|
||||
"object".to_string()
|
||||
)));
|
||||
assert!(is_data_usage_cache_absent(&Error::BucketNotFound("bucket".to_string())));
|
||||
assert!(is_data_usage_cache_absent(&Error::FileNotFound));
|
||||
assert!(is_data_usage_cache_absent(&Error::VolumeNotFound));
|
||||
|
||||
assert!(!is_data_usage_cache_absent(&Error::other("transient read failure")));
|
||||
assert!(!is_data_usage_cache_absent(&Error::DiskNotFound));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_data_usage_cache_treats_absence_as_an_empty_cache_without_retrying() {
|
||||
let name = "usage-cache";
|
||||
let store = UsageCacheReadStore::default();
|
||||
|
||||
let cache = load_data_usage_cache(&store, name).await.expect("absence is not an error");
|
||||
|
||||
assert!(cache.cache.is_empty());
|
||||
assert_eq!(
|
||||
store.read_keys().await,
|
||||
vec![prefixed_usage_key(name), name.to_string()],
|
||||
"the prefixed key is tried first, then the legacy one, and neither absence is retried"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_data_usage_cache_retries_a_transient_failure() {
|
||||
let name = "usage-cache";
|
||||
// Two transient failures, then the object reads as absent.
|
||||
let store = UsageCacheReadStore::failing_first(2);
|
||||
|
||||
let cache = load_data_usage_cache(&store, name)
|
||||
.await
|
||||
.expect("retry should reach the absent read");
|
||||
|
||||
assert!(cache.cache.is_empty());
|
||||
assert_eq!(
|
||||
store.read_keys().await,
|
||||
vec![
|
||||
prefixed_usage_key(name),
|
||||
prefixed_usage_key(name),
|
||||
prefixed_usage_key(name),
|
||||
name.to_string(),
|
||||
],
|
||||
"a transient failure retries the prefixed read rather than falling through"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_data_usage_cache_surfaces_a_persistent_failure() {
|
||||
let name = "usage-cache";
|
||||
let store = UsageCacheReadStore::failing_first(usize::MAX);
|
||||
|
||||
let err = load_data_usage_cache(&store, name)
|
||||
.await
|
||||
.expect_err("an exhausted retry must not be reported as an empty cache");
|
||||
|
||||
assert!(err.to_string().contains("transient read failure"));
|
||||
assert_eq!(
|
||||
store.read_keys().await.len(),
|
||||
DATA_USAGE_CACHE_LOAD_ATTEMPTS,
|
||||
"every attempt is used before giving up"
|
||||
);
|
||||
}
|
||||
|
||||
async fn clear_usage_memory_cache_for_test() {
|
||||
memory_cache().write().await.clear();
|
||||
*cache_updating().write().await = false;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user