secrets_provider_tests/
env.rs1use crate::retry::parse_bool_env;
2use serde_json::json;
3use std::env;
4use std::fmt::Write as _;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7#[derive(Debug, Clone)]
9pub struct TestEnv {
10 pub prefix: TestPrefix,
11 pub cleanup: bool,
12}
13
14impl TestEnv {
15 pub fn from_env(provider: &str) -> Self {
16 let prefix = TestPrefix::from_env(provider);
17
18 let keep = parse_bool_env("GREENTIC_TEST_KEEP");
19 let cleanup = if keep {
20 false
21 } else if let Ok(value) = env::var("GREENTIC_TEST_CLEANUP") {
22 parse_bool_env_value(&value, true)
23 } else {
24 true
25 };
26
27 Self { prefix, cleanup }
28 }
29}
30
31#[derive(Debug, Clone)]
32pub struct TestPrefix {
33 provider: String,
34 base: String,
35 counter: std::sync::Arc<std::sync::atomic::AtomicU64>,
36}
37
38impl TestPrefix {
39 pub fn from_env(provider: &str) -> Self {
40 if let Ok(explicit) = env::var("GREENTIC_TEST_PREFIX") {
41 return Self::new(provider, explicit);
42 }
43
44 let run_id = env::var("GITHUB_RUN_ID").ok();
45 let run_attempt = env::var("GITHUB_RUN_ATTEMPT").ok();
46 let repo = env::var("GITHUB_REPOSITORY").unwrap_or_else(|_| "local".to_string());
47
48 if let (Some(id), Some(attempt)) = (run_id, run_attempt) {
49 let base = format!("ci/{provider}/{repo}/{id}/{attempt}");
50 return Self::new(provider, base);
51 }
52
53 let now = SystemTime::now()
54 .duration_since(UNIX_EPOCH)
55 .unwrap_or_default()
56 .as_secs();
57 let pid = std::process::id();
58 let mut base = String::from("local/");
59 let _ = write!(&mut base, "{provider}/{now}/{pid}");
60 Self::new(provider, base)
61 }
62
63 fn new(provider: &str, base: String) -> Self {
64 let base = sanitize_segment(&base);
68 Self {
69 provider: provider.to_string(),
70 base,
71 counter: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
72 }
73 }
74
75 pub fn base(&self) -> String {
77 self.base.clone()
78 }
79
80 pub fn key(&self, suffix: &str) -> String {
85 let next = self
86 .counter
87 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
88 let suffix = sanitize_segment(suffix);
89 format!("{}-{suffix}-{next}", self.base)
90 }
91
92 pub fn to_metadata(&self) -> serde_json::Value {
94 json!({
95 "provider": self.provider,
96 "prefix": self.base,
97 })
98 }
99}
100
101fn sanitize_segment(value: &str) -> String {
104 let mut out = String::with_capacity(value.len());
105 for ch in value.chars() {
106 match ch {
107 'a'..='z' | '0'..='9' | '-' | '_' | '.' => out.push(ch),
108 'A'..='Z' => out.push(ch.to_ascii_lowercase()),
109 _ => out.push('-'),
110 }
111 }
112 out
113}
114
115fn parse_bool_env_value(value: &str, default_true: bool) -> bool {
116 match value {
117 "" => default_true,
118 v if v.eq_ignore_ascii_case("1") || v.eq_ignore_ascii_case("true") => true,
119 v if v.eq_ignore_ascii_case("0") || v.eq_ignore_ascii_case("false") => false,
120 _ => default_true,
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn builds_local_prefix_when_no_ci_env() {
130 let prefix = TestPrefix::new("dev", "local/test/123".to_string());
131 assert_eq!(prefix.base(), "local-test-123");
133 let k1 = prefix.key("a");
134 let k2 = prefix.key("a");
135 assert_ne!(k1, k2);
136 assert!(
138 k1.chars()
139 .all(|c| matches!(c, 'a'..='z' | '0'..='9' | '-' | '_' | '.')),
140 "key contains invalid chars: {k1}"
141 );
142 }
143}