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() { None } else { Some(token) }
36}
37
38fn get_from_keychain() -> Option<String> {
39 match keyring::Entry::new(SERVICE_NAME, TOKEN_KEY) {
40 Ok(entry) => match entry.get_password() {
41 Ok(password) => Some(password),
42 Err(keyring::Error::NoEntry) => None,
43 Err(_) => None,
44 },
45 Err(_) => None,
46 }
47}
48
49pub fn get_keychain_status() -> KeychainStatus {
51 match keyring::Entry::new(SERVICE_NAME, TOKEN_KEY) {
52 Ok(entry) => match entry.get_password() {
53 Ok(_) => KeychainStatus::HasToken,
54 Err(e) => {
55 KeychainStatus::Error(format!("get_password failed: {:?}", e))
57 }
58 },
59 Err(e) => KeychainStatus::Error(format!("Entry::new failed: {:?}", e)),
60 }
61}
62
63#[derive(Debug)]
64pub enum KeychainStatus {
65 HasToken,
66 NoToken,
67 Error(String),
68}
69
70pub fn store_token(token: &str) -> Result<()> {
72 let entry = keyring::Entry::new(SERVICE_NAME, TOKEN_KEY)
73 .map_err(|e| Error::Keyring(format!("Failed to create entry: {}", e)))?;
74
75 entry
76 .set_password(token)
77 .map_err(|e| Error::Keyring(format!("Failed to store: {}", e)))?;
78
79 let verify_entry = keyring::Entry::new(SERVICE_NAME, TOKEN_KEY)
81 .map_err(|e| Error::Keyring(format!("Failed to create verify entry: {}", e)))?;
82
83 match verify_entry.get_password() {
84 Ok(stored) if stored == token => Ok(()),
85 Ok(_) => Err(Error::Keyring("Token mismatch after storage".to_string())),
86 Err(e) => Err(Error::Keyring(format!(
87 "Storage appeared to succeed but verification failed: {}. \
88 This may be a Windows Credential Manager issue.",
89 e
90 ))),
91 }
92}
93
94pub fn delete_token() -> Result<()> {
96 let entry =
97 keyring::Entry::new(SERVICE_NAME, TOKEN_KEY).map_err(|e| Error::Keyring(e.to_string()))?;
98 match entry.delete_credential() {
99 Ok(()) => Ok(()),
100 Err(keyring::Error::NoEntry) => Ok(()), Err(e) => Err(Error::Keyring(e.to_string())),
102 }
103}
104
105pub fn has_token() -> bool {
107 get_token().is_some()
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use serial_test::serial;
114
115 #[test]
116 #[serial]
117 fn test_get_from_token_file_reads_token() {
118 let dir = tempfile::tempdir().unwrap();
119 let token_path = dir.path().join("token");
120 std::fs::write(&token_path, "my_secret_token").unwrap();
121
122 unsafe { std::env::set_var(TOKEN_PATH_ENV_VAR, token_path.to_str().unwrap()) };
125 let result = get_from_token_file();
126 unsafe { std::env::remove_var(TOKEN_PATH_ENV_VAR) };
127
128 assert_eq!(result, Some("my_secret_token".to_string()));
129 }
130
131 #[test]
132 #[serial]
133 fn test_get_from_token_file_trims_whitespace() {
134 let dir = tempfile::tempdir().unwrap();
135 let token_path = dir.path().join("token");
136 std::fs::write(&token_path, " my_token_with_whitespace \n").unwrap();
137
138 unsafe { std::env::set_var(TOKEN_PATH_ENV_VAR, token_path.to_str().unwrap()) };
140 let result = get_from_token_file();
141 unsafe { std::env::remove_var(TOKEN_PATH_ENV_VAR) };
142
143 assert_eq!(result, Some("my_token_with_whitespace".to_string()));
144 }
145
146 #[test]
147 #[serial]
148 fn test_get_from_token_file_returns_none_for_empty_file() {
149 let dir = tempfile::tempdir().unwrap();
150 let token_path = dir.path().join("token");
151 std::fs::write(&token_path, "").unwrap();
152
153 unsafe { std::env::set_var(TOKEN_PATH_ENV_VAR, token_path.to_str().unwrap()) };
155 let result = get_from_token_file();
156 unsafe { std::env::remove_var(TOKEN_PATH_ENV_VAR) };
157
158 assert_eq!(result, None);
159 }
160
161 #[test]
162 #[serial]
163 fn test_get_from_token_file_returns_none_for_whitespace_only() {
164 let dir = tempfile::tempdir().unwrap();
165 let token_path = dir.path().join("token");
166 std::fs::write(&token_path, " \n\t ").unwrap();
167
168 unsafe { std::env::set_var(TOKEN_PATH_ENV_VAR, token_path.to_str().unwrap()) };
170 let result = get_from_token_file();
171 unsafe { 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 unsafe { std::env::set_var(TOKEN_PATH_ENV_VAR, "/nonexistent/path/to/token") };
181 let result = get_from_token_file();
182 unsafe { std::env::remove_var(TOKEN_PATH_ENV_VAR) };
183
184 assert_eq!(result, None);
185 }
186
187 #[test]
188 #[serial]
189 fn test_get_from_token_file_returns_none_when_env_not_set() {
190 unsafe { std::env::remove_var(TOKEN_PATH_ENV_VAR) };
192 let result = get_from_token_file();
193 assert_eq!(result, None);
194 }
195}