nautilus_betfair/common/
credential.rs1use std::fmt::Debug;
19
20use nautilus_core::string::secret::REDACTED;
21use thiserror::Error;
22use zeroize::ZeroizeOnDrop;
23
24pub const BETFAIR_USERNAME_ENV: &str = "BETFAIR_USERNAME";
26
27pub const BETFAIR_PASSWORD_ENV: &str = "BETFAIR_PASSWORD";
29
30pub const BETFAIR_APP_KEY_ENV: &str = "BETFAIR_APP_KEY";
32
33#[derive(Debug, Error)]
35pub enum CredentialError {
36 #[error("Username provided but password is missing")]
38 MissingPassword,
39 #[error("Password provided but username is missing")]
41 MissingUsername,
42 #[error("App key is missing")]
44 MissingAppKey,
45}
46
47#[derive(Clone, ZeroizeOnDrop)]
55pub struct BetfairCredential {
56 username: Box<str>,
57 password: Box<str>,
58 app_key: Box<str>,
59}
60
61impl Debug for BetfairCredential {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 f.debug_struct(stringify!(BetfairCredential))
64 .field("username", &self.username)
65 .field("password", &REDACTED)
66 .field("app_key", &self.app_key)
67 .finish()
68 }
69}
70
71impl BetfairCredential {
72 #[must_use]
74 pub fn new(username: String, password: String, app_key: String) -> Self {
75 Self {
76 username: username.into_boxed_str(),
77 password: password.into_boxed_str(),
78 app_key: app_key.into_boxed_str(),
79 }
80 }
81
82 #[must_use]
88 pub fn from_env() -> Option<Self> {
89 let username = std::env::var(BETFAIR_USERNAME_ENV).ok()?;
90 let password = std::env::var(BETFAIR_PASSWORD_ENV).ok()?;
91 let app_key = std::env::var(BETFAIR_APP_KEY_ENV).ok()?;
92 Some(Self::new(username, password, app_key))
93 }
94
95 pub fn resolve(
104 username: Option<String>,
105 password: Option<String>,
106 app_key: Option<String>,
107 ) -> Result<Option<Self>, CredentialError> {
108 match (username, password, app_key) {
109 (Some(u), Some(p), Some(k)) => Ok(Some(Self::new(u, p, k))),
110 (None, None, None) => Ok(Self::from_env()),
111 (Some(_), None, _) => Err(CredentialError::MissingPassword),
112 (None, Some(_), _) => Err(CredentialError::MissingUsername),
113 (_, _, None) => Err(CredentialError::MissingAppKey),
114 (None, None, Some(_)) => Err(CredentialError::MissingUsername),
115 }
116 }
117
118 #[must_use]
120 pub fn username(&self) -> &str {
121 &self.username
122 }
123
124 #[must_use]
126 pub fn password(&self) -> &str {
127 &self.password
128 }
129
130 #[must_use]
132 pub fn app_key(&self) -> &str {
133 &self.app_key
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use rstest::rstest;
140
141 use super::*;
142
143 #[rstest]
144 fn test_credential_creation() {
145 let cred = BetfairCredential::new(
146 "testuser".to_string(),
147 "testpass".to_string(),
148 "appkey123".to_string(),
149 );
150
151 assert_eq!(cred.username(), "testuser");
152 assert_eq!(cred.password(), "testpass");
153 assert_eq!(cred.app_key(), "appkey123");
154 }
155
156 #[rstest]
157 fn test_debug_redacts_password() {
158 let cred = BetfairCredential::new(
159 "myuser".to_string(),
160 "supersecret".to_string(),
161 "myappkey".to_string(),
162 );
163
164 let debug_output = format!("{cred:?}");
165
166 assert!(debug_output.contains(REDACTED));
167 assert!(!debug_output.contains("supersecret"));
168 assert!(debug_output.contains("myuser"));
169 assert!(debug_output.contains("myappkey"));
170 }
171
172 #[rstest]
173 fn test_resolve_with_all_credentials() {
174 let result = BetfairCredential::resolve(
175 Some("user".to_string()),
176 Some("pass".to_string()),
177 Some("key".to_string()),
178 );
179
180 assert!(result.is_ok());
181 let cred = result.unwrap().unwrap();
182 assert_eq!(cred.username(), "user");
183 }
184
185 #[rstest]
186 fn test_resolve_missing_password() {
187 let result =
188 BetfairCredential::resolve(Some("user".to_string()), None, Some("key".to_string()));
189
190 assert!(result.is_err());
191 assert!(matches!(
192 result.unwrap_err(),
193 CredentialError::MissingPassword
194 ));
195 }
196
197 #[rstest]
198 fn test_resolve_missing_username() {
199 let result =
200 BetfairCredential::resolve(None, Some("pass".to_string()), Some("key".to_string()));
201
202 assert!(result.is_err());
203 assert!(matches!(
204 result.unwrap_err(),
205 CredentialError::MissingUsername
206 ));
207 }
208
209 #[rstest]
210 fn test_resolve_missing_app_key() {
211 let result =
212 BetfairCredential::resolve(Some("user".to_string()), Some("pass".to_string()), None);
213
214 assert!(result.is_err());
215 assert!(matches!(
216 result.unwrap_err(),
217 CredentialError::MissingAppKey
218 ));
219 }
220
221 #[rstest]
222 fn test_resolve_none_falls_back_to_env() {
223 let result = BetfairCredential::resolve(None, None, None);
225
226 assert!(result.is_ok());
227 }
229}