1use crate::WalletError;
15
16#[derive(Debug, Clone)]
21pub struct WalletConfig {
22 pub app_name: String,
24 pub app_url: String,
26 pub apple: Option<AppleConfig>,
28 pub google: Option<GoogleConfig>,
30}
31
32#[derive(Debug, Clone)]
37pub struct AppleConfig {
38 pub pass_type_id: String,
40 pub team_id: String,
42 pub cert_pem: String,
44 pub key_pem: String,
46 pub key_password: Option<String>,
48 pub wwdr_pem: String,
50}
51
52#[derive(Debug, Clone)]
57pub struct GoogleConfig {
58 pub issuer_id: String,
60 pub service_account_email: String,
62 pub service_account_private_key_pem: String,
64}
65
66impl WalletConfig {
67 pub fn from_env() -> Result<Self, WalletError> {
83 let app_name =
85 std::env::var("APP_NAME").unwrap_or_else(|_| "Ferro Application".to_string());
86 let app_url =
87 std::env::var("APP_URL").unwrap_or_else(|_| "http://localhost:8080".to_string());
88
89 let apple = AppleConfig::from_env_optional()?;
90 let google = GoogleConfig::from_env_optional()?;
91
92 Ok(Self {
93 app_name,
94 app_url,
95 apple,
96 google,
97 })
98 }
99}
100
101impl AppleConfig {
102 pub fn from_env_optional() -> Result<Option<Self>, WalletError> {
109 let pass_type_id = match non_empty_env("APPLE_WALLET_PASS_TYPE_ID") {
110 Some(v) => v,
111 None => return Ok(None),
112 };
113 let team_id = match non_empty_env("APPLE_WALLET_TEAM_ID") {
114 Some(v) => v,
115 None => return Ok(None),
116 };
117 let cert_pem = match non_empty_env("APPLE_WALLET_CERT_PEM") {
118 Some(v) => v,
119 None => return Ok(None),
120 };
121 let key_pem = match non_empty_env("APPLE_WALLET_KEY_PEM") {
122 Some(v) => v,
123 None => return Ok(None),
124 };
125 let wwdr_pem = match non_empty_env("APPLE_WALLET_WWDR_PEM") {
126 Some(v) => v,
127 None => return Ok(None),
128 };
129 let key_password = non_empty_env("APPLE_WALLET_KEY_PASSWORD");
130
131 Ok(Some(Self {
132 pass_type_id,
133 team_id,
134 cert_pem,
135 key_pem,
136 key_password,
137 wwdr_pem,
138 }))
139 }
140}
141
142impl GoogleConfig {
143 pub fn from_env_optional() -> Result<Option<Self>, WalletError> {
149 let issuer_id = match non_empty_env("GOOGLE_WALLET_ISSUER_ID") {
150 Some(v) => v,
151 None => return Ok(None),
152 };
153 let service_account_email = match non_empty_env("GOOGLE_WALLET_SERVICE_ACCOUNT_EMAIL") {
154 Some(v) => v,
155 None => return Ok(None),
156 };
157 let service_account_private_key_pem =
158 match non_empty_env("GOOGLE_WALLET_SERVICE_ACCOUNT_KEY_PEM") {
159 Some(v) => v,
160 None => return Ok(None),
161 };
162
163 Ok(Some(Self {
164 issuer_id,
165 service_account_email,
166 service_account_private_key_pem,
167 }))
168 }
169}
170
171fn non_empty_env(name: &str) -> Option<String> {
176 match std::env::var(name) {
177 Ok(v) if !v.is_empty() => Some(v),
178 _ => None,
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use std::sync::Mutex;
186
187 static ENV_LOCK: Mutex<()> = Mutex::new(());
195
196 const APP_VARS: &[&str] = &["APP_NAME", "APP_URL"];
198 const APPLE_VARS: &[&str] = &[
199 "APPLE_WALLET_PASS_TYPE_ID",
200 "APPLE_WALLET_TEAM_ID",
201 "APPLE_WALLET_CERT_PEM",
202 "APPLE_WALLET_KEY_PEM",
203 "APPLE_WALLET_WWDR_PEM",
204 "APPLE_WALLET_KEY_PASSWORD",
205 ];
206 const GOOGLE_VARS: &[&str] = &[
207 "GOOGLE_WALLET_ISSUER_ID",
208 "GOOGLE_WALLET_SERVICE_ACCOUNT_EMAIL",
209 "GOOGLE_WALLET_SERVICE_ACCOUNT_KEY_PEM",
210 ];
211
212 struct EnvGuard {
217 saved: Vec<(&'static str, Option<String>)>,
218 }
219
220 impl EnvGuard {
221 fn capture(vars: &[&'static str]) -> Self {
222 let saved = vars
223 .iter()
224 .map(|v| (*v, std::env::var(v).ok()))
225 .collect::<Vec<_>>();
226 for v in vars {
227 std::env::remove_var(v);
228 }
229 Self { saved }
230 }
231
232 fn capture_all() -> Self {
235 let mut all = Vec::with_capacity(APP_VARS.len() + APPLE_VARS.len() + GOOGLE_VARS.len());
236 all.extend(APP_VARS);
237 all.extend(APPLE_VARS);
238 all.extend(GOOGLE_VARS);
239 Self::capture(&all)
240 }
241 }
242
243 impl Drop for EnvGuard {
244 fn drop(&mut self) {
245 for (name, value) in &self.saved {
246 match value {
247 Some(v) => std::env::set_var(name, v),
248 None => std::env::remove_var(name),
249 }
250 }
251 }
252 }
253
254 fn lock_env() -> std::sync::MutexGuard<'static, ()> {
257 ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
258 }
259
260 #[test]
262 fn from_env_apple_missing_is_none() {
263 let _lock = lock_env();
264 let _env = EnvGuard::capture_all();
265
266 let cfg =
267 WalletConfig::from_env().expect("from_env must not error when wallet vars are absent");
268
269 assert!(
270 cfg.apple.is_none(),
271 "expected apple cluster to be None when APPLE_WALLET_* vars are absent"
272 );
273 }
274
275 #[test]
277 fn from_env_google_missing_is_none() {
278 let _lock = lock_env();
279 let _env = EnvGuard::capture_all();
280
281 let cfg =
282 WalletConfig::from_env().expect("from_env must not error when wallet vars are absent");
283
284 assert!(
285 cfg.google.is_none(),
286 "expected google cluster to be None when GOOGLE_WALLET_* vars are absent"
287 );
288 }
289
290 #[test]
293 fn from_env_defaults_match_appconfig() {
294 let _lock = lock_env();
295 let _env = EnvGuard::capture_all();
296
297 let cfg =
298 WalletConfig::from_env().expect("from_env must not error when wallet vars are absent");
299
300 assert_eq!(
301 cfg.app_name, "Ferro Application",
302 "APP_NAME default must match framework::config::AppConfig::from_env"
303 );
304 assert_eq!(
305 cfg.app_url, "http://localhost:8080",
306 "APP_URL default must match framework::config::AppConfig::from_env"
307 );
308 }
309
310 #[test]
313 fn from_env_apple_partial_returns_none() {
314 let _lock = lock_env();
315 let _env = EnvGuard::capture_all();
316
317 std::env::set_var("APPLE_WALLET_PASS_TYPE_ID", "pass.com.example.test");
318 std::env::set_var("APPLE_WALLET_TEAM_ID", "TEAMID1234");
319 std::env::set_var(
320 "APPLE_WALLET_CERT_PEM",
321 "-----BEGIN CERTIFICATE-----\nx\n-----END CERTIFICATE-----\n",
322 );
323 std::env::set_var(
324 "APPLE_WALLET_KEY_PEM",
325 "-----BEGIN PRIVATE KEY-----\nx\n-----END PRIVATE KEY-----\n",
326 );
327 let cfg =
330 WalletConfig::from_env().expect("from_env must not error on partial Apple cluster");
331
332 assert!(
333 cfg.apple.is_none(),
334 "expected apple cluster to be None when WWDR_PEM is unset"
335 );
336 }
337
338 #[test]
341 fn from_env_apple_all_set_returns_some() {
342 let _lock = lock_env();
343 let _env = EnvGuard::capture_all();
344
345 std::env::set_var("APPLE_WALLET_PASS_TYPE_ID", "pass.com.example.test");
346 std::env::set_var("APPLE_WALLET_TEAM_ID", "TEAMID1234");
347 std::env::set_var("APPLE_WALLET_CERT_PEM", "cert-pem-bytes");
348 std::env::set_var("APPLE_WALLET_KEY_PEM", "key-pem-bytes");
349 std::env::set_var("APPLE_WALLET_WWDR_PEM", "wwdr-pem-bytes");
350 std::env::set_var("APPLE_WALLET_KEY_PASSWORD", "secret");
351
352 let cfg =
353 WalletConfig::from_env().expect("from_env must not error when wallet vars are present");
354 let apple = cfg
355 .apple
356 .expect("apple cluster must populate when all required vars set");
357
358 assert_eq!(apple.pass_type_id, "pass.com.example.test");
359 assert_eq!(apple.team_id, "TEAMID1234");
360 assert_eq!(apple.cert_pem, "cert-pem-bytes");
361 assert_eq!(apple.key_pem, "key-pem-bytes");
362 assert_eq!(apple.wwdr_pem, "wwdr-pem-bytes");
363 assert_eq!(apple.key_password.as_deref(), Some("secret"));
364 }
365
366 #[test]
368 fn from_env_google_all_set_returns_some() {
369 let _lock = lock_env();
370 let _env = EnvGuard::capture_all();
371
372 std::env::set_var("GOOGLE_WALLET_ISSUER_ID", "3388000000000000000");
373 std::env::set_var(
374 "GOOGLE_WALLET_SERVICE_ACCOUNT_EMAIL",
375 "sa@example.iam.gserviceaccount.com",
376 );
377 std::env::set_var(
378 "GOOGLE_WALLET_SERVICE_ACCOUNT_KEY_PEM",
379 "private-key-pem-bytes",
380 );
381
382 let cfg =
383 WalletConfig::from_env().expect("from_env must not error when wallet vars are present");
384 let google = cfg
385 .google
386 .expect("google cluster must populate when all required vars set");
387
388 assert_eq!(google.issuer_id, "3388000000000000000");
389 assert_eq!(
390 google.service_account_email,
391 "sa@example.iam.gserviceaccount.com"
392 );
393 assert_eq!(
394 google.service_account_private_key_pem,
395 "private-key-pem-bytes"
396 );
397 }
398
399 #[test]
402 fn from_env_never_errors_on_missing_wallet_vars() {
403 let _lock = lock_env();
404 let _env = EnvGuard::capture_all();
405
406 assert!(
407 WalletConfig::from_env().is_ok(),
408 "from_env must never error when all wallet env vars are absent"
409 );
410 }
411}