1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
// Property-based tests for rate limiting enforcement.
//
// Feature: s3rm-rs, Property 36: Rate Limiting Enforcement
// For any rate limit configuration, the Rate Limiter should enforce that
// the deletion rate does not exceed the specified maximum objects per second.
// **Validates: Requirements 8.7**
#[cfg(test)]
mod tests {
use crate::callback::event_manager::EventManager;
use crate::callback::filter_manager::FilterManager;
use crate::config::args::parse_from_args;
use crate::config::{
CLITimeoutConfig, ClientConfig, Config, FilterConfig, ForceRetryConfig, RetryConfig,
TracingConfig,
};
use crate::storage::create_storage;
use crate::types::{AccessKeys, ClientConfigLocation, S3Credentials, StoragePath};
use crate::test_utils::init_dummy_tracing_subscriber;
use aws_smithy_types::checksum_config::RequestChecksumCalculation;
use proptest::prelude::*;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
// -----------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------
fn make_test_client_config() -> ClientConfig {
ClientConfig {
client_config_location: ClientConfigLocation {
aws_config_file: None,
aws_shared_credentials_file: None,
},
credential: S3Credentials::Credentials {
access_keys: AccessKeys {
access_key: "AKIAIOSFODNN7EXAMPLE".to_string(),
secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string(),
session_token: None,
},
},
region: Some("us-east-1".to_string()),
endpoint_url: Some("https://localhost:9000".to_string()),
force_path_style: true,
retry_config: RetryConfig {
aws_max_attempts: 3,
initial_backoff_milliseconds: 100,
},
cli_timeout_config: CLITimeoutConfig {
operation_timeout_milliseconds: None,
operation_attempt_timeout_milliseconds: None,
connect_timeout_milliseconds: None,
read_timeout_milliseconds: None,
},
disable_stalled_stream_protection: false,
request_checksum_calculation: RequestChecksumCalculation::WhenRequired,
accelerate: false,
request_payer: None,
}
}
fn make_test_config(rate_limit: Option<u32>) -> Config {
Config {
target: StoragePath::S3 {
bucket: "test-bucket".to_string(),
prefix: "prefix/".to_string(),
},
show_no_progress: false,
target_client_config: Some(make_test_client_config()),
force_retry_config: ForceRetryConfig {
force_retry_count: 0,
force_retry_interval_milliseconds: 0,
},
tracing_config: Some(TracingConfig {
tracing_level: log::Level::Info,
json_tracing: false,
aws_sdk_tracing: false,
span_events_tracing: false,
disable_color_tracing: true,
}),
worker_size: 4,
warn_as_error: false,
dry_run: false,
rate_limit_objects: rate_limit,
max_parallel_listings: 1,
object_listing_queue_size: 1000,
max_parallel_listing_max_depth: 0,
allow_parallel_listings_in_express_one_zone: false,
filter_config: FilterConfig::default(),
max_keys: 1000,
auto_complete_shell: None,
event_callback_lua_script: None,
filter_callback_lua_script: None,
allow_lua_os_library: false,
allow_lua_unsafe_vm: false,
lua_vm_memory_limit: 0,
lua_callback_timeout_milliseconds: 0,
if_match: false,
max_delete: None,
filter_manager: FilterManager::new(),
event_manager: EventManager::new(),
batch_size: 1000,
delete_all_versions: false,
force: true,
test_user_defined_callback: false,
}
}
// -----------------------------------------------------------------------
// Generators
// -----------------------------------------------------------------------
/// Generate a valid rate limit value (minimum 10 per CLI constraint).
fn arb_rate_limit() -> impl Strategy<Value = u32> {
10u32..=100_000u32
}
/// Generate a rate limit value below the minimum (invalid).
fn arb_invalid_rate_limit() -> impl Strategy<Value = u32> {
0u32..10u32
}
// -----------------------------------------------------------------------
// Feature: s3rm-rs, Property 36: Rate Limiting Enforcement
// -----------------------------------------------------------------------
proptest! {
#![proptest_config(ProptestConfig::with_cases(50))]
/// Feature: s3rm-rs, Property 36: Rate Limiting Enforcement (CLI propagation)
/// **Validates: Requirements 8.7**
///
/// For any valid rate limit value provided via CLI, the parsed Config
/// should contain the exact rate_limit_objects value.
#[test]
fn prop_rate_limit_cli_propagation(
rate_limit in arb_rate_limit(),
) {
let rate_str = rate_limit.to_string();
let args: Vec<&str> = vec![
"s3rm",
"s3://test-bucket/prefix/",
"--rate-limit-objects",
&rate_str,
"--batch-size",
"1",
];
let cli = parse_from_args(args).unwrap();
let config = Config::try_from(cli).unwrap();
prop_assert_eq!(
config.rate_limit_objects,
Some(rate_limit),
"rate_limit_objects must match CLI input"
);
}
}
/// Feature: s3rm-rs, Property 36: Rate Limiting Enforcement (no rate limit default)
/// **Validates: Requirements 8.7**
///
/// When no --rate-limit-objects is specified, rate_limit_objects in
/// Config should be None, meaning no rate limiting is applied.
#[test]
fn prop_rate_limit_default_none() {
let args: Vec<&str> = vec!["s3rm", "s3://test-bucket/prefix/"];
let cli = parse_from_args(args).unwrap();
let config = Config::try_from(cli).unwrap();
assert!(
config.rate_limit_objects.is_none(),
"rate_limit_objects must be None when not specified"
);
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(50))]
/// Feature: s3rm-rs, Property 36: Rate Limiting Enforcement (minimum value rejection)
/// **Validates: Requirements 8.7**
///
/// For any rate limit value below the minimum (10), CLI parsing should
/// reject the input. This ensures rate limiting cannot be configured
/// with unreasonably low values.
#[test]
fn prop_rate_limit_rejects_below_minimum(
rate_limit in arb_invalid_rate_limit(),
) {
let rate_str = rate_limit.to_string();
let args: Vec<&str> = vec![
"s3rm",
"s3://test-bucket/prefix/",
"--rate-limit-objects",
&rate_str,
];
let result = parse_from_args(args);
prop_assert!(
result.is_err(),
"rate_limit_objects below 10 must be rejected, but {} was accepted",
rate_limit
);
}
/// Feature: s3rm-rs, Property 36: Rate Limiting Enforcement (rate limiter creation)
/// **Validates: Requirements 8.7**
///
/// For any valid rate limit value, create_storage should successfully
/// create a storage instance with rate limiting applied. The rate
/// limiter uses a token bucket algorithm with refill intervals
/// computed from the configured rate.
#[test]
fn prop_rate_limit_storage_creation(
rate_limit in arb_rate_limit(),
) {
init_dummy_tracing_subscriber();
let config = make_test_config(Some(rate_limit));
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
let cancellation_token =
crate::types::token::create_pipeline_cancellation_token();
let (stats_sender, _stats_receiver) = async_channel::unbounded();
let has_warning = Arc::new(AtomicBool::new(false));
let storage = create_storage(
config,
cancellation_token,
stats_sender,
has_warning,
)
.await;
prop_assert!(
storage.get_client().is_some(),
"Storage must be created successfully with rate_limit_objects={}",
rate_limit
);
Ok(())
})?;
}
/// Feature: s3rm-rs, Property 36: Rate Limiting Enforcement (storage creation without rate limit)
/// **Validates: Requirements 8.7**
///
/// When rate_limit_objects is None, create_storage should still succeed
/// and create a valid storage instance (no rate limiter applied),
/// regardless of worker_size.
#[test]
fn prop_rate_limit_none_storage_creation(
worker_size in 1u16..64,
) {
init_dummy_tracing_subscriber();
let mut config = make_test_config(None);
config.worker_size = worker_size;
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
let cancellation_token =
crate::types::token::create_pipeline_cancellation_token();
let (stats_sender, _stats_receiver) = async_channel::unbounded();
let has_warning = Arc::new(AtomicBool::new(false));
let storage = create_storage(
config,
cancellation_token,
stats_sender,
has_warning,
)
.await;
prop_assert!(
storage.get_client().is_some(),
"Storage must be created successfully without rate limiting"
);
Ok(())
})?;
}
}
}