use crate::WalletError;
#[derive(Debug, Clone)]
pub struct WalletConfig {
pub app_name: String,
pub app_url: String,
pub apple: Option<AppleConfig>,
pub google: Option<GoogleConfig>,
}
#[derive(Debug, Clone)]
pub struct AppleConfig {
pub pass_type_id: String,
pub team_id: String,
pub cert_pem: String,
pub key_pem: String,
pub key_password: Option<String>,
pub wwdr_pem: String,
}
#[derive(Debug, Clone)]
pub struct GoogleConfig {
pub issuer_id: String,
pub service_account_email: String,
pub service_account_private_key_pem: String,
}
impl WalletConfig {
pub fn from_env() -> Result<Self, WalletError> {
let app_name =
std::env::var("APP_NAME").unwrap_or_else(|_| "Ferro Application".to_string());
let app_url =
std::env::var("APP_URL").unwrap_or_else(|_| "http://localhost:8080".to_string());
let apple = AppleConfig::from_env_optional()?;
let google = GoogleConfig::from_env_optional()?;
Ok(Self {
app_name,
app_url,
apple,
google,
})
}
}
impl AppleConfig {
pub fn from_env_optional() -> Result<Option<Self>, WalletError> {
let pass_type_id = match non_empty_env("APPLE_WALLET_PASS_TYPE_ID") {
Some(v) => v,
None => return Ok(None),
};
let team_id = match non_empty_env("APPLE_WALLET_TEAM_ID") {
Some(v) => v,
None => return Ok(None),
};
let cert_pem = match non_empty_env("APPLE_WALLET_CERT_PEM") {
Some(v) => v,
None => return Ok(None),
};
let key_pem = match non_empty_env("APPLE_WALLET_KEY_PEM") {
Some(v) => v,
None => return Ok(None),
};
let wwdr_pem = match non_empty_env("APPLE_WALLET_WWDR_PEM") {
Some(v) => v,
None => return Ok(None),
};
let key_password = non_empty_env("APPLE_WALLET_KEY_PASSWORD");
Ok(Some(Self {
pass_type_id,
team_id,
cert_pem,
key_pem,
key_password,
wwdr_pem,
}))
}
}
impl GoogleConfig {
pub fn from_env_optional() -> Result<Option<Self>, WalletError> {
let issuer_id = match non_empty_env("GOOGLE_WALLET_ISSUER_ID") {
Some(v) => v,
None => return Ok(None),
};
let service_account_email = match non_empty_env("GOOGLE_WALLET_SERVICE_ACCOUNT_EMAIL") {
Some(v) => v,
None => return Ok(None),
};
let service_account_private_key_pem =
match non_empty_env("GOOGLE_WALLET_SERVICE_ACCOUNT_KEY_PEM") {
Some(v) => v,
None => return Ok(None),
};
Ok(Some(Self {
issuer_id,
service_account_email,
service_account_private_key_pem,
}))
}
}
fn non_empty_env(name: &str) -> Option<String> {
match std::env::var(name) {
Ok(v) if !v.is_empty() => Some(v),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
const APP_VARS: &[&str] = &["APP_NAME", "APP_URL"];
const APPLE_VARS: &[&str] = &[
"APPLE_WALLET_PASS_TYPE_ID",
"APPLE_WALLET_TEAM_ID",
"APPLE_WALLET_CERT_PEM",
"APPLE_WALLET_KEY_PEM",
"APPLE_WALLET_WWDR_PEM",
"APPLE_WALLET_KEY_PASSWORD",
];
const GOOGLE_VARS: &[&str] = &[
"GOOGLE_WALLET_ISSUER_ID",
"GOOGLE_WALLET_SERVICE_ACCOUNT_EMAIL",
"GOOGLE_WALLET_SERVICE_ACCOUNT_KEY_PEM",
];
struct EnvGuard {
saved: Vec<(&'static str, Option<String>)>,
}
impl EnvGuard {
fn capture(vars: &[&'static str]) -> Self {
let saved = vars
.iter()
.map(|v| (*v, std::env::var(v).ok()))
.collect::<Vec<_>>();
for v in vars {
std::env::remove_var(v);
}
Self { saved }
}
fn capture_all() -> Self {
let mut all = Vec::with_capacity(APP_VARS.len() + APPLE_VARS.len() + GOOGLE_VARS.len());
all.extend(APP_VARS);
all.extend(APPLE_VARS);
all.extend(GOOGLE_VARS);
Self::capture(&all)
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
for (name, value) in &self.saved {
match value {
Some(v) => std::env::set_var(name, v),
None => std::env::remove_var(name),
}
}
}
}
fn lock_env() -> std::sync::MutexGuard<'static, ()> {
ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
#[test]
fn from_env_apple_missing_is_none() {
let _lock = lock_env();
let _env = EnvGuard::capture_all();
let cfg =
WalletConfig::from_env().expect("from_env must not error when wallet vars are absent");
assert!(
cfg.apple.is_none(),
"expected apple cluster to be None when APPLE_WALLET_* vars are absent"
);
}
#[test]
fn from_env_google_missing_is_none() {
let _lock = lock_env();
let _env = EnvGuard::capture_all();
let cfg =
WalletConfig::from_env().expect("from_env must not error when wallet vars are absent");
assert!(
cfg.google.is_none(),
"expected google cluster to be None when GOOGLE_WALLET_* vars are absent"
);
}
#[test]
fn from_env_defaults_match_appconfig() {
let _lock = lock_env();
let _env = EnvGuard::capture_all();
let cfg =
WalletConfig::from_env().expect("from_env must not error when wallet vars are absent");
assert_eq!(
cfg.app_name, "Ferro Application",
"APP_NAME default must match framework::config::AppConfig::from_env"
);
assert_eq!(
cfg.app_url, "http://localhost:8080",
"APP_URL default must match framework::config::AppConfig::from_env"
);
}
#[test]
fn from_env_apple_partial_returns_none() {
let _lock = lock_env();
let _env = EnvGuard::capture_all();
std::env::set_var("APPLE_WALLET_PASS_TYPE_ID", "pass.com.example.test");
std::env::set_var("APPLE_WALLET_TEAM_ID", "TEAMID1234");
std::env::set_var(
"APPLE_WALLET_CERT_PEM",
"-----BEGIN CERTIFICATE-----\nx\n-----END CERTIFICATE-----\n",
);
std::env::set_var(
"APPLE_WALLET_KEY_PEM",
"-----BEGIN PRIVATE KEY-----\nx\n-----END PRIVATE KEY-----\n",
);
let cfg =
WalletConfig::from_env().expect("from_env must not error on partial Apple cluster");
assert!(
cfg.apple.is_none(),
"expected apple cluster to be None when WWDR_PEM is unset"
);
}
#[test]
fn from_env_apple_all_set_returns_some() {
let _lock = lock_env();
let _env = EnvGuard::capture_all();
std::env::set_var("APPLE_WALLET_PASS_TYPE_ID", "pass.com.example.test");
std::env::set_var("APPLE_WALLET_TEAM_ID", "TEAMID1234");
std::env::set_var("APPLE_WALLET_CERT_PEM", "cert-pem-bytes");
std::env::set_var("APPLE_WALLET_KEY_PEM", "key-pem-bytes");
std::env::set_var("APPLE_WALLET_WWDR_PEM", "wwdr-pem-bytes");
std::env::set_var("APPLE_WALLET_KEY_PASSWORD", "secret");
let cfg =
WalletConfig::from_env().expect("from_env must not error when wallet vars are present");
let apple = cfg
.apple
.expect("apple cluster must populate when all required vars set");
assert_eq!(apple.pass_type_id, "pass.com.example.test");
assert_eq!(apple.team_id, "TEAMID1234");
assert_eq!(apple.cert_pem, "cert-pem-bytes");
assert_eq!(apple.key_pem, "key-pem-bytes");
assert_eq!(apple.wwdr_pem, "wwdr-pem-bytes");
assert_eq!(apple.key_password.as_deref(), Some("secret"));
}
#[test]
fn from_env_google_all_set_returns_some() {
let _lock = lock_env();
let _env = EnvGuard::capture_all();
std::env::set_var("GOOGLE_WALLET_ISSUER_ID", "3388000000000000000");
std::env::set_var(
"GOOGLE_WALLET_SERVICE_ACCOUNT_EMAIL",
"sa@example.iam.gserviceaccount.com",
);
std::env::set_var(
"GOOGLE_WALLET_SERVICE_ACCOUNT_KEY_PEM",
"private-key-pem-bytes",
);
let cfg =
WalletConfig::from_env().expect("from_env must not error when wallet vars are present");
let google = cfg
.google
.expect("google cluster must populate when all required vars set");
assert_eq!(google.issuer_id, "3388000000000000000");
assert_eq!(
google.service_account_email,
"sa@example.iam.gserviceaccount.com"
);
assert_eq!(
google.service_account_private_key_pem,
"private-key-pem-bytes"
);
}
#[test]
fn from_env_never_errors_on_missing_wallet_vars() {
let _lock = lock_env();
let _env = EnvGuard::capture_all();
assert!(
WalletConfig::from_env().is_ok(),
"from_env must never error when all wallet env vars are absent"
);
}
}