Skip to main content

s3util_rs/storage/s3/
client_builder.rs

1use aws_config::meta::region::{ProvideRegion, RegionProviderChain};
2use aws_config::retry::RetryConfig;
3use aws_config::{BehaviorVersion, ConfigLoader};
4use aws_runtime::env_config::file::{EnvConfigFileKind, EnvConfigFiles};
5use aws_sdk_s3::Client;
6use aws_sdk_s3::config::Builder;
7use std::path::PathBuf;
8use std::time::Duration;
9
10use crate::config::ClientConfig;
11use aws_smithy_runtime_api::client::stalled_stream_protection::StalledStreamProtectionConfig;
12use aws_smithy_types::timeout::TimeoutConfig;
13use aws_types::SdkConfig;
14use aws_types::region::Region;
15
16/// Build an `EnvConfigFiles` that reflects any user-provided `--aws-config-file`
17/// and `--aws-shared-credentials-file` overrides, falling back to system defaults
18/// for whichever file the user did *not* override. Returns `None` when the user
19/// overrode nothing (so the caller leaves SDK defaults untouched).
20fn build_profile_files(
21    aws_config_file: Option<&PathBuf>,
22    aws_shared_credentials_file: Option<&PathBuf>,
23) -> Option<EnvConfigFiles> {
24    if aws_config_file.is_none() && aws_shared_credentials_file.is_none() {
25        return None;
26    }
27    let mut builder = EnvConfigFiles::builder();
28    match aws_config_file {
29        Some(p) => builder = builder.with_file(EnvConfigFileKind::Config, p),
30        None => builder = builder.include_default_config_file(true),
31    }
32    match aws_shared_credentials_file {
33        Some(p) => builder = builder.with_file(EnvConfigFileKind::Credentials, p),
34        None => builder = builder.include_default_credentials_file(true),
35    }
36    Some(builder.build())
37}
38
39impl ClientConfig {
40    pub async fn create_client(&self) -> Client {
41        let mut config_builder = Builder::from(&self.load_sdk_config().await)
42            .force_path_style(self.force_path_style)
43            .request_checksum_calculation(self.request_checksum_calculation)
44            .accelerate(self.accelerate);
45
46        if let Some(timeout_config) = self.build_timeout_config() {
47            config_builder = config_builder.timeout_config(timeout_config);
48        }
49
50        Client::from_conf(config_builder.build())
51    }
52
53    async fn load_sdk_config(&self) -> SdkConfig {
54        let config_loader = if self.disable_stalled_stream_protection {
55            aws_config::defaults(BehaviorVersion::latest())
56                .stalled_stream_protection(StalledStreamProtectionConfig::disabled())
57        } else {
58            aws_config::defaults(BehaviorVersion::latest())
59                .stalled_stream_protection(StalledStreamProtectionConfig::enabled().build())
60        };
61        let mut config_loader = self
62            .load_config_credential(config_loader)
63            .region(self.build_region_provider())
64            .retry_config(self.build_retry_config());
65
66        if let Some(endpoint_url) = &self.endpoint_url {
67            config_loader = config_loader.endpoint_url(endpoint_url);
68        };
69
70        config_loader.load().await
71    }
72
73    fn load_config_credential(&self, mut config_loader: ConfigLoader) -> ConfigLoader {
74        match &self.credential {
75            crate::types::S3Credentials::Credentials { access_keys } => {
76                let credentials = aws_sdk_s3::config::Credentials::new(
77                    access_keys.access_key.to_string(),
78                    access_keys.secret_access_key.to_string(),
79                    access_keys.session_token.clone(),
80                    None,
81                    "",
82                );
83                config_loader = config_loader.credentials_provider(credentials);
84            }
85            crate::types::S3Credentials::Profile(profile_name) => {
86                let mut builder = aws_config::profile::ProfileFileCredentialsProvider::builder();
87
88                if let Some(profile_files) = build_profile_files(
89                    self.client_config_location.aws_config_file.as_ref(),
90                    self.client_config_location
91                        .aws_shared_credentials_file
92                        .as_ref(),
93                ) {
94                    builder = builder.profile_files(profile_files);
95                }
96
97                config_loader =
98                    config_loader.credentials_provider(builder.profile_name(profile_name).build());
99            }
100            crate::types::S3Credentials::FromEnvironment => {}
101            crate::types::S3Credentials::NoSignRequest => {
102                config_loader = config_loader.no_credentials();
103            }
104        }
105        config_loader
106    }
107
108    fn build_region_provider(&self) -> Box<dyn ProvideRegion> {
109        let mut builder = aws_config::profile::ProfileFileRegionProvider::builder();
110
111        if let crate::types::S3Credentials::Profile(profile_name) = &self.credential {
112            if let Some(profile_files) = build_profile_files(
113                self.client_config_location.aws_config_file.as_ref(),
114                self.client_config_location
115                    .aws_shared_credentials_file
116                    .as_ref(),
117            ) {
118                builder = builder.profile_files(profile_files);
119            }
120            builder = builder.profile_name(profile_name)
121        }
122
123        let provider_region = if matches!(
124            &self.credential,
125            crate::types::S3Credentials::FromEnvironment
126                | crate::types::S3Credentials::NoSignRequest,
127        ) {
128            RegionProviderChain::first_try(self.region.clone().map(Region::new))
129                .or_default_provider()
130        } else {
131            RegionProviderChain::first_try(self.region.clone().map(Region::new))
132                .or_else(builder.build())
133        };
134
135        Box::new(provider_region)
136    }
137
138    fn build_retry_config(&self) -> RetryConfig {
139        RetryConfig::standard()
140            .with_max_attempts(self.retry_config.aws_max_attempts)
141            .with_initial_backoff(std::time::Duration::from_millis(
142                self.retry_config.initial_backoff_milliseconds,
143            ))
144    }
145
146    fn build_timeout_config(&self) -> Option<TimeoutConfig> {
147        // TimeoutConfig is optional, but setting each timeout to None does not cause the SDK to use default timeouts.
148        let operation_timeout = self
149            .cli_timeout_config
150            .operation_timeout_milliseconds
151            .map(Duration::from_millis);
152        let operation_attempt_timeout = self
153            .cli_timeout_config
154            .operation_attempt_timeout_milliseconds
155            .map(Duration::from_millis);
156        let connect_timeout = self
157            .cli_timeout_config
158            .connect_timeout_milliseconds
159            .map(Duration::from_millis);
160        let read_timeout = self
161            .cli_timeout_config
162            .read_timeout_milliseconds
163            .map(Duration::from_millis);
164
165        if operation_timeout.is_none()
166            && operation_attempt_timeout.is_none()
167            && connect_timeout.is_none()
168            && read_timeout.is_none()
169        {
170            return None;
171        }
172
173        let mut builder = TimeoutConfig::builder();
174
175        builder = if let Some(operation_timeout) = operation_timeout {
176            builder.operation_timeout(operation_timeout)
177        } else {
178            builder
179        };
180
181        builder = if let Some(operation_attempt_timeout) = operation_attempt_timeout {
182            builder.operation_attempt_timeout(operation_attempt_timeout)
183        } else {
184            builder
185        };
186
187        builder = if let Some(connect_timeout) = connect_timeout {
188            builder.connect_timeout(connect_timeout)
189        } else {
190            builder
191        };
192
193        builder = if let Some(read_timeout) = read_timeout {
194            builder.read_timeout(read_timeout)
195        } else {
196            builder
197        };
198
199        Some(builder.build())
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::types::{AccessKeys, ClientConfigLocation};
207    use aws_smithy_types::checksum_config::RequestChecksumCalculation;
208    use std::sync::Arc;
209    use tokio::sync::Semaphore;
210    use tracing_subscriber::EnvFilter;
211
212    /// Scoped guard that sets an environment variable for the lifetime of the
213    /// guard and restores the previous value (or removes the variable) on drop.
214    ///
215    /// Rust edition 2024 requires `unsafe` for `std::env::set_var` /
216    /// `remove_var` because concurrent mutation of the process environment is
217    /// a data race. Tests that mutate the same env var must not run
218    /// concurrently.
219    struct EnvGuard {
220        key: &'static str,
221        previous: Option<String>,
222    }
223
224    impl EnvGuard {
225        fn set(key: &'static str, value: &str) -> Self {
226            let previous = std::env::var(key).ok();
227            // SAFETY: The process env is shared across threads; callers must
228            // ensure no other test mutates the same key concurrently.
229            unsafe { std::env::set_var(key, value) };
230            Self { key, previous }
231        }
232    }
233
234    impl Drop for EnvGuard {
235        fn drop(&mut self) {
236            // SAFETY: see `EnvGuard::set`.
237            match &self.previous {
238                Some(v) => unsafe { std::env::set_var(self.key, v) },
239                None => unsafe { std::env::remove_var(self.key) },
240            }
241        }
242    }
243
244    #[tokio::test]
245    async fn create_client_from_credentials() {
246        init_dummy_tracing_subscriber();
247
248        let client_config = ClientConfig {
249            client_config_location: ClientConfigLocation {
250                aws_config_file: None,
251                aws_shared_credentials_file: None,
252            },
253            credential: crate::types::S3Credentials::Credentials {
254                access_keys: AccessKeys {
255                    access_key: "my_access_key".to_string(),
256                    secret_access_key: "my_secret_access_key".to_string(),
257                    session_token: Some("my_session_token".to_string()),
258                },
259            },
260            region: Some("my-region".to_string()),
261            endpoint_url: Some("https://my.endpoint.local".to_string()),
262            force_path_style: false,
263            retry_config: crate::config::RetryConfig {
264                aws_max_attempts: 10,
265                initial_backoff_milliseconds: 100,
266            },
267            cli_timeout_config: crate::config::CLITimeoutConfig {
268                operation_timeout_milliseconds: None,
269                operation_attempt_timeout_milliseconds: None,
270                connect_timeout_milliseconds: None,
271                read_timeout_milliseconds: None,
272            },
273            disable_stalled_stream_protection: false,
274            request_checksum_calculation: RequestChecksumCalculation::WhenRequired,
275            parallel_upload_semaphore: Arc::new(Semaphore::new(1)),
276            accelerate: false,
277            request_payer: None,
278        };
279
280        let client = client_config.create_client().await;
281
282        let retry_config = client.config().retry_config().unwrap();
283        assert_eq!(retry_config.max_attempts(), 10);
284        assert_eq!(
285            retry_config.initial_backoff(),
286            std::time::Duration::from_millis(100)
287        );
288
289        let timeout_config = client.config().timeout_config().unwrap();
290        assert!(timeout_config.operation_timeout().is_none());
291        assert!(timeout_config.operation_attempt_timeout().is_none());
292        assert!(timeout_config.connect_timeout().is_some());
293        assert!(timeout_config.read_timeout().is_none());
294        assert!(timeout_config.has_timeouts());
295
296        // AWS SDK have default connect timeout
297        assert_eq!(
298            timeout_config.connect_timeout(),
299            Some(Duration::from_millis(3100))
300        );
301
302        assert_eq!(
303            client.config().region().unwrap().to_string(),
304            "my-region".to_string()
305        );
306    }
307
308    #[tokio::test]
309    async fn create_client_from_credentials_with_default_region() {
310        init_dummy_tracing_subscriber();
311
312        let client_config = ClientConfig {
313            client_config_location: ClientConfigLocation {
314                aws_config_file: None,
315                aws_shared_credentials_file: None,
316            },
317            credential: crate::types::S3Credentials::Credentials {
318                access_keys: AccessKeys {
319                    access_key: "my_access_key".to_string(),
320                    secret_access_key: "my_secret_access_key".to_string(),
321                    session_token: Some("my_session_token".to_string()),
322                },
323            },
324            region: None,
325            endpoint_url: Some("https://my.endpoint.local".to_string()),
326            force_path_style: false,
327            retry_config: crate::config::RetryConfig {
328                aws_max_attempts: 10,
329                initial_backoff_milliseconds: 100,
330            },
331            cli_timeout_config: crate::config::CLITimeoutConfig {
332                operation_timeout_milliseconds: Some(1000),
333                operation_attempt_timeout_milliseconds: Some(2000),
334                connect_timeout_milliseconds: Some(3000),
335                read_timeout_milliseconds: Some(4000),
336            },
337            disable_stalled_stream_protection: false,
338            request_checksum_calculation: RequestChecksumCalculation::WhenRequired,
339            parallel_upload_semaphore: Arc::new(Semaphore::new(1)),
340            accelerate: false,
341            request_payer: None,
342        };
343
344        let client = client_config.create_client().await;
345
346        let retry_config = client.config().retry_config().unwrap();
347        assert_eq!(retry_config.max_attempts(), 10);
348        assert_eq!(
349            retry_config.initial_backoff(),
350            std::time::Duration::from_millis(100)
351        );
352
353        let timeout_config = client.config().timeout_config().unwrap();
354        assert_eq!(
355            timeout_config.operation_timeout(),
356            Some(Duration::from_millis(1000))
357        );
358        assert_eq!(
359            timeout_config.operation_attempt_timeout(),
360            Some(Duration::from_millis(2000))
361        );
362        assert_eq!(
363            timeout_config.connect_timeout(),
364            Some(Duration::from_millis(3000))
365        );
366        assert_eq!(
367            timeout_config.read_timeout(),
368            Some(Duration::from_millis(4000))
369        );
370        assert!(timeout_config.has_timeouts());
371    }
372
373    #[tokio::test]
374    async fn create_client_from_custom_profile() {
375        init_dummy_tracing_subscriber();
376
377        let client_config = ClientConfig {
378            client_config_location: ClientConfigLocation {
379                aws_config_file: Some("./test_data/test_config/config".into()),
380                aws_shared_credentials_file: Some("./test_data/test_config/credentials".into()),
381            },
382            credential: crate::types::S3Credentials::Profile("aws".to_string()),
383            region: Some("my-region".to_string()),
384            endpoint_url: Some("https://my.endpoint.local".to_string()),
385            force_path_style: false,
386            retry_config: crate::config::RetryConfig {
387                aws_max_attempts: 10,
388                initial_backoff_milliseconds: 100,
389            },
390            cli_timeout_config: crate::config::CLITimeoutConfig {
391                operation_timeout_milliseconds: None,
392                operation_attempt_timeout_milliseconds: None,
393                connect_timeout_milliseconds: None,
394                read_timeout_milliseconds: None,
395            },
396            disable_stalled_stream_protection: false,
397            request_checksum_calculation: RequestChecksumCalculation::WhenRequired,
398            parallel_upload_semaphore: Arc::new(Semaphore::new(1)),
399            accelerate: false,
400            request_payer: None,
401        };
402
403        let client = client_config.create_client().await;
404
405        let retry_config = client.config().retry_config().unwrap();
406        assert_eq!(retry_config.max_attempts(), 10);
407        assert_eq!(
408            retry_config.initial_backoff(),
409            std::time::Duration::from_millis(100)
410        );
411
412        assert_eq!(
413            client.config().region().unwrap().to_string(),
414            "my-region".to_string()
415        );
416    }
417
418    #[tokio::test]
419    async fn create_client_from_custom_timeout() {
420        init_dummy_tracing_subscriber();
421
422        let client_config = ClientConfig {
423            client_config_location: ClientConfigLocation {
424                aws_config_file: Some("./test_data/test_config/config".into()),
425                aws_shared_credentials_file: Some("./test_data/test_config/credentials".into()),
426            },
427            credential: crate::types::S3Credentials::Profile("aws".to_string()),
428            region: Some("my-region".to_string()),
429            endpoint_url: Some("https://my.endpoint.local".to_string()),
430            force_path_style: false,
431            retry_config: crate::config::RetryConfig {
432                aws_max_attempts: 10,
433                initial_backoff_milliseconds: 100,
434            },
435            cli_timeout_config: crate::config::CLITimeoutConfig {
436                operation_timeout_milliseconds: Some(1000),
437                operation_attempt_timeout_milliseconds: None,
438                connect_timeout_milliseconds: None,
439                read_timeout_milliseconds: None,
440            },
441            disable_stalled_stream_protection: false,
442            request_checksum_calculation: RequestChecksumCalculation::WhenRequired,
443            parallel_upload_semaphore: Arc::new(Semaphore::new(1)),
444            accelerate: false,
445            request_payer: None,
446        };
447
448        let client = client_config.create_client().await;
449
450        let retry_config = client.config().retry_config().unwrap();
451        assert_eq!(retry_config.max_attempts(), 10);
452        assert_eq!(
453            retry_config.initial_backoff(),
454            std::time::Duration::from_millis(100)
455        );
456
457        let timeout_config = client.config().timeout_config().unwrap();
458        assert_eq!(
459            timeout_config.operation_timeout(),
460            Some(Duration::from_millis(1000))
461        );
462        assert!(timeout_config.operation_attempt_timeout().is_none());
463        assert!(timeout_config.connect_timeout().is_some());
464        assert!(timeout_config.read_timeout().is_none());
465        assert!(timeout_config.has_timeouts());
466
467        assert_eq!(
468            client.config().region().unwrap().to_string(),
469            "my-region".to_string()
470        );
471    }
472
473    #[tokio::test]
474    async fn create_client_from_custom_timeout_case2() {
475        init_dummy_tracing_subscriber();
476
477        let client_config = ClientConfig {
478            client_config_location: ClientConfigLocation {
479                aws_config_file: Some("./test_data/test_config/config".into()),
480                aws_shared_credentials_file: Some("./test_data/test_config/credentials".into()),
481            },
482            credential: crate::types::S3Credentials::Profile("aws".to_string()),
483            region: Some("my-region".to_string()),
484            endpoint_url: Some("https://my.endpoint.local".to_string()),
485            force_path_style: false,
486            retry_config: crate::config::RetryConfig {
487                aws_max_attempts: 10,
488                initial_backoff_milliseconds: 100,
489            },
490            cli_timeout_config: crate::config::CLITimeoutConfig {
491                operation_timeout_milliseconds: None,
492                operation_attempt_timeout_milliseconds: None,
493                connect_timeout_milliseconds: Some(1000),
494                read_timeout_milliseconds: None,
495            },
496            disable_stalled_stream_protection: false,
497            request_checksum_calculation: RequestChecksumCalculation::WhenRequired,
498            parallel_upload_semaphore: Arc::new(Semaphore::new(1)),
499            accelerate: false,
500            request_payer: None,
501        };
502
503        let client = client_config.create_client().await;
504
505        let retry_config = client.config().retry_config().unwrap();
506        assert_eq!(retry_config.max_attempts(), 10);
507        assert_eq!(
508            retry_config.initial_backoff(),
509            std::time::Duration::from_millis(100)
510        );
511
512        let timeout_config = client.config().timeout_config().unwrap();
513        assert!(timeout_config.connect_timeout().is_some());
514        assert!(timeout_config.operation_attempt_timeout().is_none());
515        assert!(timeout_config.connect_timeout().is_some());
516        assert!(timeout_config.read_timeout().is_none());
517        assert!(timeout_config.has_timeouts());
518
519        assert_eq!(
520            client.config().region().unwrap().to_string(),
521            "my-region".to_string()
522        );
523    }
524
525    #[tokio::test]
526    async fn create_client_from_default_profile() {
527        init_dummy_tracing_subscriber();
528
529        let client_config = ClientConfig {
530            client_config_location: ClientConfigLocation {
531                aws_config_file: Some("./test_data/test_config/config".into()),
532                aws_shared_credentials_file: Some("./test_data/test_config/credentials".into()),
533            },
534            credential: crate::types::S3Credentials::Profile("default".to_string()),
535            region: None,
536            endpoint_url: Some("https://my.endpoint.local".to_string()),
537            force_path_style: false,
538            retry_config: crate::config::RetryConfig {
539                aws_max_attempts: 10,
540                initial_backoff_milliseconds: 100,
541            },
542            cli_timeout_config: crate::config::CLITimeoutConfig {
543                operation_timeout_milliseconds: None,
544                operation_attempt_timeout_milliseconds: None,
545                connect_timeout_milliseconds: None,
546                read_timeout_milliseconds: None,
547            },
548            disable_stalled_stream_protection: false,
549            request_checksum_calculation: RequestChecksumCalculation::WhenRequired,
550            parallel_upload_semaphore: Arc::new(Semaphore::new(1)),
551            accelerate: false,
552            request_payer: None,
553        };
554
555        let client = client_config.create_client().await;
556
557        let retry_config = client.config().retry_config().unwrap();
558        assert_eq!(retry_config.max_attempts(), 10);
559        assert_eq!(
560            retry_config.initial_backoff(),
561            std::time::Duration::from_millis(100)
562        );
563
564        assert_eq!(
565            client.config().region().unwrap().to_string(),
566            "us-west-1".to_string()
567        );
568    }
569
570    // In cloud environment, this test may fail because of the lack of credentials.
571    #[cfg(e2e_test)]
572    #[tokio::test]
573    async fn create_client_from_env() {
574        init_dummy_tracing_subscriber();
575
576        let client_config = ClientConfig {
577            client_config_location: ClientConfigLocation {
578                aws_config_file: Some("./test_data/test_config/config".into()),
579                aws_shared_credentials_file: Some("./test_data/test_config/credentials".into()),
580            },
581            credential: crate::types::S3Credentials::FromEnvironment,
582            region: None,
583            endpoint_url: Some("https://my.endpoint.local".to_string()),
584            force_path_style: false,
585            retry_config: crate::config::RetryConfig {
586                aws_max_attempts: 10,
587                initial_backoff_milliseconds: 100,
588            },
589            cli_timeout_config: crate::config::CLITimeoutConfig {
590                operation_timeout_milliseconds: None,
591                operation_attempt_timeout_milliseconds: None,
592                connect_timeout_milliseconds: None,
593                read_timeout_milliseconds: None,
594            },
595            disable_stalled_stream_protection: false,
596            request_checksum_calculation: RequestChecksumCalculation::WhenRequired,
597            parallel_upload_semaphore: Arc::new(Semaphore::new(1)),
598            accelerate: false,
599            request_payer: None,
600        };
601
602        let _ = client_config.create_client().await;
603    }
604
605    #[tokio::test]
606    async fn create_client_from_custom_profile_overriding_region() {
607        init_dummy_tracing_subscriber();
608
609        let client_config = ClientConfig {
610            client_config_location: ClientConfigLocation {
611                aws_config_file: Some("./test_data/test_config/config".into()),
612                aws_shared_credentials_file: Some("./test_data/test_config/credentials".into()),
613            },
614            credential: crate::types::S3Credentials::Profile("aws".to_string()),
615            region: Some("my-region2".to_string()),
616            endpoint_url: Some("https://my.endpoint.local".to_string()),
617            force_path_style: false,
618            retry_config: crate::config::RetryConfig {
619                aws_max_attempts: 10,
620                initial_backoff_milliseconds: 100,
621            },
622            cli_timeout_config: crate::config::CLITimeoutConfig {
623                operation_timeout_milliseconds: None,
624                operation_attempt_timeout_milliseconds: None,
625                connect_timeout_milliseconds: None,
626                read_timeout_milliseconds: None,
627            },
628            disable_stalled_stream_protection: false,
629            request_checksum_calculation: RequestChecksumCalculation::WhenRequired,
630            parallel_upload_semaphore: Arc::new(Semaphore::new(1)),
631            accelerate: false,
632            request_payer: None,
633        };
634
635        let client = client_config.create_client().await;
636
637        let retry_config = client.config().retry_config().unwrap();
638        assert_eq!(retry_config.max_attempts(), 10);
639        assert_eq!(
640            retry_config.initial_backoff(),
641            std::time::Duration::from_millis(100)
642        );
643
644        assert_eq!(
645            client.config().region().unwrap().to_string(),
646            "my-region2".to_string()
647        );
648    }
649
650    #[tokio::test]
651    async fn create_client_with_no_sign_request_credential() {
652        init_dummy_tracing_subscriber();
653
654        let client_config = ClientConfig {
655            client_config_location: ClientConfigLocation {
656                aws_config_file: None,
657                aws_shared_credentials_file: None,
658            },
659            credential: crate::types::S3Credentials::NoSignRequest,
660            region: Some("my-region".to_string()),
661            endpoint_url: Some("https://my.endpoint.local".to_string()),
662            force_path_style: false,
663            retry_config: crate::config::RetryConfig {
664                aws_max_attempts: 10,
665                initial_backoff_milliseconds: 100,
666            },
667            cli_timeout_config: crate::config::CLITimeoutConfig {
668                operation_timeout_milliseconds: None,
669                operation_attempt_timeout_milliseconds: None,
670                connect_timeout_milliseconds: None,
671                read_timeout_milliseconds: None,
672            },
673            disable_stalled_stream_protection: false,
674            request_checksum_calculation: RequestChecksumCalculation::WhenRequired,
675            parallel_upload_semaphore: Arc::new(Semaphore::new(1)),
676            accelerate: false,
677            request_payer: None,
678        };
679
680        let client = client_config.create_client().await;
681        assert_eq!(
682            client.config().region().unwrap().to_string(),
683            "my-region".to_string()
684        );
685        let config_debug = format!("{:?}", client.config());
686        assert!(
687            config_debug.contains("identity_resolvers: None"),
688            "NoSignRequest must not install a sigv4 credentials identity resolver, got: {config_debug}"
689        );
690    }
691
692    #[tokio::test]
693    async fn no_sign_request_uses_default_region_chain_not_profile_files() {
694        // NoSignRequest must not consult profile files for region resolution.
695        // Point at a nonexistent config file; if the code consulted it, client
696        // construction would fail. With NoSignRequest it should fall through
697        // to the default region chain.
698        init_dummy_tracing_subscriber();
699
700        let client_config = ClientConfig {
701            client_config_location: ClientConfigLocation {
702                aws_config_file: Some("/definitely/does/not/exist/config".into()),
703                aws_shared_credentials_file: Some("/definitely/does/not/exist/creds".into()),
704            },
705            credential: crate::types::S3Credentials::NoSignRequest,
706            region: Some("us-east-1".to_string()),
707            endpoint_url: Some("https://my.endpoint.local".to_string()),
708            force_path_style: false,
709            retry_config: crate::config::RetryConfig {
710                aws_max_attempts: 10,
711                initial_backoff_milliseconds: 100,
712            },
713            cli_timeout_config: crate::config::CLITimeoutConfig {
714                operation_timeout_milliseconds: None,
715                operation_attempt_timeout_milliseconds: None,
716                connect_timeout_milliseconds: None,
717                read_timeout_milliseconds: None,
718            },
719            disable_stalled_stream_protection: false,
720            request_checksum_calculation: RequestChecksumCalculation::WhenRequired,
721            parallel_upload_semaphore: Arc::new(Semaphore::new(1)),
722            accelerate: false,
723            request_payer: None,
724        };
725
726        let client = client_config.create_client().await;
727        assert_eq!(
728            client.config().region().unwrap().to_string(),
729            "us-east-1".to_string(),
730        );
731    }
732
733    #[tokio::test]
734    async fn no_sign_request_no_region_falls_through_to_env_default_chain() {
735        // With `region: None`, `RegionProviderChain::first_try(None)` yields no
736        // region, so resolution falls through to whatever provider chain the
737        // `matches!` arm selects. If NoSignRequest is included in the
738        // `matches!` pattern, `or_default_provider()` reads AWS_REGION from
739        // the environment. If NoSignRequest is removed from the `matches!`
740        // pattern, we fall into the `else` branch which builds a
741        // `ProfileFileRegionProvider` pointed at nonexistent files, yielding
742        // `None` and panicking on `unwrap()` below.
743        //
744        // NOTE: Rust tests run in parallel by default, so concurrent mutation
745        // of `AWS_REGION` is a data race. This is tolerated here because no
746        // other test in this file reads `AWS_REGION`. If additional
747        // env-dependent tests are added, introduce a `Mutex` to serialize
748        // them.
749        init_dummy_tracing_subscriber();
750
751        let _guard = EnvGuard::set("AWS_REGION", "eu-west-3");
752
753        let client_config = ClientConfig {
754            client_config_location: ClientConfigLocation {
755                aws_config_file: Some("/definitely/does/not/exist/config".into()),
756                aws_shared_credentials_file: Some("/definitely/does/not/exist/creds".into()),
757            },
758            credential: crate::types::S3Credentials::NoSignRequest,
759            region: None,
760            endpoint_url: Some("https://my.endpoint.local".to_string()),
761            force_path_style: false,
762            retry_config: crate::config::RetryConfig {
763                aws_max_attempts: 10,
764                initial_backoff_milliseconds: 100,
765            },
766            cli_timeout_config: crate::config::CLITimeoutConfig {
767                operation_timeout_milliseconds: None,
768                operation_attempt_timeout_milliseconds: None,
769                connect_timeout_milliseconds: None,
770                read_timeout_milliseconds: None,
771            },
772            disable_stalled_stream_protection: false,
773            request_checksum_calculation: RequestChecksumCalculation::WhenRequired,
774            parallel_upload_semaphore: Arc::new(Semaphore::new(1)),
775            accelerate: false,
776            request_payer: None,
777        };
778
779        let client = client_config.create_client().await;
780        assert_eq!(
781            client.config().region().unwrap().to_string(),
782            "eu-west-3".to_string(),
783        );
784    }
785
786    #[test]
787    fn build_profile_files_returns_none_when_no_overrides() {
788        // With neither file overridden, the SDK defaults are left untouched.
789        assert!(build_profile_files(None, None).is_none());
790    }
791
792    #[test]
793    fn build_profile_files_with_only_config_file_falls_back_to_default_credentials() {
794        // Exercises the None arm of the credentials match — the helper must
795        // still include the system default credentials file.
796        let config_path = PathBuf::from("/tmp/fake-aws-config");
797        assert!(build_profile_files(Some(&config_path), None).is_some());
798    }
799
800    #[test]
801    fn build_profile_files_with_only_credentials_file_falls_back_to_default_config() {
802        // Exercises the None arm of the config match — the helper must still
803        // include the system default config file.
804        let creds_path = PathBuf::from("/tmp/fake-aws-creds");
805        assert!(build_profile_files(None, Some(&creds_path)).is_some());
806    }
807
808    #[test]
809    fn build_profile_files_with_both_overrides() {
810        let config_path = PathBuf::from("/tmp/fake-aws-config");
811        let creds_path = PathBuf::from("/tmp/fake-aws-creds");
812        assert!(build_profile_files(Some(&config_path), Some(&creds_path)).is_some());
813    }
814
815    #[tokio::test]
816    async fn disable_stalled_stream_protection_branch_builds_client() {
817        // Exercises the `disable_stalled_stream_protection: true` arm in
818        // load_sdk_config, which uses StalledStreamProtectionConfig::disabled()
819        // instead of the enabled default. We don't need to make a real S3
820        // call — successful client construction proves the branch ran.
821        init_dummy_tracing_subscriber();
822
823        let client_config = ClientConfig {
824            client_config_location: ClientConfigLocation {
825                aws_config_file: None,
826                aws_shared_credentials_file: None,
827            },
828            credential: crate::types::S3Credentials::NoSignRequest,
829            region: Some("us-east-1".to_string()),
830            endpoint_url: None,
831            force_path_style: false,
832            retry_config: crate::config::RetryConfig {
833                aws_max_attempts: 1,
834                initial_backoff_milliseconds: 0,
835            },
836            cli_timeout_config: crate::config::CLITimeoutConfig {
837                operation_timeout_milliseconds: None,
838                operation_attempt_timeout_milliseconds: None,
839                connect_timeout_milliseconds: None,
840                read_timeout_milliseconds: None,
841            },
842            disable_stalled_stream_protection: true,
843            request_checksum_calculation: RequestChecksumCalculation::WhenRequired,
844            parallel_upload_semaphore: Arc::new(Semaphore::new(1)),
845            accelerate: false,
846            request_payer: None,
847        };
848
849        let client = client_config.create_client().await;
850        assert_eq!(
851            client.config().region().unwrap().to_string(),
852            "us-east-1".to_string()
853        );
854    }
855
856    fn init_dummy_tracing_subscriber() {
857        let _ = tracing_subscriber::fmt()
858            .with_env_filter(
859                EnvFilter::try_from_default_env()
860                    .or_else(|_| EnvFilter::try_new("dummy=trace"))
861                    .unwrap(),
862            )
863            .try_init();
864    }
865}