Skip to main content

gonzalo_soak/
target.rs

1//! The S3 target for the soak, resolved from the environment.
2//!
3//! Mirrors the existing `gonzalo-store-s3/tests/integration.rs` convention: the
4//! backend is provisioned *externally* (see `scripts/rustfs-up.sh`) and its
5//! coordinates arrive via env vars. When they are unset the soak **skips**
6//! (returns `None`) rather than fails — so `cargo test --workspace` on a machine
7//! without an S3 backend / docker stays green.
8
9/// S3 backend coordinates for spawning S3-backed `gonzalod` replicas.
10#[derive(Debug, Clone)]
11pub struct S3Target {
12    pub endpoint: String,
13    pub bucket: String,
14    pub access_key: String,
15    pub secret_key: String,
16    pub region: Option<String>,
17}
18
19impl S3Target {
20    /// Resolve from an env accessor. Returns `None` (→ skip) unless the endpoint,
21    /// bucket, and AWS credentials are all present. Region is optional.
22    pub fn from_env(get: impl Fn(&str) -> Option<String>) -> Option<Self> {
23        Some(Self {
24            endpoint: non_empty(get("GONZALO_S3_TEST_ENDPOINT"))?,
25            bucket: non_empty(get("GONZALO_S3_TEST_BUCKET"))?,
26            access_key: non_empty(get("AWS_ACCESS_KEY_ID"))?,
27            secret_key: non_empty(get("AWS_SECRET_ACCESS_KEY"))?,
28            region: non_empty(get("GONZALO_S3_TEST_REGION")),
29        })
30    }
31
32    /// Resolve from the process environment, or `None` to skip.
33    pub fn from_process_env() -> Option<Self> {
34        Self::from_env(|k| std::env::var(k).ok())
35    }
36}
37
38fn non_empty(v: Option<String>) -> Option<String> {
39    v.filter(|s| !s.trim().is_empty())
40}
41
42#[cfg(test)]
43mod tests {
44    use super::*;
45    use std::collections::HashMap;
46
47    fn env(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> {
48        let map: HashMap<String, String> = pairs
49            .iter()
50            .map(|(k, v)| (k.to_string(), v.to_string()))
51            .collect();
52        move |k: &str| map.get(k).cloned()
53    }
54
55    #[test]
56    fn resolves_when_all_required_present() {
57        let t = S3Target::from_env(env(&[
58            ("GONZALO_S3_TEST_ENDPOINT", "http://127.0.0.1:3900"),
59            ("GONZALO_S3_TEST_BUCKET", "soak"),
60            ("AWS_ACCESS_KEY_ID", "AK"),
61            ("AWS_SECRET_ACCESS_KEY", "SK"),
62        ]))
63        .expect("all required present");
64        assert_eq!(t.bucket, "soak");
65        assert_eq!(t.region, None);
66    }
67
68    #[test]
69    fn skips_when_endpoint_missing() {
70        assert!(
71            S3Target::from_env(env(&[
72                ("GONZALO_S3_TEST_BUCKET", "soak"),
73                ("AWS_ACCESS_KEY_ID", "AK"),
74                ("AWS_SECRET_ACCESS_KEY", "SK"),
75            ]))
76            .is_none()
77        );
78    }
79
80    #[test]
81    fn skips_when_credentials_missing() {
82        assert!(
83            S3Target::from_env(env(&[
84                ("GONZALO_S3_TEST_ENDPOINT", "http://127.0.0.1:3900"),
85                ("GONZALO_S3_TEST_BUCKET", "soak"),
86            ]))
87            .is_none()
88        );
89    }
90
91    #[test]
92    fn treats_blank_as_unset() {
93        assert!(
94            S3Target::from_env(env(&[
95                ("GONZALO_S3_TEST_ENDPOINT", "  "),
96                ("GONZALO_S3_TEST_BUCKET", "soak"),
97                ("AWS_ACCESS_KEY_ID", "AK"),
98                ("AWS_SECRET_ACCESS_KEY", "SK"),
99            ]))
100            .is_none()
101        );
102    }
103
104    #[test]
105    fn region_is_optional_but_carried() {
106        let t = S3Target::from_env(env(&[
107            ("GONZALO_S3_TEST_ENDPOINT", "http://127.0.0.1:3900"),
108            ("GONZALO_S3_TEST_BUCKET", "soak"),
109            ("AWS_ACCESS_KEY_ID", "AK"),
110            ("AWS_SECRET_ACCESS_KEY", "SK"),
111            ("GONZALO_S3_TEST_REGION", "garage"),
112        ]))
113        .unwrap();
114        assert_eq!(t.region.as_deref(), Some("garage"));
115    }
116}