nautilus_architect_ax/common/
credential.rs1use core::fmt::Debug;
19
20use nautilus_core::{
21 env::resolve_env_var_pair,
22 string::secret::{REDACTED, mask_api_key},
23};
24use zeroize::ZeroizeOnDrop;
25
26#[must_use]
28pub fn credential_env_vars() -> (&'static str, &'static str) {
29 ("AX_API_KEY", "AX_API_SECRET")
30}
31
32#[derive(Clone, ZeroizeOnDrop)]
37pub struct Credential {
38 api_key: Box<str>,
39 api_secret: Box<str>,
40}
41
42impl Debug for Credential {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.debug_struct(stringify!(Credential))
45 .field("api_key", &self.masked_api_key())
46 .field("api_secret", &REDACTED)
47 .finish()
48 }
49}
50
51impl Credential {
52 #[must_use]
54 pub fn new(api_key: impl Into<String>, api_secret: impl Into<String>) -> Self {
55 Self {
56 api_key: api_key.into().into_boxed_str(),
57 api_secret: api_secret.into().into_boxed_str(),
58 }
59 }
60
61 #[must_use]
66 pub fn resolve(api_key: Option<String>, api_secret: Option<String>) -> Option<Self> {
67 let (key_var, secret_var) = credential_env_vars();
68 let (k, s) = resolve_env_var_pair(api_key, api_secret, key_var, secret_var)?;
69 Some(Self::new(k, s))
70 }
71
72 #[must_use]
74 pub fn api_key(&self) -> &str {
75 &self.api_key
76 }
77
78 #[must_use]
84 pub fn api_secret(&self) -> &str {
85 &self.api_secret
86 }
87
88 #[must_use]
90 pub fn masked_api_key(&self) -> String {
91 mask_api_key(&self.api_key)
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use rstest::rstest;
98
99 use super::*;
100
101 const API_KEY: &str = "test_api_key_123";
102 const API_SECRET: &str = "test_secret_456";
103
104 #[rstest]
105 fn test_credential_creation() {
106 let credential = Credential::new(API_KEY, API_SECRET);
107
108 assert_eq!(credential.api_key(), API_KEY);
109 assert_eq!(credential.api_secret(), API_SECRET);
110 }
111
112 #[rstest]
113 fn test_masked_api_key() {
114 let credential = Credential::new(API_KEY, API_SECRET);
115 let masked = credential.masked_api_key();
116
117 assert_eq!(masked, "test..._123");
118 assert!(!masked.contains("api_key"));
119 }
120
121 #[rstest]
122 fn test_masked_api_key_short() {
123 let credential = Credential::new("short", API_SECRET);
124 let masked = credential.masked_api_key();
125
126 assert_eq!(masked, "*****");
127 }
128
129 #[rstest]
130 fn test_debug_does_not_leak_secret() {
131 let credential = Credential::new(API_KEY, API_SECRET);
132 let debug_string = format!("{credential:?}");
133
134 assert!(!debug_string.contains(API_SECRET));
135 assert!(debug_string.contains(REDACTED));
136 assert!(debug_string.contains("test..."));
137 }
138
139 #[rstest]
140 fn test_resolve_with_both_args() {
141 let result = Credential::resolve(Some("my_key".to_string()), Some("my_secret".to_string()));
142
143 assert!(result.is_some());
144 assert_eq!(result.unwrap().api_key(), "my_key");
145 }
146
147 #[rstest]
148 fn test_resolve_with_no_args_no_env() {
149 let (key_var, secret_var) = credential_env_vars();
150 if std::env::var(key_var).is_ok() || std::env::var(secret_var).is_ok() {
151 return;
152 }
153
154 let result = Credential::resolve(None, None);
155
156 assert!(result.is_none());
157 }
158}