1use crate::{Error, Result};
6
7const SERVICE_NAME: &str = "socorro-cli";
8const TOKEN_KEY: &str = "api-token";
9
10const TOKEN_PATH_ENV_VAR: &str = "SOCORRO_API_TOKEN_PATH";
15
16pub fn get_token() -> Option<String> {
22 if let Some(token) = get_from_keychain() {
24 return Some(token);
25 }
26
27 get_from_token_file()
29}
30
31fn get_from_token_file() -> Option<String> {
32 let path = std::env::var(TOKEN_PATH_ENV_VAR).ok()?;
33 let content = std::fs::read_to_string(&path).ok()?;
34 let token = content.trim().to_string();
35 if token.is_empty() {
36 None
37 } else {
38 Some(token)
39 }
40}
41
42fn get_from_keychain() -> Option<String> {
43 match keyring::Entry::new(SERVICE_NAME, TOKEN_KEY) {
44 Ok(entry) => match entry.get_password() {
45 Ok(password) => Some(password),
46 Err(keyring::Error::NoEntry) => None,
47 Err(_) => None,
48 },
49 Err(_) => None,
50 }
51}
52
53pub fn get_keychain_status() -> KeychainStatus {
55 match keyring::Entry::new(SERVICE_NAME, TOKEN_KEY) {
56 Ok(entry) => match entry.get_password() {
57 Ok(_) => KeychainStatus::HasToken,
58 Err(e) => {
59 KeychainStatus::Error(format!("get_password failed: {:?}", e))
61 }
62 },
63 Err(e) => KeychainStatus::Error(format!("Entry::new failed: {:?}", e)),
64 }
65}
66
67#[derive(Debug)]
68pub enum KeychainStatus {
69 HasToken,
70 NoToken,
71 Error(String),
72}
73
74pub fn store_token(token: &str) -> Result<()> {
76 let entry = keyring::Entry::new(SERVICE_NAME, TOKEN_KEY)
77 .map_err(|e| Error::Keyring(format!("Failed to create entry: {}", e)))?;
78
79 entry
80 .set_password(token)
81 .map_err(|e| Error::Keyring(format!("Failed to store: {}", e)))?;
82
83 let verify_entry = keyring::Entry::new(SERVICE_NAME, TOKEN_KEY)
85 .map_err(|e| Error::Keyring(format!("Failed to create verify entry: {}", e)))?;
86
87 match verify_entry.get_password() {
88 Ok(stored) if stored == token => Ok(()),
89 Ok(_) => Err(Error::Keyring("Token mismatch after storage".to_string())),
90 Err(e) => Err(Error::Keyring(format!(
91 "Storage appeared to succeed but verification failed: {}. \
92 This may be a Windows Credential Manager issue.",
93 e
94 ))),
95 }
96}
97
98pub fn delete_token() -> Result<()> {
100 let entry =
101 keyring::Entry::new(SERVICE_NAME, TOKEN_KEY).map_err(|e| Error::Keyring(e.to_string()))?;
102 match entry.delete_credential() {
103 Ok(()) => Ok(()),
104 Err(keyring::Error::NoEntry) => Ok(()), Err(e) => Err(Error::Keyring(e.to_string())),
106 }
107}
108
109pub fn has_token() -> bool {
111 get_token().is_some()
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use serial_test::serial;
118
119 #[test]
120 #[serial]
121 fn test_get_from_token_file_reads_token() {
122 let dir = tempfile::tempdir().unwrap();
123 let token_path = dir.path().join("token");
124 std::fs::write(&token_path, "my_secret_token").unwrap();
125
126 std::env::set_var(TOKEN_PATH_ENV_VAR, token_path.to_str().unwrap());
128 let result = get_from_token_file();
129 std::env::remove_var(TOKEN_PATH_ENV_VAR);
130
131 assert_eq!(result, Some("my_secret_token".to_string()));
132 }
133
134 #[test]
135 #[serial]
136 fn test_get_from_token_file_trims_whitespace() {
137 let dir = tempfile::tempdir().unwrap();
138 let token_path = dir.path().join("token");
139 std::fs::write(&token_path, " my_token_with_whitespace \n").unwrap();
140
141 std::env::set_var(TOKEN_PATH_ENV_VAR, token_path.to_str().unwrap());
142 let result = get_from_token_file();
143 std::env::remove_var(TOKEN_PATH_ENV_VAR);
144
145 assert_eq!(result, Some("my_token_with_whitespace".to_string()));
146 }
147
148 #[test]
149 #[serial]
150 fn test_get_from_token_file_returns_none_for_empty_file() {
151 let dir = tempfile::tempdir().unwrap();
152 let token_path = dir.path().join("token");
153 std::fs::write(&token_path, "").unwrap();
154
155 std::env::set_var(TOKEN_PATH_ENV_VAR, token_path.to_str().unwrap());
156 let result = get_from_token_file();
157 std::env::remove_var(TOKEN_PATH_ENV_VAR);
158
159 assert_eq!(result, None);
160 }
161
162 #[test]
163 #[serial]
164 fn test_get_from_token_file_returns_none_for_whitespace_only() {
165 let dir = tempfile::tempdir().unwrap();
166 let token_path = dir.path().join("token");
167 std::fs::write(&token_path, " \n\t ").unwrap();
168
169 std::env::set_var(TOKEN_PATH_ENV_VAR, token_path.to_str().unwrap());
170 let result = get_from_token_file();
171 std::env::remove_var(TOKEN_PATH_ENV_VAR);
172
173 assert_eq!(result, None);
174 }
175
176 #[test]
177 #[serial]
178 fn test_get_from_token_file_returns_none_for_missing_file() {
179 std::env::set_var(TOKEN_PATH_ENV_VAR, "/nonexistent/path/to/token");
180 let result = get_from_token_file();
181 std::env::remove_var(TOKEN_PATH_ENV_VAR);
182
183 assert_eq!(result, None);
184 }
185
186 #[test]
187 #[serial]
188 fn test_get_from_token_file_returns_none_when_env_not_set() {
189 std::env::remove_var(TOKEN_PATH_ENV_VAR);
190 let result = get_from_token_file();
191 assert_eq!(result, None);
192 }
193}